Skip to content

test(bench): lint, security and LSP benchmark matrix [W0-06] - #910

Merged
ajitpratap0 merged 7 commits into
mainfrom
perf/w0-06-bench-lint-sec-lsp
Nov 1, 2026
Merged

ajitpratap0 merged 7 commits into
mainfrom
perf/w0-06-bench-lint-sec-lsp

Conversation

@ajitpratap0

@ajitpratap0 ajitpratap0 commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Summary

Test-only. Adds the PLAN.md 2.2 benchmark rows for lint, security and LSP, all fed from internal/benchcorpus (W0-01).

  • pkg/linter/bench_matrix_test.go: BenchmarkLintRW/<dialect> (8 dialects), BenchmarkLintPhase/{tokenize,parse,rules}/<dialect>, BenchmarkLintRule/L001..L030.
  • pkg/sql/security/bench_matrix_test.go: BenchmarkScanSQLRW/<dialect>, BenchmarkScanSQLParams/{pg_dollar,qmark,at}, BenchmarkScanSQLLarge/{30KB,1MB}, BenchmarkScanASTRW/<dialect>.
  • pkg/lsp/bench_matrix_test.go: BenchmarkLSP/{semanticTokens,documentSymbol,diagnostics,editCycle,didChangeIncremental}/{1KB,30KB,300KB}/{postgresql,mysql,sqlserver}.

Notes on shape:

  • Linter and security use external _test packages because benchcorpus imports pkg/gosqlx, which imports both.
  • One op of an RW benchmark is one pass over all non-Large statements of the dialect; ns/stmt is reported.
  • LSP documents come from benchcorpus.Script; the didChange benchmarks cancel the 300 ms debounce timer so no validation runs in the background. editCycle = didChange + validateDocument + documentSymbol + semanticTokens (3 tokenizations, 2 parses).
  • Behaviour tests pin the inputs: default rule set is L001..L030, LintWithDialect equals the rule set run over an equivalent context, LSP docs are valid in their dialect and deterministic, every LSP op has an observable effect, scan counts are deterministic, parameterized statements scan finding-free.

Test Plan

  • task check (fmt, vet, golangci-lint 0 issues, go test -race over all non-cbinding packages): pass. A first run during a full-disk episode on the shared machine failed with no package named; the identical CGO_ENABLED=1 go test -race -timeout 120s $(go list ./... | grep -v /cbinding) re-run passed.
  • task test:cbinding: pass
  • go test -count=1 ./pkg/compatibility/...: pass
  • go run honnef.co/go/tools/cmd/staticcheck@latest ./pkg/linter/ ./pkg/sql/security/ ./pkg/lsp/: clean
  • All new benchmarks run under -race -benchtime 1x (1MB scan excluded for time).
  • Smoke numbers (M4, -benchtime 1x/3x, not A/B data): LintRW ~22-30 us/stmt; ScanSQLLarge/1MB ~1.6 s/op; LSP documentSymbol 30KB 5-7 ms vs 300KB 230-385 ms.

Review fixes (c3b9f69)

  • editCycle now also runs documentSymbol (didChange + validateDocument + documentSymbol + semanticTokens); lspEditCycle is shared with TestLSPBenchOps, so the test pins the same sequence. editCycle/1KB/postgresql is 487 allocs/op.
  • The didChange params are marshalled once in lspBenchHandler, outside the timed loop (lspBenchEdit shared by the benchmark and the test). Before, per-op json.Marshal was about 26% of didChangeIncremental/1KB time and 4 of its 13 allocs/op; now didChangeIncremental/1KB/postgresql is 1568 ns/op, 2704 B/op, 9 allocs/op (M4, GOGC=off, -cpu 1, 20000x).
  • Merged origin/main (bfb8e41); no production code touched.

Review fixes (d80003f)

  • ScanASTRW: trees are released once from the top-level benchmark cleanup (not per sub-run); the sub-benchmark fails if any input tree was emptied. allocs/op is now independent of N (go test -bench 'BenchmarkScanASTRW/(postgresql|mysql)$' -benchmem): postgresql 3155 / 3141 / 3140 and mysql 3757 / 3740 / 3740 at 1x / 20x / 200x (was 3164 -> 1220 and 3750 -> 1656). Also passes under -race at 20x.
  • Lint rule benchmarks reset a per-input scratch Context by value (*scratch = *proto) per Check, so the harness allocates nothing; TestLintContextHasNoCachedState pins the Context field set so W6-05 must update lintInput.reset.
  • LintPhase/parse no longer releases the AST (mirrors Linter.LintString); header says the phases approximately decompose LintRW.
  • TestLSPBenchOps: documentSymbol type assertion failure now fails; validateDocument must publish an empty-diagnostics publishDiagnostics message.
  • Gates: task check, task test:cbinding, go test ./pkg/compatibility/..., staticcheck@latest on pkg/linter, pkg/sql/security, pkg/lsp: all pass.

Review fixes (3021a5e)

  • Removed the per-Check heap copy of linter.Context (112 B x 2813 inputs dominated allocs/op and B/op). BenchmarkLintRule/L001 now reports 0 B/op, 0 allocs/op; L015 256 B/op, 2 allocs/op over the whole loop (was ~315 KB, ~2813 allocs).
  • LintPhase/rules/<dialect> resets one Context per statement and shares it across all 30 rules, like Linter.LintString (postgresql: 61398 -> 52249 allocs/op), so a W6-05 cached Context.Nodes() is measured as in production.
  • New guard TestBenchContextResetIsAllocFree: reset and reset+Check of L001/L015 on a violation-free input must allocate 0.
  • Gates re-run: task check, task test:cbinding, compatibility tests, staticcheck@latest on pkg/linter, pkg/sql/security, pkg/lsp: all pass.

Breaking Changes

None. Test files only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2

Adds the PLAN.md 2.2 benchmarks for the linter (LintRW, LintPhase,
LintRule L001-L030), the security scanner (ScanSQLRW, ScanSQLParams,
ScanSQLLarge, ScanASTRW) and the LSP handler (semanticTokens,
documentSymbol, diagnostics, editCycle, didChangeIncremental at
1KB/30KB/300KB for postgresql, mysql, sqlserver), all fed from
internal/benchcorpus. Behaviour tests pin that the inputs are valid,
deterministic and exercise the code they claim to.

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.

1 Skipped Deployment
Project Deployment Actions Updated
gosqlx-website Ignored Ignored Preview Nov 1, 2026 3:45pm UTC

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

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


Review of PR #910: lint, security and LSP benchmark matrix

This is a test-only PR, and the structure is good. The _test external packages are justified by the import cycle. Each benchmark has a behaviour test that pins its inputs, which guards against timing a no-op. I only read the code and did not run the tests or benchmarks.

Bug (medium)

BenchmarkScanASTRW reuses AST trees after they are released (pkg/sql/security/bench_matrix_test.go:176-190)

  • b.Cleanup is registered inside the b.Run closure.
  • Go calls that closure several times as b.N ramps up (1, 100, and so on). Cleanups run at the end of every call, not once at the end of the benchmark.
  • After the first call, the trees have gone back to the AST pool via ast.ReleaseAST. Later calls scan released, and possibly recycled, nodes.
  • The result is a benchmark that measures empty or reset trees, or hits pool-reuse corruption. It would be misleading in A/B comparisons and could surface as a race under -race.
  • It would not show up with -benchtime 1x, which calls the closure once. That matches the PR's validation approach.

Fix: release the trees once, after the synchronous b.Run returns.

trees := parseTrees(b, rwSQL(b, d), d)
b.Run(d, func(b *testing.B) { ... })
for _, t := range trees { ast.ReleaseAST(t) }

Alternatively, parse inside the closure before b.ResetTimer(). Use defer if it's inside the loop body.

