Skip to content

perf(stacktrace): defer frame symbolization to first read 🔥 😍😍😍#137

Merged
samber merged 3 commits into
mainfrom
perf/defer-stacktrace-symbolization
Jul 5, 2026
Merged

perf(stacktrace): defer frame symbolization to first read 🔥 😍😍😍#137
samber merged 3 commits into
mainfrom
perf/defer-stacktrace-symbolization

Conversation

@samber

@samber samber commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Program counters are still captured eagerly at error-creation time via runtime.Callers, but resolving them into file/function/line strings via runtime.CallersFrames (the dominant cost of error creation, ~30% of BenchmarkNew) is now deferred behind a sync.Once, triggered on first access to Stacktrace()/Source()/StackFrames().
  • Most errors are created, checked against nil, and discarded without ever being formatted, so they no longer pay for symbolization at all. Errors that are formatted pay the same total cost as before, just on first read instead of at creation.
  • Filtering rules and their inputs (StackTraceMaxDepth snapshotted at capture time) are unchanged. GOROOT and the examples-package prefix are now computed once at init instead of per error.
  • Adds BenchmarkCreateThenStacktrace to keep the deferred cost measurable, since the existing read benchmarks reuse a single error and would otherwise amortize resolution away.

Benchmarks

benchstat (arm64, Apple M3, -count=6):

                                  │   before    │    after                            │
New                                 4.335µ ± 3%   1.779µ ± 1%  -58.96% (p=0.002 n=6)
Wrap                                4.618µ ± 3%   1.517µ ± 1%  -67.15% (p=0.002 n=6)
NewStacktrace                       4.506µ ± 5%   1.773µ ± 4%  -60.65% (p=0.002 n=6)
CreatedNotRead/simple/Wrap          5.689µ ± 4%   1.837µ ± 3%  -67.72% (p=0.002 n=6)
CreatedNotRead/rich/chain=7         5.333µ ± 3%   2.042µ ± 1%  -61.71% (p=0.002 n=6)
CreateThenStacktrace                6.405µ ± 7%   6.302µ ± 4%        ~ (p=0.589 n=6)
ToMap                               8.644µ ± 5%   8.316µ ± 3%   -3.80% (p=0.026 n=6)

                                  │   B/op      │    B/op                             │
New                                 2.336Ki ± 0%  1.273Ki ± 0%  -45.48% (p=0.002 n=6)
Wrap                                2.320Ki ± 0%  1.258Ki ± 0%  -45.79% (p=0.002 n=6)
CreateThenStacktrace                3.477Ki ± 0%  3.523Ki ± 0%   +1.35% (p=0.002 n=6)

Program counters are still captured eagerly at error-creation time via
runtime.Callers (the stack is gone afterwards, so this cannot move), but
resolving PCs into file/function/line strings through
runtime.CallersFrames — the dominant cost of error creation per CPU
profile (~30% of BenchmarkNew) — now happens behind a sync.Once on
first access to Stacktrace()/Sources()/StackFrames().

Most errors in production are created, checked against nil, and
discarded without ever being formatted (BenchmarkBuilderCreatedNotRead
models this), so they no longer pay for symbolization at all. Errors
that are formatted pay the same total cost as before, on first read.

Filtering rules, their inputs (StackTraceMaxDepth is snapshotted at
capture time) and output are unchanged. GOROOT and the examples-package
prefix are now computed once at init instead of per error —
runtime.GOROOT re-reads an env var on every call.

This implements the existing "@todo: filtering should be done lazily,
not at creation time".

benchstat (linux/amd64, Intel Xeon @ 2.80GHz, -count=6 -cpu=1):

                                  │   before    │    after                            │
New                                 4.335µ ± 3%   1.779µ ± 1%  -58.96% (p=0.002 n=6)
Wrap                                4.618µ ± 3%   1.517µ ± 1%  -67.15% (p=0.002 n=6)
NewStacktrace                       4.506µ ± 5%   1.773µ ± 4%  -60.65% (p=0.002 n=6)
CreatedNotRead/simple/Wrap          5.689µ ± 4%   1.837µ ± 3%  -67.72% (p=0.002 n=6)
CreatedNotRead/rich/chain=7         5.333µ ± 3%   2.042µ ± 1%  -61.71% (p=0.002 n=6)
CreateThenStacktrace                6.405µ ± 7%   6.302µ ± 4%        ~ (p=0.589 n=6)
ToMap                               8.644µ ± 5%   8.316µ ± 3%   -3.80% (p=0.026 n=6)

                                  │   B/op      │    B/op                             │
New                                 2.336Ki ± 0%  1.273Ki ± 0%  -45.48% (p=0.002 n=6)
Wrap                                2.320Ki ± 0%  1.258Ki ± 0%  -45.79% (p=0.002 n=6)
CreateThenStacktrace                3.477Ki ± 0%  3.523Ki ± 0%   +1.35% (p=0.002 n=6)

BenchmarkCreateThenStacktrace is added in this commit to keep the
deferred cost measurable: the existing read benchmarks reuse a single
error, which would otherwise amortize resolution away.
@samber samber changed the title perf(stacktrace): defer frame symbolization to first read perf(stacktrace): defer frame symbolization to first read 🔥 😍😍😍 Jul 5, 2026
@samber samber marked this pull request as ready for review July 5, 2026 14:57
Copilot AI review requested due to automatic review settings July 5, 2026 14:57
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.36842% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.50%. Comparing base (b313311) to head (9046a7a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
stacktrace.go 97.29% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #137      +/-   ##
==========================================
+ Coverage   90.17%   90.50%   +0.33%     
==========================================
  Files          15       15              
  Lines        1231     1243      +12     
==========================================
+ Hits         1110     1125      +15     
+ Misses         96       93       -3     
  Partials       25       25              
Flag Coverage Δ
unittests 90.50% <97.36%> (+0.33%) ⬆️

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.

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 optimizes stacktrace handling by capturing program counters eagerly but deferring expensive symbolization (via runtime.CallersFrames) until the stacktrace is actually read, reducing the cost of creating errors that are never formatted.

Changes:

  • Added lazy stack frame resolution behind a sync.Once in oopsStacktrace.
  • Moved invariant filtering inputs (GOROOT, examples-package prefix) to package init-time.
  • Added a benchmark to measure create+resolve+format cost now that resolution is deferred.

Reviewed changes

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

File Description
stacktrace.go Defers symbolization to first read, adds sync.Once, and hoists invariant filtering inputs.
stacktrace_test.go Updates tests to force resolution via resolvedFrames() before asserting on frames.
error.go Ensures formatting paths use resolvedFrames() so lazy resolution happens when needed.
benchmarks/benchmark_test.go Adds BenchmarkCreateThenStacktrace to measure deferred resolution cost.

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

Comment thread stacktrace.go
Comment thread stacktrace.go Outdated
samber and others added 2 commits July 5, 2026 17:01
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@samber samber merged commit 4579c2c into main Jul 5, 2026
10 checks passed
@samber samber deleted the perf/defer-stacktrace-symbolization branch July 5, 2026 15:09
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