Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4603,6 +4603,12 @@ impl VirtualMachine {
self.wait_for_promise(jsc::AnyPromise::Internal(promise));
}

// The entry promise has settled and the test runner continues
// synchronously, so pre-arm the waker: otherwise, when a ref'd handle
// (e.g. a pending user timer) keeps the loop active, this final
// `auto_tick` parks until the next timer deadline, typically JSC's
// incremental sweeper armed ~100ms out by the per-file GC (#36450).
Comment thread
robobun marked this conversation as resolved.
Outdated
self.wakeup();
self.auto_tick();
Ok(self.pending_internal_promise.unwrap())
}
Expand Down
65 changes: 65 additions & 0 deletions test/regression/issue/36450.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";

// https://github.com/oven-sh/bun/issues/36450
// A pending ref'd timer left behind by one test file made every subsequent
// test file that loads a module stall until the next JSC housekeeping timer
// (~100ms): after the file's entry promise settled, the runner's final
// blocking tick parked in the poller because the ref'd timer kept the loop
// active. The stall sits between module evaluation and the first test
// callback, so that gap is what we measure, paired against a control run
// without the pending timer so machine speed cancels out.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
test("pending ref'd timer does not stall subsequent test files", async () => {
const fileCount = 6;
const files: Record<string, string> = {
"leak.test.ts": `
import { test, expect } from "bun:test";
test("leaves one pending ref'd timer", () => {
setTimeout(() => {}, 300_000);
expect(1).toBe(1);
});
`,
};
for (let i = 1; i <= fileCount; i++) {
// The import must be used, otherwise it is elided and no module loads.
files[`mod${i}.ts`] = `export const v = ${i};`;
files[`plain${i}.test.ts`] = `
import { test, expect } from "bun:test";
import { v } from "./mod${i}";
const loadedAt = performance.now();
test("t${i}", () => {
console.log("GAP${i}:" + (performance.now() - loadedAt).toFixed(2));
expect(v).toBe(${i});
});
`;
Comment thread
robobun marked this conversation as resolved.
}
using dir = tempDir("issue-36450", files);
const plainFiles = Array.from({ length: fileCount }, (_, i) => `plain${i + 1}.test.ts`);

async function medianGap(withLeak: boolean): Promise<number> {
await using proc = Bun.spawn({
cmd: [bunExe(), "test", ...(withLeak ? ["leak.test.ts"] : []), ...plainFiles],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const gaps: number[] = [];
for (const match of (stdout + stderr).matchAll(/GAP\d+:([\d.]+)/g)) {
gaps.push(Number(match[1]));
}
expect(gaps).toHaveLength(fileCount);
expect(exitCode).toBe(0);
Comment thread
robobun marked this conversation as resolved.
Outdated
return gaps.toSorted((a, b) => a - b)[Math.floor(gaps.length / 2)];
}

const withoutTimer = await medianGap(false);
const withTimer = await medianGap(true);

// Unfixed, the pending timer pins every gap to the next JSC timer deadline
// (~15ms release, ~90ms debug on an idle machine); fixed, both runs behave
// identically.
expect(withTimer - withoutTimer).toBeLessThan(10);
});
Loading