Skip to content

refactor: hoist LoaderFunc to package-level var in phpheaders#2053

Merged
dunglas merged 4 commits intomainfrom
perf/bulk-otter
Mar 10, 2026
Merged

refactor: hoist LoaderFunc to package-level var in phpheaders#2053
dunglas merged 4 commits intomainfrom
perf/bulk-otter

Conversation

@dunglas
Copy link
Member

@dunglas dunglas commented Dec 1, 2025

Summary

Hoists the otter.LoaderFunc closure in GetUnCommonHeader to a package-level loader var, so it is allocated once at init time instead of being re-created on every call.

This is a minor cleanup — the previous code created a new LoaderFunc closure each time GetUnCommonHeader was called. While otter's cache-hit path is fast enough that this doesn't show a measurable difference in end-to-end benchmarks, avoiding the repeated allocation is strictly better.

What changed

Before (closure created per call):

func GetUnCommonHeader(ctx context.Context, key string) string {
    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
        }),
    )
    ...
}

After (closure allocated once):

var loader = otter.LoaderFunc[string, string](func(_ context.Context, key string) (string, error) {
    return "HTTP_" + headerNameReplacer.Replace(strings.ToUpper(key)) + "\x00", nil
})

func GetUnCommonHeader(ctx context.Context, key string) string {
    phpHeaderKey, err := headerKeyCache.Get(ctx, key, loader)
    ...
}

Benchmarks

Apple M1 Pro, 8 runs, benchstat comparison — no regressions, no extra allocations:

Benchmark main PR vs base
HelloWorld 41.81µ ± 2% 42.75µ ± 5% ~ (p=0.065)
ServerSuperGlobal 73.36µ ± 2% 74.20µ ± 3% ~ (p=0.105)
UncommonHeaders 69.03µ ± 3% 68.71µ ± 1% ~ (p=0.382)

All results within noise. Zero change in allocations.

@dunglas dunglas marked this pull request as ready for review March 10, 2026 11:01
Copilot AI review requested due to automatic review settings March 10, 2026 11:01
@dunglas
Copy link
Member Author

dunglas commented Mar 10, 2026

I implemented @maxm86545 suggestion, this is now ready to be merged:

Benchmarks

Apple M1 Pro, 6 runs, benchstat comparison (main vs this PR):

Latency (sec/op)

Benchmark main PR vs base
HelloWorld 55.07µ ± 107% 44.08µ ± 5% ~ (p=0.065)
ServerSuperGlobal 85.78µ ± 64% 76.37µ ± 6% -10.98% (p=0.004)
UncommonHeaders 75.59µ ± 13% 70.53µ ± 1% -6.70% (p=0.002)

Memory (B/op)

Benchmark main PR vs base
HelloWorld 1.404Ki ± 1% 1.384Ki ± 1% -1.43% (p=0.026)
ServerSuperGlobal 18.15Ki ± 34% 16.15Ki ± 8% -11.02% (p=0.004)
UncommonHeaders 15.96Ki ± 14% 15.37Ki ± 1% ~ (p=0.065)

Allocations (allocs/op)

Benchmark main PR vs base
HelloWorld 17 17 ~
ServerSuperGlobal 17 17 ~
UncommonHeaders 18 21 +16.67% (p=0.002)

Summary

ServerSuperGlobal is the most realistic benchmark (simulates a typical browser request with common headers + env vars). It shows an 11% improvement in both latency and memory, with strong
statistical significance (p=0.004).

The optimization works by:

  1. Early return when all request headers are common (the typical case) — skipping the uncommon header path entirely
  2. Batching uncommon header lookups into a single GetUnCommonHeaders call using Get+LoaderFunc (per review feedback — avoids cache-miss storms vs BulkGet)

The +3 allocs in UncommonHeaders come from the uncommonKeys slice and uncommonHeaders map needed to batch the lookups. This only applies when uncommon headers are present.

@henderkes
Copy link
Contributor

Based on just the benchmarks with zero further insight on the topic, I think it's worth it. Runtime performance is almost always worth it over a slightly lower memory footprint and as a php server, it's not like FrankenPHP is expected to consume little memory.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to improve request handling performance by batching uncommon HTTP header name conversions/caching, and updates tests accordingly.

Changes:

  • Replace per-header uncommon header conversion with a batched GetUnCommonHeaders helper backed by the existing Otter cache.
  • Update CGI header-to-$_SERVER registration to precompute uncommon header names in bulk.
  • Adjust header correctness tests to use the new batched helper (and add a duplicate copy in phpmainthread_test.go).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
internal/phpheaders/phpheaders.go Replaces single-key conversion with a batched API that returns a map of converted header keys.
cgi.go Refactors header registration to count/register common headers first, then batch-convert/register uncommon headers.
internal/phpheaders/phpheaders_test.go Updates the common-header correctness test to use the new batched API.
phpmainthread_test.go Adds a second copy of the common-header correctness test in the main package tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@dunglas dunglas changed the title perf: bulk calls to Otter refactor: hoist LoaderFunc to package-level var in phpheaders Mar 10, 2026
@dunglas
Copy link
Member Author

dunglas commented Mar 10, 2026

Re-running the benchmarks properly, it's not worth it. I just made a tiny cleanup.

@dunglas dunglas merged commit 03c26ec into main Mar 10, 2026
31 checks passed
@dunglas dunglas deleted the perf/bulk-otter branch March 10, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants