Repository navigation
perf(ast): stop heap-copying nodes in Children() so Walk/Inspect allocate less (#557) - #588
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @ajitpratap0's task in 24s —— View job Review of PR #588 (perf(ast): stop heap-copying nodes in Children())I read the non-test diff in Overall: The change looks correct and well scoped. Replacing
Observations
TestsThe golden-order test, the Walk-versus-Children equivalence test and the mutation regression test are the right safeguards. The golden was generated from the parent commit, which is a good way to prove the visit order is unchanged. Security: No concerns. Verdict: Looks good to merge once the |
7b173af to
87c84da
Compare
|
Base PR #581 has merged, so this branch was rebased onto |
|
Claude finished @ajitpratap0's task in 17s —— View job Review of PR #588: stop heap-copying nodes in
|
|
Closing and reopening to trigger CI on the rebased branch against |
|
Claude finished @ajitpratap0's task in 18s —— View job Review of PR #588: stop heap-copying nodes in
|
|
Windows CI failure was a line-ending issue: the runner checks |
|
Claude finished @ajitpratap0's task in 15s —— View job Review of PR #588: stop heap-copying nodes in
|
Git on the windows-latest CI runner has core.autocrlf=true, so without a .gitattributes it checks text files out with CRLF. Byte-exact golden comparisons and content-hashed fixtures then pass on macOS/Linux and fail only on Windows: PR #588 hit this with pkg/sql/ast/testdata/visit_order.golden, and the #548 tokenizer golden corpus hashes 40 corpus .sql files. Set `* text=auto eol=lf` repo-wide, pin Go sources, module files, fixtures and shell scripts to LF explicitly, and mark image/font/wasm/archive assets binary. No tracked file currently has CRLF endings (`git ls-files --eol` reports none) and the repo has no Windows scripts, so `git add --renormalize .` changes nothing. Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve Co-authored-by: Ajit Pratap Singh <ajitpratapsingh@Ajits-Mac-mini-2655.local> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…cate less (#557) Children() used to copy every value-typed child (TableReference, JoinClause, WindowSpec, OrderByExpression, WhenClause, UpdateExpression, ...) to the heap through the `x := x; append(children, &x)` pattern. It also built unsized slices through temporary nodifyExpressions slices. Every Walk/Inspect user paid for this: linter, security scanner, metadata extraction, fingerprint/normalize and transpiler. Visitors that mutated those nodes changed copies, not the AST. - Take addresses by index (&s.From[i], &s.Joins[i], &f.OrderBy[i], ...) in every Children(), removing all G601 copy sites outside sql.go rendering. - Presize the result and append expressions directly in SelectStatement, InsertStatement, WindowSpec, FunctionCall, CaseExpression and MergeStatement. Each now makes exactly one allocation. - Add unexported pointer-receiver childrenInPlace on *JoinClause, *WindowFrame and *MergeStatement, and an unexported childrenOf that Walk uses. Walk now visits Left/Right, Start and TargetTable/SourceTable in place without copying the receiver. The exported value-receiver Children()/TokenLiteral are unchanged in signature and output, so no method set changes. - Document the aliasing and lifetime contract on Node and Walk. Behaviour change (intended): visitor mutations of value-typed children now reach the AST, and those pointers share the AST's lifetime. Measured (benchstat, -count=6, allocs/op and B/op): Inspect/select_join_window 47 -> 26 allocs, 3216 -> 928 B Inspect/insert_multirow 16 -> 1 allocs, 2144 -> 576 B Fingerprint 182 -> 170 allocs, 20.8 -> 19.0 KiB Normalize 178 -> 166 allocs Lint/Realistic 728 -> 580 allocs, 56.8 -> 36.6 KiB ExtractMetadataLarge 587 -> 466 allocs, 49.6 -> 31.8 KiB Scan*, Transpile unchanged Tests: a visit-order golden generated from the pre-change code, a Walk-vs-Children equivalence check, pointer-identity tests for every rewritten Children(), childrenOf/Children() agreement, one-alloc budgets, visitor-mutation regression tests checked through SQL() and the formatter, and a concurrent pooled-tree Inspect test under -race. New BenchmarkInspect, BenchmarkFingerprint and BenchmarkNormalize. Fixes #557 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
On the windows-latest CI runner git checks testdata/visit_order.golden out with CRLF line endings (core.autocrlf), so TestWalk_VisitOrderGolden failed with "got" and "want" lines that print identically. Normalize CRLF to LF when reading the golden; the test is about visit order, not line endings. Verified locally against a CRLF-converted copy of the file. Part of #557 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
…ing with a reflection test (#557 review follow-up) Pre-merge review found that Insert, Delete and Update still had value-receiver Children() methods returning &x.Table, a pointer into the receiver copy, so visitor mutations of Table during Walk/Inspect were silently lost. Give each a pointer-receiver childrenInPlace and route them through childrenOf, as already done for JoinClause, WindowFrame and MergeStatement. The exported value-receiver Children() is unchanged (on a value it still returns copies). TestChildrenOf_InPlaceFastPath covers the three new cases. Add TestWalk_ChildPointersAliasParentStorage, which reflectively fills every listed Node type and asserts each child pointer Walk yields lies inside the parent's storage. It reported exactly Insert/Delete/Update before this change and nothing after, and fails if a future value-receiver node returns &field outside childrenOf. Part of #557 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
6081b9b to
aa8cbea
Compare
|
Claude finished @ajitpratap0's task in 13s —— View job Review of PR #588: stop heap-copying nodes in
|
…resh performance baselines (#566) CHANGELOG [Unreleased] now covers every PR merged since v1.14.0, with a Behavior changes section for the observable changes in #597, #595, #584, #585, #588, #581, #582/#570, #517/#518 and #511, plus Added, Changed, Fixed, Performance, Security and Internal entries. The two existing entries (#526 and the tokenizer-pool dialect fix) are kept verbatim. performance_baselines.json is re-measured on an Apple M4 with BenchmarkPerformanceBaseline -count=10 -benchmem (median). Baselines are set at about 2.5x the M4 median rather than 1.5x: the CI Benchmark job on ubuntu-latest measures these queries about 1.5-3.2x slower than M4 (last 9 Tests runs on main, 0715bb9..61ca6c8), so 1.5x with the unchanged tolerances would fail CI's slowest runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve
…xes; refresh performance baselines (#566) (#598) * docs: consolidate CHANGELOG for the perf audit and batch-2 fixes; refresh performance baselines (#566) CHANGELOG [Unreleased] now covers every PR merged since v1.14.0, with a Behavior changes section for the observable changes in #597, #595, #584, #585, #588, #581, #582/#570, #517/#518 and #511, plus Added, Changed, Fixed, Performance, Security and Internal entries. The two existing entries (#526 and the tokenizer-pool dialect fix) are kept verbatim. performance_baselines.json is re-measured on an Apple M4 with BenchmarkPerformanceBaseline -count=10 -benchmem (median). Baselines are set at about 2.5x the M4 median rather than 1.5x: the CI Benchmark job on ubuntu-latest measures these queries about 1.5-3.2x slower than M4 (last 9 Tests runs on main, 0715bb9..61ca6c8), so 1.5x with the unchanged tolerances would fail CI's slowest runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * docs(changelog): add batch-3 follow-up fixes (#599-#620) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * docs(changelog): record #632 render-injection hardening and #624 quote guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * docs(changelog): correct batch-3 entries after review; record measured costs - #628: SQLite still rejects count(*) [cnt]; only SQL Server keeps it. - #631: bare FORMAT / FORMAT <name> rules apply after SETTINGS. - Performance: record the measured costs of the batch-3 correctness fixes (Identifier and TableReference growth, quoting in SQL(), lineStarts reallocation, sorted Extract results). - #632: note the lıke, WITH CHECK OPTION and MINUS rendering changes. - Fix the [Unreleased] compare link (v1.14.0...HEAD) and add [1.14.0]. - Keep each section a single tight list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve * docs(changelog): describe the release as source-compatible with runtime behaviour changes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
#557 perf(ast): stop heap-copying nodes in Children() so Walk/Inspect allocate less
Children() in pkg/sql/ast no longer copies each value-typed child to the heap (
x := x; &x). It now returns pointers into the parent's slices (&s.From[i], &s.Joins[i], &f.OrderBy[i], ...) and sizes the result slice up front. Walk and Inspect use an unexported childrenOf, so JoinClause, WindowFrame and MergeStatement are visited in place, and the exported method signatures stay unchanged. Measured with benchstat (-count=6, p=0.002):Visit order is unchanged: a golden generated from the parent commit passes on both parent and HEAD, and a Walk-vs-Children equivalence test passes. One intended behaviour change: a visitor that mutates value-typed children (table references, ORDER BY items, WHEN clauses, update assignments, frame bounds, MERGE target/source) now changes the AST, and the change shows up in SQL() and the formatter. The new regression test fails on the parent and passes on HEAD. The full race gate (uncached), go vet and golangci-lint all pass.
Commits (19), one per issue:
146 files changed, 13989 insertions(+), 4969 deletions(-)Test Plan
CGO_ENABLED=1 go test -race -timeout 120s $(go list ./... | grep -v /cbinding)),go vet, andgolangci-linton this branch.benchstat -count=6before/after on the same machine; allocs/op and B/op are the deterministic signal, ns/op is directional on a loaded machine.#557 benchstat
#557 race gate: PASS. Ran
CGO_ENABLED=1 go test -race -timeout 120s $(go list ./... | grep -v /cbinding)twice. The first run was served from cache. The second used -count=1 so every test actually ran, and it exited 0: 44 packages ok, 18 with no test files, no FAIL, no DATA RACE, no panic. Logs are in /private/tmp/claude-501/-Users-ajitpratapsingh-dev-GoSQLX/b99cd3cf-cb92-4294-a0db-d6a7c9a86253/scratchpad/b2/557/val/race_nocache.txt.Breaking Changes
The issue's step 2 (pointer receivers) is deliberately declined. It would drop Node from the value method sets, a compile-time break for external callers such as
ast.Walk(v, sel.Joins[i]), and the ground rules call for non-breaking options; the in-place fast path in Walk gets the same allocation win.Observable behaviour change, which is intended and fixes the latent bug the issue describes:
The CHANGELOG entry (Performance, plus a behaviour note) is to be written during the later consolidation step, not on this branch.
Notes
perf/ast-pool-release-fixes.Fixes #557
Part of #566
🤖 Generated with Claude Code
https://claude.ai/code/session_01UZSk1292jPc8ipDboNxeve