Don't request a GC before waiting on the entry point
What does this PR do?
Bun requested a garbage collection right before it waited on the entry module's promise. VirtualMachine::load_entry_point, its worker and test runner variants, and load_preloads() (once per preload module) each called perform_gc(), which is JSC::VM::collectAsync(). At that point the heap holds little more than the new global object, so the collection frees nothing. JSC serves the request at the next allocation slow path, inside loadAndEvaluateModule. That is a synchronous eden collection on the main thread, and it also starts the marker threads, before the first line of the program runs. BUN_JSC_logGC=1 bun empty.js logs it on every start:
[GC<0x...>: START M 400kb => EdenCollection, ... ]
This PR removes those requests. JSC's own activity callbacks and Bun's idle GC timer still collect once there is something to collect. The full collection that web_worker.rs runs after a worker's entry point is a different mechanism and is not changed.
Windows x64 release build, hyperfine -N (150 to 200 runs) plus per-launch counters:
| before | after | |
|---|---|---|
bun empty.js | 28.7 ms | 26.8 ms |
bun hello.js | 29.8 ms | 27.4 ms |
| CPU cycles per launch | 55.7 M | 47.0 M |
| threads created | 15 | 8 |
| peak commit | 85.9 MB | 68.7 MB |
Two tests depended on the startup collection. Both read numbers that JSC only fills in while it collects.
process.memoryUsage()(BunProcess.cpp) reportsheapUsedfrom the figure JSC measured at the end of the most recent collection (#39593), andexternalfromheap.extraMemorySize(). With no collection, both are 0. Before the first collectionheapUsednow reportsheapTotal: nothing has been freed yet, so the whole heap is in use. This is one branch, no collection.externalstays 0 until the first collection. JSC only measures it while it collects, and there is no public counter for it.test/js/node/test/parallel/test-memory-usage.jsassertsr.external > 0at startup, which only a collection can satisfy, so that node test is removed. AHeapaccessor for the bytes allocated since the last collection in the WebKit fork would make both numbers exact without a collection and bring the test back.test/js/web/abort/abort.test.tscountsAbortSignalwrappers withheapStats()before and afterBun.gc(true).heapStats()runs a full collection itself when nothing has collected yet, so the first count was taken after the wrappers were gone and the difference was 0. The test now collects once before it creates the signals.
How did you verify your code works?
test/js/bun/gc/gc-controller-cadence.test.ts: new tests start a script, a script with a preload, a test file, and a worker withBUN_JSC_logGCand check that no eden collection is logged. On main they see one per entry point and per preload. The full collections Bun runs on purpose (VM teardown on the ASAN lanes, a worker after its entry point and at teardown) are not counted.test/js/node/process/process.test.js: a new case in the #39593 describe block readsprocess.memoryUsage()in a fresh process withBUN_GC_TIMER_DISABLE=1and expectsheapUsedto equalheapTotal. On main it reportsheapUsed: 0.- With the debug build, rebased on main at 91cdf1595f: the #39593 cases in
process.test.js,test/js/web/abort/abort.test.ts,test/js/bun/globals.test.js("cleans up memory"),test/js/node/diagnostics_channel/diagnostics_channel.test.ts("references are not leaked"),test/js/bun/jsc/bun-jsc.test.ts,test/js/node/v8/v8-module.test.ts, and the node teststest-v8-stats.js,test-worker-heap-statistics.js,test-v8-collect-gc-profile.js,test-gc-tls-external-memory.js,test-vm-measure-memory.jspass.test-memory-usage.jsfails on itsr.external > 0line only. BUN_JSC_logGC=1 bun hello.jsno longer logs a collection before the entry module evaluates. ETW traces of 25 launches no longer containHeap::collectInMutatorThreador marker thread creation during startup.
Background
JSC's collector is generational. An eden collection only examines the objects allocated since the previous collection. A full collection examines everything. At the end of a collection, Heap stores the live size in the counter for that kind of collection only: sizeAfterLastEdenCollection() or sizeAfterLastFullCollection(). extraMemorySize() is the memory outside the heap (string contents, buffers) that the cells visited by the collections so far reported. heap.size() is computed from the mark bits, so it is also 0 before the first collection, and it walks every block of the heap on each call. That is why process.memoryUsage() does not use it.
no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/process/process.test.js test/js/web/abort/abort.test.ts