Skip to content

perf(void): reuse unchanged source analysis during dev rescans - #543

Merged
Brooooooklyn merged 9 commits into
voidzero-dev:mainfrom
yangshun:perf/incremental-source-analysis
Oct 22, 2026
Merged

Brooooooklyn merged 9 commits into
voidzero-dev:mainfrom
yangshun:perf/incremental-source-analysis

Conversation

@yangshun

@yangshun yangshun commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Issue

Saving a page currently re-reads and analyzes unchanged source files, then regenerates routing output even when only the page body changed. This adds unnecessary work as the app grows.

Fix

This change caches file-derived analysis, invalidates edited files through the watcher, and skips route-entry regeneration and route-module invalidation when routing inputs remain unchanged. Recent writes are conservatively re-analyzed to preserve freshness. Layout inventories are deterministic, and returned Markdown metadata cannot mutate the cached analysis. Startup and file changes serialize regeneration so the generated files and successful-input checkpoint stay consistent. Watcher failures are logged and retried on a later save. Auth provider/dialect settings participate in change detection, and missing generated routing files are repaired on the next source save.

The implementation is shared across React, Vue, Svelte, and Solid Pages. The benchmarks below measure React only.

Results

Measured from file write to the updated heading appearing in Chrome, using Cloudflare dev with local workerd on an Apple M5:

Generated pages Update Before After Time saved Change
1 Component HMR 97.0 ms 96.4 ms 0.6 ms −0.7%
1 Server-page reload 132.9 ms 142.9 ms −10.0 ms +7.5%
100 Component HMR 95.2 ms 96.8 ms −1.6 ms +1.7%
100 Server-page reload 119.5 ms 141.6 ms −22.2 ms +18.5%
1,000 Component HMR 350.1 ms 98.6 ms 251.5 ms −71.8%
1,000 Server-page reload 329.8 ms 167.2 ms 162.6 ms −49.3%

Each fixture includes two additional fixed pages. Results average session medians across ten paired sessions per size, with 360 timed saves overall and seeded, sequential variant ordering after warmup.

Both 1,000-page improvements are statistically significant after Holm correction across six comparisons (p = 0.0117). The 95% savings intervals are 235–268 ms for HMR and 123–197 ms for reloads. Smaller fixtures show no statistically clear improvement or regression.

Validation:

  • Regression coverage includes overlapping startup/saves; retry after write, auth-generation, and invalidation failures; changed auth settings; deleted outputs; stable layout ordering; and metadata isolation. Body-only saves return to the fast path after recovery.
  • Latest-base verification (36a3697f): core build, 465 routing/cache/navigation and email unit/integration tests, and all 33 workspace typecheck tasks passed. The targeted run skipped 13 tests.
  • With preceding base cd480dde: affected suite passed 10,936 tests; full suite passed 14,314 tests, with 31 skipped.
  • Formatting and lint passed.
  • Real Cloudflare dev with Chrome: loader and middleware body edits ran fresh code without restarting the isolate. A forced generated-file write failure was logged without terminating the server; the next save regenerated the POST route and its types. A body-only save also repaired deleted route types. No browser runtime errors.
  • Node production Pages coverage is required. Optional Bun/Deno availability probes retain the 10-second timeout.

The performance figures above are from the original benchmark run; this follow-up was verified for correctness.

yangshun and others added 7 commits October 22, 2026 20:01
## Problem

Every file event in a source convention directory reran the whole
application analysis. Binding inference and the page scanner each read
every source file synchronously, and the page scanner parsed every
`*.server.ts` with Oxc again. In a 5,000-page app (the many-pages
benchmark), one rescan made 20,006 `readFileSync` calls, two per page
file, and took about 800 ms on an idle machine.

After the rescan, the routing plugin regenerated `virtual:void-routes`
even when the save changed nothing the scan reports. That module lists
every page, so Vite re-transformed and re-ran it on every save.

In a CPU profile of `vp dev` while editing one page of that app, the
rescan took 1.0-1.6 s of each 2.0-2.7 s HMR update, and re-transforming
and re-running the route table took most of the rest. The benchmark's
HMR time grew from 209 ms at 100 pages to 2.21 s at 5,000.

The React adapter's pages and islands plugins also scan pages on their
own when files are added or removed and at build start, so the same
files were read and parsed several times.

## Fix

- `deriveFromSource` (`shared/source-analysis-cache.ts`) caches values
  derived from a file's contents, keyed by path and analysis kind, and
  reuses them while the file's inode, size, mtime and ctime are
  unchanged. Binding inference, page server metadata, component
  `layout` exports and Markdown frontmatter go through it. The cache
  is module-level, so the core plugin and the framework adapters share
  it.
