From 43d99e1ed8a0ebaaed99a0555f82f0adc4c6f162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 1 Dec 2025 11:28:43 +0100 Subject: [PATCH] perf: bulk calls to Otter --- cgi.go | 31 ++++++++++++++++++++++++++----- internal/phpheaders/phpheaders.go | 25 ++++++++++++++++--------- phpmainthread_test.go | 7 +++++-- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/cgi.go b/cgi.go index 09c60f484..64a77e3a0 100644 --- a/cgi.go +++ b/cgi.go @@ -187,18 +187,39 @@ func packCgiVariable(key *C.zend_string, value string) C.ht_key_value_pair { } func addHeadersToServer(ctx context.Context, request *http.Request, trackVarsArray *C.zval) { + var totalCommonHeaders int + for field, val := range request.Header { if k := mainThread.commonHeaders[field]; k != nil { + totalCommonHeaders++ v := strings.Join(val, ", ") C.frankenphp_register_single(k, toUnsafeChar(v), C.size_t(len(v)), trackVarsArray) + } + } + + if totalCommonHeaders == len(request.Header) { + return + } + + // if the header name could not be cached, it needs to be registered safely + // this is more inefficient but allows additional sanitizing by PHP + nbUncommonHeaders := len(request.Header)-totalCommonHeaders + uncommonKeys := make([]string, nbUncommonHeaders) + uncommonHeaders := make(map[string]string, nbUncommonHeaders) + var i int + + for field, val := range request.Header { + if k := mainThread.commonHeaders[field]; k != nil { continue } - // if the header name could not be cached, it needs to be registered safely - // this is more inefficient but allows additional sanitizing by PHP - k := phpheaders.GetUnCommonHeader(ctx, field) - v := strings.Join(val, ", ") - C.frankenphp_register_variable_safe(toUnsafeChar(k), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray) + uncommonKeys[i] = field + uncommonHeaders[field] = strings.Join(val, ", ") + } + + keys := phpheaders.GetUnCommonHeaders(ctx, uncommonKeys) + for k, v := range uncommonHeaders { + C.frankenphp_register_variable_safe(toUnsafeChar(keys[k]), toUnsafeChar(v), C.size_t(len(v)), trackVarsArray) } } diff --git a/internal/phpheaders/phpheaders.go b/internal/phpheaders/phpheaders.go index 19f1908d2..454fd2079 100644 --- a/internal/phpheaders/phpheaders.go +++ b/internal/phpheaders/phpheaders.go @@ -118,21 +118,28 @@ var CommonRequestHeaders = map[string]string{ // Cache up to 256 uncommon headers // This is ~2.5x faster than converting the header each time -var headerKeyCache = otter.Must[string, string](&otter.Options[string, string]{MaximumSize: 256}) +var ( + headerKeyCache = otter.Must[string, string](&otter.Options[string, string]{MaximumSize: 256}) + headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") + bulkLoader = otter.BulkLoaderFunc[string, string](func(ctx context.Context, keys []string) (map[string]string, error) { + result := make(map[string]string, len(keys)) + for _, k := range keys { + result[k] = "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(k)) + "\x00" + } -var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") + return result, nil + }) +) -func GetUnCommonHeader(ctx context.Context, key string) string { - phpHeaderKey, err := headerKeyCache.Get( +func GetUnCommonHeaders(ctx context.Context, keys []string) map[string]string { + phpHeaderKeys, err := headerKeyCache.BulkGet( ctx, - key, - otter.LoaderFunc[string, string](func(_ context.Context, key string) (string, error) { - return "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(key)) + "\x00", nil - }), + keys, + bulkLoader, ) if err != nil { panic(err) } - return phpHeaderKey + return phpHeaderKeys } diff --git a/phpmainthread_test.go b/phpmainthread_test.go index bf842ef66..84fa5561f 100644 --- a/phpmainthread_test.go +++ b/phpmainthread_test.go @@ -3,10 +3,12 @@ package frankenphp import ( "io" "log/slog" + "maps" "math/rand/v2" "net/http/httptest" "path/filepath" "runtime" + "slices" "sync" "sync/atomic" "testing" @@ -249,12 +251,13 @@ func allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpT } func TestAllCommonHeadersAreCorrect(t *testing.T) { + keys := slices.Collect(maps.Keys(phpheaders.CommonRequestHeaders)) + uncommonHeaders := phpheaders.GetUnCommonHeaders(t.Context(), keys) fakeRequest := httptest.NewRequest("GET", "http://localhost", nil) for header, phpHeader := range phpheaders.CommonRequestHeaders { // verify that common and uncommon headers return the same result - expectedPHPHeader := phpheaders.GetUnCommonHeader(t.Context(), header) - assert.Equal(t, phpHeader+"\x00", expectedPHPHeader, "header is not well formed: "+phpHeader) + assert.Equal(t, phpHeader+"\x00", uncommonHeaders[header], "header is not well formed: "+phpHeader) // net/http will capitalize lowercase headers, verify that headers are capitalized fakeRequest.Header.Add(header, "foo")