Skip to content

Build checker cache keys in an inline buffer with one-shot hashing - #4784

Open
mds-ant wants to merge 1 commit into
microsoft:mainfrom
mds-ant:perf/checker-oneshot-key-hash
Open

Build checker cache keys in an inline buffer with one-shot hashing#4784
mds-ant wants to merge 1 commit into
microsoft:mainfrom
mds-ant:perf/checker-oneshot-key-hash

Conversation

@mds-ant

@mds-ant mds-ant commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Context

Every structural cache key in the checker is built through keyBuilder, whose only field was a by-value xxh3.Hasher. That's a streaming hasher with an internal block buffer of over a kilobyte, so every var b keyBuilder zeroed the whole struct on the stack and fed each ID through the streaming Write path, even though a typical key is about a dozen bytes and Sum128 on such input just one-shot-hashes the buffered bytes anyway.

This PR

keyBuilder now accumulates the key's bytes in a small inlineBuffer, spilling to a heap overflowBuffer only for the rare oversized key, and hashes them with the one-shot xxh3.Hash128. The hashWrite32/hashWrite64 helpers become writeUint32/writeUint64 methods with the same little-endian layout, so the byte stream is unchanged and every key value is identical to before.

On a full check of VS Code the keys average 11.5 bytes with a p99.9 of 129 bytes. Only 0.04% exceed the 192-byte buffer, so the spill path is effectively cold.

Performance

Metric main branch Δ
Instructions (single-threaded) 108.78 B 105.86 B −2.68%
Wall time (single-threaded) 19.39 s 19.08 s −1.60%
Wall time (4 checkers) 6.273 s 6.188 s −1.36%

This PR was assisted by Claude Code.

Cache keys were built through a by-value `xxh3.Hasher` embedded in `keyBuilder`. That is a streaming hasher with a large internal block buffer, so every key build zeroed a sizeable struct on the stack and pushed each ID through the streaming `Write` path, even though a typical key is only a handful of bytes.

`keyBuilder` now accumulates the key bytes in a small inline buffer, spilling to a slice for the rare oversized key, and hashes them with the one-shot `xxh3.Hash128`. The byte stream is unchanged (`hashWrite32`/`hashWrite64` become `writeUint32`/`writeUint64` with the same little-endian layout), so every key value is identical to before.
Copilot AI review requested due to automatic review settings July 29, 2026 11:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes checker cache-key construction by buffering small keys inline and hashing them in one shot.

Changes:

  • Replaces streaming hashing with a 192-byte inline buffer and overflow path.
  • Preserves little-endian key encoding.
  • Adds boundary and overflow encoding tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
internal/checker/checker.go Implements buffered cache-key construction and one-shot hashing.
internal/checker/keybuilder_test.go Verifies key encoding and buffer-boundary behavior.

@jakebailey

Copy link
Copy Markdown
Member

I swear Claude is addicted to introducing one off test files

@typescript-bot perf test this

@typescript-automation

typescript-automation Bot commented Jul 29, 2026

Copy link
Copy Markdown

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
perf test this ✅ Started 👀 Results

@mds-ant

mds-ant commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

I swear Claude is addicted to introducing one off test files

I actually asked it to add a test to make sure that the resulting hash is still identical. Happy to get rid of it if you don't think it's worth keeping.

@jakebailey

Copy link
Copy Markdown
Member

I'm on the fence 😄

I'm actually triggering all these perf runs on my phone, so haven't seen the test file without terrible word wrapping; my immediate uncertainty was just the maintenance cost. But I'll check

A lot of this key builder stuff might just go away in a future version of Go when they add maps that have custom hashers.

@jakebailey

Copy link
Copy Markdown
Member

Mainly, we already have checker_test.go. It just really loves adding new files

Comment thread internal/checker/keybuilder_test.go Outdated
Comment on lines +17363 to +17365
inlineLength int
overflowBuffer []byte
inlineBuffer [192]byte

@jakebailey jakebailey Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this be a lot easier to just initialize it with say:

type keyBuilder struct {
    buffer []byte
    space  [192]byte
}

func (b *keyBuilder) init() {
    if b.buffer == nil {
        b.buffer = b.space[:0]
    }
}

func (b *keyBuilder) writeByte(c byte) {
    b.init()
    // ...
}

And let Go manage overflowing?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(or a new func, or even a pool get/put)

@mds-ant mds-ant Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, the self-referential slice defeat's Go's escape analysis, which means every var b keyBuilder gets moved to the heap. On the VS Code corpus that's +10.7M allocations and a significant wall time regression.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. Theoretically it could just go onto the heap anyway in a pool

