Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "72e75e4ec1f9a8571874328c1502f4d8226bc2bf";
export const WEBKIT_VERSION = "40e43a82a755af3cc9eb4a4e025e4e020a7a3cfd";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
8 changes: 6 additions & 2 deletions src/jsc/GarbageCollectionController.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,16 @@ impl GarbageCollectionController {
);
}

pub(crate) fn perform_gc(&self, full: bool) {
pub(crate) fn perform_gc(&self, idle_full: bool) {
if self.disabled.get() {
return;
}
let vm = VirtualMachine::get().jsc_vm();
vm.collect_async(full);
if idle_full {
vm.collect_async_idle();
} else {
vm.collect_async(false);
}
self.gc_last_heap_size.set(vm.block_bytes_allocated());
}

Expand Down
6 changes: 6 additions & 0 deletions src/jsc/VM.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ unsafe extern "C" {
safe fn JSC__VM__runGC(vm: &VM, sync: bool) -> usize;
safe fn JSC__VM__heapSize(vm: &VM) -> usize;
safe fn JSC__VM__collectAsync(vm: &VM, full: bool);
safe fn JSC__VM__collectAsyncIdle(vm: &VM);
safe fn JSC__VM__executionForbidden(vm: &VM) -> bool;
safe fn JSC__VM__notifyNeedTermination(vm: &VM);
safe fn JSC__VM__isEntered(vm: &VM) -> bool;
Expand Down Expand Up @@ -99,6 +100,11 @@ impl VM {
JSC__VM__collectAsync(self, full)
}

/// A full collection tagged as the embedder's idle collection, in which JSC may also let idle optimized code go.
pub(crate) fn collect_async_idle(&self) {
JSC__VM__collectAsyncIdle(self)
}

pub fn execution_forbidden(&self) -> bool {
JSC__VM__executionForbidden(self)
}
Expand Down
10 changes: 10 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3171,6 +3171,16 @@ void JSC__VM__collectAsync(JSC::VM* vm, bool full)
vm->heap.collectAsync();
}

// The full collection GarbageCollectionController requests because the heap has gone quiet: tagged so JSC may let idle
// optimized code age out in it (GCRequest::isIdle), which it never does in a collection the program forces or allocation paces.
void JSC__VM__collectAsyncIdle(JSC::VM* vm)
{
JSC::JSLockHolder lock(*vm);
JSC::GCRequest request(JSC::CollectionScope::Full);
request.isIdle = true;
vm->heap.collectAsync(request);
}

size_t JSC__VM__heapSize(JSC::VM* arg0)
{
return arg0->heap.size();
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/headers.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 67 additions & 0 deletions test/js/bun/gc/gc-controller-cadence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,70 @@ describe("idle release", () => {
expect(exitCode).toBe(0);
});
});

// Those idle full collections are tagged (GCRequest::isIdle) so JSC may also let idle FTL code — which has no execution
// counter of its own and pins every baseline CodeBlock it inlined — age out in them, and only in them: a program that
// forces collections itself while running hot code must not lose that code. Eager JIT TTLs make it observable in seconds.
describe("idle release lets FTL code age out", () => {
const script = /* js */ `
const { heapStats, noInline } = require("bun:jsc");
const fns = [];
for (let i = 0; i < 40; i++) {
const f = new Function("o", "h", "let s = 0; for (let k = 0; k < 40; k++) s += h(o, k) + " + i + "; return s;");
noInline(f);
fns.push(f);
}
const helper = (o, k) => o.a * k + o.b;
const o = { a: 1, b: 2 };
globalThis.keep = [helper, o, fns];
for (let r = 0; r < 100000; r++) for (let j = 0; j < fns.length; j++) fns[j](o, helper);
const count = () => heapStats().objectTypeCounts.FunctionCodeBlock ?? 0;
Bun.gc(true);
const before = count();
if (process.env.MODE === "forced") {
let n = 0;
const id = setInterval(() => {
Bun.gc(true);
if (++n >= 6) { clearInterval(id); console.log(JSON.stringify({ before, after: count() })); }
}, 700);
} else {
setTimeout(() => { Bun.gc(true); console.log(JSON.stringify({ before, after: count() })); }, 4500);
}
`;

async function run(env: Record<string, string>) {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: {
...bunEnv,
BUN_GC_TIMER_DISABLE: undefined,
BUN_GC_TIMER_INTERVAL: undefined,
BUN_JSC_useEagerCodeBlockJettisonTiming: "1",
BUN_JSC_optimizedCodeAgingQuietSeconds: "0.5",
...env,
},
stdout: "pipe",
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
const counts = (stdout.trim().startsWith("{") ? JSON.parse(stdout.trim()) : {}) as {
before?: number;
after?: number;
};
return { ...counts, stdout, exitCode };
}

test.concurrent("the idle collections drop the warmed-up code", async () => {
const { before, after, stdout, exitCode } = await run({ BUN_IDLE_GC_SECONDS: "1,1,1" });
expect(before, stdout).toBeGreaterThan(40);
expect(after, stdout).toBeLessThan(before! / 4);
expect(exitCode).toBe(0);
});

test.concurrent("collections the program forces itself do not", async () => {
const { before, after, stdout, exitCode } = await run({ BUN_IDLE_GC_SECONDS: "0", MODE: "forced" });
expect(before, stdout).toBeGreaterThan(40);
expect(after, stdout).toBeGreaterThan(before! / 2);
expect(exitCode).toBe(0);
});
});
Loading