Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions cgi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
25 changes: 16 additions & 9 deletions internal/phpheaders/phpheaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +135 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s a good idea to make a single call to retrieve the required headers. However, Get+LoaderFunc solves the cache‑miss storm problem, while BulkGet allows duplicated computations. In our case, I believe it’s better to use Get+LoaderFunc.

BenchmarkGetUnCommonHeadersBulkGetSequential-12                          5209970               210.0 ns/op           336 B/op          2 allocs/op
BenchmarkGetUnCommonHeadersForGetSequential-12                           5961091               198.2 ns/op           336 B/op          2 allocs/op

BenchmarkGetUnCommonHeadersBulkGetSequentialMulti4Keys-12                2924588               418.0 ns/op           336 B/op          2 allocs/op
BenchmarkGetUnCommonHeadersForGetSequentialMulti4Keys-12                 3214368               370.8 ns/op           336 B/op          2 allocs/op

BenchmarkGetUnCommonHeadersBulkGetSequentialMulti20Keys-12                604546              1788 ns/op            1240 B/op          4 allocs/op
BenchmarkGetUnCommonHeadersForGetSequentialMulti20Keys-12                 695522              1626 ns/op            1240 B/op          4 allocs/op

BenchmarkGetUnCommonHeadersBulkGetParallelOneKey-12                      8923716               135.9 ns/op           336 B/op          2 allocs/op
BenchmarkGetUnCommonHeadersForGetParallelOneKey-12                       8317735               136.2 ns/op           336 B/op          2 allocs/op

BenchmarkGetUnCommonHeadersBulkGetParallelRandomMaximumSize-12           7624581               166.9 ns/op           336 B/op          2 allocs/op
BenchmarkGetUnCommonHeadersForGetParallelRandomMaximumSize-12            7207694               168.4 ns/op           336 B/op          2 allocs/op

BenchmarkGetUnCommonHeadersBulkGetParallelRandomLenKeys-12               6282062               170.2 ns/op           427 B/op          3 allocs/op
BenchmarkGetUnCommonHeadersForGetParallelRandomLenKeys-12                9184005               137.4 ns/op           349 B/op          2 allocs/op

BenchmarkGetUnCommonHeadersBulkGetParallelMulti4Keys-12                  7604196               158.3 ns/op           336 B/op          2 allocs/op
BenchmarkGetUnCommonHeadersForGetParallelMulti4Keys-12                   8003053               149.9 ns/op           336 B/op          2 allocs/op

BenchmarkGetUnCommonHeadersBulkGetParallelMulti20Keys-12                 1747748               738.1 ns/op          1240 B/op          4 allocs/op
BenchmarkGetUnCommonHeadersForGetParallelMulti20Keys-12                  1716158               702.0 ns/op          1240 B/op          4 allocs/o

Copy link
Contributor

@maxm86545 maxm86545 Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ForGet (Get+LoaderFunc):

func GetUnCommonHeadersForGet(ctx context.Context, keys []string) map[string]string {
	phpHeaderKeys := make(map[string]string, len(keys))
	for _, key := range keys {
		phpHeaderKey, err := headerKeyCache.Get(
			ctx,
			key,
			otter.LoaderFunc[string, string](func(_ context.Context, key string) (string, error) {
				return "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(key)) + "\x00", nil
			}),
		)
		if err != nil {
			panic(err)
		}
		phpHeaderKeys[key] = phpHeaderKey
	}

	return phpHeaderKeys
}

Copy link
Contributor

@maxm86545 maxm86545 Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S. I’m puzzled that individual calls to GetUnCommonHeader #2040 take less total time than a single call to GetUnCommonHeaders. It seems that extra time is being spent on working with the map and its allocations in the heap. Hopefully, in a real scenario, the overall performance of the addHeadersToServerfunction has improved, but I don’t yet have the opportunity to verify this.

}
7 changes: 5 additions & 2 deletions phpmainthread_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
Loading