@mds-ant
mds-ant force-pushed the perf/checker-oneshot-key-hash branch from b9d8773 to e143e5a Compare July 29, 2026 15:39
@typescript-automation

Copy link
Copy Markdown

@jakebailey
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - native
Errors 4 4 ~ ~ ~ p=1.000 n=6
Symbols 81,812 (± 0.03%) 81,800 (± 0.05%) ~ 81,729 81,840 p=0.810 n=6
Types 98,817 98,817 ~ ~ ~ p=1.000 n=6
Memory Used 174,079k (± 0.21%) 173,476k (± 0.38%) ~ 172,487k 174,160k p=0.093 n=6
Memory Allocs 2,694,780 (± 0.01%) 2,708,238 (± 0.01%) +13,458 (+ 0.50%) 2,707,953 2,708,444 p=0.005 n=6
Config Time 0.001s (±109.43%) 0.001s (±77.38%) ~ 0.000s 0.001s p=0.640 n=6
Parse Time 0.054s (± 3.23%) 0.059s (±10.55%) ~ 0.052s 0.067s p=0.195 n=6
Bind Time 0.020s (±11.39%) 0.019s (±10.12%) ~ 0.017s 0.022s p=0.570 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 0.971s (± 2.45%) 0.980s (± 1.91%) ~ 0.960s 1.012s p=0.470 n=6
Total Time 1.048s (± 2.45%) 1.060s (± 1.81%) ~ 1.044s 1.097s p=0.173 n=6
angular-1 - native
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 871,658 (± 0.12%) 871,871 (± 0.12%) ~ 869,876 872,631 p=0.575 n=6
Types 263,961 (± 0.00%) 263,963 (± 0.00%) +1 (+ 0.00%) 263,962 263,964 p=0.039 n=6
Memory Used 812,702k (± 0.07%) 813,544k (± 0.11%) ~ 812,687k 814,819k p=0.093 n=6
Memory Allocs 13,147,124 (± 0.05%) 13,149,247 (± 0.07%) ~ 13,139,642 13,164,108 p=0.936 n=6
Config Time 0.027s (± 9.25%) 0.028s (±13.13%) ~ 0.024s 0.034s p=0.571 n=6
Parse Time 0.250s (± 4.18%) 0.253s (± 3.98%) ~ 0.238s 0.268s p=0.521 n=6
Bind Time 0.062s (±33.07%) 0.054s (±34.47%) ~ 0.042s 0.092s p=1.000 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 2.151s (± 1.26%) 2.125s (± 1.46%) ~ 2.091s 2.165s p=0.149 n=6
Total Time 2.503s (± 0.98%) 2.474s (± 0.96%) ~ 2.443s 2.504s p=0.065 n=6
mui-docs - native
Errors 11,282 (± 0.01%) 11,280 (± 0.03%) ~ 11,274 11,282 p=0.214 n=6
Symbols 4,384,004 4,384,004 ~ ~ ~ p=1.000 n=6
Types 1,536,962 1,536,962 ~ ~ ~ p=1.000 n=6
Memory Used 4,964,594k (± 0.02%) 4,964,607k (± 0.03%) ~ 4,963,138k 4,966,480k p=0.936 n=6
Memory Allocs 46,001,769 (± 0.05%) 46,029,983 (± 0.05%) ~ 46,004,739 46,059,844 p=0.093 n=6
Config Time 0.029s (±10.60%) 0.029s (± 3.55%) ~ 0.028s 0.031s p=0.466 n=6
Parse Time 0.548s (± 2.08%) 0.553s (± 2.45%) ~ 0.535s 0.572s p=0.630 n=6
Bind Time 0.003s (±19.35%) 0.002s (±22.11%) ~ 0.002s 0.003s p=0.311 n=6
Check Time 18.802s (± 0.48%) 18.730s (± 0.42%) ~ 18.584s 18.823s p=0.093 n=6
Emit Time 0.549s (± 4.86%) 0.556s (± 5.33%) ~ 0.526s 0.587s p=0.629 n=6
Total Time 20.775s (± 0.36%) 20.694s (± 0.47%) ~ 20.524s 20.818s p=0.173 n=6
self-build-src - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,394,220 1,394,220 ~ ~ ~ p=1.000 n=6
Types 442,210 442,210 ~ ~ ~ p=1.000 n=6
Memory Used 1,608,036k (± 0.29%) 1,607,677k (± 0.37%) ~ 1,597,681k 1,614,752k p=1.000 n=6
Memory Allocs 97,057,704 (± 0.06%) 97,339,284 (± 0.07%) +281,580 (+ 0.29%) 97,222,705 97,420,316 p=0.005 n=6
Config Time 0.019s (±33.94%) 0.016s (±39.36%) ~ 0.008s 0.023s p=0.521 n=6
Parse Time 0.257s (± 2.31%) 0.261s (± 4.14%) ~ 0.244s 0.275s p=0.297 n=6
Bind Time 0.000s (±244.70%) 0.000s ~ ~ ~ p=0.405 n=6
Check Time 2.175s (± 0.75%) 2.163s (± 0.61%) ~ 2.153s 2.187s p=0.230 n=6
Emit Time 0.307s (± 2.96%) 0.304s (± 2.66%) ~ 0.295s 0.318s p=0.378 n=6
Total Time 28.739s (± 0.80%) 28.575s (± 0.79%) ~ 28.277s 28.804s p=0.471 n=6
self-compiler - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 337,681 337,681 ~ ~ ~ p=1.000 n=6
Types 199,559 199,559 ~ ~ ~ p=1.000 n=6
Memory Used 320,631k (± 0.04%) 320,445k (± 0.04%) ~ 320,272k 320,612k p=0.066 n=6
Memory Allocs 4,676,736 (± 0.01%) 4,692,679 (± 0.01%) +15,942 (+ 0.34%) 4,692,041 4,693,307 p=0.005 n=6
Config Time 0.001s 0.001s (±34.96%) ~ 0.001s 0.002s p=0.405 n=6
Parse Time 0.116s (± 2.15%) 0.116s (± 2.02%) ~ 0.112s 0.118s p=0.627 n=6
Bind Time 0.000s 0.000s ~ ~ ~ p=1.000 n=6
Check Time 1.276s (± 0.46%) 1.263s (± 0.97%) ~ 1.252s 1.284s p=0.077 n=6
Emit Time 0.118s (± 7.86%) 0.130s (±10.66%) ~ 0.114s 0.153s p=0.172 n=6
Total Time 1.568s (± 0.79%) 1.560s (± 1.02%) ~ 1.539s 1.579s p=0.336 n=6
ts-pre-modules - native
Errors 3 3 ~ ~ ~ p=1.000 n=6
Symbols 97,488 97,488 ~ ~ ~ p=1.000 n=6
Types 356 356 ~ ~ ~ p=1.000 n=6
Memory Used 128,707k (± 0.01%) 128,707k (± 0.01%) ~ 128,695k 128,723k p=0.872 n=6
Memory Allocs 177,222 (± 0.19%) 177,337 (± 0.19%) ~ 176,998 177,760 p=0.810 n=6
Config Time 0.001s (±77.38%) 0.001s (±77.38%) ~ 0.000s 0.001s p=1.000 n=6
Parse Time 0.111s (± 5.49%) 0.108s (± 9.22%) ~ 0.091s 0.120s p=0.630 n=6
Bind Time 0.033s (±26.14%) 0.044s (±22.55%) ~ 0.034s 0.055s p=0.053 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=6
Total Time 0.148s (± 9.21%) 0.155s (±11.46%) ~ 0.128s 0.173s p=0.518 n=6
vscode - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 7,499,780 7,499,780 ~ ~ ~ p=1.000 n=6
Types 2,616,632 2,616,632 ~ ~ ~ p=1.000 n=6
Memory Used 5,049,559k (± 0.02%) 5,050,495k (± 0.02%) ~ 5,048,682k 5,052,056k p=0.230 n=6
Memory Allocs 38,242,688 (± 0.07%) 38,262,722 (± 0.11%) ~ 38,224,996 38,320,454 p=0.575 n=6
Config Time 0.079s (± 7.96%) 0.082s (± 7.43%) ~ 0.073s 0.088s p=0.375 n=6
Parse Time 0.901s (± 4.20%) 0.908s (± 3.66%) ~ 0.856s 0.947s p=0.936 n=6
Bind Time 0.223s (±36.72%) 0.192s (±30.92%) ~ 0.158s 0.308s p=0.518 n=6
Check Time 8.578s (± 1.14%) 8.382s (± 1.44%) -0.196s (- 2.28%) 8.286s 8.582s p=0.031 n=6
Emit Time 2.415s (±11.49%) 2.422s (± 9.85%) ~ 2.082s 2.696s p=1.000 n=6
Total Time 12.219s (± 1.18%) 12.009s (± 1.08%) -0.210s (- 1.72%) 11.833s 12.164s p=0.031 n=6
webpack - native
Errors 2 2 ~ ~ ~ p=1.000 n=6
Symbols 193,872 193,872 ~ ~ ~ p=1.000 n=6
Types 340 340 ~ ~ ~ p=1.000 n=6
Memory Used 230,421k (± 0.17%) 230,411k (± 0.15%) ~ 229,758k 230,762k p=1.000 n=6
Memory Allocs 977,616 (± 0.54%) 972,550 (± 0.39%) ~ 967,552 978,380 p=0.093 n=6
Config Time 0.012s (±10.91%) 0.012s (±23.15%) ~ 0.008s 0.016s p=0.406 n=6
Parse Time 0.153s (± 4.48%) 0.150s (± 3.37%) ~ 0.145s 0.158s p=0.470 n=6
Bind Time 0s 0s ~ ~ ~ p=1.000 n=6
Check Time 0s 0s ~ ~ ~ p=1.000 n=6
Emit Time 0.045s (±15.88%) 0.047s (±14.98%) ~ 0.041s 0.058s p=0.420 n=6
Total Time 0.211s (± 4.90%) 0.209s (± 3.53%) ~ 0.200s 0.220s p=0.630 n=6
xstate-main - native
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,064,618 1,064,618 ~ ~ ~ p=1.000 n=6
Types 392,776 392,776 ~ ~ ~ p=1.000 n=6
Memory Used 637,845k (± 0.03%) 637,997k (± 0.02%) ~ 637,767k 638,097k p=0.230 n=6
Memory Allocs 4,989,099 (± 0.19%) 4,990,807 (± 0.07%) ~ 4,987,620 4,995,300 p=0.128 n=6
Config Time 0.004s (±18.82%) 0.005s (± 8.44%) ~ 0.004s 0.005s p=0.248 n=6
Parse Time 0.128s (± 2.74%) 0.127s (± 3.43%) ~ 0.122s 0.134s p=0.872 n=6
Bind Time 0.039s (±21.06%) 0.034s (±21.83%) ~ 0.024s 0.042s p=0.629 n=6
Check Time 1.237s (± 1.05%) 1.151s (± 1.24%) 🟩-0.085s (- 6.90%) 1.139s 1.175s p=0.005 n=6
Emit Time 0.001s 0.001s ~ ~ ~ p=1.000 n=6
Total Time 1.411s (± 0.80%) 1.322s (± 1.00%) 🟩-0.090s (- 6.34%) 1.313s 1.348s p=0.005 n=6
System info unknown
Hosts
  • native
