Repository navigation
Conversation
…end of the stale bit set `receive_chunk` cleared the stale bit of a file only when `stale_files` already covered the index of the file. A file past the end got no bit. A later `insert_empty` or `insert_failure` in the same bundle grew the set with `true` over that file. The next bundle then parsed the file again and sent it in the hot update. `receive_chunk` now grows the set to exactly the file it receives, with the bit cleared. The grow is exact because `insert_empty` and `insert_css_file_on_server` read the bits that a 512-bit step fills.
|
Updated 11:46 PM PT - Oct 15th, 2026
✅ @robobun, your commit 3955b3b45cd7cdb5825a4d341c5e17c3e64cada3 passed in 🧪 To try this PR locally: bunx bun-pr 45455That installs a local version of the PR into your bun-45455 --bun |
|
Status Reproduced on a debug build of main (5f5799e). To reproduce, check out this branch, restore
With the change in PR: #45455 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
WalkthroughChangesThe incremental graph now resizes its stale-file bitset when it receives a file index beyond the current length. Regression tests cover hot updates and incremental bundling with more than 512 modules. Incremental graph tracking
Suggested reviewers: Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to
|
Related to #29418
Problem
[Bun] Hot update was not accepted because it or its importers do not call import.meta.hot.accept.receive_chunk(src/runtime/bake/dev_server/incremental_graph.rs:596) clears a file's stale bit only ifstale_filescovers it. A laterinsert_emptyorinsert_failurein that bundle fills those files withtrue.Fix
receive_chunkgrows the set to exactly the file it receives:resize(index + 1, false). A later grow writes only the bits after it.insert_emptyandinsert_css_file_on_serverread what a 512-bit step fills.test/bake/dev/hot.test.tsfail on main. Alsotest/bake/and the html tests intest/js/bun/http/.Background
stale_fileshas one bit per dev server file and grows by 512 bits of one fill value. A set bit makes the next bundle parse that file again.receive_chunk:finalize_bundlegrows the set after it.insert_emptyruns before that.Downsides
panic: cached asset not foundwhen a second page links a json file it imports. Smaller pages already do.resizecall: 3 -> 69 instructions.Notes
Why not the two lines of #44340 (
ensure_stale_bit_capacity, then write the bit)IncrementalGraph::bundled_filesis meant to have a bit instale_files". It says thatreceive_chunkandinsert_css_file_on_serverneed no change, because they "only run while a bundle is finalized, after all resolution, andfinalize_bundlegrows the set right after them."receive_chunkand that last grow, and each fills withtrue:insert_emptyfromAssets::replace_pathfor a new css root,insert_emptyfor a css@importplaceholder in the second pass, andinsert_failurefor a failed import.ensure_stale_bit_capacity(false)inreceive_chunkrepairs the received files too. It also clears a whole 512-bit step, and a placeholder thatinsert_emptyadds later in that step then reads as cached. That is the failure dev server: leave out an html link to a cached json module instead of panicking, and mark a new placeholder file stale #41157 repairs. After dev server: leave out an html link to a cached json module instead of panicking, and mark a new placeholder file stale #41157 both shapes give the same bits, and the exact grow can become the two lines.ensure_stale_bit_capacity(false)calls at the end offinalize_bundlestay on purpose. The server call still gives a css file frominsert_css_file_on_serverits bit. The client call now only aligns the length to 512 and fills the bits of files that do not exist yet.Who reaches it
@import, or an import that fails to resolve.K.onDispose is not iterableduring HMR when index.html links a stylesheet + entry has both accept() and dispose() #29418 has this shape: a stylesheet link, and an entry withaccept()anddispose(). The same shape without svelte, on 1.4.2: with the link, one edit ends inTypeError: V.onDispose is not iterable. Without the link it is a clean hot update of one module. On main the throw comes back past 510 modules (mod.onDispose is not iterable). With this PR it does not. The throw itself is in the HMR runtime, and bake: dispose a self-accepting HMR boundary once per hot update #41944 repairs it.Measurements (main 5f5799e against the same commit with this change, linux x64. The page has 600 modules and a plain stylesheet)
a.ts, 600 modules + stylesheet: 90 on main, 0 on PR (controls, 600 modules without the stylesheet and 400 with it: 0 and 0, unchanged). Debug builds,BUN_DEBUG_Bundle=1.modules/ids in the hot chunk after editinga.ts: 91 on main (page reloads), 0 on PR.resizecalls onstale_files6 -> 97, bit-set reallocs 2 -> 4 (gdb breakpoint counts, debug builds, main vs PR. Same with no stylesheet: 5 -> 96, 2 -> 4. 400 modules: 6 -> 6 and 1 -> 1).receive_chunk, counted from the statement before it to the statement after it: client copy 27 -> 27 instructions, server copy (inlined intofinalize_bundle) 32 -> 36, conditional branches 3 -> 3 and 4 -> 4, 0 calls, 0 allocations (objdump, release builds, main vs PR).resizecall, once per file (gdb stepi, PR release build). The two files that start a new 64-bit word take 184 and 266, with one realloc each.receive_chunkclient 2070 -> 2129 bytes,finalize_bundle25241 -> 24982 bytes.sizeandnm, main vs PR). The stripped file is 106178312 bytes in both.The panic in Downsides
first.htmlhas a stylesheet and a script that imports N modules andmanifest.json.second.htmlhas<link rel="manifest" href="./manifest.json">. Load/, then/second.panic: cached asset not found.What this PR does not change
insert_emptywrites no bit for a new placeholder. dev server: leave out an html link to a cached json module instead of panicking, and mark a new placeholder file stale #41157 changes that and this PR does not touch those lines.insert_css_file_on_serverwrites no bit. The css file is stale below about 510 server files (css onLoad runs 1, 2, 3) and cached above (1, 1, 1). Which one is correct is a decision for a maintainer. bake: consolidate the open robobun dev-server, HMR runtime, production build and router fixes #39488 carries a guarded clear.receive_chunkgets. Onlyinsert_css_file_on_serveradds such a file, and it runs after every serverreceive_chunkof a bundle.ensure_stale_bit_capacityand its callers.is_file_cachednever answers for it (read from code, not run).Tests
bun bd test test/bake/dev/hot.test.ts: 14 pass. The three new tests on a debug build of main:Client exited while applying hot update: Unexpected reload{"marker":"a2","loads":{"client":0,"server":691}}, expected"server":600{"loads":{"client":2,"server":1}}, expected"client":1@import, so it does not depend on theinsert_emptycall inAssets::replace_path.insert_failuregrower. There is no client-graph copy of it: it is a third 600-module run, and the grower and the new branch are the same generic code for both graphs.test/bake/andtest/bake/dev/passes.production.test.tspasses with--timeout 90000(the timeout of the CI runner). With the default 5000 ms it times out on a debug build, on main too.bun-serve-html.test.ts,bun-serve-html-hot-reload-drop.test.ts,bun-serve-html-405.test.ts,test/regression/issue/42989.test.ts,test/regression/issue/21792.test.ts.