Skip to content

perf(error): cut intermediate allocations in LogValue, ToMap and formatVerbose#135

Merged
samber merged 2 commits into
mainfrom
perf/logvalue-tomap-formatverbose-allocs
Jul 5, 2026
Merged

perf(error): cut intermediate allocations in LogValue, ToMap and formatVerbose#135
samber merged 2 commits into
mainfrom
perf/logvalue-tomap-formatverbose-allocs

Conversation

@samber

@samber samber commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • 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.
  • writePrefixedLines: take its input as []byte instead of string. Both callers already hold the httputil dump as []byte and were converting it to string just for this call; accepting []byte directly removes that copy.

Benchmarks

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.843µ ± 4%   3.812µ ± 6%       ~ (p=0.393 n=10)
LogValue            3.803µ ± 4%   3.857µ ± 3%       ~ (p=0.353 n=10)
MarshalJSON         5.852µ ± 5%   5.754µ ± 8%       ~ (p=0.353 n=10)
geomean             4.406µ        4.390µ       -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.

For the writePrefixedLines []byte change specifically, benchstat vs 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 external benchmarks/ package and writePrefixedLines is unexported):

                            │ 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)

Test plan

Verified with go build ./..., go vet ./..., go test ./..., and the benchmark runs above compared with benchstat.

…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

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.10345% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.43%. Comparing base (470a3a2) to head (bf516c4).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
error.go 93.10% 2 Missing ⚠️
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              
Flag Coverage Δ
unittests 90.43% <93.10%> (+0.26%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@samber samber marked this pull request as ready for review July 5, 2026 14:23
Copilot AI review requested due to automatic review settings July 5, 2026 14:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 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: constructs slog.Group argument lists directly to avoid lo.MapToSlice + lo.ToAnySlice intermediate allocations.
  • ToMap: reuses snapshot-owned user/tenant maps directly instead of copying them.
  • formatVerbose: replaces Split/Map/Join request/response dump formatting with a single-pass helper writePrefixedLines.

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

Comment thread error.go
Comment thread error.go
Comment thread error.go Outdated
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).
@samber samber merged commit 9be8af4 into main Jul 5, 2026
10 checks passed
@samber samber deleted the perf/logvalue-tomap-formatverbose-allocs branch July 5, 2026 14:50
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.

2 participants