Don't request a GC before waiting on the entry point

Merged
DY
3mo ago
+93-63

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:

beforeafter
bun empty.js28.7 ms26.8 ms
bun hello.js29.8 ms27.4 ms
CPU cycles per launch55.7 M47.0 M
threads created158
peak commit85.9 MB68.7 MB

Two tests depended on the startup collection. Both read numbers that JSC only fills in while it collects.

  • process.memoryUsage() (BunProcess.cpp) reports heapUsed from the figure JSC measured at the end of the most recent collection (#39593), and external from heap.extraMemorySize(). With no collection, both are 0. Before the first collection heapUsed now reports heapTotal: nothing has been freed yet, so the whole heap is in use. This is one branch, no collection. external stays 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.js asserts r.external > 0 at startup, which only a collection can satisfy, so that node test is removed. A Heap accessor 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.ts counts AbortSignal wrappers with heapStats() before and after Bun.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 with BUN_JSC_logGC and 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 reads process.memoryUsage() in a fresh process with BUN_GC_TIMER_DISABLE=1 and expects heapUsed to equal heapTotal. On main it reports heapUsed: 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 tests test-v8-stats.js, test-worker-heap-statistics.js, test-v8-collect-gc-profile.js, test-gc-tls-external-memory.js, test-vm-measure-memory.js pass. test-memory-usage.js fails on its r.external > 0 line only.
  • BUN_JSC_logGC=1 bun hello.js no longer logs a collection before the entry module evaluates. ETW traces of 25 launches no longer contain Heap::collectInMutatorThread or 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