Skip to content

perf: stop a TypeScript run nothing will report once the next rebuild starts - #389

Closed
alexander-akait wants to merge 2 commits into
mainfrom
perf/ts-abort-stale-runs
Closed

alexander-akait wants to merge 2 commits into
mainfrom
perf/ts-abort-stale-runs

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Summary

A check that reportAs keeps off the compilation ("log", false, …) runs after a watch rebuild. When a newer rebuild starts, the older run's report is dropped. Until now the TypeScript worker still finished checking the whole program for that run, and only then started the run whose result is printed.

  • Generic: check instances get an optional cancel(). When a runner takes over a detached run of an older compilation (takeDetachedReport), it calls cancel() before waiting for that run to settle. Nothing prints that report any more.
  • TypeScript (threads on):
    • createSolo numbers each run and shares an Int32Array over a SharedArrayBuffer with the worker. cancel() marks every run issued so far as stopped. A message would wait behind the run it is meant to stop.
    • The worker passes a CancellationToken to the builder: syntactic, global, declaration and semantic diagnostics.
    • TypeScript stops between files with OperationCanceledException. The builder keeps what it got through, and the next run checks the rest. The cached declaration diagnostics are dropped, because which affected files were drained is lost with the run.
    • The cancelled run still reports what it reads and writes, so the watcher's lists stay right.
  • On webpack's own thread a run is synchronous, so there is nothing to stop.

Measurements

  • Setup: 600 .ts files (scratchpad/bench-heavy), watch mode, reportAs: "log", threads: "auto".
  • Edits: a global-scope file is edited, forcing a full recheck. 300 ms after that rebuild is done, it is edited again to add an error. The timer runs from the second edit until its error is logged.
edit → result process CPU
main 4186 / 4460 / 4230 / 4363 ms 13.3–15.5 s
this PR 2374 / 2404 / 2559 / 2268 ms 11.4–12.0 s

The logged report is identical on both: 601 errors, same hash after stripping the temp dir.

Tests

  • test/typescript/cancel.test.js:
    • It uses a TypeScript mock that stalls (up to 30 s) on a program holding a // stall file until the run is cancelled, and writes which way it ended.
    • The test edits that file in a watch rebuild and waits for the stale run to get stuck. Then it edits again, and asserts three things: the stale run ended by cancellation, its report was never printed, and the new error was logged.
    • With the Atomics.store in cancel() removed, the test fails with 'timed out' !== 'cancelled'.
  • It is skipped where countThreads("auto") < 2, as the check only has a worker of its own there.
  • Lint, tsc, schema lint, prettier, cspell and the full suite (331 tests) pass locally.

Notes

This touches typescript-program.js check(), which #383 and #379 also change, so whichever lands second needs a small merge.


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • TypeScript checks running in the background can be stopped when a newer watch rebuild starts. Completed work is retained, and remaining checks can resume.
  • Bug Fixes
    • Prevented outdated check results from being reported after a newer rebuild.
  • Documentation
    • Updated the watch-mode example with the revised timing and behavior for edits made during a long-running check.

… starts

A check that reportAs keeps off the compilation runs after a watch
rebuild. When the next rebuild starts, its report is dropped, yet the
TypeScript worker kept checking the whole program before it would
start the run that matters. The plugin now tells a check to stop a run
of an older compilation as soon as it takes over from it, through an
optional cancel() on the check instance. The TypeScript check passes a
CancellationToken backed by a SharedArrayBuffer to its worker, so the
run stops between one file and the next and the builder keeps what it
got through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf
@linux-foundation-easycla

linux-foundation-easycla Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

CLA Not Signed

One or more co-authors of this pull request were not found. You must specify co-authors in commit message trailer via:

Co-authored-by: name <email>

Supported Co-authored-by: formats include:

  1. Anything <id+login@users.noreply.github.com> - it will locate your GitHub user by id part.
  2. Anything <login@users.noreply.github.com> - it will locate your GitHub user by login part.
  3. Anything <public-email> - it will locate your GitHub user by public-email part. Note that this email must be made public on Github.
  4. Anything <other-email> - it will locate your GitHub user by other-email part but only if that email was used before for any other CLA as a main commit author.
  5. login <any-valid-email> - it will locate your GitHub user by login part, note that login part must be at least 3 characters long.

Alternatively, if the co-author should not be included, remove the Co-authored-by: line from the commit message.

Please update your commit message(s) by doing git commit --amend and then git push [--force] and then request re-running CLA check via commenting on this pull request:

/easycla

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 5 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ee035eff-f3f0-46ee-a2dc-10c3981660bf
📥 Commits

Reviewing files that changed from the base of the PR and between 6d8905b and 84b755e.

📒 Files selected for processing: 3
  • src/check.js
  • test/store.test.js
  • types/check.d.ts

Walkthrough

The changes add cancellation for TypeScript checks running in a worker. New runs can mark older runs as stopped, and TypeScript diagnostic queries receive a cancellation token. Detached reports request cancellation and retain a promise that settles after the report completes. A test exercises cancellation during watch builds with a stalling TypeScript mock. The README and changeset describe the behavior.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to 6d890