- Coarse file system timestamps and inode reuse can leave a rewritten
  file's stat unchanged. As Git does for racily clean index entries, a
  file that changed within the last 3 s is always read again, and the
  dev watcher drops a file's entries on every event.
- On a `change` event, the routing plugin compares the scan before and
  after the rescan. When nothing changed, it skips regenerating route
  types and entries and leaves `virtual:void-routes` alone; Vite still
  invalidates the saved module. Adds, deletes, auth config, env files
  and SSR entries keep the previous behavior.

## Impact

Measured on the 5,000-page benchmark app:

- A warm rescan reads 2 files instead of 20,006 and takes about 100 ms
  instead of 530-640 ms.
- HMR after editing a page (median of five edits, two runs each):
  1,166 and 1,216 ms before, 299 and 248 ms after. The benchmark
  measured 209 ms at 100 pages, so HMR now barely grows with the page
  count.
- Loader and route handler edits also reach the next SSR response
  sooner: about 1.6 s to 0.5 s for a loader, and 0.8-1.2 s to 0.1 s for
  a route handler.

## Verification

- New `source-analysis-cache.test.ts`. `pages-scan.test.ts` covers
  metadata edits between cached scans and fails if the cache ignores
  the file signature. `plugin-routing.test.ts` covers saves that do
  and do not change scanned facts.
- `vp run check` (33/33, after generating playground codegen in a
  fresh worktree).
- `vp run test --base origin/main`: 511 affected files pass, including
  the Pages HMR integration suites.
- On `vp dev` with the 5,000-page app, edits to a page component, its
  loader and a route handler all reach the next SSR response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove redundant cache guidance from AGENTS.md while keeping the implementation unchanged.
Keep the generated routing checkpoint pending until writes and module invalidation succeed, including forced updates with unchanged scan facts. Cover write failures, forced updates, invalidation failures, and recovery to the body-only save fast path.
Stop an unresponsive runtime version probe after ten seconds so test discovery can skip unavailable optional runtimes instead of hanging before Vitest timeouts apply.
@yangshun
yangshun force-pushed the perf/incremental-source-analysis branch from ccd7809 to 18a4733 Compare October 22, 2026 12:02
@yangshun
yangshun marked this pull request as ready for review October 22, 2026 12:13
Brooooooklyn and others added 2 commits October 22, 2026 22:42
# Conflicts:
#	packages/void/src/plugin-routing.ts
#	packages/void/test/unit/plugin-routing.test.ts
…alysis on watch rebuilds

- The fast path compared only that the generated files existed. Another
  void process in the same root (`void build`, `void db generate`, or the
  previous server during a restart) can rewrite them, and the dev watcher
  ignores `.void/`, so they stayed stale until the scan changed. Record
  their size and mtime at each checkpoint and regenerate when they differ.
- `vite build --watch` reruns buildStart without the dev watcher, so the
  `watchChange` hook now forgets the analysis of each changed file too.
- Tests: an environment-file save with an unchanged scan, a forced update
  whose invalidation failed, a reverted edit after a failed update, a
  generated file rewritten by another process, and a stronger queue check
  that fails for a queue that only defers by a microtask.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member

Thanks for this PR. I merged main into the branch (the conflict was with the asset Worker bypass hook in plugin-routing.ts; I kept both sides) and pushed one polish commit after a deep review:

fast path check     before: generated files exist
                    after:  generated files have the size + mtime the last checkpoint wrote
                    why:    `void build` / `void db generate` in the same root, or the old
                            server during a restart, can rewrite .void/* and the dev watcher
                            ignores .void/, so they stayed stale until the scan changed

vite build --watch  new watchChange hook clears the cache entry (no dev watcher there)

tests               + env-file save with an unchanged scan (forced path had no coverage)
                    + env-file save whose module invalidation fails, then retry
                    + reverted edit after a failed update
                    + generated file rewritten by another process
                    ~ queue test now waits a macrotask, so a queue that only defers
                      by a microtask fails it

Each new test fails when its fix is removed. Checked and left alone: the cache key (ino, size, mtime, ctime, plus the 3 s window) is sound on local disks, and callers don't mutate cached values.

@Brooooooklyn

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T15:12:58.297666Z 1aec808 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 1aec80875c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@yangshun

yangshun commented Oct 22, 2026 via email

Copy link
Copy Markdown
Contributor Author

@Brooooooklyn
Brooooooklyn merged commit dbbdcd3 into voidzero-dev:main Oct 22, 2026
18 checks passed
@yangshun
yangshun deleted the perf/incremental-source-analysis branch October 22, 2026 23:03
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