Repository navigation
Dense link stores - #64711
Dense link stores#64711
Conversation
|
TypeScript Bot (@typescript-bot) test it |
There was a problem hiding this comment.
🟡 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.
| 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 |
|
Hey Anders Hejlsberg (@ahejlsberg), the results of running the DT tests are ready. Everything looks the same! |
|
Anders Hejlsberg (@ahejlsberg) Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Anders Hejlsberg (@ahejlsberg) Here are the results of running the user tests with tsc comparing Everything looks good! |
|
Anders Hejlsberg (@ahejlsberg) Here are the results of running the top 400 repos with tsc comparing Everything looks good! |
- 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.
- 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.
- 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.
- 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%.
- 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.
- 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.
- 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.
- 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%.

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.