A compiler sharing a cancelled TypeScript check can miss diagnostics. Prevent cancelled results from reaching other compilers before merging.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main change: canceling a stale TypeScript run when the next rebuild starts. It is specific and related to the pull request objective.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 13 files. (2 skipped: …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 13 files. (2 skipped: 2 unsupported.)

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5d97d564-c8c9-4b66-a709-eae767cb54c4
📥 Commits

Reviewing files that changed from the base of the PR and between 0abded0 and 6d8905b.

📒 Files selected for processing: 15
  • .changeset/ts-stop-stale-runs.md
  • README.md
  • src/check.js
  • src/checks/index.js
  • src/checks/typescript-program.js
  • src/checks/typescript-worker.js
  • src/checks/typescript.js
  • src/threads.js
  • test/typescript/cancel.test.js
  • test/typescript/fixtures/cancel/index.js
  • test/typescript/mock/typescript-stall/index.js
  • types/checks/index.d.ts
  • types/checks/typescript-program.d.ts
  • types/checks/typescript-worker.d.ts
  • types/threads.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

Comment thread src/check.js Resolved
Two compilers linting the same files at once share one run. Stopping it
for the compiler whose compilation is stale would have handed the other
one an empty answer. A run another compiler joined is now left to
finish, and a run that is stopped is taken out of the shared runs, so a
compiler coming later starts its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf

Copy link
Copy Markdown
Member Author

EasyCLA fails because the commits carry a Co-Authored-By: Claude trailer that maps to no GitHub account. It is not a code failure. Whether to drop the trailer or accept the check is for the maintainers to decide, so I haven't rewritten history.


Generated by Claude Code

@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.64%. Comparing base (0abded0) to head (84b755e).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #389   +/-   ##
=======================================
  Coverage   99.64%   99.64%           
=======================================
  Files          19       19           
  Lines        5618     5706   +88     
=======================================
+ Hits         5598     5686   +88     
  Misses         20       20           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

alexander-akait added a commit that referenced this pull request Oct 29, 2026
…red TypeScript runs (#392)

* perf: watch rebuilds, TypeScript 7, and shared or stopped TypeScript runs

One change in place of #371, #384, #387, #389, #390 and #391.

Core:
- One place keeps what lives as long as a compiler (src/compiler-state.js):
  keptFor() for state kept per compiler (results, detached reports, the
  import graph, the TypeScript program), and acquire()/release() for the
  workers and processes compilers share, started once per key and stopped
  when the last compiler using them shuts down or closes its watch. The
  TypeScript check's worker and TypeScript 7's process both go through it,
  in place of a lifecycle of their own each.
- lintDependentsAfterBuild (opt-in): a watch rebuild of a check that reads
  across files lints only what webpack rebuilt, and what the change
  reaches after the build.
- A check can stop a run of an older compilation nothing will report
  (cancel()); one another compiler joined is never stopped.
- Biome and oxlint are handed every file at once rather than in batches.

TypeScript:
- TypeScript 7 runs through the API it ships, in a process of its own
  kept open while webpack watches; the import graph answers without a
  graph there.
- A stale run stops between files; compilers checking the same project
  alike share one worker and one program.

Nothing is listed by hand where the tool can say it: assets and the
scripts a config file picks up are told apart by the extensions
TypeScript reads, the fallback resolution mode and the error category
come from the loaded TypeScript, and each run carries its own stop flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf

* test: cover the TypeScript 7 check's error paths rather than ignore them

The line under a diagnostic is printed from the text the native compiler
checked (Program.getSourceFile) rather than read from disk again, and a
file gone since webpack saw it is told by statSync without throwing, so
neither needs a catch. A project with no tsconfig.json above it is now
tested from a directory outside this repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf

* fix: match the files webpack saw change against the native compiler's on Windows

webpack names a changed file with the platform's separators, the native
compiler with forward slashes, and Windows tells no case apart, so both
are now compared by the same key. The cancel test accepts a run already
stopped by the time it looks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf

* fix: take turns on a shared TypeScript 7 process, and forget a stopped run

Compilers checking the same project share the native compiler's process,
and a run replaces the snapshot and disposes of the last one, which a run
for another compiler could still be reading: runs on one process now take
turns. A run stopped for a newer compilation found nothing, so it is no
longer kept as what a rebuild the program does not see is answered with.
A directory no config file covers is now tested in the import graph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf

* refactor: check TypeScript 7 through its 7.1 API, and drop what did not pay off

TypeScript 7.1 ships its API as typescript/async, makes snapshots with
createSnapshot and snapshot.update, and formats diagnostics itself. The
native check now uses that API, found by what the package exports rather
than by its version, and TypeScript's own formatter in place of one
written here. TypeScript 7.0's API was an unstable preview with none of
these calls; a TypeScript offering no API to check with now says so.

Measured again on this branch, two additions did not earn their place:
- lintDependentsAfterBuild finished the build sooner but printed an
  error it found in an importer no sooner (1.96-2.64 s against
  2.09-2.21 s without it), at the cost of an option and a second report.
- cancel() on the checker API only ever served a TypeScript 6 check on
  a worker reporting as advisory, and TypeScript 7 does not use it.
Both are removed, with what they needed: a stop flag per worker run,
bookkeeping of which runs another compiler joined, and their tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf

* fix: watch the config files a TypeScript 7 project extends, and let a run finish before closing

The native check reported only the root config file to the watcher, so an
edit to a config file it extends, outside the folder watched, went
unnoticed unless webpack's own tsconfig resolution happened to watch it.
It now reports every config file the program names (getConfigFileNames),
and tells the compiler of a change to one as a change rather than a new
file. Closing the native process now waits for a run still going.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01App18sYbe6Vf6HDCjCXbvf

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants