Repository navigation
perf(gosqlx): build wrapped error messages lazily [W1-03] - #905
ajitpratap0 wants to merge 5 commits into
Conversation
The 21 fmt.Errorf("%w: %w", sentinel, err) sites in pkg/gosqlx formatted
the full message on every error, although many callers only test the error
with errors.Is. They now return an unexported wrapErr holding both errors
in a [2]error array: Error() builds the identical "sentinel: inner" text on
demand, and Unwrap() []error returns a slice of the array, so errors.Is and
errors.As traverse it without allocating. Sites with extra context text
keep fmt.Errorf unchanged.
Tests: a compatibility suite over 242 distinct invalid inputs across every
public entry point that wraps errors compares Error() text, errors.Is for
every sentinel and errors.As results against the former fmt.Errorf
construction, computed independently from the tokenizer and parser.
M4, GOGC=off -cpu 1, 10 interleaved process pairs:
Parse syntax_mid -28.1%, syntax_end -28.8%, tokenize -62.8%;
Validate syntax_mid -26.9%, syntax_end -29.9%, tokenize -28.3%
(all p <= 0.029); allocs e.g. 21 -> 12, 20 -> 4. long_prefix (128
identifiers before the error) -1% to -3%. Success paths unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Claude finished @ajitpratap0's task in 22s —— View job Review of PR #905: lazy
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 25s —— View job Review of PR #905: lazy
|
Perf gate: PASS (shadow mode: report only, this check never fails)Base
Rules: time regresses at >= +3% with p < 0.01 (Mann-Whitney U), or a suite geomean >= +1.5% (per-round geomeans, same test); allocs/op must not increase; B/op must not rise more than 2%; instructions/op must not rise more than 1.5%. An A/A control with more than 10% of benchmarks moving over 3% is noisy: re-run once, then inconclusive (neutral). Gated suite (GOGC=off, GOMAXPROCS=1)
A/A control: 0 of 26 benchmarks moved more than 3%. Instructions per operation (cmd/perfdriver)
Secondary metric: default GC, GOMAXPROCS=1 (informational)
Environmentbenchstat (primary metric) |
…zed-message gain [W1-03] Add a flipping-context test that kills the two surviving mutants in ParseWithContext (mid-tokenize and mid-parse cancel paths). CHANGELOG now states the gain applies to callers that drop the message, gives the materialized-message numbers, and notes Error() reflects later mutation of the inner error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 31s —— View job Review of PR #905: lazy
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 28s —— View job Review of PR #905: lazy
|
Summary
Wave-1 task W1-03 (MEM-3) from the 2026-11 performance audit, including the review change from CRITIQUE item 14. Codex ported the change, and Opus reviewed it and ran the full gate.
pkg/gosqlxhad 21 call sites usingfmt.Errorf("%w: %w", sentinel, err). Each one built the full message on every error, even though many callers only check the error witherrors.Is. They now return an unexportedwrapErr:[2]errorarray.Error(): builds the same"sentinel: inner"text on demand.Unwrap() []error: returns a slice of that array, soerrors.Isanderrors.Aswalk it without allocating. Same contract as the two-%wfmt.Errorf.Call sites whose message adds extra context text keep
fmt.Errorfas before.Wave 0 is now on
main; this branch has mergedorigin/main, the W0-02 goldens pass, and our privateBenchmarkParseError/BenchmarkParseSuccessfile was dropped in favour of the wave-0BenchmarkParseError/*matrix. A CHANGELOG [Unreleased] entry was added.Test Plan
wrap_compat_test.go,wrap_test.go): 242 distinct invalid inputs across every public entry point that wraps errors. Expected errors are built separately from direct tokenizer and parser calls with the oldfmt.Errorf` construction, so a missing sentinel or a changed prefix can't pass by matching its own output.Error()text is byte-identical.errors.Ismatches exactly the same sentinels.errors.Asfinds the same structured error, with the same code and position.go run honnef.co/go/tools/cmd/staticcheck@latest ./pkg/gosqlx/...go test -race -count=1 ./pkg/gosqlx/...task checktask test:cbindinggo test -count=1 ./pkg/compatibility/...Local A/B (wave-0
BenchmarkParseError/*,task bench:ab BASE=origin/main). Apple M4, shared and heavily loaded machine (load ~14, timing CIs of +-40-60%), so timings are indicative only; B/op and allocs/op are deterministic.CI amd64 perf gate (shadow): PASS, "no gated metric moved past its threshold". The gated suite (ParseE2E, ParseRW, ParseFresh, PoolGetPut, Tokenize) is neutral: geomean -0.13% (p=0.436), no allocs/op or B/op change, instructions/op within +-0.41%, A/A control clean. These are success-path benchmarks, so neutrality is expected and confirms no regression.
Message-materialized case (reviewer re-measurement, n=6,
Parsethenerr.Error(), base origin/main vs PR).BenchmarkParseErrornever callserr.Error(), so the table above mostly reflects never formatting the inner*errors.Error. When the message is built, which the LSP (createDiagnosticFromError) and CLI do, the gain is small: allocs/op syntax_mid 21 -> 19, syntax_end 25 -> 23, tokenize 19 -> 17, long_prefix 229 -> 227; B/op -2.8% to -4.2% (long_prefix not significant); time indistinguishable on the loaded M4. The large gain applies to callers that only checkerrors.Is/errors.Asor drop the message.Behaviour note.
Error()is now built lazily, so it reflects later mutation of the inner*errors.Errorreached viaerrors.As; withfmt.Errorfthe text was fixed at creation. Not a documented contract; no in-repo caller mutates errors. Noted in CHANGELOG.Review follow-up. Added
wrap_ctx_test.go(context whoseErr()flips to Canceled after N calls) which kills the two previously surviving mutants (newWrap(ErrTimeout, ctxErr)inParseWithContextmid-tokenize and mid-parse paths); verified by mutating both lines. Gate re-run on the new head:task check,task test:cbinding,./pkg/compatibility/..., staticcheck on./pkg/gosqlx/...all pass.Still open (merge blocker, outside this PR's files): add
./pkg/gosqlx ^BenchmarkParseError$/^(syntax_mid|tokenize)$/^(parse|validate)$toscripts/perf/gate-suite.txt(W0 follow-up), re-run the gate here, and merge only on a statistically significant amd64 gain.Targets and caveats (reported, not hidden):
BenchmarkParseError/*, so there is NO amd64 CI measurement of this task's target benchmarks. Evidence for the gain is the deterministic allocation drop (B/op -35% to -83% on short inputs, allocs -35% to -79%) plus local arm64 timings. An amd64 run ofBenchmarkParseErroris an open question for the orchestrator (PLAN 5.3 rule 5).ParseError/*<= -12%, tokenize <= -40%): met on syntax_mid, syntax_end and tokenize/parse (-60%);tokenize/validateis -41% locally (earlier run -28%, load-dependent).long_prefixis not met: only -3.4% B/op and -4.4% allocs, and the time change is not significant. Its cost is tokenizing and parsing 128 identifiers, which lazy wrapping cannot move.Breaking Changes
None. The concrete type of the returned error changes from fmt's unexported wrapper to this package's unexported
wrapErr. Message text anderrors.Is/errors.Asbehaviour are identical.🤖 Generated with Claude Code
https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2