Scenarios
  • Compiler-Unions - native
  • angular-1 - native
  • mui-docs - native
  • self-build-src - native
  • self-compiler - native
  • ts-pre-modules - native
  • vscode - native
  • webpack - native
  • xstate-main - native
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

lsp

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsLSP - native
Req 1 - updateOpen 13ms (± 3.87%) 14ms (±10.20%) ~ 12ms 16ms p=0.928 n=6
Req 2 - geterr 1,035ms (± 0.64%) 1,039ms (± 0.75%) ~ 1,028ms 1,049ms p=0.423 n=6
Req 3 - references 55ms (±11.36%) 49ms (± 6.08%) ~ 45ms 54ms p=0.087 n=6
Req 4 - navto 21ms (±11.00%) 20ms (± 5.79%) ~ 19ms 22ms p=0.933 n=6
Req 5 - completionInfo count 1,357 1,357 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 19ms (±11.63%) 22ms (± 6.63%) 🔻+3ms (+14.66%) 21ms 25ms p=0.043 n=6
CompilerLSP - native
Req 1 - updateOpen 11ms 11ms (± 3.65%) ~ 11ms 12ms p=0.405 n=6
Req 2 - geterr 439ms (± 2.27%) 436ms (± 2.30%) ~ 421ms 450ms p=0.748 n=6
Req 3 - references 49ms (± 5.95%) 48ms (± 4.36%) ~ 44ms 49ms p=0.318 n=6
Req 4 - navto 18ms 19ms (± 8.35%) ~ 18ms 22ms p=0.074 n=6
Req 5 - completionInfo count 1,519 1,519 ~ ~ ~ p=1.000 n=6
Req 5 - completionInfo 20ms (±14.13%) 21ms (±20.44%) ~ 16ms 28ms p=1.000 n=6
System info unknown
Hosts
  • native
Scenarios
  • CompilerLSP - native
  • Compiler-UnionsLSP - native
  • xstate-main-1-LSP - native
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
lsp-startup - native
Execution time 4.77ms (±516.54%) 4.78ms (±516.55%) +0.00ms (+ 0.10%) 0.00ms 486.16ms p=0.000 n=600
tsgo-startup - native
Execution time 5.86ms (±516.54%) 5.87ms (±516.55%) +0.01ms (+ 0.18%) 0.00ms 591.87ms p=0.000 n=600
System info unknown
Hosts
  • native
Scenarios
  • lsp-startup - native
  • tsgo-startup - native
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants