Repository navigation
Conversation
…allocation The test "keeps the object alive across a GC so require() can create the module again" ran its child process with BUN_JSC_slowPathAllocsBetweenGCs=1. With that option a debug build takes over ten seconds to start. The test needed 45 seconds there and timed out at the default of 5 seconds. The child process now calls Bun.gc(true) before each require(). It runs with BUN_JSC_useZombieMode=1, which overwrites the cells that a GC frees. The test takes under one second on a debug build.
|
Status: ready for review. The test of this PR passes on every CI lane. CI is red only on tests that also fail on main. How I reproduced it. On main at 7ca0042, with a debug (ASAN) build on Linux x64: With CI, build 138365, finished.
PR: #48135 |
|
No actionable comments were generated in the recent review. WalkthroughThe object-loader GC test now calls ChangesObject-loader GC test
Suggested reviewers: Merge Risk:
|
Behaviour change: none
Problem
bun bd test test/js/bun/plugin/plugins.test.tsfails on a debug (ASAN) build of main.keeps the object alive across a GC so require() can create the module againneeds 45 s and ends withthis test timed out after 5000ms.BUN_JSC_slowPathAllocsBetweenGCs=1: a full GC at each slow-path allocation. A debug build then needs 13.8 s to print one line.Fix
Bun.gc(true)before eachrequire()and runs withBUN_JSC_useZombieMode=1instead.Background
onLoadcan return{ exports, loader: "object" }. Bun readsexportseach time JSC creates the module. bun test: Vitest compatibility #44833 keeps it alive with the module's source.require()runs one microtask turn afterimport()created it. The 8 rounds try 8 turns. Round 1 hits it today.useZombieModeoverwrites the cells that a GC frees. Without it, a freed object often reads as before, and about half of the unfixed runs pass.slowPathAllocsBetweenGCsinterval: a GC falls into that turn only for some intervals. The explicit call gives up GCs at other points.Notes
All numbers are from one Linux x64 machine (12 cores) and main at 7ca0042. The debug build is
bun bd. The release build is--profile=release --lto=off.Time
--timeout 300000. By default:(fail) ... [5009.09ms]console.log("hi")takes 0.27 s on the debug build, and 13.8 s withBUN_JSC_slowPathAllocsBetweenGCs=1. So fewer rounds cannot bring the old test under 5 s.On main nothing stops the child process of the timed-out test. It was still at 75 % of one core more than 20 s after the test run ended.
The test without the fix
For these runs only,
src/jsc/modules/ObjectModule.cppgot a switch that disables the fix of #44833 in two ways. The switch is not part of this PR.gcProtectthe object,gcUnprotectit after the first read, keep a raw pointer.WeakHandleOwnerthat the fix added never reports the object as reachable.runtime error: member call on null pointer of type 'JSC::JSCell'where Bun reads the exportsASSERTION FAILED: !source.isCell() || handle->get() == source.asCell()panic(main thread): Segmentation fault at address 0x0panic(main thread): abort() calledBUN_JSC_useZombieMode=1, way 1 passes 12 of 20 runs on the debug build and 8 of 20 on the release build. A dead cell often keeps its bytes after the GC.test/js/bun/test/jest-each-gc-root.test.tsandtest/js/bun/transpiler/transpiler-error-gc-uaf.test.tsalready useuseZombieModefor the same purpose.Where JSC creates the module again
A log line in the function that reads the exports shows one read of each
served*.tsanddata*.jsonin rounds 0 and 2 to 7. Round 1 has two reads ofserved1.tsand two ofdata1.json: one fromimport()and one fromrequire().moduleRegistryFetchSettledcallsmakeModuleand queuesmoduleRegistryModuleSettled. A synchronous load that arrives between the two callsmakeModuleagain (JSModuleLoader::hostLoadImportedModule).In round 0 the
Bun.gc(true)runs before the first read. Way 2 already fails there. Way 1 fails in round 1.The old test ran a GC at each slow-path allocation of the whole process. The new test runs 8, one before each
require(). It has no GC insiderequire()or inside the function that reads the exports.A longer interval instead of the explicit call
The old test body on the release build, with the fix disabled in way 1. Failed runs of 10, for each interval:
slowPathAllocsBetweenGCsuseZombieModeOn the debug build the old body takes 7.7 s at an interval of 5, about 3 s at 10, 1.5 s at 20 and 0.5 s at 50. An interval that is fast enough finds the bug only while the allocation counts line up. At 50 they do not.
Runs of the whole file on the debug build
patched Promise methods can't observe module loading > modules from plugins(two tests, 2.9 to 4.9 s when they pass),module mocksof the same block, and twice one of thedoesn't leak the global of a test filetests. This PR does not change them.jsoncwhen--loader, bunfig[loader]orBun.buildnames it #36648 and resolver: passpathsas an argument and deleteResolver::custom_dir_paths#47752 report the timeout of this test in their own debug runs.