Repository navigation
perf(gosqlx): build wrapped error messages lazily [W1-03] - #905
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.
|
|
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: REGRESSION (shadow mode: report only, this check never fails)Base Runner CPU:
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: 4 of 50 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
|
#896 (L007), #902 (L010) and #905 (lazy error wrap) target benchmarks the PR-run gate never measured, so none of them could be accepted or closed on an amd64 verdict. long_prefix is included so #905's expected miss is still checked for regressions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
…w suite (#927) * ci(perf): add BenchmarkLintRW to the shadow gate suite PR #906's gate run moved 0 of 26 benchmarks because no linter benchmark was gated. Add it so W6-05b, W6-06 and W6-07 are measured by the PR-run gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2 * ci(perf): gate LintRule L007/L010 and ParseError in the shadow suite #896 (L007), #902 (L010) and #905 (lazy error wrap) target benchmarks the PR-run gate never measured, so none of them could be accepted or closed on an amd64 verdict. long_prefix is included so #905's expected miss is still checked for regressions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Resolve CHANGELOG (and gate-suite) conflicts by placing this PR's entries in main's Unreleased sections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 17s —— View job Review of PR #905: lazy
|
|
Claude finished @ajitpratap0's task in 15s —— 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.Errorfconstruction, 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.amd64 CI perf gate on the target benchmarks (shadow): PASS. The ParseError matrix is now in
scripts/perf/gate-suite.txt(#927). Base5b0818f8, head47f43382, AMD EPYC 7763, 10 interleaved rounds; A/A control: 0 of 50 benchmarks moved more than 3%.All p < 0.01; suite geomean -6.1% (p < 0.001), ParseError geomean -9.6%. The default-GC secondary run agrees (syntax_mid -30.9%, tokenize/parse -63.6%). The success-path suite stays neutral (instructions/op and allocs unchanged).
Targets and caveats (reported, not hidden):
ParseError/*<= -12%, tokenize <= -40%): met on syntax_mid, syntax_end and tokenize/parse (-60%);tokenize/validateis -34.5% on amd64 CI (-41% locally).long_prefixis not met: only -3.4% B/op and -4.4% allocs, and time -3.7% (parse) / -0.9% (validate) on amd64. Its cost is tokenizing and parsing 128 identifiers, which lazy wrapping cannot move.ParseWithTimeout(pkg/gosqlx/gosqlx.go:258,newWrap(ErrTimeout, ctxErr)when the deadline expires after a successful parse) is not exercised by a test. It only fires when the deadline elapses during a parse that still succeeds, and its wrap is identical to the covered mid-parse paths.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