Repository navigation
perf(linter): shared cached preorder node list for AST rules [W6-05a] - #906
Conversation
Thirteen AST-based lint rules each walked the whole tree with ast.Inspect, so every lint ran the same traversal 13 times. linter.Context gains Nodes(): every node of every statement in ast.Inspect preorder, built on first use, cached per Context and reset by WithAST. The 13 rules iterate that list. None of their Inspect callbacks returned false, so iterating the flat list visits exactly the nodes in the same order and violations are identical. Part (b) (aliasing and the 4 Visitor rules) and the L022/L029 scoped walks are unchanged. Context.Nodes is public-additive and documents its lifetime: valid for the Context's AST, read-only, not shared across Contexts. Tests: Nodes() order equals ast.Inspect order over the real-world corpora; nil AST/statement handling and cache reset on WithAST; concurrent use of separate Contexts under -race. Before submission, ordered violations from LintWithDialect were verified byte-identical to origin/main 319b55b on 3,740 inputs (all 8 dialects, statements and whole files). M4, GOGC=off -cpu 1, 10 interleaved rounds, LintWithDialect per dialect: -25.5% to -32.6% (geomean -27.9%), allocs -37.3% to -40.1%, B/op -8% to -10%. 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 21s —— View job Review of PR #906: shared cached preorder node listVerdict: looks good. I found no blocking issues. What I checked
Minor suggestions (non-blocking)
Process note: the equivalence golden over 3,740 inputs was a one-off run against the base commit and is not in the repo. Since it is not committed, |
…uard [W6-05a] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 15s —— View job Review of PR #906: shared cached preorder node listVerdict: looks good, no blocking issues. What I checked
Correctness
Tests
Non-blocking suggestions
|
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 32 benchmarks moved more than 3%. Instructions per operation (cmd/perfdriver)
Secondary metric: default GC, GOMAXPROCS=1 (informational)
Environmentbenchstat (primary metric) |
Direct ctx.AST assignment or a by-value Context copy with a new AST can no longer return a stale node list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 21s —— View job Review of PR #906: shared cached preorder node listVerdict: looks good, no blocking issues. The latest commit ( What I checked
Correctness
Non-blocking suggestions
· · branch |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
|
Claude finished @ajitpratap0's task in 21s —— View job Review of PR #906: shared cached preorder node listVerdict: looks good, no blocking issues. What I checked
Correctness
Non-blocking suggestions
|
…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>
Summary
PLAN W6-05(a). Thirteen linter rules each ran their own
ast.Inspectwalk over every statement.linter.Contextnow has an additiveNodes()method that builds the preorder node list once per Context (lazily, cached) and all 13 rules iterate it instead. The list is inast.Inspectpreorder with nil nodes filtered, and every original callback returnedtrueon every path, so rules visit the same nodes in the same order and violations are identical (TestGoldenLint from W0-02 passes with no golden diffs).The cache is keyed on the AST pointer (
nodesAST), so a directctx.AST = otheror a by-value Context copy with a new AST rebuilds the list instead of returning stale nodes;WithASTalso resets it.Why the #566 rejection no longer holds: #566 rejected this as "Linter per-rule AST walks ... plausible but unmeasured; would change public Rule/Context API". The gain is now measured (below: about -26% time, -39% allocs/op, -9% B/op on amd64 CI), and the API change is additive only (a new method; no existing signature changes).
Test Plan
task check,task test:cbinding,go test -count=1 ./pkg/compatibility/...,go run honnef.co/go/tools/cmd/staticcheck@latest ./pkg/linter/...: all pass after merging origin/main (69a5a72, W2-01) and the pointer-keyed cache.TestContextNodesInspectOrder(preorder identity vsast.Inspecton 3,574 parsed inputs, all 8 dialects),TestContextNodesNilAndReplacement,TestContextNodesDirectASTAssignment(direct assignment and by-value copy), concurrent-Contexts test under-race. Mutation checks (pruning children, dropping the reset, top-level-only iteration) were caught by the tests.Local A/B (M4, shared machine, indicative only):
task bench:ab BASE=origin/main BENCH='./pkg/linter=^BenchmarkLintRW$'(10 rounds, 200ms)All p=0.000, n=10.
CI perf gate, PR run (shadow, amd64): passed, 0 of 26 benchmarks moved, geomean +0.13%, allocs/op and B/op unchanged. Stated plainly: the gate suite (
scripts/perf/gate-suite.txt) has no lint benchmark, so this run does NOT measure the target.CI perf gate with the target included (workflow_dispatch run 41332537014, amd64, base origin/main vs a throwaway branch = this PR + one extra suite line
./pkg/linter ^BenchmarkLintRW$; the branch is deleted and the suite line is not part of this PR):No other gated benchmark regressed in that run (time geomean +0.07% over the full suite). Meets the PLAN W6-05(a) target of <= -20%. Adding
BenchmarkLintRWto the permanent gate suite is left for a separate PR.Breaking Changes
None.
Context.Nodes()is additive.Contextgains three unexported fields (nodes,nodesAST,nodesBuilt); the exported fields and theRuleinterface are unchanged. Violations are identical for every corpus input.🤖 Generated with Claude Code
https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2