Skip to content

perf(core): cache dead-code results so unchanged rescans skip the analysis#1053

Merged
rayhanadev merged 1 commit into
mainfrom
ray/perf-dead-code-cache
Jul 4, 2026
Merged

perf(core): cache dead-code results so unchanged rescans skip the analysis#1053
rayhanadev merged 1 commit into
mainfrom
ray/perf-dead-code-cache

Conversation

@rayhanadev

@rayhanadev rayhanadev commented Jul 4, 2026

Copy link
Copy Markdown
Member

Why

Dead-code analysis re-derives the whole import graph from scratch on every scan — measured at ~9s of a ~19.8s warm rescan on getsentry/sentry (8,899 TS/TSX files), the single largest uncached component. The whole-repo scan cache covers the nothing-changed case, but any miss (dirty tree today, any config/flag difference) pays the full pass again.

Before:

warm rescan, dirty tree, nothing dead-code reads changed: ~26.8s

After:

same rescan: ~17.8s — dead-code cache HIT, worker never spawns (~9.0s saved)
touch one .tsx: MISS → fresh analysis → re-stored (correct invalidation)

What changed

  • New single-entry result cache (dead-code-result-cache.ts): dead-code reachability is a whole-project property, so one entry keyed by a fingerprint over everything the analysis reads — sorted (path, mtime, size) stats of all deslop-parsed extensions, the manifests it consults (package.json/knip/expo/workspace files, all lockfiles as the node_modules proxy, .gitignore at every level), tsconfig/jsconfig anywhere (extends chains), resolved entry/ignore patterns, and the deslop-js version + schema constant. Additions/deletions invalidate via the hashed path list; the mtime+size blind spot is the documented parse-source-file precedent.
  • A hit replays diagnostics without spawning the 8GB analysis worker or taking a worker slot; only complete successful passes store (crash/timeout/abort reject before the store — the shouldStoreScanPayload philosophy). Reads are schema-validated and fail open.
  • Off by default at the checkDeadCode API level (direct callers/tests keep fresh semantics); the DeadCode service enables it via a new DeadCodeResultCacheEnabled Reference, disabled by REACT_DOCTOR_NO_CACHE or granular REACT_DOCTOR_NO_DEAD_CODE_CACHE.
  • Telemetry mirrors the lintCacheHitRatio precedent exactly: deadCode.cacheHit threaded outside CachedScanPayload (null on whole-repo hits and when no cache ran) — no scan-cache schema bump.
  • DeadCodeOverlap semantics untouched.

Test plan

  • packages/core: 1158 passed (baseline 1145; +13 new — key stability/sensitivity for touch/add/delete/manifest/tsconfig/config, hit-skips-worker spy, store-barred-on-failure, NO_CACHE bypass); packages/react-doctor: 2124 / 26 unchanged. Root typecheck + lint + smoke:json-report clean.
  • Sentry e2e: run1 cold stores; run2 warm hits (cache-file mtime proof) with sorted diagnostic tuples identical to run1 (all 262 dead-code findings replayed exactly); run3 after touching one source file misses and re-stores.
  • Deliberately not covered (documented): CI fresh checkouts (mtimes reset), same-size-same-mtime edits, node_modules surgery that skips the lockfile.

🤖 Generated with Claude Code


Note

Low Risk
Performance-only caching with fail-open reads, schema validation, stat-based invalidation, and no storage on failed passes; wrong replays are unlikely and env vars allow disabling.

Overview
Warm rescans can skip dead-code analysis when nothing the pass reads has changed. A new single-entry on-disk cache stores the last complete dead-code diagnostic set, keyed by a fingerprint of the analyzed source tree (path/mtime/size stats), manifests, lockfiles, tsconfigs, knip/entry/ignore config, and deslop-js version. A hit returns cached diagnostics without spawning the heavy analysis worker; a miss runs analysis and persists the result only on success (crashes/timeouts/aborts never write).

Caching is on by default for normal scans via DeadCodeResultCacheEnabled, with opt-out through REACT_DOCTOR_NO_CACHE or REACT_DOCTOR_NO_DEAD_CODE_CACHE. Direct checkDeadCode callers still default cache off so tests keep fresh semantics. Telemetry adds deadCode.cacheHit (and InspectResult.deadCodeCacheHit), aligned with the lint cache pattern; lint failure clears the dead-code cache signal so outcomes do not leak.

