Skip to content
Open
Changes from 2 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
47 changes: 31 additions & 16 deletions test/js/web/workers/worker.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { once } from "events";
import { bunEnv, bunExe, tempDir } from "harness";
import { bunEnv, bunExe, isDebug, tempDir } from "harness";
import path from "path";
import wt from "worker_threads";

Expand Down Expand Up @@ -381,6 +381,8 @@
// terminate() landing while the worker reports that its entry point does not
// resolve: the report is skipped, not turned into a panic.
test("terminate() while the entry point fails to resolve", async () => {
// Every round covers the eight terminate offsets once.
const rounds = isDebug ? 3 : 12;

Check warning on line 385 in test/js/web/workers/worker.test.ts

View check run for this annotation

Claude / Claude Code Review

PR description is stale after the second commit

The PR description was written for commit e0752663 and says the entry-resolution and natural-exit tests "are left as they are" and that the readFile churn runs "10 workers... in two batches of 5" — but commit af4c0c78 then gates both of those tests on `isDebug` and restructures the readFile churn to `isDebug ? 1 : 5` batches of 10. Since the description becomes the squash-commit message, it'd be worth re-syncing it before merge (the second commit's message already covers what changed).
Comment thread
robobun marked this conversation as resolved.
await using proc = Bun.spawn({
cmd: [
bunExe(),
Expand All @@ -394,15 +396,15 @@
await closed;
done++;
}
for (let r = 0; r < 12; r++) await Promise.all(Array.from({ length: 8 }, (_, i) => one(r * 8 + i)));
for (let r = 0; r < ${rounds}; r++) await Promise.all(Array.from({ length: 8 }, (_, i) => one(r * 8 + i)));
console.log("done", done);`,
],
env: bunEnv,
stdout: "pipe",
stderr: "inherit",
});
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
expect(stdout).toBe("done 96\n");
expect(stdout).toBe(`done ${rounds * 8}\n`);
expect(exitCode).toBe(0);
});

Expand All @@ -427,12 +429,16 @@
const src = `import vm from "node:vm"; postMessage("busy");
for (;;) { try { vm.runInNewContext("for(let i=0;i<1e7;i++){}", {}, { timeout: 1000 }) } catch {} await new Promise(r => setImmediate(r)) }`;
const url = URL.createObjectURL(new Blob([src]));
for (let r = 0; r < 6; r++) {
const w = new Worker(url);
await new Promise(res => (w.onmessage = res));
w.terminate();
await once(w, "close");
}
// Concurrently: loading node:vm dominates each worker's start, and six of
// them one after another add up to seconds on debug builds.
await Promise.all(
Array.from({ length: 6 }, async () => {
const w = new Worker(url);
await new Promise(res => (w.onmessage = res));
w.terminate();
await once(w, "close");
}),
);
});

// terminate() mid `import "node:*"`: the native module's export walk stops
Expand Down Expand Up @@ -468,8 +474,9 @@
"side.js": `globalThis.sideRan = true;`,
"preload.js": `import("./side.js");`,
// big enough that the entry graph is still transpiling when side.js evaluates
// (debug builds transpile far slower, so a quarter of it keeps the same margin)
"big.js": Array.from(
{ length: 4000 },
{ length: isDebug ? 1000 : 4000 },
(_, i) => `export function f${i}(x) { return x * ${i} + ${i % 7}; }`,
).join("\n"),
"worker.js": `import "./big.js";
Expand All @@ -489,10 +496,11 @@

// Everything a worker posted before it exited arrives before 'close'.
test("messages posted right before a natural exit are all delivered before close", async () => {
// Several drain batches' worth (1024 each); receiving them is ~1s per round on debug builds.
const K = 5000;
const src = `const p = Buffer.alloc(256, "x").toString(); for (let i = 0; i < ${K}; i++) postMessage({ i, p })`;
const url = URL.createObjectURL(new Blob([src]));
for (let r = 0; r < 3; r++) {
for (let r = 0; r < (isDebug ? 1 : 3); r++) {
let got = 0;
const w = new Worker(url);
w.onmessage = () => got++;
Expand All @@ -513,18 +521,25 @@

// fs completions racing terminate(): whatever completes on the worker
// after the request must release, not build script values under it.
// busy is posted from the first completion so that every terminate offset
// (0-9ms, each batch covers them once) lands mid-churn on debug builds
// too, where the worker's first loop turn alone outlasts the offset range.
// Starting one of these workers costs ~0.5s on debug builds, hence one
// batch there; require() because importing node:fs also builds its ESM
// namespace, which loads the stream classes.
test("terminate() while fs.readFile completions keep arriving", async () => {
using dir = tempDir("worker-readfile-churn", { "f.bin": Buffer.alloc(65536, 7) });
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const src = \`import { readFile } from "node:fs";
let n = 0; (function pump(){ while (n < 16) { n++; readFile(\${JSON.stringify(process.argv[1])}, () => { n--; setImmediate(pump) }) } })();
postMessage("busy")\`;
`const src = \`const { readFile } = require("node:fs");
let n = 0, busy = false;
(function pump(){ while (n < 16) { n++; readFile(\${JSON.stringify(process.argv[1])}, () => {
if (!busy) { busy = true; postMessage("busy") } n--; setImmediate(pump) }) } })()\`;
const url = URL.createObjectURL(new Blob([src]));
for (let r = 0; r < 12; r++) await Promise.all(Array.from({ length: 4 }, (_, i) => new Promise(res => {
const w = new Worker(url); w.addEventListener("close", res); w.onmessage = () => setTimeout(() => w.terminate(), (r + i) % 10) })));
for (let r = 0; r < ${isDebug ? 1 : 5}; r++) await Promise.all(Array.from({ length: 10 }, (_, i) => new Promise(res => {
const w = new Worker(url); w.addEventListener("close", res); w.onmessage = () => setTimeout(() => w.terminate(), i) })));
console.log("PASS");`,
path.join(String(dir), "f.bin"),
],
Expand Down