Skip to content

Compute signatures of dependent files in parallel on incremental rebuilds - #64659

Open
Gadzhi Gadzhiev (resure) wants to merge 6 commits into
microsoft:mainfrom
resure:fix/parallel-signature-computation
Open

Gadzhi Gadzhiev (resure) wants to merge 6 commits into
microsoft:mainfrom
resure:fix/parallel-signature-computation

Conversation

@resure

@resure Gadzhi Gadzhiev (resure) commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

After an edit to a widely imported file, the incremental builder computes the declaration signature of every file that references it, to find what is affected. It did that one file at a time, so this phase ran on about one core. Each referencing file is now its own task in the existing work group.

This is the second half of #64469 (for #64464), which removed the checker scan that took most of the time.

Most of this change was generated with AI tooling; I have hit this problem myself and have reviewed the result.

The 6,000-leaf reproducer from #64464, first rebuild after a comment-only edit to hub.ts with noEmit and incremental. 32-core Linux, median of 5 runs, main already includes #64469.

wall time CPU peak RSS
main 3.4 s 206% 368 MiB
this PR 1.5 s 524% 370 MiB

The tsbuildinfo after the rebuild is byte-identical to main's. The gain is smaller on a real project: on n8n's packages/nodes-base the same kind of edit to a file that 433 files reference goes from 5.4 s to 4.5 s, and an edit to its most referenced file takes about 2.5 s either way.

Signatures in flight are capped at GOMAXPROCS to keep memory down: after an edit to all 6,000 leaves the rebuild peaks at 377 MiB against 456 MiB on main, in the same 1.5 s. The cap lives in the work group. core.NewWorkGroup now takes a concurrency instead of singleThreaded (0 = unlimited), which is why so many call sites change. The existing ones pass 1 or 0 and behave as before, only the two groups in collectAllAffectedFiles get a cap. It is per group, so tsc -b building several projects at once can exceed it in total. main has no cap there at all.

The speedup also depends on the number of checkers, since a signature needs its file's checker. The default is four, and with --checkers 8 the rebuild takes 1.2 s. There is nothing to gain under isolatedModules, where referencing files are not walked.

Separately, updateShapeSignature returned false when another traversal had already computed the file's signature. It now returns whether the signature changed. On main the old answer makes the result depend on which changed file is processed first: in a case like the one in TestUpdateShapeSignatureCachedResult, with --singleThreaded 20 of 40 runs wrote a different tsbuildinfo than the rest, and with assumeChangesOnlyAffectDirectDependencies 23 of 40 missed the resulting error. This does not depend on the parallelism, so I can split it out if that is easier to review.

After a shared dependency changes, the incremental builder computed the
declaration signatures of the files referencing it one at a time. Each
referencing file now gets its own task in the work group, with at most
GOMAXPROCS signatures in flight so peak memory stays where it was.

The cached path of updateShapeSignature returned false when another
traversal had already computed the file's signature, which made the
result depend on which changed file was processed first: a changed file
that affects the global scope could fail to invalidate every file. It
now reports whether that signature changed.
Copilot AI balanced review requested due to automatic review settings October 6, 2026 20:42
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Oct 6, 2026
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Oct 6, 2026
@typescript-automation

Copy link
Copy Markdown
Contributor

This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise.

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.

Copilot review overview

🟡 Changes recommended

Root changed-file signature computations bypass the new concurrency limit, allowing unbounded concurrent declaration emits.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Parallelizes incremental declaration-signature computation while preserving deterministic invalidation behavior.

Changes:

  • Queues dependent signature computations concurrently with deduplication.
  • Corrects cached signature-change reporting.
  • Adds regression tests, baselines, and a performance benchmark.
File Description
tsc/​internal/​execute/​incremental/​affectedfileshandler.go Implements parallel signature traversal and caching changes.
tsc/​internal/​execute/​incremental/​affectedfileshandler_test.go Adds an incremental rebuild benchmark.
tsc/​internal/​execute/​incremental/​affectedfileshandler_internal_test.go Tests cached signature-change behavior.
tsc/​internal/​execute/​tsctests/​tsc_test.go Adds incremental and watch regression scenarios.
tsc/​testdata/​baselines/​reference/​tsc/​incremental/​shared-dependency-with-inferred-types.js Records incremental compiler output.
tsc/​testdata/​baselines/​reference/​tscWatch/​incremental/​shared-dependency-with-inferred-types.js Records watch-mode output.

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

Comment thread tsc/internal/execute/incremental/affectedfileshandler.go
The limit only covered files reached through references, so a rebuild
with many directly changed files still started all of their declaration
emits at once. Take the semaphore inside computeDtsSignature instead.

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.

Copilot review overview

🔵 Needs a closer look

The concurrency-sensitive compiler traversal warrants final human review despite comprehensive regression coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@weswigham Wesley Wigham (weswigham) left a comment •

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.

It's cute to use the existing workgroup for this - but actually, if we need to limit parallelism for performance, why not limit the original workgroup size instead of taking a semaphore for just this path? Feels like we should be able to swap the singleThreaded parameter to a concurrency parameter that takes a max-parallel-threads count (0=infinity, I suppose) and do the limiting in the core concurrency construct.

@resure

Copy link
Copy Markdown
Contributor Author

Makes sense, done in 7f0e5e1. NewWorkGroup now takes concurrency instead of singleThreaded (0 = unlimited) and the semaphore is gone. Only the two groups in collectAllAffectedFiles get a limit (GOMAXPROCS), the rest pass 1 or 0, so nothing changes for them.

The limit is per group now, not process-wide, so with tsc -b building several projects at once there can be more than GOMAXPROCS signatures running in total. main has no limit there at all, so I left it that way.

Time and memory on the reproducer are the same as with the semaphore, about 1.5 s and 375 MiB.

@weswigham Wesley Wigham (weswigham) left a comment

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.

I think this looks good, but Andrew Branch (@andrewbranch) should also take a look.

We also don't actually have a benchmark that this inherently helps with, but I will throw out a
TypeScript Bot (@typescript-bot) test this
just in case.

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

typescript-automation Bot commented Oct 19, 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

@typescript-automation

Copy link
Copy Markdown
Contributor

Hey Wesley Wigham (@weswigham), 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

Wesley Wigham (@weswigham)
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 113,525k (± 0.07%) 113,479k (± 0.04%) ~ 113,337k 113,590k p=0.843 n=12
Memory Allocs 2,212,520 (± 0.00%) 2,212,547 (± 0.01%) ~ 2,212,350 2,212,986 p=0.843 n=12
Config Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Parse Time 0.049s (± 3.36%) 0.049s (± 2.29%) ~ 0.046s 0.051s p=0.309 n=12
Bind Time 0.018s (± 9.45%) 0.016s (± 9.27%) 🟩-0.003s (-15.32%) 0.014s 0.021s p=0.015 n=12
Check Time 0.564s (± 0.71%) 0.564s (± 0.63%) ~ 0.554s 0.576s p=0.680 n=12
Emit Time 0.330s (± 3.21%) 0.325s (± 3.35%) ~ 0.298s 0.350s p=0.599 n=12
Total Time 0.969s (± 1.22%) 0.962s (± 1.01%) ~ 0.936s 0.988s p=0.523 n=12
angular-1 - native
Errors 3 3 ~ ~ ~ p=1.000 n=12
Symbols 847,238 (± 0.05%) 847,176 (± 0.08%) ~ 844,995 848,588 p=1.000 n=12
Types 247,033 (± 0.00%) 247,032 (± 0.00%) ~ 247,030 247,042 p=0.491 n=12
Memory Used 596,416k (± 0.03%) 596,366k (± 0.02%) ~ 596,067k 596,665k p=0.713 n=12
Memory Allocs 13,150,823 (± 0.18%) 13,141,003 (± 0.03%) ~ 13,135,745 13,157,472 p=0.887 n=12
Config Time 0.017s (± 1.70%) 0.017s (± 1.70%) ~ 0.016s 0.017s p=1.000 n=12
Parse Time 0.223s (± 2.95%) 0.225s (± 2.28%) ~ 0.212s 0.239s p=0.434 n=12
Bind Time 0.081s (±12.07%) 0.077s (±14.06%) ~ 0.060s 0.115s p=0.354 n=12
Check Time 0s 0s ~ ~ ~ p=1.000 n=12
Emit Time 1.555s (± 1.57%) 1.551s (± 1.39%) ~ 1.495s 1.617s p=0.899 n=12
Total Time 1.896s (± 1.69%) 1.888s (± 1.43%) ~ 1.819s 1.956s p=0.561 n=12
mui-docs - native
Errors 11,405 11,405 ~ ~ ~ p=1.000 n=12
Symbols 4,331,983 4,331,983 ~ ~ ~ p=1.000 n=12
Types 1,370,520 1,370,520 ~ ~ ~ p=1.000 n=12
Memory Used 2,626,229k (± 0.05%) 2,625,108k (± 0.06%) ~ 2,621,913k 2,629,965k p=0.143 n=12
Memory Allocs 27,707,410 (± 0.04%) 27,722,147 (± 0.12%) ~ 27,656,505 27,827,028 p=0.713 n=12
Config Time 0.017s (± 1.45%) 0.017s (± 1.45%) ~ 0.016s 0.017s p=1.000 n=12
Parse Time 0.480s (± 1.80%) 0.482s (± 2.15%) ~ 0.459s 0.519s p=1.000 n=12
Bind Time 0.002s 0.002s ~ ~ ~ p=1.000 n=12
Check Time 7.265s (± 0.76%) 7.255s (± 0.44%) ~ 7.199s 7.342s p=0.943 n=12
Emit Time 0.512s (±12.96%) 0.511s (±11.39%) ~ 0.416s 0.658s p=0.766 n=12
Total Time 9.036s (± 0.47%) 8.992s (± 0.91%) ~ 8.733s 9.145s p=0.478 n=12
strada-build-src - native
Errors 0 0 ~ ~ ~ p=1.000 n=12
Symbols 1,388,680 1,388,680 ~ ~ ~ p=1.000 n=12
Types 441,862 441,862 ~ ~ ~ p=1.000 n=12
Memory Used 1,315,836k (± 0.74%) 1,314,643k (± 0.40%) ~ 1,303,370k 1,333,004k p=0.671 n=12
Memory Allocs 78,192,522 (± 0.16%) 78,177,318 (± 0.15%) ~ 77,816,744 78,382,650 p=0.799 n=12
Config Time 0.004s (±11.30%) 0.004s (±12.82%) ~ 0.003s 0.006s p=0.742 n=12
Parse Time 0.308s (± 2.33%) 0.306s (± 2.13%) ~ 0.296s 0.328s p=0.660 n=12
Bind Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Check Time 2.003s (± 0.33%) 2.012s (± 0.55%) ~ 1.980s 2.044s p=0.325 n=12
Emit Time 0.355s (± 3.95%) 0.361s (± 3.97%) ~ 0.325s 0.400s p=0.723 n=12
Total Time 23.804s (± 0.56%) 23.963s (± 0.36%) +0.159s (+ 0.67%) 23.669s 24.169s p=0.023 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 247,555k (± 0.03%) 247,529k (± 0.03%) ~ 247,408k 247,702k p=0.619 n=12
Memory Allocs 4,329,973 (± 0.01%) 4,329,666 (± 0.01%) ~ 4,328,355 4,331,098 p=0.242 n=12
Config Time 0.001s 0.001s ~ ~ ~ p=1.000 n=12
Parse Time 0.114s (± 4.39%) 0.113s (± 4.78%) ~ 0.101s 0.128s p=0.854 n=12
Bind Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Check Time 0.963s (± 0.52%) 0.959s (± 0.43%) ~ 0.948s 0.972s p=0.223 n=12
Emit Time 0.150s (± 7.27%) 0.152s (± 4.61%) ~ 0.132s 0.171s p=0.723 n=12
Total Time 1.284s (± 0.95%) 1.280s (± 0.89%) ~ 1.248s 1.305s p=0.600 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 215,230k (± 0.03%) 215,258k (± 0.02%) ~ 215,192k 215,374k p=0.876 n=12
Memory Allocs 1,401,651 (± 0.02%) 1,401,529 (± 0.02%) ~ 1,400,816 1,402,241 p=0.843 n=12
Config Time 0.000s 0.000s (±217.90%) ~ 0.000s 0.001s p=1.000 n=12
Parse Time 0.096s (± 2.93%) 0.096s (± 3.53%) ~ 0.089s 0.106s p=0.853 n=12
Bind Time 0.039s (± 9.01%) 0.041s (± 9.15%) ~ 0.031s 0.050s p=0.578 n=12
Check Time 0.741s (± 0.63%) 0.740s (± 0.64%) ~ 0.733s 0.753s p=0.920 n=12
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Total Time 0.892s (± 0.30%) 0.892s (± 0.58%) ~ 0.880s 0.903s p=0.598 n=12
vscode - native
Errors 396 396 ~ ~ ~ p=1.000 n=12
Symbols 12,420,498 12,420,498 ~ ~ ~ p=1.000 n=12
Types 4,065,846 4,065,846 ~ ~ ~ p=1.000 n=12
Memory Used 5,826,310k (± 0.02%) 5,826,166k (± 0.02%) ~ 5,824,366k 5,829,787k p=0.887 n=12
Memory Allocs 59,303,049 (± 0.03%) 59,299,628 (± 0.02%) ~ 59,284,854 59,334,865 p=0.932 n=12
Config Time 0.076s (± 0.37%) 0.076s ~ ~ ~ p=0.217 n=12
Parse Time 1.793s (± 1.53%) 1.822s (± 3.07%) ~ 1.729s 1.975s p=0.965 n=12
Bind Time 0.772s (±11.01%) 0.705s (±13.37%) ~ 0.511s 0.914s p=0.319 n=12
Check Time 15.030s (± 0.71%) 15.112s (± 0.90%) ~ 14.872s 15.377s p=0.401 n=12
Emit Time 5.577s (± 5.92%) 5.322s (± 5.19%) ~ 4.682s 5.769s p=0.235 n=12
Total Time 23.340s (± 1.18%) 23.125s (± 0.79%) ~ 22.666s 23.439s p=0.124 n=12
webpack - native
Errors 598 598 ~ ~ ~ p=1.000 n=12
Symbols 1,398,880 1,398,880 ~ ~ ~ p=1.000 n=12
Types 674,795 674,795 ~ ~ ~ p=1.000 n=12
Memory Used 887,239k (± 0.01%) 887,473k (± 0.02%) +234k (+ 0.03%) 887,052k 887,977k p=0.017 n=12
Memory Allocs 6,938,712 (± 0.04%) 6,938,072 (± 0.03%) ~ 6,935,913 6,945,732 p=1.000 n=12
Config Time 0.009s 0.009s ~ ~ ~ p=1.000 n=12
Parse Time 0.313s (± 3.59%) 0.307s (± 3.15%) ~ 0.293s 0.340s p=0.385 n=12
Bind Time 0.081s (± 8.58%) 0.081s (± 9.45%) ~ 0.070s 0.113s p=0.921 n=12
Check Time 2.682s (± 0.38%) 2.676s (± 0.48%) ~ 2.653s 2.714s p=0.370 n=12
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Total Time 3.124s (± 0.31%) 3.111s (± 0.52%) ~ 3.063s 3.145s p=0.417 n=12
xstate-main - native
Errors 0 0 ~ ~ ~ p=1.000 n=12
Symbols 1,060,810 1,060,810 ~ ~ ~ p=1.000 n=12
Types 389,671 389,671 ~ ~ ~ p=1.000 n=12
Memory Used 471,807k (± 0.02%) 471,858k (± 0.03%) ~ 471,609k 472,265k p=0.514 n=12
Memory Allocs 4,219,996 (± 0.07%) 4,218,673 (± 0.06%) ~ 4,210,489 4,223,674 p=0.443 n=12
Config Time 0.003s (±11.61%) 0.003s (± 6.23%) ~ 0.002s 0.003s p=0.317 n=12
Parse Time 0.131s (± 2.52%) 0.136s (± 3.35%) ~ 0.124s 0.146s p=0.091 n=12
Bind Time 0.054s (±12.43%) 0.043s (±16.68%) 🟩-0.010s (-19.31%) 0.034s 0.065s p=0.027 n=12
Check Time 1.086s (± 0.54%) 1.085s (± 0.95%) ~ 1.068s 1.113s p=0.468 n=12
Emit Time 0.000s 0.000s ~ ~ ~ p=1.000 n=12
Total Time 1.283s (± 0.67%) 1.274s (± 0.88%) ~ 1.245s 1.296s p=0.385 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

@typescript-automation

Copy link
Copy Markdown
Contributor

Wesley Wigham (@weswigham) Here are the results of running the user tests with tsc comparing baseline and pr:

Everything looks good!

@typescript-automation

Copy link
Copy Markdown
Contributor

Wesley Wigham (@weswigham) Here are the results of running the top 400 repos with tsc comparing baseline and pr:

Everything looks good!

Comment on lines +20 to +21
// A concurrency of 1 runs the functions on the goroutine that calls RunAndWait; 0 means no limit.
func NewWorkGroup(concurrency int) WorkGroup {

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.

Why is all of this change needed? Why can't we just do a parallel work group and then use a bounded semaphore where needed?

@weswigham Wesley Wigham (weswigham) Oct 19, 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.

Because "where needed" depends on the resource you're trying to limit based on - when that resource is "system cores", "where needed" is everywhere the workgroup is used, so for a case like this one, which reuses the same workgroup at multiple task levels, requires threading a semaphore to a bunch of places, alongside the workgroup itself.

It's just nicer to build the concurrency limiter into the concurrency abstraction itself, imo, and the existing singleThreaded parameter was already a concurrency limit - just not as configurable as needed.

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.

🔵 Needs a closer look

Concurrent incremental invalidation warrants human review supported by race-enabled testing and rebuild-output comparisons.

No open issues 🎉

Review effort: Balanced

@jakebailey Jake Bailey (jakebailey) left a comment

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.

I guess it's fine, just feels at least a little inconsistent with other parts of the codebase that maybe could make use of this.

This branch has not been deployed

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

Labels

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

Projects

Status: Needs merge

Development

Successfully merging this pull request may close these issues.

5 participants