Smaller issues

  1. BenchmarkLSP builds every document eagerly (pkg/lsp/bench_matrix_test.go:120).

    • lspBenchDoc runs in the loop body, outside b.Run. That is 5 ops × 3 sizes × 3 dialects, including 300KB scripts, even when -bench selects one case.
    • Build the documents once, either cached per (dialect, size) or created lazily inside the sub-benchmark.
  2. The ns/stmt denominators differ between phases (pkg/linter/bench_matrix_test.go:208).

    • The parse sub-benchmark divides by len(tokenized), which excludes tokenize failures. tokenize and rules use len(stmts) and len(inputs).
    • The header comment says the three phases "add up to LintRW". That holds only when nothing is skipped. Reword it, or assert in a test that len(tokenized) == len(stmts).
  3. Possible NaN from a zero divisor (bench_matrix_test.go:188 in security). ns/stmt divides by len(trees), which is 0 if no statement parses. TestRWScansAreDeterministicAndConsistent catches this for the dialects it covers. A b.Fatal guard in the benchmark itself would be cheap.

  4. The AST built by buildInputs is never released (linter, buildInputs). This is harmless for a bounded test process. It does mean LintPhase and LintRule keep pooled ASTs out of the pool, which slightly skews the allocation numbers relative to LintRW. A short comment would help.

  5. The benchRules() mirror can drift. TestBenchRulesAreTheDefaultSet pins only the id set. If gosqlx.allRules changes configuration, such as MaxLineLength or a rule option, the mirror diverges without ids changing. TestLintRWMatchesRuleSet partly covers this, on 60 statements for 3 dialects. That seems acceptable, but exporting the rule constructor would be more robust.

  6. The parameterized statements in the at style omit LIMIT. This is fine. It just means the three styles are not the same five statements.

  7. TestLSPBenchDocuments parses the 300KB documents unless -short is set. That is slow and runs in the default go test path. Consider skipping it under -short for 30KB and above.

Positives

  • Debounce cancellation keeps background validation out of the timed region.
  • The insert/delete edit pair keeps the document size constant, and a test pins it.
  • SetBytes and ns/stmt make the numbers comparable across corpora.
  • There are no security concerns. This is test code, and the inputs are static.

The Cleanup issue should be fixed before this is used as A/B baseline data. The rest are optional.
· branch perf/w0-06-bench-lint-sec-lsp

ajitpratap0 and others added 2 commits November 1, 2026 02:33
…n LSP checks [W0-06]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
Reset a per-input scratch Context by value instead of heap-copying one per
Check, share one Context per statement across all rules in LintPhase/rules,
and add an alloc guard so harness overhead cannot return.

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

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

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


Re-review of PR #910 after the latest commits

The medium bug from my earlier review is fixed in the current head. I read pkg/sql/security/bench_matrix_test.go. I did not run the tests or benchmarks.

  • BenchmarkScanASTRW: the trees are now released once, from a b.Cleanup on the top-level benchmark (lines 174-184). They are no longer released after each b.N probe. The sub-benchmark also fails with b.Fatal if the input ASTs were emptied during the run (lines 196-201). That covers the reuse-after-release problem and the zero-trees NaN case.

  • Not re-checked in this pass: the optional items from my earlier review. These are eager LSP document construction, the ns/stmt denominators in LintPhase, the benchRules() mirror drifting from the default rule set, and the 300KB parse in TestLSPBenchDocuments under -short. None of them blocks merging, and I'd treat them as follow-ups.

Nothing blocking from my side.
· branch perf/w0-06-bench-lint-sec-lsp

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

ajitpratap0 and others added 2 commits November 1, 2026 12:43
editCycle now runs didChange, validateDocument, documentSymbol and
semanticTokens (3 tokenizations, 2 parses), the workload W6-12's
per-document cache targets. TestLSPBenchOps runs the shared cycle body and
checks symbols and tokens are non-empty.

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>
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 13s —— View job


Re-review of PR #910 (latest head bfb8e41)

I read the current code and did not run the tests or benchmarks.

Blocking issues: none. The earlier medium bug is fixed. BenchmarkScanASTRW releases its trees once, from a top-level b.Cleanup, and fails if an input tree was emptied. The latest commits also remove the per-Check Context heap copy, so the lint numbers are not skewed by harness allocations. TestBenchContextResetIsAllocFree guards that.

