Skip to content

Dense link stores - #64711

Merged
Anders Hejlsberg (ahejlsberg) merged 2 commits into
mainfrom
dense-link-stores
Oct 9, 2026
Merged

Anders Hejlsberg (ahejlsberg) merged 2 commits into
mainfrom
dense-link-stores

Conversation

@ahejlsberg

Copy link
Copy Markdown
Member

Analysis has revealed that our paged link stores (see microsoft/typescript-go#4329) are inefficient when compilation uses multiple checkers. For example, with 8 checkers as much as 80% of paged link storage is wasted. With this PR we change the paged link store implementation to ensure dense allocation and more efficient utilization of storage. For example, the new implementation reduces the memory usage by 8-9% when compiling VS Code with 8 checkers.

Copilot AI balanced review requested due to automatic review settings October 9, 2026 22:43
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Oct 9, 2026
@typescript-automation typescript-automation Bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Oct 9, 2026
@ahejlsberg

Copy link
Copy Markdown
Member Author

TypeScript Bot (@typescript-bot) test it

@typescript-automation

typescript-automation Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

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

Command Status Results
test top400 ✅ Started ✅ Results
user test this ✅ Started ✅ Results
run dt ✅ Started ✅ Results
perf test this faster ✅ Started 👀 Results

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.

🟡 Changes recommended

The new ID allocation and fallback invariants need focused regression tests.

1 open finding
What changed in this PR

Reworks checker link storage to reduce memory waste across parallel checkers through dense, block-based ID allocation.

Changes:

  • Adds reserved 256-ID block generators for nodes and symbols.
  • Stores paged links indirectly in dense arenas.
  • Routes out-of-range IDs through map-backed storage.
File Description
tsc/​internal/​core/​linkstore.go Implements arena-backed paged links.
tsc/​internal/​checker/​links.go Adds dense paged/fallback routing.
tsc/​internal/​checker/​checker.go Adopts the new symbol link store.
tsc/​internal/​ast/​utilities.go Adds block-based ID generators.

🧠 Review effort: Balanced


💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +58 to +62
func (a *NodeIdGenerator) GetNodeId(node *Node) NodeId {
id := node.id.Load()
if id == 0 {
if a.nextId == a.lastId {
a.nextId = nextNodeBlockId.Add(BlockIdSize) - BlockIdSize
@typescript-automation

Copy link
Copy Markdown
Contributor

Hey Anders Hejlsberg (@ahejlsberg), the results of running the DT tests are ready.

Everything looks the same!

You can check the log here.

@typescript-automation

Copy link
Copy Markdown
Contributor

Anders Hejlsberg (@ahejlsberg)
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 41 41 ~ ~ ~ p=1.000 n=12
Symbols 115,276 115,276 ~ ~ ~ p=1.000 n=12
Types 79,613 79,613 ~ ~ ~ p=1.000 n=12
Memory Used 135,338k (± 0.06%) 133,927k (± 0.07%) -1,411k (- 1.04%) 133,687k 134,136k p=0.000 n=12
Memory Allocs 2,190,669 (± 0.01%) 2,190,141 (± 0.01%) -528 (- 0.02%) 2,189,697 2,190,653 p=0.000 n=12
Config Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Parse Time 0.041s (± 3.93%) 0.041s (± 3.39%) ~ 0.039s 0.045s p=0.572 n=12
Bind Time 0.012s (±11.13%) 0.013s (±15.30%) ~ 0.010s 0.019s p=0.442 n=12
Check Time 0.493s (± 1.03%) 0.502s (± 1.04%) +0.010s (+ 1.93%) 0.492s 0.524s p=0.013 n=12
Emit Time 0.266s (± 2.93%) 0.269s (± 2.66%) ~ 0.248s 0.284s p=0.369 n=12
Total Time 0.818s (± 1.19%) 0.832s (± 0.90%) +0.015s (+ 1.78%) 0.811s 0.847s p=0.012 n=12
angular-1 - native
Errors 3 3 ~ ~ ~ p=1.000 n=12
Symbols 845,791 (± 0.10%) 845,668 (± 0.05%) ~ 844,332 847,119 p=0.713 n=12
Types 248,126 (± 0.00%) 248,124 (± 0.00%) ~ 248,122 248,126 p=0.154 n=12
Memory Used 764,604k (± 0.02%) 750,472k (± 0.02%) -14,132k (- 1.85%) 750,142k 750,721k p=0.000 n=12
Memory Allocs 12,926,045 (± 0.09%) 12,911,604 (± 0.01%) -14,442 (- 0.11%) 12,908,798 12,918,710 p=0.000 n=12
Config Time 0.017s 0.017s ~ ~ ~ p=1.000 n=12
Parse Time 0.257s (± 3.01%) 0.259s (± 1.55%) ~ 0.245s 0.269s p=0.559 n=12
Bind Time 0.059s (± 4.29%) 0.059s (± 3.44%) ~ 0.057s 0.069s p=0.889 n=12
Check Time 0s 0s ~ ~ ~ p=1.000 n=12
Emit Time 1.564s (± 0.96%) 1.558s (± 1.05%) ~ 1.532s 1.633s p=0.173 n=12
Total Time 1.908s (± 0.98%) 1.905s (± 0.97%) ~ 1.882s 1.993s p=0.660 n=12
mui-docs - native
Errors 11,405 11,405 ~ ~ ~ p=1.000 n=12
Symbols 4,359,614 4,359,614 ~ ~ ~ p=1.000 n=12
Types 1,376,379 1,376,379 ~ ~ ~ p=1.000 n=12
Memory Used 2,872,408k (± 0.05%) 2,813,378k (± 0.04%) -59,031k (- 2.06%) 2,810,707k 2,817,220k p=0.000 n=12
Memory Allocs 32,834,662 (± 0.06%) 32,834,726 (± 0.11%) ~ 32,776,933 32,951,605 p=0.514 n=12
Config Time 0.017s 0.017s ~ ~ ~ p=1.000 n=12
Parse Time 0.490s (± 3.18%) 0.498s (± 1.87%) ~ 0.468s 0.522s p=0.580 n=12
Bind Time 0.002s 0.002s ~ ~ ~ p=1.000 n=12
Check Time 8.599s (± 0.52%) 8.641s (± 0.49%) ~ 8.518s 8.713s p=0.198 n=12
Emit Time 0.545s (±14.54%) 0.459s (± 7.50%) ~ 0.420s 0.575s p=0.369 n=12
Total Time 10.411s (± 0.73%) 10.385s (± 0.51%) ~ 10.218s 10.491s p=0.117 n=12
strada-build-src - native
Errors 0 0 ~ ~ ~ p=1.000 n=12
Symbols 1,390,229 1,390,229 ~ ~ ~ p=1.000 n=12
Types 441,498 441,498 ~ ~ ~ p=1.000 n=12
Memory Used 1,654,977k (± 0.82%) 1,635,393k (± 0.86%) -19,584k (- 1.18%) 1,606,760k 1,666,229k p=0.028 n=12
Memory Allocs 92,198,939 (± 0.16%) 92,089,944 (± 0.18%) ~ 91,488,261 92,327,860 p=0.219 n=12
Config Time 0.004s (± 9.04%) 0.003s (±12.31%) ~ 0.002s 0.004s p=0.733 n=12
Parse Time 0.197s (± 5.03%) 0.193s (± 4.21%) ~ 0.178s 0.218s p=0.486 n=12
Bind Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Check Time 1.793s (± 0.51%) 1.808s (± 0.46%) +0.015s (+ 0.81%) 1.783s 1.825s p=0.011 n=12
Emit Time 0.295s (± 3.76%) 0.310s (± 3.17%) ~ 0.291s 0.340s p=0.086 n=12
Total Time 23.173s (± 0.77%) 23.546s (± 0.79%) +0.373s (+ 1.61%) 22.996s 24.143s p=0.008 n=12
strada-compiler - native
Errors 0 0 ~ ~ ~ p=1.000 n=12
Symbols 334,953 334,953 ~ ~ ~ p=1.000 n=12
Types 197,616 197,616 ~ ~ ~ p=1.000 n=12
Memory Used 302,628k (± 0.04%) 297,337k (± 0.02%) -5,292k (- 1.75%) 297,194k 297,607k p=0.000 n=12
Memory Allocs 4,623,394 (± 0.01%) 4,620,754 (± 0.01%) -2,641 (- 0.06%) 4,619,083 4,621,509 p=0.000 n=12
Config Time 0.001s 0.001s ~ ~ ~ p=1.000 n=12
Parse Time 0.111s (± 5.37%) 0.110s (± 3.55%) ~ 0.100s 0.120s p=0.788 n=12
Bind Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Check Time 1.034s (± 0.51%) 1.039s (± 0.35%) ~ 1.031s 1.053s p=0.138 n=12
Emit Time 0.143s (± 9.59%) 0.136s (± 5.63%) ~ 0.120s 0.159s p=0.061 n=12
Total Time 1.345s (± 0.96%) 1.338s (± 0.80%) ~ 1.308s 1.371s p=0.103 n=12
ts-pre-modules - native
Errors 87 87 ~ ~ ~ p=1.000 n=12
Symbols 303,834 303,834 ~ ~ ~ p=1.000 n=12
Types 181,663 181,663 ~ ~ ~ p=1.000 n=12
Memory Used 263,563k (± 0.03%) 258,162k (± 0.01%) -5,401k (- 2.05%) 258,110k 258,242k p=0.000 n=12
Memory Allocs 1,615,965 (± 0.01%) 1,613,559 (± 0.01%) -2,406 (- 0.15%) 1,613,189 1,613,885 p=0.000 n=12
Config Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Parse Time 0.098s (± 2.82%) 0.100s (± 4.39%) ~ 0.085s 0.109s p=0.146 n=12
Bind Time 0.038s (± 9.10%) 0.039s (±11.73%) ~ 0.027s 0.048s p=0.680 n=12
Check Time 0.814s (± 0.71%) 0.816s (± 0.63%) ~ 0.802s 0.828s p=0.503 n=12
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Total Time 0.963s (± 0.70%) 0.968s (± 0.89%) ~ 0.945s 0.989s p=0.297 n=12
vscode - native
Errors 395 395 ~ ~ ~ p=1.000 n=12
Symbols 11,178,984 11,178,984 ~ ~ ~ p=1.000 n=12
Types 3,695,590 3,695,590 ~ ~ ~ p=1.000 n=12
Memory Used 6,740,328k (± 0.01%) 6,481,132k (± 0.01%) 🟩-259,196k (- 3.85%) 6,478,380k 6,483,083k p=0.000 n=12
Memory Allocs 55,027,552 (± 0.01%) 54,921,776 (± 0.02%) -105,776 (- 0.19%) 54,905,185 54,963,880 p=0.000 n=12
Config Time 0.075s (± 0.56%) 0.075s (± 0.24%) ~ 0.074s 0.075s p=0.784 n=12
Parse Time 1.822s (± 3.42%) 1.816s (± 3.31%) ~ 1.682s 1.986s p=0.671 n=12
Bind Time 0.613s (±16.45%) 0.619s (±19.84%) ~ 0.448s 0.951s p=0.919 n=12
Check Time 15.172s (± 1.07%) 14.914s (± 0.97%) -0.258s (- 1.70%) 14.613s 15.211s p=0.024 n=12
Emit Time 5.064s (±11.66%) 5.147s (± 9.15%) ~ 4.203s 5.980s p=0.875 n=12
Total Time 22.836s (± 2.13%) 22.662s (± 1.79%) ~ 21.782s 23.431s p=0.266 n=12
webpack - native
Errors 584 584 ~ ~ ~ p=1.000 n=12
Symbols 1,306,646 1,306,646 ~ ~ ~ p=1.000 n=12
Types 618,489 618,489 ~ ~ ~ p=1.000 n=12
Memory Used 967,360k (± 0.02%) 945,626k (± 0.02%) -21,734k (- 2.25%) 945,186k 946,190k p=0.000 n=12
Memory Allocs 6,628,579 (± 0.02%) 6,619,097 (± 0.01%) -9,482 (- 0.14%) 6,617,822 6,622,643 p=0.000 n=12
Config Time 0.009s 0.009s ~ ~ ~ p=1.000 n=12
Parse Time 0.285s (± 3.41%) 0.288s (± 2.84%) ~ 0.266s 0.310s p=0.384 n=12
Bind Time 0.088s (±17.78%) 0.079s (±11.11%) ~ 0.062s 0.101s p=0.541 n=12
Check Time 1.979s (± 0.54%) 1.957s (± 0.45%) -0.022s (- 1.10%) 1.931s 1.975s p=0.004 n=12
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Total Time 2.396s (± 0.65%) 2.369s (± 0.25%) -0.026s (- 1.10%) 2.354s 2.388s p=0.001 n=12
xstate-main - native
Errors 0 0 ~ ~ ~ p=1.000 n=12
Symbols 1,061,928 1,061,928 ~ ~ ~ p=1.000 n=12
Types 389,645 389,645 ~ ~ ~ p=1.000 n=12
Memory Used 571,326k (± 0.02%) 554,309k (± 0.02%) -17,017k (- 2.98%) 554,052k 554,537k p=0.000 n=12
Memory Allocs 4,891,140 (± 0.09%) 4,882,072 (± 0.07%) -9,068 (- 0.19%) 4,873,109 4,889,991 p=0.001 n=12
Config Time 0.003s 0.003s ~ ~ ~ p=1.000 n=12
Parse Time 0.141s (± 3.35%) 0.143s (± 4.04%) ~ 0.130s 0.164s p=0.681 n=12
Bind Time 0.053s (±16.81%) 0.047s (±17.11%) ~ 0.033s 0.075s p=0.271 n=12
Check Time 1.144s (± 0.65%) 1.129s (± 0.53%) -0.015s (- 1.31%) 1.113s 1.144s p=0.002 n=12
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Total Time 1.350s (± 0.95%) 1.334s (± 0.70%) -0.016s (- 1.17%) 1.304s 1.352s p=0.021 n=12
System info unknown
Hosts
  • native
Scenarios
  • Compiler-Unions - native
  • angular-1 - native
  • mui-docs - native
  • strada-build-src - native
  • strada-compiler - native
  • ts-pre-modules - native
  • vscode - native
  • webpack - native
  • xstate-main - native
Benchmark Name Iterations
Current pr 12
Baseline baseline 12

Developer Information:

Download Benchmarks

@github-project-automation github-project-automation Bot moved this from Not started to Needs merge in PR Backlog Oct 9, 2026
@typescript-automation

Copy link
Copy Markdown
Contributor

Anders Hejlsberg (@ahejlsberg) Here are the results of running the user tests with tsc comparing baseline and pr:

Everything looks good!

@ahejlsberg
Anders Hejlsberg (ahejlsberg) added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit aa81492 Oct 9, 2026
30 checks passed
@ahejlsberg
Anders Hejlsberg (ahejlsberg) deleted the dense-link-stores branch October 9, 2026 23:51
@github-project-automation github-project-automation Bot moved this from Needs merge to Done in PR Backlog Oct 9, 2026
@typescript-automation

Copy link
Copy Markdown
Contributor

Anders Hejlsberg (@ahejlsberg) Here are the results of running the top 400 repos with tsc comparing baseline and pr:

Everything looks good!

Can Bölük (can1357) added a commit to can1357/TypeScript that referenced this pull request Oct 16, 2026
- Moved nodeLinks, symbolReferenceLinks, mappedSymbolLinks and aliasSymbolLinks off pointer-keyed maps onto the id-indexed nodeLinkStore and symbolLinkStore, which keep exact Has/TryGet membership and stable pointers.
- Only node and symbol ids are shared across checkers; pages and entries stay per checker (checker -race clean).
- Measured on the original series, before microsoft#64711: Sentry user CPU -5% with one checker, -2.5% with four; peak RSS within noise.
Can Bölük (can1357) added a commit to can1357/TypeScript that referenced this pull request Oct 16, 2026
- PagedLinkStore pages now hold uint32 offsets into stable 1024-entry value blocks rather than pointers to arena entries, so the collector no longer scans the page arrays.
- Membership, zero initialization and returned addresses are unchanged; an explicit guard panics past 4,294,967,295 live entries per store.
- Measured on the original series, before microsoft#64711: Sentry, four checkers: retained heap -4.9%, peak RSS -12%, pointer page memory 501MB -> 220MB; single-threaded instructions with GC off drop on every measured app.
Can Bölük (can1357) added a commit to can1357/TypeScript that referenced this pull request Oct 16, 2026
- Fresh heap memory comes zeroed from the OS and is usually read before it is written (nil checks, lazily set fields). On Linux that first read maps the shared zero page, and the following write takes a second, copy-on-write fault whose TLB shootdown interrupts every other thread of the process.
- Arenas and paged link stores now write one byte to each page of a new chunk, so each page faults once, writable.
- Measured on the original series, before microsoft#64711: Playwright with 8 checkers on an 8-core Linux machine: TLB flushes 513k -> 99k, CPU time -22%, wall time -20%. With the rest of that series applied, removing it costs 19-36% wall time on Playwright, TypeORM and VS Code.
Can Bölük (can1357) added a commit to can1357/TypeScript that referenced this pull request Oct 16, 2026
- The merged-symbol table and eleven of the symbol link stores were Go maps keyed by symbol pointer. getMergedSymbol alone runs on nearly every symbol resolution, and each lookup hashed the pointer and probed a table of hundreds of thousands of entries.
- They are now symbolLinkStores indexed by dense symbol ids, like the other hot link kinds. ReverseMappedSymbolLinks stays a map: it is an exported field, which would expose the unexported store type. Merged-symbol lookups assign no ids to symbols that have none, as the map did not: unique symbols' internal property names embed ids.
- Measured on the original series, before microsoft#64711: interleaved runs on an 8-core Linux machine: Sentry -6% and VS Code -3% wall time with 4 checkers, Playwright -6% with 8; the other runs are within noise. Peak RSS rises by 1-2%.
Can Bölük (can1357) added a commit to can1357/TypeScript that referenced this pull request Oct 16, 2026
- Moved nodeLinks, symbolReferenceLinks, mappedSymbolLinks and aliasSymbolLinks off pointer-keyed maps onto the id-indexed nodeLinkStore and symbolLinkStore, which keep exact Has/TryGet membership and stable pointers.
- Only node and symbol ids are shared across checkers; pages and entries stay per checker (checker -race clean).
- Measured on the original series, before microsoft#64711: Sentry user CPU -5% with one checker, -2.5% with four; peak RSS within noise.
Can Bölük (can1357) added a commit to can1357/TypeScript that referenced this pull request Oct 16, 2026
- PagedLinkStore pages now hold uint32 offsets into stable 1024-entry value blocks rather than pointers to arena entries, so the collector no longer scans the page arrays.
- Membership, zero initialization and returned addresses are unchanged; an explicit guard panics past 4,294,967,295 live entries per store.
- Measured on the original series, before microsoft#64711: Sentry, four checkers: retained heap -4.9%, peak RSS -12%, pointer page memory 501MB -> 220MB; single-threaded instructions with GC off drop on every measured app.
Can Bölük (can1357) added a commit to can1357/TypeScript that referenced this pull request Oct 16, 2026
- Fresh heap memory comes zeroed from the OS and is usually read before it is written (nil checks, lazily set fields). On Linux that first read maps the shared zero page, and the following write takes a second, copy-on-write fault whose TLB shootdown interrupts every other thread of the process.
- Arenas and paged link stores now write one byte to each page of a new chunk, so each page faults once, writable.
- Measured on the original series, before microsoft#64711: Playwright with 8 checkers on an 8-core Linux machine: TLB flushes 513k -> 99k, CPU time -22%, wall time -20%. With the rest of that series applied, removing it costs 19-36% wall time on Playwright, TypeORM and VS Code.
Can Bölük (can1357) added a commit to can1357/TypeScript that referenced this pull request Oct 16, 2026
- The merged-symbol table and eleven of the symbol link stores were Go maps keyed by symbol pointer. getMergedSymbol alone runs on nearly every symbol resolution, and each lookup hashed the pointer and probed a table of hundreds of thousands of entries.
- They are now symbolLinkStores indexed by dense symbol ids, like the other hot link kinds. ReverseMappedSymbolLinks stays a map: it is an exported field, which would expose the unexported store type. Merged-symbol lookups assign no ids to symbols that have none, as the map did not: unique symbols' internal property names embed ids.
- Measured on the original series, before microsoft#64711: interleaved runs on an 8-core Linux machine: Sentry -6% and VS Code -3% wall time with 4 checkers, Playwright -6% with 8; the other runs are within noise. Peak RSS rises by 1-2%.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants