Repository navigation
Conversation
The scan checks the log before it builds a bundle, so a config error starts no worker pool, and it tears its bundle down when enqueueing fails. `filter` owns the scan transpiler and its log through a guard that releases both on every exit. `bundlerPoolsCreated` in `bun:internal-for-testing` reads how many bundler thread pools the process made, so the tests can tell a scan that built a bundle from one that did not.
|
Warning Review limit reachedYou'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 22 seconds. View limit details
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. Comment |
|
Updated 4:32 PM PT - Oct 24th, 2026
✅ @robobun, your commit a1c94ef9a667e2b71b7a12748d90a05e5cb8101f passed in 🧪 To try this PR locally: bunx bun-pr 46810That installs a local version of the PR into your bun-46810 --bun |
|
Status: reproduced on debug ASAN builds of main with the LeakSanitizer options of the CI ASAN lane. |
…hanges The teardown on the error exits of `scan_module_graph_from_cli` is now in #46810, which checks the log before `BundleV2::init`. The drain helper called `wait_for_parse()` after an enqueue error, where the scan counter can stay above zero. #43428 moves that counter first. What stays here: `get_worker_slow` inserts a complete `Worker`, and `worker_pool()` checks its pointer in release builds.
Problem
bun test --changed --define a-b=1(a config error) printswarn: --changed: failed to build module graph (BuildFailed); running all tests. It then leaks 33560 bytes in 924 allocations, and up to 8Bun Poolthreads stay parked.filter(src/runtime/cli/test/ChangedFilesFilter.rs) released the scanTranspileronly on success, and itsLognever.BundleV2::scan_module_graph_from_cli(src/bundler/bundle_v2.rs) started its pool, then returned on the log error without teardown.Fix
BundleV2::init, so a config error starts no pool. Its other error exit tears the bundle down. ADropguard infilterfrees the scan log and the transpiler on every exit.test/cli/test/test-changed.test.ts, with the newbundlerPoolsCreatedofbun:internal-for-testing. Also rantest/bundler/bun-build-api.test.ts.Background
bun test --changedscans the module graph of the test files with the bundler. A failed scan runs every test.Drop.Transpiler::deinit()is the explicit release, as inpack_command.rs.filteralone: the scan keeps an exit that leaves its pool alive.Downsides
bun test --changedruns 27 more instructions, once per process. The binary gains 1 host function (92 bytes).Notes
Reach. There is no issue for this. A release build prints the same output and exits 0. It keeps about 33 KB and up to min(8, cores) idle threads, once per process. The triggers are a
--defineor--dropkey that is not an identifier and a--tsconfig-overridefile that does not exist, from the command line or from bunfig. What the change buys: the fallback gets its first tests, andtest-changed.test.tsis leak-checked on the ASAN lane, so a test that reaches the fallback or logs a scan error no longer aborts there.Measurements. Debug ASAN builds of main (179664e) and this branch, with the environment of the CI ASAN lane (
BUN_DESTRUCT_VM_ON_EXIT=1,detect_leaks=1:abort_on_error=1,test/leaksan.supp):bundlerPoolsCreated)Bun Poolthreads, tasksbun test: 0 and 4)--changed: 179)Bun Poolthreads after 3 sThe 6 triggers:
--define a-b=1,--drop=a-b,--tsconfig-override ./missing.json,--changed=HEAD --define a-b=1, bunfig[define] "a-b" = "1", bunfigexternal = "a*b*". Live allocations are malloc calls minus free calls fromASAN_OPTIONS=print_stats=1:atexit=1. The thread numbers are from/proc/self/taskon Linux.Tests. Debug ASAN build of this branch: 27 pass in 5 of 5 runs, and once more under the environment of the CI ASAN lane. Release build of this branch: 26 pass, 1 skipped (the case that only LeakSanitizer can see). A debug ASAN build of main with only the getter added: the 4 config error cases fail with
bundlerPools: "1", the syntax error case fails with690 byte(s) leaked in 5 allocation(s), and the control (a scan with no config error makes 1 pool) passes. Plain main has no getter, so there the cases that read it fail withbundlerPools: undefined.test/bundler/bun-build-api.test.tson the release build of this branch: 89 pass, 0 fail. On the local debug ASAN build 3 of its cases reach their time limit (two bytecode cases at 5 s, "can be called thousands of times" at 180 s).The tests count pools and not threads. A pool thread takes its
Bun Poolname when it first runs, so a count of names reads 0 on a loaded machine while the pool exists.bundlerPoolsCreatedreads the existingPOOL_GENERATIONcounter, whichBundleV2::initis the only code to advance.Why the check is in the scan. A
has_errors()return infilterbefore the call passes the same tests. The scan would keep its owninit, then check, thenreturn Errwith the pool alive: an exit that no input reaches and that leaks when one does. With the check aboveinit, the function that starts the pool refuses the log, there is one check, and anErrof the scan leaves no bundle for any caller.initadds an error only for code splitting with a format that is not ESM and for a bytecode order file.bun testsets neither, so the copy of the check afterinitis deleted.Left as they are, on purpose.
generate_from_cli,generate_from_bake_production_cliandrun_from_js_in_new_thread(Bun.build) read the log afterinit. The CLI drivers run their body in a closure and tear the bundle down on every exit. Their check must stay afterinitbecauseinitcan add an error for their options.Bun.buildleaks its transpiler when option setup fails (src/bundler/BundleThread.rs:305returns before the release). bundler: own the Bun.build transpiler with a Box so it drops when option setup fails #35312 owns that.wait_for_parse()does not return whenenqueue_entry_itemfails afterincrement_scan_counter(). The scan's enqueue-error exit already had that on main. bundler: count an entry point's parse task after the last fallible step #43428 moves the counter.Other open pull requests on these lines.
Workerinto the pool map and check the pool pointer in release builds #39855: this change takes over its hunk in the scan, in the check-first shape. Hardening: fix crashes across the runtime, bundler, dev server, shell and CLI #46021 merged the crash fix of bundler: insert a completeWorkerinto the pool map and check the pool pointer in release builds #39855 and says of such pull requests that they "should stay open for their other half". bundler: insert a completeWorkerinto the pool map and check the pool pointer in release builds #39855 drops the hunk.bundle.transpiler.log_mut().clear_and_free()) and test --changed: keep the imports of a file that only a linked bundle refuses #46695 (drop(core::mem::take(bundle.transpiler.log_mut()))) each free the log on the success exit only. The guard covers that exit, so those two lines are not needed after this change.bun test --changedscan #45938, bundler: run no macro in a bundle made only for its import graph (test --changed,--analyze,bun create <component>) #46281 and test --changed: keep the imports of a file that only a linked bundle refuses #46695 addthis.scan_only = true;afterinit. That line stays afterinit.git merge-treeagainst each head: test --changed: bind test files to the module graph by position, not by path #46583 3, test --changed: keep the imports of a file that only a linked bundle refuses #46695 2, test --changed: follow bare specifiers that resolve to local files #44836 2, test --changed: forget what the module graph scan cached before the tests run #44835 1, bundler: turn automatic barrel mode off for --ignore-dce-annotations and for thebun test --changedscan #45938 1, bundler: run no macro in a bundle made only for its import graph (test --changed,--analyze,bun create <component>) #46281 1. test --changed: read NUL-terminated names from git for the whole work tree #46608 and resolver: keep every main field of a package.json in the shared cache, select per resolver #46652 merge clean.runTestChangedhas the signature that test --changed: read NUL-terminated names from git for the whole work tree #46608 gives it.Release builds of main and this branch.
bun test --changed: the body offilterruns 1159 then 1157 instructions, the body of the scan 120 then 119, and the one new call,Log::clear_and_free, runs 30 with 0 frees. That is +27 (a gdbnextiandstepicount, two runs each, same result)..text82164617 to 82164873 (+256),.bun_builtins2710563 to 2710620 (+57), file size 107730656 for both.filter8239 to 8261 bytes, the scan 843 to 858,jsPoolsCreated92 (size -Aandnm -S).straceis not installed here, so there is noclone3count.Design alternatives beyond the one in Background:
impl Drop for BundleV2(every bundle of all five drivers pays a flag and a branch, and it cannot join tasks without the accounting in #40438), aBoxfor the scan transpiler (the shape of #35312, it has no defect, the guard is the shape that #44968 landed for this call pattern inpack_command.rs), and a guard shared withpack_command.rs(that one must not free a log).no test proof · iteration 3 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/test/test-changed.test.ts