Optional follow-ups

  1. LSP documents are built eagerly. In pkg/lsp/bench_matrix_test.go, BenchmarkLSP calls lspBenchDoc in the loop body, outside b.Run. All 5 ops × 3 sizes × 3 dialects get built, including the 300KB scripts, even when -bench selects a single case. Building lazily inside the sub-benchmark, or caching per (dialect, size), would fix that.
  2. ns/stmt denominators differ between phases. In pkg/linter/bench_matrix_test.go, LintPhase/parse divides by len(tokenized). The other phases divide by len(stmts) or len(inputs). The header says the phases approximately decompose LintRW, so that is only true if nothing is skipped. A test asserting len(tokenized) == len(stmts) would pin it.
  3. The benchRules() mirror can drift. TestBenchRulesAreTheDefaultSet pins only the rule IDs. A change to rule options in gosqlx would not be caught by it. TestLintRWMatchesRuleSet partly covers this. An exported constructor would be more robust.
  4. Pooled ASTs from buildInputs are never released. This is harmless for a test process. A short comment would explain why the pool counters differ from LintRW.

Item 7 from my first review, the 300KB parse under -short, is already handled by the testing.Short() guard.

The test-only scope, the behaviour tests that pin each benchmark's inputs, and the debounce cancellation are all good. I'd merge and treat the items above as follow-ups.
· branch perf/w0-06-bench-lint-sec-lsp

Per-op json.Marshal was ~26% of didChangeIncremental/1KB time and 4 of
its 13 allocs/op. Build the insert/delete pair once in lspBenchHandler and
share the edit closure between BenchmarkLSP and TestLSPBenchOps.

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 20s —— View job


Re-review of PR #910 (head acef2cb)

I read the current code and did not run the tests or benchmarks. This head only changes the LSP benchmark, which now marshals the didChange params once outside the timed loop.

Blocking issues: none.

The earlier medium bug is fixed. BenchmarkScanASTRW releases its trees once, from a top-level b.Cleanup, and fails if an input tree was emptied. The lint harness allocates nothing per Check, and TestBenchContextResetIsAllocFree guards that.

Optional follow-ups (still open from earlier reviews)

  1. LSP documents are built eagerly. pkg/lsp/bench_matrix_test.go:147 calls lspBenchDoc outside b.Run. That builds all 5 ops × 3 sizes × 3 dialects, including the 300KB scripts, even when -bench selects one case. Build the document lazily inside the sub-benchmark, or cache it per (dialect, size).
  2. ns/stmt denominators differ between lint phases. LintPhase/parse divides by len(tokenized) (pkg/linter/bench_matrix_test.go:219), while the other phases use len(stmts). A test asserting len(tokenized) == len(stmts) would pin the "phases decompose LintRW" claim.
  3. The benchRules() mirror can drift. TestBenchRulesAreTheDefaultSet pins only the rule IDs, so a change to rule options in gosqlx would not be caught. TestLintRWMatchesRuleSet partly covers this.
  4. Pooled ASTs from buildInputs are never released. This is harmless in a test process. A one-line comment would explain why the pool counters differ from LintRW.

The test-only scope, the behaviour tests that pin each benchmark's inputs, and the debounce cancellation are all good. I'd merge and treat the items above as follow-ups.
· branch perf/w0-06-bench-lint-sec-lsp

