Skip to content

perf(fingerprint,formatter,transform): tokenize into the pooled scratch buffer [W5-01] - #903

Merged
ajitpratap0 merged 8 commits into
mainfrom
perf/w5-01-tokscratch-consumers
Nov 1, 2026
Merged

ajitpratap0 merged 8 commits into
mainfrom
perf/w5-01-tokscratch-consumers

Conversation

@ajitpratap0

@ajitpratap0 ajitpratap0 commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Wave-1 task W5-01 from the 2026-11 performance audit (TOK-SCRATCH). The prototype was confirmed by an independent verifier. Codex ported it; Opus reviewed it and ran the full gate.

Five call sites used the public TokenizeString, which copies every token into a fresh slice that the parser then copies again:

  • fingerprint.Normalize (also used by Fingerprint)
  • Formatter.Format
  • transform.parseSQL, transform.ParseSQL and transform.ParseSQLWithDialect

They now call internal/tokscratch.String, the path gosqlx.Parse already uses. The tokens stay in the pooled tokenizer's buffer for the duration of the call.

Why this is safe. The tokscratch contract says the slice and its Word values belong to the tokenizer until it is reused, while token strings, being substrings of the input, stay valid. Each caller follows that contract:

  • It finishes with the tokens before the deferred PutTokenizer. In Format, that includes formatASTCheckedWithTokens.
  • The parser's preprocess step copies what the AST needs.
  • No caller returns the slice.

Outputs are byte-identical, and no exported API changes.

Status: ready for review. The wave-0 behaviour goldens (W0-02) and the perf gate (W0-09) are on main; this branch has merged origin/main (cda05b2).

