perf(error): cut intermediate allocations in LogValue, ToMap and formatVerbose#135
Conversation
…atVerbose
- LogValue: build the []any slog.Group argument lists directly instead
of lo.MapToSlice followed by lo.ToAnySlice, which allocated two
intermediate slices per group (context, user, tenant).
- ToMap: use the snapshot-owned user/tenant maps directly instead of
copying them with lo.Assign - snapshot() always returns freshly
merged maps, so mutating them is safe.
- formatVerbose: replace the Split -> lo.Map(prefix) -> Join pipeline
for request/response dumps with a single-pass writer into the
existing strings.Builder (writePrefixedLines), preserving output
byte-for-byte.
benchstat vs main (darwin/arm64, Apple M3, -count=10 -cpu=1):
| before2.bench.txt | after2.bench.txt |
| sec/op | sec/op vs base |
ToMap 3.843u ± 4% 3.812u ± 6% ~ (p=0.393 n=10)
LogValue 3.803u ± 4% 3.857u ± 3% ~ (p=0.353 n=10)
MarshalJSON 5.852u ± 5% 5.754u ± 8% ~ (p=0.353 n=10)
geomean 4.406u 4.390u -0.37%
| before2.bench.txt | after2.bench.txt |
| B/op | B/op vs base |
ToMap 5.901Ki ± 0% 5.573Ki ± 0% -5.56% (p=0.000 n=10)
LogValue 5.977Ki ± 0% 5.758Ki ± 0% -3.66% (p=0.000 n=10)
MarshalJSON 6.665Ki ± 0% 6.665Ki ± 0% ~ (p=1.000 n=10)
geomean 6.172Ki 5.980Ki -3.10%
| before2.bench.txt | after2.bench.txt |
| allocs/op | allocs/op vs base |
ToMap 72.00 ± 0% 70.00 ± 0% -2.78% (p=0.000 n=10)
LogValue 70.00 ± 0% 66.00 ± 0% -5.71% (p=0.000 n=10)
MarshalJSON 92.00 ± 0% 92.00 ± 0% ~ (p=1.000 n=10)
geomean 77.40 75.19 -2.86%
Note: wall-clock deltas were not statistically significant on this
run (background noise on the benchmark machine), but the B/op and
allocs/op reductions are fully reproducible (0% variance, p<0.001).
MarshalJSON's benchmark fixture does not set User/Tenant data, so it
does not exercise the ToMap fast path.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #135 +/- ##
==========================================
+ Coverage 90.17% 90.43% +0.26%
==========================================
Files 15 15
Lines 1231 1234 +3
==========================================
+ Hits 1110 1116 +6
+ Misses 96 93 -3
Partials 25 25
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR focuses on reducing allocations in OopsError’s logging/serialization and verbose formatting paths, primarily by avoiding intermediate slices/maps and by streaming line-prefix formatting directly into an existing strings.Builder.
Changes:
LogValue: constructsslog.Groupargument lists directly to avoidlo.MapToSlice+lo.ToAnySliceintermediate allocations.ToMap: reuses snapshot-owned user/tenant maps directly instead of copying them.formatVerbose: replacesSplit/Map/Joinrequest/response dump formatting with a single-pass helperwritePrefixedLines.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Both callers (formatVerbose's request/response dumps) hold the
httputil dump as []byte already and were converting it to string just
to call writePrefixedLines. Accepting []byte directly removes that
copy; bytes.IndexByte replaces strings.IndexByte and output.Write
replaces output.WriteString for the same single-pass, no-intermediate-
slice behavior.
benchstat vs previous string-based version (darwin/arm64, Apple M3,
-count=10 -cpu=1, dump ~100 bytes of typical request headers):
| wpl_before.txt | wpl_after.txt |
| sec/op | sec/op vs base |
TempWritePrefixedLinesSmall 149.7n ± 2% 134.5n ± 4% -10.15% (p=0.001 n=10)
| wpl_before.txt | wpl_after.txt |
| B/op | B/op vs base |
TempWritePrefixedLinesSmall 344.0 ± 0% 232.0 ± 0% -32.56% (p=0.000 n=10)
| wpl_before.txt | wpl_after.txt |
| allocs/op | allocs/op vs base |
TempWritePrefixedLinesSmall 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=10)
Measured with a throwaway internal benchmark exercising writePrefixedLines
directly (not committed, since existing benchmarks live in the external
benchmarks/ package and writePrefixedLines is unexported).
Summary
LogValue: build the[]anyslog.Groupargument lists directly instead oflo.MapToSlicefollowed bylo.ToAnySlice, which allocated two intermediate slices per group (context, user, tenant).ToMap: use the snapshot-owned user/tenant maps directly instead of copying them withlo.Assign-snapshot()always returns freshly merged maps, so mutating them is safe.formatVerbose: replace theSplit->lo.Map(prefix)->Joinpipeline for request/response dumps with a single-pass writer into the existingstrings.Builder(writePrefixedLines), preserving output byte-for-byte.writePrefixedLines: take its input as[]byteinstead ofstring. Both callers already hold the httputil dump as[]byteand were converting it tostringjust for this call; accepting[]bytedirectly removes that copy.Benchmarks
benchstatvsmain(darwin/arm64, Apple M3,-count=10 -cpu=1):Note: wall-clock deltas were not statistically significant on this run (background noise on the benchmark machine), but the
B/opandallocs/opreductions are fully reproducible (0% variance, p<0.001).MarshalJSON's benchmark fixture does not set User/Tenant data, so it does not exercise theToMapfast path.For the
writePrefixedLines[]bytechange specifically,benchstatvs the previous string-based version (same machine,-count=10 -cpu=1, ~100-byte dump of typical request headers), measured with a throwaway internal benchmark not included in this PR (existing benchmarks live in the externalbenchmarks/package andwritePrefixedLinesis unexported):Test plan
Verified with
go build ./...,go vet ./...,go test ./..., and the benchmark runs above compared withbenchstat.