@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 77c08d2921f0, head a5d0fb90d20f. 10 interleaved rounds, benchtime 200ms, -cpu 1, pinned to CPU 3 (SMT siblings 2-3) with taskset.

  • no gated metric moved past its threshold

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.15% 0.256 +0.20% +0.00% 5 → 5 ok
ParseE2E/select_where +0.93% 0.212 +0.14% +0.00% 18 → 18 ok
ParseE2E/join -0.69% 0.165 -0.04% +0.00% 17 → 17 ok
ParseE2E/aggregate -0.27% 0.450 -0.59% +0.00% 18 → 18 ok
ParseE2E/subquery -0.40% 0.226 -0.03% +0.00% 14 → 14 ok
ParseE2E/cte +0.85% 0.089 +0.16% +0.00% 21 → 21 ok
ParseE2E/window +0.12% 0.970 -0.35% +0.00% 11 → 11 ok
ParseE2E/complex -0.84% 0.165 -0.53% +0.00% 50 → 50 ok
ParseE2E/insert -0.28% 0.240 +0.44% +0.00% 17 → 17 ok
ParseE2E/update +0.13% 0.970 +0.18% +0.00% 13 → 13 ok
ParseE2E/delete +0.03% 0.939 -0.26% +0.00% 5 → 5 ok
ParseRW/postgresql/medium -0.50% 0.190 -0.03% +0.00% 646 → 646 ok
ParseRW/mysql/medium -0.04% 0.597 +0.40% +0.00% 365 → 365 ok
ParseRW/mariadb/medium +0.63% 0.105 +0.29% -0.00% 523 → 523 ok
ParseRW/sqlserver/medium -1.39% 0.003 -0.84% +0.00% 936 → 936 ok
ParseRW/oracle/medium -0.06% 0.912 -0.38% +0.00% 1093 → 1093 ok
ParseRW/sqlite/medium -0.80% 0.143 +0.27% +0.00% 346 → 346 ok
ParseRW/snowflake/medium -0.81% 0.529 +0.05% +0.00% 777 → 777 ok
ParseRW/clickhouse/medium -0.89% 0.063 +0.13% +0.00% 384 → 384 ok
ParseFresh/simple_select +0.63% 0.162 +0.33% +0.00% 6 → 6 ok
ParseFresh/join +0.55% 0.529 +0.51% +0.00% 18 → 18 ok
ParseFresh/complex +0.66% 0.218 +0.44% +0.00% 51 → 51 ok
PoolGetPut/serial +0.11% 0.285 +0.14% +0.00% 0 → 0 ok
Tokenize/scratch/join -0.79% <0.001 -0.49% +0.00% 0 → 0 ok
Tokenize/scratch/complex -0.59% 0.043 -0.09% +0.00% 0 → 0 ok
Tokenize/scratch/insert -0.10% 0.424 -0.27% +0.00% 0 → 0 ok
geomean -0.15% 0.912 -0.01%

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

Instructions per operation (cmd/perfdriver)

case instructions/op status
e2e/aggregate -0.14% ok
e2e/complex -0.03% ok
e2e/cte +0.05% ok
e2e/delete +0.11% ok
e2e/insert +0.19% ok
e2e/join +0.13% ok
e2e/select_where +0.10% ok
e2e/simple_select -0.13% ok
e2e/subquery +0.17% ok
e2e/update +0.03% ok
e2e/window -0.04% ok
rw/postgresql/medium -0.09% ok
Secondary metric: default GC, GOMAXPROCS=1 (informational)
benchmark time p
ParseE2E/simple_select +0.25% 0.545
ParseE2E/select_where -0.10% 0.436
ParseE2E/join -0.24% 0.880
ParseE2E/aggregate +0.34% 0.631
ParseE2E/subquery +0.07% 0.677
ParseE2E/cte +0.22% 0.796
ParseE2E/window +0.43% 0.406
ParseE2E/complex +0.76% 0.520
ParseE2E/insert +0.76% 0.028
ParseE2E/update -0.70% 0.820
ParseE2E/delete +0.04% 0.880
ParseRW/postgresql/medium +0.31% 0.912
ParseRW/mysql/medium -0.44% 0.631
ParseRW/mariadb/medium -1.11% 0.023
ParseRW/sqlserver/medium -1.27% 0.023
ParseRW/oracle/medium +1.06% 0.143
ParseRW/sqlite/medium -0.08% 0.912
ParseRW/snowflake/medium +0.21% 0.579
ParseRW/clickhouse/medium +0.41% 0.853
ParseFresh/simple_select +0.99% 0.001
ParseFresh/join +0.08% 0.307
ParseFresh/complex +0.93% 0.064
PoolGetPut/serial -0.21% 0.210
Tokenize/scratch/join -0.20% 0.622
Tokenize/scratch/complex -0.35% 0.143
Tokenize/scratch/insert -0.10% 0.820
geomean +0.08%
Environment
date:        2026-11-01T15:48:26Z
base:        77c08d2921f06fcbe5d26e725bec9bcc0250bdfe
head:        working tree of a5d0fb90d20f375755e00cfa66fd8e78cd7f1149
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.40, 0.64, 0.22
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:                              AMD EPYC 7763 64-Core Processor
Thread(s) per core:                      2
Core(s) per socket:                      2
Socket(s):                               1
Hypervisor vendor:                       Microsoft
L1d cache:                               64 KiB (2 instances)
L1i cache:                               64 KiB (2 instances)
L2 cache:                                1 MiB (2 instances)
L3 cache:                                32 MiB (1 instance)
benchstat (primary metric)
goos: linux
goarch: amd64
pkg: github.com/ajitpratap0/GoSQLX/pkg/gosqlx
cpu: AMD EPYC 7763 64-Core Processor                
                          │    base     │                head                │
                          │   sec/op    │   sec/op     vs base               │