Test Plan

  • Differential tests: Normalize, Fingerprint, Format and the transform parse helpers give identical output through the old public-tokenizer path and the new path, over every statement in testdata/realworld/* and testdata/corpus/*.
  • Concurrency: 8+ goroutines over the corpus under -race, with results checked against a sequential run.
  • Pool reuse: returned strings, ASTs, conditions and joins stay correct after later calls reuse the pooled buffer.
  • Gate, run outside the Codex sandbox:
Check Result
go run honnef.co/go/tools/cmd/staticcheck@latest on the three packages ✅
task check ✅
task test:cbinding ✅
go test -count=1 ./pkg/compatibility/... ✅

A/B setup (re-measured after merging wave 0, 2026-11-01): task bench:ab-equivalent (scripts/perf/ab.sh -r 10 -t 200ms) against origin/main 962b458, Apple M4, GOGC=off, -cpu 1, 10 interleaved ABBA rounds, wave-0 benchmark names, benchstat alpha 0.01. Shared machine (load ~5-7), so indicative only; the amd64 CI gate below is authoritative.

Benchmark time B/op allocs/op
Normalize/* (11) geomean -4.3% (simple_select -11.1%, p=0.002; rest n.s.) -42% to -70% (geomean -55.4%) -2 to -3 each
TransformParse/* (11) -2.7% to -15.3%, 6 significant -49% to -78% -2 to -3 each
Render/* (control, untouched) within noise +0.00% unchanged

Plan targets: Normalize B/op <= -40% (met), TransformParse B/op <= -45% (met). Time is not worse anywhere.

Merge notes. Merged origin/main into the branch. One conflict: pkg/fingerprint/fingerprint_bench_test.go, resolved by taking main's wave-0 benchmark matrix (BenchmarkNormalize, BenchmarkFingerprint, BenchmarkNormalizeScale over the shared benchcorpus). The PR's own pkg/transform/tokscratch_bench_test.go duplicated wave-0's BenchmarkTransformParse and was removed. No golden changed. Added the CHANGELOG entry under ## [Unreleased] / ### Performance (moved there from the released 1.17.0 section after review).

Gate on the merged head: task check, task test:cbinding, go test -count=1 ./pkg/compatibility/... and staticcheck on the changed packages all pass.

CI perf gate (amd64, shadow mode): PENDING on head d36d27d. This push adds Normalize and TransformParse (simple_select, join, complex) to scripts/perf/gate-suite.txt (ab.sh reads the suite from the head checkout), so the gate now measures this PR's targets. The gate table and verdict will be pasted here once the run completes. The earlier PASS (merge ref 743f682) did not cover these benchmarks.

Review-fix additions (d36d27d): gate-suite lines for the targets; TestCrossConsumerSharedTokenizerPool (gosqlx.Parse, Formatter.Format, fingerprint.Normalize, transform.ParseSQL in parallel on the shared tokenizer pool, -race); CHANGELOG wording. Re-ran on this head: task check, task test:cbinding, go test -count=1 ./pkg/compatibility/..., staticcheck on fingerprint/formatter/transform: all pass.

Breaking Changes

None.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2

…ch buffer [W5-01]

Normalize/Fingerprint, Formatter.Format and transform's parse helpers
called the public TokenizeString, which copies every token into a fresh
slice that the parser immediately copies again. They now use
internal/tokscratch, the path gosqlx.Parse already uses: tokens live in
the pooled tokenizer's buffer for the duration of the call. Each caller
finishes with the tokens before the deferred PutTokenizer, the parser
copies what the AST needs, and token strings are substrings of the input,
so nothing borrowed outlives the call. Outputs are byte-identical.

Tests: differential tests against the public tokenizer path over every
statement in the real-world corpora; concurrency tests (8+ goroutines,
-race) checked against a sequential run; pool-reuse tests that returned
strings, ASTs, conditions and joins stay correct after later calls reuse
the buffer.

M4, GOGC=off -cpu 1, 10 interleaved rounds, the 11 compare queries:
Normalize B/op -42% to -70%, Fingerprint -40% to -65%, TransformParse
-49% to -78% (geomean -54% / -63%), allocs -5% to -29%. Time: no
regression; the shared machine was too noisy for a time claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
gosqlx-website Ready Ready Preview Nov 1, 2026 6:20pm UTC

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 19s —— View job


Review of PR #903: tokenize into the pooled scratch buffer

Verdict: looks good. I have no blocking issues.

I read the five production call-site changes and the tokscratch contract. I did not run the test suite in this session. The test and benchmark files (about 800 added lines) I only sampled, and I did not check the benchmark numbers.

Safety of the swap

  • Lifetime: In every site, the tokens are only used before the deferred PutTokenizer. This holds in fingerprint.Normalize, Formatter.Format, transform.parseSQL, ParseSQL and ParseSQLWithDialect. Defers run last-in-first-out, so the parser and AST are finished before the tokenizer goes back to the pool.
  • Comments in Format: tkz.Comments is copied into parsedAST.Comments before the tokenizer is released, so it is not left pointing at pooled memory.
  • Oracle path in Format: parser.ParseWithDialect(sql, dialect) re-tokenizes on its own, so it doesn't depend on the scratch slice.
  • No escape: No caller returns or stores tokens. The parser's preprocess step copies what the AST needs, as gosqlx.Parse already relies on.
  • Error paths: These return before any token use, so nothing else changes.
  • Public API: No exported signatures change.

Suggestions (non-blocking)

  1. Guard the contract: Each call site now relies on a rule written only in the tokscratch doc comment. A short comment at each call site, such as "tokens are tokenizer-owned; do not retain past PutTokenizer", would help the next editor who adds code between tokenizing and the defer.
  2. Hidden dependency: The tokscratch panic message is "tokenizer not linked". Each of the three packages imports tokenizer directly, so the registration init is guaranteed to run. This is fine now, but a future refactor that removes that import would only fail at runtime.
  3. Benchmarks: The PR says time gains are within noise on the shared machine. That is honest, and the allocation reductions are well supported. The amd64 perf gate should confirm the time numbers before merge, as the PR notes.
  4. Rollout: The PR is marked as a draft that waits on W0-02 and W0-09. That ordering seems right, and I'd keep it.

Tests: The differential tests against the old path over the realworld and corpus data, the -race concurrency test, and the pool-reuse test cover the main risks, which are aliasing and stale data. I'd like to see one more case. It would call Format or transform.ParseSQL with a dialect, such as MySQL or Oracle, between runs on the same pooled tokenizer. That would confirm the scratch buffer doesn't carry dialect-specific state across SetDialect calls. The PR may already cover this in the corpus tests, which I didn't read in full.

The change is small and mechanical. The risk is limited to the lifetime rule above, and the call sites follow it.
· branch perf/w5-01-tokscratch-consumers

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 23s —— View job


Review of PR #903: tokenize into the pooled scratch buffer

Verdict: looks good. I found no blocking issues.

I read the diff for the three production files and the internal/tokscratch contract. I did not run the tests or benchmarks in this session, and I only skimmed the test files.

Production changes

  • The change is five one-line swaps from tkz.TokenizeString to tokscratch.String(tkz, ...): fingerprint.Normalize, Formatter.Format, transform.parseSQL, ParseSQL and ParseSQLWithDialect. No exported signatures change.
  • In each function, defer tokenizer.PutTokenizer(tkz) is registered before tokenizing. The tokens are only used inside the function, so the lifetime contract holds. Defers run last-in-first-out, so parsing finishes before the tokenizer returns to the pool.
  • Nothing returns or stores the slice. The parser's preprocess step copies what the AST needs, which is the same path gosqlx.Parse already uses.
  • tokscratch.String returns the same tokens and errors as the public method, so the error paths are unchanged.

Suggestions (non-blocking)

  1. Call-site comments: The ownership rule lives only in the tokscratch package doc. A one-line comment at each call site would help, for example "tokens are tokenizer-owned; do not retain past PutTokenizer". Someone adding code between tokenizing and the defer could otherwise break the rule without noticing.
  2. Hidden dependency: tokscratch panics with "tokenizer not linked" if the tokenizer package's init hasn't run. All three packages import tokenizer directly, so this is safe now. If a later refactor drops that import, it would fail at runtime rather than at compile time.
  3. Dialect coverage: I'd like a test that alternates dialects (for example MySQL, then Oracle, then the default) through Formatter.Format and transform.ParseSQLWithDialect on pooled tokenizers. It would confirm that scratch-buffer reuse leaks no dialect state. The corpus tests may already cover this, but I didn't check.
  4. Perf evidence: The allocation (B/op) reductions are well supported. The time gains were within noise on the shared machine, as the PR says. The amd64 perf gate should confirm them before merge.
  5. Merge order: Keep the wave-0 ordering (W0-02 goldens and W0-09 perf gate before this merges).

The differential, -race concurrency and pool-reuse tests target the main risks, which are aliasing and stale data.
· branch perf/w5-01-tokscratch-consumers

@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Perf gate: PASS (shadow mode: report only, this check never fails)

Base 11f55dd7edfd, head 63349247d2d0. 10 interleaved rounds, benchtime 200ms, -cpu 1, pinned to CPU 3 (SMT siblings 2-3) with taskset.

  • 6 benchmark(s) faster by 3% or more

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)

benchmark time p A/A B/op allocs/op status
ParseE2E/simple_select -0.13% 0.545 -0.16% +0.00% 5 → 5 ok
ParseE2E/select_where +0.23% 0.791 +0.00% +0.00% 18 → 18 ok
ParseE2E/join -0.25% 0.161 -0.02% +0.00% 17 → 17 ok
ParseE2E/aggregate -0.19% 0.762 -0.27% +0.00% 18 → 18 ok
ParseE2E/subquery -0.17% 0.734 +0.18% +0.00% 14 → 14 ok
ParseE2E/cte -0.60% 0.069 +0.09% +0.00% 21 → 21 ok
ParseE2E/window -0.07% 0.544 -0.10% +0.00% 11 → 11 ok
ParseE2E/complex +0.29% 0.623 +0.06% +0.00% 50 → 50 ok
ParseE2E/insert +0.50% 0.064 +0.26% +0.00% 17 → 17 ok
ParseE2E/update +0.21% 0.940 +0.11% +0.00% 13 → 13 ok
ParseE2E/delete -0.35% 0.596 -0.31% +0.00% 5 → 5 ok
ParseRW/postgresql/medium +0.11% 0.739 +0.76% +0.00% 646 → 646 ok
ParseRW/mysql/medium +0.17% 0.684 +0.02% +0.00% 365 → 365 ok
ParseRW/mariadb/medium +0.22% 0.436 +0.36% +0.00% 523 → 523 ok
ParseRW/sqlserver/medium +0.36% 0.218 -0.23% +0.00% 936 → 936 ok
ParseRW/oracle/medium +0.40% 0.190 +0.15% +0.00% 1093 → 1093 ok
ParseRW/sqlite/medium +0.51% 0.123 +0.55% +0.00% 346 → 346 ok
ParseRW/snowflake/medium -0.03% 0.684 -0.28% +0.00% 777 → 777 ok
ParseRW/clickhouse/medium +0.06% 0.912 +0.13% +0.00% 384 → 384 ok
ParseFresh/simple_select +0.31% 0.880 +0.44% +0.00% 6 → 6 ok
ParseFresh/join -0.26% 0.529 +0.09% +0.00% 18 → 18 ok
ParseFresh/complex -0.15% 1.000 +0.33% +0.00% 51 → 51 ok
PoolGetPut/serial +0.39% 0.496 +0.81% +0.00% 0 → 0 ok
Tokenize/scratch/join -0.14% 0.121 +0.18% +0.00% 0 → 0 ok
Tokenize/scratch/complex +0.12% 0.571 +0.68% +0.00% 0 → 0 ok
Tokenize/scratch/insert -0.04% 0.762 +0.41% +0.00% 0 → 0 ok
Normalize/simple_select -20.88% <0.001 -0.08% -58.06% 12 → 10 faster, -allocs
Normalize/join -10.13% <0.001 +0.07% -54.51% 42 → 40 faster, -allocs
Normalize/complex -7.46% <0.001 +0.14% -56.58% 113 → 110 faster, -allocs
TransformParse/simple_select -31.32% <0.001 -0.20% -63.83% 7 → 5 faster, -allocs
TransformParse/join -12.45% <0.001 -0.09% -62.71% 19 → 17 faster, -allocs
TransformParse/complex -13.42% <0.001 -0.11% -64.56% 53 → 50 faster, -allocs
geomean -3.25% <0.001 +0.12%

A/A control: 0 of 32 benchmarks moved more than 3%.

Instructions per operation (cmd/perfdriver)

case instructions/op status
e2e/aggregate -0.02% ok
e2e/complex -0.03% ok
e2e/cte -0.00% ok
e2e/delete -0.05% ok
e2e/insert +0.02% ok
e2e/join +0.04% ok
e2e/select_where +0.02% ok
e2e/simple_select +0.05% ok
e2e/subquery +0.07% ok
e2e/update +0.05% ok
e2e/window -0.04% ok
rw/postgresql/medium +0.04% ok
Secondary metric: default GC, GOMAXPROCS=1 (informational)
benchmark time p
ParseE2E/simple_select +0.53% 0.173
ParseE2E/select_where +0.12% 0.912
ParseE2E/join -0.44% 0.472
ParseE2E/aggregate -0.36% 0.364
ParseE2E/subquery -0.08% 0.325
ParseE2E/cte +0.61% 0.307
ParseE2E/window -0.48% 0.364
ParseE2E/complex +1.04% 0.165
ParseE2E/insert +0.10% 0.393
ParseE2E/update -0.30% 0.344
ParseE2E/delete +0.77% 0.211
ParseRW/postgresql/medium +0.09% 0.631
ParseRW/mysql/medium +0.76% 0.089
ParseRW/mariadb/medium -0.34% 0.247
ParseRW/sqlserver/medium -0.07% 0.684
ParseRW/oracle/medium -0.12% 0.796
ParseRW/sqlite/medium -0.51% 0.353
ParseRW/snowflake/medium -0.62% 0.218
ParseRW/clickhouse/medium -0.06% 0.796
ParseFresh/simple_select -0.03% 0.971
ParseFresh/join +0.35% 0.052
ParseFresh/complex -0.06% 0.631
PoolGetPut/serial +0.17% 0.406
Tokenize/scratch/join -0.14% 0.970
Tokenize/scratch/complex +0.06% 0.520
Tokenize/scratch/insert +0.29% 0.173
Normalize/simple_select -10.12% <0.001
Normalize/join -4.32% <0.001
Normalize/complex -5.88% <0.001
TransformParse/simple_select -18.00% <0.001
TransformParse/join -6.92% <0.001
TransformParse/complex -10.38% <0.001
geomean -1.79%
Environment
date:        2026-11-01T18:23:47Z
base:        11f55dd7edfd832fdf1eeed2c216032cdcd1b2e2
head:        working tree of 63349247d2d0be85331f5e45f092cb9953232c42
go:          go version go1.26.1 linux/amd64
kernel:      Linux 7.0.0-1017-azure x86_64
pinning:     pinned to CPU 3 (SMT siblings 2-3) with taskset
clocksource: tsc
load:        2.58, 0.79, 0.28
instr count: cachegrind
perf probe:  Error: No supported events found. The instructions:u event is not supported. 

Architecture:                            x86_64
CPU(s):                                  4
Model name:                              INTEL(R) XEON(R) PLATINUM 8573C
Thread(s) per core:                      2
Core(s) per socket:                      2
Socket(s):                               1
CPU(s) scaling MHz:                      130%
Hypervisor vendor:                       Microsoft
L1d cache:                               96 KiB (2 instances)
L1i cache:                               64 KiB (2 instances)
L2 cache:                                4 MiB (2 instances)
L3 cache:                                260 MiB (1 instance)
benchstat (primary metric)
goos: linux
goarch: amd64
pkg: github.com/ajitpratap0/GoSQLX/pkg/fingerprint
cpu: INTEL(R) XEON(R) PLATINUM 8573C
                        │    base     │                head                 │
                        │   sec/op    │   sec/op     vs base                │
Normalize/simple_select   3.199µ ± 1%   2.531µ ± 1%  -20.88% (p=0.000 n=10)
Normalize/join            9.771µ ± 1%   8.781µ ± 1%  -10.13% (p=0.000 n=10)
Normalize/complex         24.11µ ± 1%   22.31µ ± 1%   -7.46% (p=0.000 n=10)
geomean                   9.100µ        7.915µ       -13.02%

                        │     base      │                 head                 │
                        │     B/op      │     B/op      vs base                │
Normalize/simple_select    2.422Ki ± 0%   1.016Ki ± 0%  -58.06% (p=0.000 n=10)
Normalize/join             7.109Ki ± 0%   3.234Ki ± 0%  -54.51% (p=0.000 n=10)
Normalize/complex         20.602Ki ± 0%   8.945Ki ± 0%  -56.58% (p=0.000 n=10)
geomean                    7.079Ki        3.086Ki       -56.41%

                        │    base    │                head                │
                        │ allocs/op  │ allocs/op   vs base                │
Normalize/simple_select   12.00 ± 0%   10.00 ± 0%  -16.67% (p=0.000 n=10)
Normalize/join            42.00 ± 0%   40.00 ± 0%   -4.76% (p=0.000 n=10)
Normalize/complex         113.0 ± 0%   110.0 ± 0%   -2.65% (p=0.000 n=10)
geomean                   38.47        35.30        -8.24%

pkg: github.com/ajitpratap0/GoSQLX/pkg/gosqlx
                          │    base     │                head                │
                          │   sec/op    │   sec/op     vs base               │
ParseE2E/simple_select      1.580µ ± 1%   1.578µ ± 1%       ~ (p=0.540 n=10)
ParseE2E/select_where       3.501µ ± 1%   3.509µ ± 1%       ~ (p=0.782 n=10)
ParseE2E/join               5.327µ ± 0%   5.313µ ± 0%       ~ (p=0.159 n=10)
ParseE2E/aggregate          3.939µ ± 1%   3.932µ ± 1%       ~ (p=0.755 n=10)
ParseE2E/subquery           3.298µ ± 0%   3.292µ ± 1%       ~ (p=0.724 n=10)
ParseE2E/cte                4.670µ ± 1%   4.642µ ± 1%       ~ (p=0.066 n=10)
ParseE2E/window             2.965µ ± 1%   2.963µ ± 2%       ~ (p=0.540 n=10)
ParseE2E/complex            13.96µ ± 0%   14.00µ ± 1%       ~ (p=0.617 n=10)
ParseE2E/insert             2.487µ ± 1%   2.499µ ± 0%       ~ (p=0.060 n=10)
ParseE2E/update             2.183µ ± 1%   2.187µ ± 1%       ~ (p=0.928 n=10)
ParseE2E/delete             1.292µ ± 2%   1.288µ ± 1%       ~ (p=0.591 n=10)
ParseRW/postgresql/medium   159.9µ ± 1%   160.0µ ± 1%       ~ (p=0.739 n=10)
ParseRW/mysql/medium        89.68µ ± 1%   89.83µ ± 1%       ~ (p=0.684 n=10)
ParseRW/mariadb/medium      132.2µ ± 1%   132.5µ ± 0%       ~ (p=0.436 n=10)
ParseRW/sqlserver/medium    288.9µ ± 0%   289.9µ ± 4%       ~ (p=0.218 n=10)
ParseRW/oracle/medium       350.6µ ± 1%   352.0µ ± 1%       ~ (p=0.190 n=10)
ParseRW/sqlite/medium       77.31µ ± 1%   77.70µ ± 1%       ~ (p=0.123 n=10)
ParseRW/snowflake/medium    197.4µ ± 1%   197.4µ ± 1%       ~ (p=0.684 n=10)
ParseRW/clickhouse/medium   87.24µ ± 2%   87.30µ ± 1%       ~ (p=0.912 n=10)
ParseFresh/simple_select    9.344µ ± 2%   9.373µ ± 1%       ~ (p=0.868 n=10)
ParseFresh/join             21.31µ ± 0%   21.26µ ± 1%       ~ (p=0.529 n=10)
ParseFresh/complex          31.84µ ± 1%   31.79µ ± 1%       ~ (p=0.986 n=10)
PoolGetPut/serial           94.71n ± 1%   95.08n ± 1%       ~ (p=0.492 n=10)
geomean                     13.37µ        13.38µ       +0.07%

                          │     base     │                head                 │
                          │     B/s      │     B/s       vs base               │
ParseE2E/simple_select      15.70Mi ± 1%   15.71Mi ± 1%       ~ (p=0.565 n=10)
ParseE2E/select_where       28.06Mi ± 1%   28.00Mi ± 1%       ~ (p=0.810 n=10)
ParseE2E/join               22.56Mi ± 0%   22.62Mi ± 0%       ~ (p=0.148 n=10)
ParseE2E/aggregate          20.34Mi ± 1%   20.38Mi ± 1%       ~ (p=0.753 n=10)
ParseE2E/subquery           23.71Mi ± 0%   23.76Mi ± 1%       ~ (p=0.724 n=10)
ParseE2E/cte                25.32Mi ± 1%   25.48Mi ± 1%       ~ (p=0.098 n=10)
ParseE2E/window             30.24Mi ± 1%   30.25Mi ± 1%       ~ (p=0.565 n=10)
ParseE2E/complex            25.82Mi ± 0%   25.74Mi ± 1%       ~ (p=0.643 n=10)
ParseE2E/insert             29.15Mi ± 1%   29.01Mi ± 0%       ~ (p=0.060 n=10)
ParseE2E/update             24.03Mi ± 1%   23.98Mi ± 1%       ~ (p=0.897 n=10)
ParseE2E/delete             38.39Mi ± 2%   38.52Mi ± 1%       ~ (p=0.631 n=10)
ParseRW/postgresql/medium   22.44Mi ± 1%   22.42Mi ± 1%       ~ (p=0.696 n=10)
ParseRW/mysql/medium        30.18Mi ± 1%   30.13Mi ± 1%       ~ (p=0.725 n=10)
ParseRW/mariadb/medium      26.83Mi ± 1%   26.77Mi ± 0%       ~ (p=0.425 n=10)
ParseRW/sqlserver/medium    23.03Mi ± 0%   22.95Mi ± 4%       ~ (p=0.182 n=10)
ParseRW/oracle/medium       23.91Mi ± 1%   23.81Mi ± 1%       ~ (p=0.171 n=10)
ParseRW/sqlite/medium       26.72Mi ± 1%   26.59Mi ± 1%       ~ (p=0.118 n=10)
ParseRW/snowflake/medium    25.75Mi ± 1%   25.76Mi ± 1%       ~ (p=0.697 n=10)
ParseRW/clickhouse/medium   25.83Mi ± 2%   25.81Mi ± 1%       ~ (p=0.870 n=10)
geomean                     25.29Mi        25.27Mi       -0.07%

                          │      base      │                 head                  │
                          │      B/op      │     B/op      vs base                 │
ParseE2E/simple_select        816.0 ± 0%       816.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/select_where       1.523Ki ± 0%     1.523Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/join               2.305Ki ± 0%     2.305Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/aggregate          1.812Ki ± 0%     1.812Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/subquery           1.828Ki ± 0%     1.828Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/cte                2.336Ki ± 0%     2.336Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/window             1.281Ki ± 0%     1.281Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/complex            6.399Ki ± 0%     6.399Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/insert               824.0 ± 0%       824.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/update               736.0 ± 0%       736.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/delete               400.0 ± 0%       400.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/postgresql/medium   70.51Ki ± 0%     70.51Ki ± 0%       ~ (p=0.300 n=10)
ParseRW/mysql/medium        43.73Ki ± 0%     43.73Ki ± 0%       ~ (p=1.000 n=10)
ParseRW/mariadb/medium      64.84Ki ± 0%     64.84Ki ± 0%       ~ (p=1.000 n=10)
ParseRW/sqlserver/medium    90.60Ki ± 0%     90.60Ki ± 0%       ~ (p=0.719 n=10)
ParseRW/oracle/medium       113.8Ki ± 0%     113.8Ki ± 0%       ~ (p=0.489 n=10)
ParseRW/sqlite/medium       39.89Ki ± 0%     39.89Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/snowflake/medium    83.38Ki ± 0%     83.38Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/clickhouse/medium   37.13Ki ± 0%     37.13Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/simple_select      896.0 ± 0%       896.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/join             2.383Ki ± 0%     2.383Ki ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/complex          6.477Ki ± 0%     6.477Ki ± 0%       ~ (p=1.000 n=10) ¹
PoolGetPut/serial             0.000 ± 0%       0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                  ²                 +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                          │     base      │                 head                 │
                          │   allocs/op   │  allocs/op   vs base                 │
ParseE2E/simple_select       5.000 ± 0%      5.000 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/select_where        18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/join                17.00 ± 0%      17.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/aggregate           18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/subquery            14.00 ± 0%      14.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/cte                 21.00 ± 0%      21.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/window              11.00 ± 0%      11.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/complex             50.00 ± 0%      50.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/insert              17.00 ± 0%      17.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/update              13.00 ± 0%      13.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseE2E/delete              5.000 ± 0%      5.000 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/postgresql/medium    646.0 ± 0%      646.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/mysql/medium         365.0 ± 0%      365.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/mariadb/medium       523.0 ± 0%      523.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/sqlserver/medium     936.0 ± 0%      936.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/oracle/medium       1.093k ± 0%     1.093k ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/sqlite/medium        346.0 ± 0%      346.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/snowflake/medium     777.0 ± 0%      777.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseRW/clickhouse/medium    384.0 ± 0%      384.0 ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/simple_select     6.000 ± 0%      6.000 ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/join              18.00 ± 0%      18.00 ± 0%       ~ (p=1.000 n=10) ¹
ParseFresh/complex           51.00 ± 0%      51.00 ± 0%       ~ (p=1.000 n=10) ¹
PoolGetPut/serial            0.000 ± 0%      0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                 ²                +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                          │    base     │                head                │
                          │  sec/stmt   │  sec/stmt    vs base               │
ParseRW/postgresql/medium   13.32µ ± 1%   13.34µ ± 1%       ~ (p=0.699 n=10)
ParseRW/mysql/medium        11.21µ ± 1%   11.23µ ± 1%       ~ (p=0.698 n=10)
ParseRW/mariadb/medium      14.68µ ± 1%   14.72µ ± 0%       ~ (p=0.436 n=10)
ParseRW/sqlserver/medium    13.76µ ± 0%   13.81µ ± 4%       ~ (p=0.197 n=10)
ParseRW/oracle/medium       14.61µ ± 1%   14.67µ ± 1%       ~ (p=0.183 n=10)
ParseRW/sqlite/medium       12.88µ ± 1%   12.95µ ± 1%       ~ (p=0.123 n=10)
ParseRW/snowflake/medium    15.19µ ± 1%   15.18µ ± 1%       ~ (p=0.684 n=10)
ParseRW/clickhouse/medium   12.46µ ± 2%   12.47µ ± 1%       ~ (p=0.912 n=10)
geomean                     13.46µ        13.49µ       +0.22%

pkg: github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer
                         │    base     │                head                │
                         │   sec/op    │   sec/op     vs base               │
Tokenize/scratch/join      2.466µ ± 0%   2.462µ ± 1%       ~ (p=0.118 n=10)
Tokenize/scratch/complex   6.369µ ± 1%   6.377µ ± 0%       ~ (p=0.565 n=10)
Tokenize/scratch/insert    1.220µ ± 1%   1.219µ ± 1%       ~ (p=0.753 n=10)
geomean                    2.675µ        2.675µ       -0.02%

                         │     base     │                head                 │
                         │     B/s      │     B/s       vs base               │
Tokenize/scratch/join      48.74Mi ± 0%   48.81Mi ± 1%       ~ (p=0.128 n=10)
Tokenize/scratch/complex   56.60Mi ± 1%   56.53Mi ± 0%       ~ (p=0.565 n=10)
Tokenize/scratch/insert    59.44Mi ± 1%   59.47Mi ± 1%       ~ (p=0.779 n=10)
geomean                    54.73Mi        54.75Mi       +0.03%

                         │     base     │                head                 │
                         │     B/op     │    B/op     vs base                 │
Tokenize/scratch/join      0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
Tokenize/scratch/complex   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
Tokenize/scratch/insert    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                               ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                         │     base     │                head                 │
                         │  allocs/op   │ allocs/op   vs base                 │
Tokenize/scratch/join      0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
Tokenize/scratch/complex   0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
Tokenize/scratch/insert    0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                               ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

pkg: github.com/ajitpratap0/GoSQLX/pkg/transform
                             │    base     │                head                 │
                             │   sec/op    │   sec/op     vs base                │
TransformParse/simple_select   2.251µ ± 1%   1.546µ ± 1%  -31.32% (p=0.000 n=10)
TransformParse/join            6.530µ ± 6%   5.717µ ± 0%  -12.45% (p=0.000 n=10)
TransformParse/complex         17.02µ ± 1%   14.74µ ± 0%  -13.42% (p=0.000 n=10)
geomean                        6.301µ        5.069µ       -19.56%

                             │     base      │                 head                 │
                             │     B/op      │     B/op      vs base                │
TransformParse/simple_select     2256.0 ± 0%     816.0 ± 0%  -63.83% (p=0.000 n=10)
TransformParse/join             6.180Ki ± 0%   2.305Ki ± 0%  -62.71% (p=0.000 n=10)
TransformParse/complex         18.055Ki ± 0%   6.398Ki ± 0%  -64.56% (p=0.000 n=10)
geomean                         6.264Ki        2.273Ki       -63.71%

                             │    base    │                head                │
                             │ allocs/op  │ allocs/op   vs base                │
TransformParse/simple_select   7.000 ± 0%   5.000 ± 0%  -28.57% (p=0.000 n=10)
TransformParse/join            19.00 ± 0%   17.00 ± 0%  -10.53% (p=0.000 n=10)
TransformParse/complex         53.00 ± 0%   50.00 ± 0%   -5.66% (p=0.000 n=10)
geomean                        19.17        16.20       -15.52%

Run, raw data and benchstat

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 14s —— View job


Review of PR #903: tokenize into the pooled scratch buffer

Verdict: looks good. I found no blocking issues.

I read the diff for the three production files (fingerprint.go, formatter.go, transform.go) and the internal/tokscratch contract. I did not run the tests or benchmarks in this session, and I only skimmed the test files.

Safety of the swap

  • Change size: Five one-line swaps from tkz.TokenizeString to tokscratch.String(tkz, ...). No exported signatures change.
  • Lifetime: In each function, defer tokenizer.PutTokenizer(tkz) is registered before tokenizing, and the tokens are only used inside the function. Defers run last-in-first-out, so parsing finishes before the tokenizer returns to the pool.
  • No escape: No caller returns or stores the slice. The parser's preprocess step copies what the AST needs, which is the same path gosqlx.Parse already uses.
  • Dialect state: SetDialect is still called before tokenizing in Format and ParseSQLWithDialect.
  • Error paths: tokscratch.String is documented to return the same errors as the public method, so the early returns are unchanged.

Suggestions (non-blocking)

  1. Call-site comments: The ownership rule lives only in the tokscratch package doc. A one-line comment at each of the five sites would help, for example "tokens are tokenizer-owned; do not retain past PutTokenizer". Someone adding code between tokenizing and the defer could otherwise break the rule without noticing.
  2. Hidden dependency: tokscratch panics with "tokenizer not linked" if the tokenizer package's init hasn't run. All three packages import tokenizer directly, so this is safe today. A later refactor that drops that import would fail at runtime, not compile time.
  3. Dialect-alternation test: I'd like one test that alternates dialects (for example MySQL, Oracle, then the default) through Formatter.Format and transform.ParseSQLWithDialect on pooled tokenizers. It would confirm that scratch-buffer reuse leaks no dialect state. The corpus tests may already cover this, but I didn't check.
  4. Perf evidence: The B/op and allocs/op reductions are well supported by the local tables. The amd64 perf gate passes with no regressions, but its suite does not include Normalize or TransformParse, so it cannot confirm the time gain. The PR body already says this. Adding those benchmarks to scripts/perf/gate-suite.txt as the follow-up would close the gap.

The differential, -race concurrency and pool-reuse tests target the main risks, which are aliasing and stale data.
· branch perf/w5-01-tokscratch-consumers

…race test [W5-01]

Add fingerprint.Normalize and transform ParseSQL cases to the perf gate suite,
add a parallel gosqlx/formatter/fingerprint/transform race test on the shared
tokenizer pool, and fix the CHANGELOG wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 17s —— View job


Review of PR #903: tokenize into the pooled scratch buffer

Verdict: looks good. I found no blocking issues.

I read the diff of the production files (fingerprint.go, formatter.go, transform.go), CHANGELOG.md and scripts/perf/gate-suite.txt. I did not run the tests or benchmarks in this session. I did not read the test files in detail.

Production changes

  • There are five one-line swaps from tkz.TokenizeString to tokscratch.String(tkz, ...). No exported signatures change.
  • In every function, defer tokenizer.PutTokenizer(tkz) is registered before tokenizing. Defers run last-in-first-out, so parsing finishes before the tokenizer goes back to the pool.
  • In Format and ParseSQLWithDialect, SetDialect is still called before tokenizing.
  • The error paths are unchanged.

Gate suite change

  • This push adds BenchmarkNormalize and BenchmarkTransformParse (simple_select|join|complex) to scripts/perf/gate-suite.txt. That closes the gap earlier reviews flagged, where the gate couldn't measure this lane.
  • The PR body still lists it as a separate follow-up task. Update the body to say it's included, or split it out.
  • The earlier perf-gate comment covers only the previous head (743f6821). It does not cover the new suite entries. Check the next gate run to confirm these benchmarks produce stable A/A numbers.
  • Benchmarks that already allocate (B/op > 0) work under the allocs and B/op rules. This PR only lowers those numbers, so the gate should pass. A baseline run against origin/main still needs to include these benchmarks. The gate script should handle that, because it builds both sides from the same suite file.

Suggestions (non-blocking)

  1. Call-site comments: A one-line comment at each of the five sites would help. For example: "tokens are tokenizer-owned; do not retain past PutTokenizer". The rule currently lives only in the tokscratch package doc.
  2. Hidden dependency: tokscratch panics with "tokenizer not linked" if tokenizer's init hasn't run. All three packages import tokenizer directly, so this is safe today. A refactor that drops that import would fail at runtime, not compile time.
  3. Dialect alternation: I'd like a test that alternates dialects (for example MySQL, Oracle, then the default) through Formatter.Format and transform.ParseSQLWithDialect on pooled tokenizers. It would confirm that buffer reuse leaks no dialect state. The corpus tests may already cover this, but I did not check.
  4. CHANGELOG: The entry is correctly under [Unreleased] / ### Performance. If the gate-suite change stays in this PR, you could mention it there. That is optional.

The differential, -race concurrency and pool-reuse tests target the main risks, which are aliasing and stale data. The B/op and allocs/op reductions are well supported.
· branch perf/w5-01-tokscratch-consumers

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 16s —— View job


Review of PR #903: tokenize into the pooled scratch buffer

Verdict: looks good. I found no blocking issues.

I read the diff of the production files (fingerprint.go, formatter.go, transform.go), CHANGELOG.md, scripts/perf/gate-suite.txt and the latest perf gate report. I did not run the tests in this session, and I only skimmed the test files.

Production changes

  • There are five one-line swaps from tkz.TokenizeString to tokscratch.String(tkz, ...). No exported signatures change.
  • In each function, defer tokenizer.PutTokenizer(tkz) is registered before tokenizing. Defers run last-in-first-out, so the tokens are only used while the tokenizer is still held.
  • SetDialect is still called before tokenizing in Format and ParseSQLWithDialect.
  • The error paths are unchanged.

Perf gate (amd64, shadow mode)

  • The gate passes. The suite now includes Normalize and TransformParse, so it measures this PR's targets.
  • Normalize is 8-20% faster, with B/op down 55-58%.
  • TransformParse is 10-27% faster, with B/op down 63-65%.
  • The control benchmarks (ParseE2E, ParseRW, ParseFresh) are unchanged.
  • The A/A control was clean: 0 of 32 benchmarks moved by more than 3%.
  • Tokenize/scratch/join is +1.21% (p=0.006). The A/A control for that row was +1.01%, and this PR doesn't touch the tokenizer, so I read it as noise.

Suggestions (non-blocking)

  1. Call-site comments: The ownership rule lives only in the tokscratch package doc. A one-line comment at each of the five sites would help, for example "tokens are tokenizer-owned; do not retain past PutTokenizer".
  2. Hidden dependency: tokscratch panics with "tokenizer not linked" if the tokenizer package's init hasn't run. All three packages import tokenizer directly, so this is safe today. A later refactor that dropped the import would fail at runtime, not compile time.
  3. Dialect alternation: I'd like one test that alternates dialects (for example MySQL, Oracle, then the default) through Formatter.Format and transform.ParseSQLWithDialect on pooled tokenizers. It would confirm that buffer reuse leaks no dialect state. The corpus tests may already cover this, but I did not check.
  4. PR body: The "PENDING" perf-gate line is now out of date. The run has completed with a PASS.

The differential, -race concurrency, pool-reuse and cross-consumer tests target the main risks, which are aliasing and stale data. The change is small and mechanical, and I'd be comfortable merging it.
· branch perf/w5-01-tokscratch-consumers

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @ajitpratap0's task in 17s —— View job


Review of PR #903: tokenize into the pooled scratch buffer

Verdict: looks good. I found no blocking issues.

I read the diff of the five production call sites, CHANGELOG.md and scripts/perf/gate-suite.txt, plus the latest perf-gate report. I did not run the tests in this session, and I only skimmed the test files.

Production changes

  • There are five one-line swaps from tkz.TokenizeString to tokscratch.String(tkz, ...): fingerprint.Normalize, Formatter.Format, transform.parseSQL, ParseSQL and ParseSQLWithDialect. No exported signatures change.
  • In each function, defer tokenizer.PutTokenizer(tkz) is registered before tokenizing. Defers run last-in-first-out, so the tokens are only used while the tokenizer is still held.
  • SetDialect is still called before tokenizing in Format and ParseSQLWithDialect.
  • Error paths return before any token use, so they are unchanged.
  • Nothing returns or stores the slice.

Perf gate (amd64, shadow mode)

  • The gate reports PASS and the suite now includes Normalize and TransformParse.
  • Normalize is 7-21% faster, with B/op down 55-58%.
  • TransformParse is 12-31% faster, with B/op down 63-65%.
  • The control benchmarks are unchanged, and the A/A control was clean.
  • The CHANGELOG entry is under [Unreleased] / ### Performance. The CHANGELOG range of "42-70% fewer bytes/op" for Normalize comes from the Apple M4 numbers in the PR body. The amd64 gate shows 55-58%, so the two sets of numbers differ. Both are inside the stated ranges.

Suggestions (non-blocking)

  1. Call-site comments: The ownership rule lives only in the tokscratch package doc. A one-line comment at each of the five sites would help, for example "tokens are tokenizer-owned; do not retain past PutTokenizer".
  2. Hidden dependency: tokscratch panics with "tokenizer not linked" if the tokenizer package's init hasn't run. All three packages import tokenizer directly, so this is safe today. A refactor that dropped the import would fail at runtime, not compile time.
  3. Dialect alternation: I'd like one test that alternates dialects (for example MySQL, Oracle, then the default) through Formatter.Format and transform.ParseSQLWithDialect on pooled tokenizers. It would confirm that buffer reuse leaks no dialect state. The corpus tests may already cover this, but I did not check.
  4. PR body: The "PENDING" perf-gate line is out of date. The run has completed with a PASS.

The differential, -race concurrency, pool-reuse and cross-consumer tests target the main risks, which are aliasing and stale data. The change is small and mechanical, and I'd be comfortable merging it.
· branch perf/w5-01-tokscratch-consumers

@ajitpratap0
ajitpratap0 merged commit 160c417 into main Nov 1, 2026
48 checks passed
@ajitpratap0
ajitpratap0 deleted the perf/w5-01-tokscratch-consumers branch November 1, 2026 18:37

This branch was successfully deployed

1 active deployment
Preview — 0161ac1c Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation performance transform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant