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
@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.

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: Not started

Development

Successfully merging this pull request may close these issues.

4 participants