Skip to content

dev server: grow the stale bit set in one add_node helper when a file is added to the incremental graph - #48096

Open
robobun wants to merge 5 commits into
mainfrom
robobun/49933471/dev-server-stale-bit-add-node
Open

robobun wants to merge 5 commits into
mainfrom
robobun/49933471/dev-server-stale-bit-add-node

Conversation

@robobun

@robobun robobun commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #48078

Problem

  • Dev server with HMR, a client graph of more than 512 files plus a css import. The first hot update after an edit also carries unchanged modules (gen/h.ts in the issue). In a large React app this re-runs hundreds of modules and can force a full reload.
  • receive_chunk (incremental_graph.rs:614) cleared a file's stale bit only when the bit set already covered the index. A css asset registered later in the same bundle called ensure_stale_bit_capacity(true), which grew the set with every new bit set. Every file already received at index >= 512 was now stale, and the next bundle re-parsed and re-sent it.

Fix

  • One private add_node on IncrementalGraph now owns every file insertion: it interns the key, pushes the EdgeLists entry, grows stale_files with fresh bits cleared, and sets, clears, or keeps the new file's own bit (StaleBit::{Set, Clear, Keep}). The five former get_or_put sites call it.
  • ensure_stale_bit_capacity and its fill flag are deleted, with the two end-of-bundle grows in finalize.rs and the two startup grows in lifecycle.rs. No code can write a bit for a file other than the one it inserts, so the bug class is gone for every current and future inserter on both graphs.
  • Correct because every inserter that marked a file stale already set its own bit on the next line. The true fill only ever reached files that receive_chunk or insert_css_file_on_server had added, and those are never stale. This completes the invariant dev server: fix an index out of bounds panic when a file with an unresolved import is imported twice #44340 states: every file in bundled_files has a bit in stale_files.
  • Verified: test/bake/dev/css.test.ts (700 modules + css, two edits each ship only gen/last.ts) and three cases in test/bake/dev/hot.test.ts. All four fail on a debug build of main. Also all of test/bake/dev.

Background

Downsides

  • None found for callers. Per received file: same get_or_put, same push, same compare of the index against the bit length, plus one call into add_node (not inlined in release). stale_files resize calls on the 700-module fixture (debug counter at the call site): first bundle 4 -> 2, each later bundle 2 -> 0, startup 2 -> 0. Reallocs stay at 2.
  • Release binary .text: 140289307 -> 140287259 bytes (-2048). incremental_graph symbols: 35384 -> 34074 bytes (nm -S on bun-profile, main 7ca0042 vs this branch). No callgrind: valgrind is not installed in the build container, so no instruction count per call.
  • A server css node that a css edit marks stale stays stale. Every later server rebundle of the route parses that stylesheet again. That is unchanged from main and is noted, not fixed, here.
Notes

Mechanism on main, 700 modules + css, first bundle. ensure_route.rs:506 inserts the html route and grows the client set to 512 bits. receive_chunk adds 700 files, clearing bits 0..511 and skipping 512..699. assets.rs:96 registers the css asset through insert_empty, which grows the set to 1024 with bits 512..1023 set. finalize.rs:628 then grows with false, which touches no existing bit. On the next edit is_file_cached returns None for every file in 512..699 that the changed file reaches, so gen/h.ts is re-parsed and shipped. receive_chunk clears it then, so the second edit is clean. Without css nothing grows the set between receive_chunk and the final grow. Below 512 files every index fits the initial set. A framework with no client entry and no react refresh starts with an empty client set, so there the first bundle hit this at any size (second hot.test.ts case).

Siblings fixed by the same change. insert_failure after more than 512 received files (finalize.rs:299/397, failures.rs), insert_empty for css @import children and directory placeholders (incremental_graph.rs:1025, directory_watch_store.rs:203), insert_stale_extra on the server graph (failures.rs:392, lifecycle.rs:465), and insert_css_file_on_server (finalize.rs:414/489), which never touched the set at all.

Behaviour table per site. receive_chunk: Clear (was: clear if covered). insert_stale_extra, insert_failure(AbsPath): Set (was: grow with true, then set). insert_empty: SetIfNew (was: grow with true and set only for a new file). insert_failure(Index): set(i) directly, always in bounds now. insert_css_file_on_server: Keep (was: no write, value came from the next grow). The is_route write on an existing server route moved from gop.value_ptr to values_mut()[idx].

Review. One bot finding fixed after the first push: insert_empty set the bit of an existing file, the base did not (SetIfNew restores that). Two pre-existing findings noted in their threads. Self-review before the push: 9 concerns raised, 9 addressed: the three should-fix items were one coverage gap around the server css node (the third hot.test.ts case), the rest were PR body wording, the STEP back-link comment, and the #45455 / #29418 links above.

Measurements. Debug builds of main and this branch with an eprintln! at the resize call site (not committed), 700-module css fixture, first load plus two edits. Release builds of main 7ca0042d3b and this branch on linux x64, size and nm -S. New tests under debug+ASAN, median of 3 isolated runs: css 3351 ms, hot failed-import 4128 ms, hot client component 1599 ms, hot server css 1886 ms (1 run).

Suites run. bun bd test test/bake/dev/ (191 pass, 1 fail: production > works with sourcemaps - error thrown in React component timed out under load and passes alone; it runs a production build, not the dev server). test/bake/dev/css.test.ts, hot.test.ts, incremental-graph-edge-deletion.test.ts separately.


no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bake/dev/hot.test.ts

… is added to the incremental graph

Every file added to bundled_files now gets a stale bit at once, with fresh
bits cleared. ensure_stale_bit_capacity and its fill flag are removed. A css
asset or a failed import registered during the first bundle no longer marks
the modules received before it as stale.
@robobun

robobun commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:45 AM PT - Nov 7th, 2026

❌ @robobun, your commit 319eb3a has 40 failures in Build #138294 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 48096

That installs a local version of the PR into your bun-48096 executable, so you can run:

bun-48096 --bun

@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

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 15 minutes.

Check out review usage here.

View limit details

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

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 7ca0042 and 319eb3a.

Files selected for processing: 5

  • src/runtime/bake/dev_server/finalize.rs
  • src/runtime/bake/dev_server/incremental_graph.rs
  • src/runtime/bake/dev_server/lifecycle.rs
  • test/bake/dev/css.test.ts
  • test/bake/dev/hot.test.ts
  • 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

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

Beyond the inline findings, I also checked the direct stale_files.set(...) callers that no longer have a capacity grow ahead of them (bundle.rs:551, ensure_route.rs:406/412, and the InsertFailureKey::Index arm plus the invalidate paths in incremental_graph.rs): add_node is now the only get_or_put on bundled_files and nothing shrinks stale_files, so every index those sites use already has a bit.

Extended reasoning...

The change collapses five file-insertion sites in the Bake dev server's IncrementalGraph into one add_node helper that grows the stale bitset with cleared bits, and deletes the ensure_stale_bit_capacity grows in finalize.rs and lifecycle.rs. It touches no security-sensitive surface. The inline findings already flag the insert_empty polarity change on existing files, so the body only records the sibling-site bounds check that was ruled out.

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

Code review found no new issues

No new issues were found in this update; 3 findings from earlier reviews are still open above.

Still open from earlier reviews (3):

If you have decided not to act on one of these findings, resolve its thread (a reply alone leaves it open) and the next review stops counting it. To review this commit again now, use Re-run on its "Claude Code Review" check.

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

The StaleBit::SetIfNew change in 319eb3a restores the base contract for insert_empty on an existing file, and this pass found no new issues; since this consolidates every insertion path of a core dev-server data structure and deliberately changes when a server css node starts cached, a human look is still worthwhile.

What was reviewed:

  • Traced all five add_node call sites against the old stale-bit behavior: receive_chunk, insert_stale, insert_empty and insert_failure(AbsPath) are equivalent; only insert_css_file_on_server changes (new node born cached), which the third hot.test.ts case pins.
  • Checked the InsertFailureKey::Index arm: both callers in failures.rs pass indices of files already in bundled_files, so the direct set is in bounds under the new invariant.
  • Checked the deleted grows in lifecycle.rs/finalize.rs: at startup only insert_stale inserts and sets its own bit, and the finalize grow used a false fill, so no bit value is lost.
  • Grepped for get_or_put/edge_lists.push/stale_files.resize/ensure_stale_bit_capacity outside add_node: none remain.
Extended reasoning...

The PR touches the Bake dev server's IncrementalGraph (src/runtime/bake/dev_server/incremental_graph.rs) plus two call-site deletions in finalize.rs and lifecycle.rs, replacing the stale-bitset growth helper with a single add_node helper that owns every file insertion, and adds four dev-server tests. It touches no security-sensitive surface. The latest commit addressed the one blocking finding from the prior review; the two remaining inline threads are pre-existing behaviors kept by the refactor, not regressions. Defer rather than approve because the change rewrites all insertion paths of a central dev-server data structure with one intentional behavior change, which merits a maintainer's judgment despite the test coverage; no CODEOWNERS entry covers these paths.

@robobun

robobun commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

#45455 fixed the same bug with a smaller change in receive_chunk. It is now closed in favor of this PR.

One of its tests is not ported here: hot update sends only the changed module in a page with a stylesheet and over 512 modules, in test/bake/dev/hot.test.ts on the branch robobun/42b18a5f/dev-server-stale-bit-on-insert. Its stylesheet has an @import, so the placeholder that insert_empty adds in the second pass is what grows the set. The test passes on a debug build of this branch at 319eb3a, together with the other tests of hot.test.ts and css.test.ts.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dev server: first hot update resends unchanged imports in a large module graph with CSS

2 participants