ParseE2E/simple_select      2.280µ ± 1%   2.277µ ± 1%       ~ (p=0.255 n=10)
ParseE2E/select_where       4.782µ ± 2%   4.827µ ± 3%       ~ (p=0.210 n=10)
ParseE2E/join               7.504µ ± 1%   7.452µ ± 2%       ~ (p=0.165 n=10)
ParseE2E/aggregate          5.339µ ± 1%   5.325µ ± 7%       ~ (p=0.447 n=10)
ParseE2E/subquery           4.584µ ± 1%   4.565µ ± 1%       ~ (p=0.223 n=10)
ParseE2E/cte                6.291µ ± 1%   6.345µ ± 1%       ~ (p=0.086 n=10)
ParseE2E/window             4.179µ ± 1%   4.184µ ± 1%       ~ (p=0.956 n=10)
ParseE2E/complex            19.77µ ± 1%   19.60µ ± 1%       ~ (p=0.165 n=10)
ParseE2E/insert             3.408µ ± 1%   3.399µ ± 1%       ~ (p=0.238 n=10)
ParseE2E/update             3.029µ ± 2%   3.033µ ± 1%       ~ (p=0.971 n=10)
ParseE2E/delete             1.919µ ± 3%   1.920µ ± 4%       ~ (p=0.927 n=10)
ParseRW/postgresql/medium   230.6µ ± 1%   229.4µ ± 1%       ~ (p=0.190 n=10)
ParseRW/mysql/medium        132.1µ ± 1%   132.0µ ± 1%       ~ (p=0.593 n=10)
ParseRW/mariadb/medium      193.4µ ± 1%   194.7µ ± 2%       ~ (p=0.105 n=10)
ParseRW/sqlserver/medium    402.0µ ± 1%   396.4µ ± 1%  -1.39% (p=0.003 n=10)
ParseRW/oracle/medium       468.5µ ± 1%   468.2µ ± 1%       ~ (p=0.912 n=10)
ParseRW/sqlite/medium       111.2µ ± 1%   110.3µ ± 1%       ~ (p=0.143 n=10)
ParseRW/snowflake/medium    279.9µ ± 1%   277.6µ ± 1%       ~ (p=0.529 n=10)
ParseRW/clickhouse/medium   126.7µ ± 1%   125.5µ ± 2%       ~ (p=0.063 n=10)
ParseFresh/simple_select    10.60µ ± 1%   10.67µ ± 2%       ~ (p=0.159 n=10)
ParseFresh/join             25.48µ ± 1%   25.62µ ± 1%       ~ (p=0.529 n=10)
ParseFresh/complex          40.63µ ± 3%   40.89µ ± 2%       ~ (p=0.218 n=10)
PoolGetPut/serial           140.4n ± 0%   140.6n ± 4%       ~ (p=0.284 n=10)
geomean                     18.50µ        18.48µ       -0.11%

                          │     base     │                head                 │
                          │     B/s      │     B/s       vs base               │