Reviewed by Cursor Bugbot for commit e741684. Bugbot is set up for automated code reviews on this repo. Configure here.

…lysis

Dead-code re-analyzed the whole import graph every run (~9s of a warm
sentry rescan). A stat-fingerprint over the analyzed file set, manifests,
tsconfigs, and the analyzer version now keys a single-entry result cache;
a hit replays the diagnostics without spawning the worker, and only
complete successful passes are stored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 4, 2026 00:47

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@pkg-pr-new

pkg-pr-new Bot commented Jul 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@1053
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@1053
npm i https://pkg.pr.new/react-doctor@1053

commit: e741684

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +50 to +88
const ANALYZED_FILE_EXTENSIONS = new Set([
".ts",
".tsx",
".js",
".jsx",
".mts",
".mjs",
".cts",
".cjs",
".mdx",
".astro",
".graphql",
".gql",
".css",
".scss",
".vue",
".svelte",
]);

// Non-source files deslop's entry/workspace/dependency analysis reads:
// manifests (workspace layout, dependency declarations, knip/expo config),
// lockfiles (the proxy for installed `node_modules` metadata — deslop reads
// installed packages' bin/peer fields, which only change through an install
// that rewrites the lockfile), and `.gitignore` files at every level (deslop
// consults git's ignore state for its walk).
const ANALYZED_MANIFEST_NAMES = new Set([
"package.json",
"knip.json",
"app.json",
"pnpm-workspace.yaml",
"lerna.json",
"pnpm-lock.yaml",
"package-lock.json",
"yarn.lock",
"bun.lock",
"bun.lockb",
"deno.lock",
".gitignore",
]);

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.

🚩 Cache fingerprint file set must stay in sync with deslop's actual file walk

The ANALYZED_FILE_EXTENSIONS set at packages/core/src/dead-code/dead-code-result-cache.ts:50-67 and ANALYZED_MANIFEST_NAMES at lines 75-88 define which files contribute to the cache key. If deslop adds support for new file extensions (e.g., .elm, .res) or starts reading additional manifest files, the cache won't invalidate on changes to those files — silently replaying stale diagnostics. There's no automated mechanism to keep these sets synchronized with deslop's DEFAULT_EXTENSIONS or its workspace/dependency resolution inputs. This is an accepted maintenance burden (the comment on line 48-49 documents the intent), but a deslop version bump that adds extensions would need a corresponding update here or a DEAD_CODE_CACHE_SCHEMA_VERSION bump to force invalidation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in #1056: the extension and manifest lists are now imported from a new dependency-free deslop-js/analyzed-inputs subpath whose constants deslop's own readers consume — the copies can no longer drift (and indeed were already missing ng-package.json and pnpm-workspace.yml).

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e741684. Configure here.

files: collectAnalyzedFileFingerprints(input.rootDirectory),
}),
)
.digest("hex");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cache key omits react-doctor version

Medium Severity

The dead-code result fingerprint includes deslopVersion and DEAD_CODE_CACHE_SCHEMA_VERSION but not the installed @react-doctor/core version. Cached entries store fully materialized diagnostics after checkDeadCode post-processing (for example REACT_DOCTOR_TOOLCHAIN_PACKAGES filtering and message text). After upgrading react-doctor with an unchanged source tree, the key can still match and replay stale dead-code findings while the rest of the scan uses new logic.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e741684. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in #1056: CORE_PACKAGE_VERSION (build-time-injected, mirroring the CLI's VERSION mechanism) now joins the cache key, so a react-doctor upgrade invalidates post-processed cached diagnostics even on an unchanged tree.

@rayhanadev rayhanadev merged commit e257a5e into main Jul 4, 2026
26 of 29 checks passed
rayhanadev added a commit that referenced this pull request Jul 4, 2026
…ist drift (#1056)

Addresses the two review findings on #1053: the fingerprint's extension and
manifest lists are now imported from a new dependency-free
deslop-js/analyzed-inputs subpath (single source of truth with the analyzer's
own readers — the hand-copies were already missing ng-package.json and
pnpm-workspace.yml), and the core package version joins the key so an
upgrade invalidates post-processed cached diagnostics.

Co-authored-by: Claude Fable 5 <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