Skip to content

fileserver: reduce allocations in calculateEtag#7852

Open
jvoisin wants to merge 1 commit into
caddyserver:masterfrom
jvoisin:etagphonehome
Open

fileserver: reduce allocations in calculateEtag#7852
jvoisin wants to merge 1 commit into
caddyserver:masterfrom
jvoisin:etagphonehome

Conversation

@jvoisin

@jvoisin jvoisin commented Jun 30, 2026

Copy link
Copy Markdown

calculateEtag runs for every static file served (and on conditional If-None-Match revalidations). It used a non-presized strings.Builder plus two strconv.FormatInt calls, each allocating an intermediate string and triggering builder growth, which is about six allocations per call.

This commit builds the quoted etag into a fixed 32-byte stack array with strconv.AppendInt instead, so only the final string is heap-allocated. The output format is byte-for-byte identical. Two base-36 int64 values plus the two quote bytes are at most 30 bytes (the worst case is MinInt64, which is 1y2p0ij32e8e8, so 13 digits plus a minus sign, so two times 14, plus the quotes, for a total of 30 characters), so the buffer never spills. And even if it did, which it doesn't append would simply grow it onto the heap without changing the result.

Adds a benchmark, as required by the policy:

CalculateEtag:

  • sec/op 347.6n -> 186.2n (-46.42%)
  • B/op 112 -> 56 (-50%)
  • allocs/op 6 -> 2 (-66.67%)

It removes some CPU work, but more interestingly four of six per-file allocations on a hot path of a very common handler, lowering per-request garbage and GC pressure.

Assistance Disclosure

"No AI was used."

calculateEtag runs for every static file served (and on conditional
If-None-Match revalidations). It used a non-presized strings.Builder plus
two strconv.FormatInt calls, each allocating an intermediate string and
triggering builder growth, which is about six allocations per call.

This commit builds the quoted etag into a fixed 32-byte stack array with
strconv.AppendInt instead, so only the final string is heap-allocated. The
output format is byte-for-byte identical. Two base-36 int64 values plus the
two quote bytes are at most 30 bytes (the worst case is MinInt64, which is
`1y2p0ij32e8e8`, so 13 digits plus a minus sign, so two times 14, plus the
quotes, for a total of 30 characters), so the buffer never spills. And even if
it did, which it doesn't append would simply grow it onto the heap without
changing the result.

Adds a benchmark, as required by the policy:

CalculateEtag:
- sec/op  347.6n -> 186.2n  (-46.42%)
- B/op 112 -> 56  (-50%)
- allocs/op 6 -> 2  (-66.67%)

It removes some CPU work, but more interestingly four of six per-file
allocations on a hot path of a very common handler, lowering per-request
garbage and GC pressure.
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.

1 participant