ParseE2E/simple_select      10.88Mi ± 1%   10.89Mi ± 1%       ~ (p=0.340 n=10)
ParseE2E/select_where       20.54Mi ± 2%   20.35Mi ± 3%       ~ (p=0.239 n=10)
ParseE2E/join               16.02Mi ± 1%   16.13Mi ± 2%       ~ (p=0.159 n=10)
ParseE2E/aggregate          15.00Mi ± 1%   15.04Mi ± 6%       ~ (p=0.470 n=10)
ParseE2E/subquery           17.07Mi ± 1%   17.13Mi ± 1%       ~ (p=0.254 n=10)
ParseE2E/cte                18.80Mi ± 1%   18.64Mi ± 1%       ~ (p=0.084 n=10)
ParseE2E/window             21.46Mi ± 1%   21.43Mi ± 1%       ~ (p=0.911 n=10)
ParseE2E/complex            18.24Mi ± 1%   18.39Mi ± 1%       ~ (p=0.160 n=10)
ParseE2E/insert             21.26Mi ± 1%   21.33Mi ± 1%       ~ (p=0.239 n=10)
ParseE2E/update             17.32Mi ± 2%   17.29Mi ± 1%       ~ (p=0.927 n=10)
ParseE2E/delete             25.84Mi ± 3%   25.82Mi ± 4%       ~ (p=1.000 n=10)
ParseRW/postgresql/medium   15.56Mi ± 1%   15.64Mi ± 1%       ~ (p=0.196 n=10)
ParseRW/mysql/medium        20.49Mi ± 1%   20.50Mi ± 1%       ~ (p=0.491 n=10)
ParseRW/mariadb/medium      18.33Mi ± 1%   18.22Mi ± 2%       ~ (p=0.101 n=10)
ParseRW/sqlserver/medium    16.55Mi ± 1%   16.78Mi ± 1%  +1.41% (p=0.003 n=10)
ParseRW/oracle/medium       17.90Mi ± 1%   17.91Mi ± 1%       ~ (p=0.868 n=10)
ParseRW/sqlite/medium       18.57Mi ± 1%   18.72Mi ± 1%       ~ (p=0.138 n=10)
ParseRW/snowflake/medium    18.17Mi ± 2%   18.32Mi ± 1%       ~ (p=0.517 n=10)
ParseRW/clickhouse/medium   17.79Mi ± 1%   17.95Mi ± 2%       ~ (p=0.060 n=10)
geomean                     17.94Mi        17.98Mi       +0.23%

                          │      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.53Ki ± 0%     70.53Ki ± 0%       ~ (p=0.961 n=10)
ParseRW/mysql/medium        43.73Ki ± 0%     43.73Ki ± 0%       ~ (p=1.000 n=10)
ParseRW/mariadb/medium      64.86Ki ± 0%     64.86Ki ± 0%       ~ (p=0.841 n=10)
ParseRW/sqlserver/medium    90.62Ki ± 0%     90.62Ki ± 0%       ~ (p=0.394 n=10)
ParseRW/oracle/medium       113.8Ki ± 0%     113.8Ki ± 0%       ~ (p=1.000 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   19.22µ ± 1%   19.12µ ± 1%       ~ (p=0.190 n=10)
ParseRW/mysql/medium        16.51µ ± 1%   16.50µ ± 1%       ~ (p=0.566 n=10)
ParseRW/mariadb/medium      21.49µ ± 1%   21.63µ ± 2%       ~ (p=0.105 n=10)
ParseRW/sqlserver/medium    19.14µ ± 1%   18.88µ ± 1%  -1.39% (p=0.003 n=10)
ParseRW/oracle/medium       19.52µ ± 1%   19.51µ ± 1%       ~ (p=0.912 n=10)
ParseRW/sqlite/medium       18.54µ ± 1%   18.39µ ± 1%       ~ (p=0.138 n=10)
ParseRW/snowflake/medium    21.53µ ± 1%   21.35µ ± 1%       ~ (p=0.529 n=10)
ParseRW/clickhouse/medium   18.09µ ± 1%   17.93µ ± 2%       ~ (p=0.063 n=10)
geomean                     19.19µ        19.10µ       -0.48%

pkg: github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer
                         │    base     │                head                │
                         │   sec/op    │   sec/op     vs base               │
Tokenize/scratch/join      3.334µ ± 1%   3.307µ ± 0%  -0.79% (p=0.000 n=10)
Tokenize/scratch/complex   8.646µ ± 1%   8.595µ ± 1%       ~ (p=0.043 n=10)
Tokenize/scratch/insert    1.506µ ± 1%   1.504µ ± 0%       ~ (p=0.422 n=10)
geomean                    3.514µ        3.497µ       -0.50%

                         │     base     │                head                 │
                         │     B/s      │     B/s       vs base               │
Tokenize/scratch/join      36.05Mi ± 1%   36.33Mi ± 0%  +0.79% (p=0.000 n=10)
Tokenize/scratch/complex   41.69Mi ± 1%   41.94Mi ± 1%       ~ (p=0.041 n=10)
Tokenize/scratch/insert    48.15Mi ± 1%   48.18Mi ± 0%       ~ (p=0.517 n=10)
geomean                    41.67Mi        41.88Mi       +0.49%

                         │     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

Run, raw data and benchstat

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

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


Re-review of PR #910 (head 7b831b4)

I read the code. I did not run the tests or benchmarks. The head is a merge of main into the branch, and it adds no new benchmark code beyond what earlier reviews covered.

Blocking issues: none.

  • The earlier BenchmarkScanASTRW problem is fixed: trees are released once from a top-level b.Cleanup, and the sub-benchmark fails if an input tree was emptied.
  • The lint harness does not allocate per Check, and TestBenchContextResetIsAllocFree guards that.
  • The didChange params are marshalled outside the timed loop.
  • The perf gate passes. This PR only adds test files, so it can't move the gated metrics. The small ParseRW deltas on mariadb, sqlserver, sqlite and snowflake (+0.5% to +1.1%) are within the gate's thresholds. Allocs/op and B/op are unchanged.

Optional follow-ups (still open from earlier reviews)

  1. LSP documents are built eagerly. In pkg/lsp/bench_matrix_test.go, BenchmarkLSP calls lspBenchDoc in the loop body, outside b.Run. That builds all 5 ops × 3 sizes × 3 dialects, including the 300KB scripts, even when -bench selects one case. Build the document lazily inside the sub-benchmark, or cache it per (dialect, size).
  2. ns/stmt denominators differ between lint phases. LintPhase/parse divides by len(tokenized), and the other phases use len(stmts) or len(inputs). A test asserting len(tokenized) == len(stmts) would back up the claim that the phases approximately decompose LintRW.
  3. The benchRules() mirror can drift. TestBenchRulesAreTheDefaultSet pins only the rule IDs. A change to rule options in gosqlx would not be caught by it. TestLintRWMatchesRuleSet partly covers this.
  4. Pooled ASTs from buildInputs are never released. This is harmless in a test process. A one-line comment would explain why the pool counters differ from LintRW.

The test-only scope, the behaviour tests that pin each benchmark's inputs, and the debounce cancellation are all good. I'd merge and treat the items above as follow-ups.
· branch perf/w0-06-bench-lint-sec-lsp

@ajitpratap0
ajitpratap0 merged commit 962b458 into main Nov 1, 2026
47 checks passed
ajitpratap0 added a commit that referenced this pull request Nov 1, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnbRnTFErhoMJdAjVEeCg2
ajitpratap0 added a commit that referenced this pull request Nov 1, 2026
…0-12] (#917)

* feat(bench): per-arch baselines, matrix/pgo tasks, methodology doc [W0-12]

Add benchmarks/baselines/{amd64,arm64}.json (ns/op trend-only, B/op and
allocs/op gated) with a scripts/perf/baseline checker and -update flag,
task bench:matrix, bench:baseline, bench:baseline:update and pgo:refresh,
docs/PERFORMANCE_METHODOLOGY.md, and one consolidated wave-0 CHANGELOG
entry. performance_baselines.json and TestPerformanceRegression are
documented as a smoke test only; its two never-enforced entries are removed.

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

* fix(bench): fail stale/disjoint baselines, check provenance, pipefail matrix, doc fixes [W0-12]

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

* fix(bench): external baseline provenance, go1.26 re-record, local-only docs [W0-12]

-update -input now requires -go-version and records external provenance
instead of stamping the local toolchain. Re-record arm64.json with go1.26.9,
drop the stale tolerance advice from PERFORMANCE_TUNING, and state that
bench:baseline is local until W0-13.

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

* docs(perf): describe the gate's secondary arm accurately; cite #917 in CHANGELOG

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

* docs(changelog): list #910 and #914 in the wave-0 entry

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>
@ajitpratap0
ajitpratap0 deleted the perf/w0-06-bench-lint-sec-lsp branch November 1, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant