From 9a8f408beb9cd984ca93717342539c7030ad0701 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:01:31 +0000 Subject: [PATCH 1/3] test: merge the zlib estimatedSize GC fixtures into one process and strengthen assertions The two GC-guard tests each spawned a bun child; in contended parallel CI batches on Windows the per-process overhead (spawn, startup, zlib module load) stacked up past the per-test timeout while the file passes alone. Compression volume is nearly irrelevant on the repetitive buffer, so the fix is structural: one child interleaves 8 brotli + 8 zstd streams with the same 34 full-GC sample count as the old two processes combined, with samples timed at write-queued, mid drive loop, and after close. Also lowers compression quality/level (the guarded race is marking-thread vs work-pool timing, not compression effort) and tightens assertions: native class names, footprint stability while live, a footprint ceiling, _handle nulled on destroy, per-stream output required, exact fixture stdout. --- .../node/zlib/zlib-estimated-size-gc.test.ts | 114 ++++++++++-------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/test/js/node/zlib/zlib-estimated-size-gc.test.ts b/test/js/node/zlib/zlib-estimated-size-gc.test.ts index 4970f74b872e..4c98aed1e6c4 100644 --- a/test/js/node/zlib/zlib-estimated-size-gc.test.ts +++ b/test/js/node/zlib/zlib-estimated-size-gc.test.ts @@ -12,93 +12,107 @@ import zlib from "node:zlib"; // estimateShallowMemoryUsageOf(cell) == sizeof(cell) + estimated_size(). `min` // is a floor below the per-mode footprint the constructor caches (brotli encode -// 5143, brotli decode 855, zstd compress 5272, zstd decompress 95968). -function checkFootprintStableAcrossClose(create: () => any, min: number) { +// 5143, brotli decode 855, zstd compress 5272, zstd decompress 95968); `max` +// has enough headroom to survive dependency bumps while still catching a +// garbage estimate from an uninitialized or racing read. +function checkFootprintStableAcrossClose(create: () => any, className: string, min: number, max: number) { const engine = create(); engine.on("error", () => {}); engine.on("data", () => {}); const handle = engine._handle; + expect(handle.constructor.name).toBe(className); const before = estimateShallowMemoryUsageOf(handle); + expect(estimateShallowMemoryUsageOf(handle)).toBe(before); // stable while live engine.destroy(); // closes the handle once and nulls engine._handle + expect(engine._handle).toBeNull(); const after = estimateShallowMemoryUsageOf(handle); expect(before).toBeGreaterThan(min); + expect(before).toBeLessThan(max); expect(after).toBe(before); } test("brotli compress: estimated size stays tied to construction mode across close", () => { - checkFootprintStableAcrossClose(() => zlib.createBrotliCompress(), 5000); + checkFootprintStableAcrossClose(() => zlib.createBrotliCompress(), "NativeBrotli", 5000, 60_000); }); test("brotli decompress: estimated size stays tied to construction mode across close", () => { - checkFootprintStableAcrossClose(() => zlib.createBrotliDecompress(), 855); + checkFootprintStableAcrossClose(() => zlib.createBrotliDecompress(), "NativeBrotli", 855, 60_000); }); test("zstd compress: estimated size stays tied to construction mode across close", () => { - checkFootprintStableAcrossClose(() => zlib.createZstdCompress(), 5000); + checkFootprintStableAcrossClose(() => zlib.createZstdCompress(), "NativeZstd", 5000, 60_000); }); test("zstd decompress: estimated size stays tied to construction mode across close", () => { - checkFootprintStableAcrossClose(() => zlib.createZstdDecompress(), 90000); + checkFootprintStableAcrossClose(() => zlib.createZstdDecompress(), "NativeZstd", 90_000, 1_000_000); }); -// GC-safety guard: drive a write so the JS thread enters with_mut, then force -// GC so estimatedSize/visitChildren fires on the marking thread against a live -// stream. Asserts the stream still works and the process exits cleanly (this -// build has ASAN under `bun bd`). -const brotliGcFixture = /* js */ ` +// GC-safety guard: one spawned process interleaves 8 brotli and 8 zstd +// compression streams, forcing a full GC right after each write is queued on +// the work pool and again on the first data event (mid drive loop), so +// estimatedSize/visitChildren fires on the marking thread against live +// streams; each group's final GC marks the already-closed handles. Asserts +// every stream produced output and the process exits cleanly (this build has +// ASAN under `bun bd`). +// +// The guarded race is marking-thread vs work-pool timing, not compression +// effort, so quality/level are set low and both classes share one process: +// spawn + module load dominate the fixture's cost, which matters on slow +// contended CI runners (this file used to time out in parallel batches on +// Windows). +const gcFixture = /* js */ ` const zlib = require("zlib"); - const buf = Buffer.alloc(256 * 1024, "abcdefgh"); - let remaining = 0; - for (let i = 0; i < 8; i++) { - remaining++; - const z = zlib.createBrotliCompress({ chunkSize: 64 * 1024 }); - z.on("error", e => { throw e; }); - z.on("data", () => {}); - z.write(buf, () => { z.end(); if (--remaining === 0) console.log("OK"); }); - Bun.gc(true); - Bun.gc(true); + const compressible = Buffer.alloc(128 * 1024, "abcdefgh"); + const random = require("crypto").randomBytes(128 * 1024); + function drive(z, buf, bucket) { + bucket.push(new Promise((resolve, reject) => { + let out = 0; + let sampled = false; + z.on("error", reject); + z.on("data", c => { + out += c.length; + if (!sampled) { sampled = true; Bun.gc(true); } + }); + z.on("end", () => (out > 0 ? resolve() : reject(new Error("stream produced no output")))); + z.write(buf, () => z.end()); + Bun.gc(true); + })); } - Bun.gc(true); -`; - -const zstdGcFixture = /* js */ ` - const zlib = require("zlib"); - const crypto = require("crypto"); - const buf = crypto.randomBytes(256 * 1024); - let remaining = 0; + const brotli = [], zstd = []; for (let i = 0; i < 8; i++) { - remaining++; - const z = zlib.createZstdCompress({ chunkSize: 64 * 1024 }); - z.on("error", e => { throw e; }); - z.on("data", () => {}); - z.write(buf, () => { z.end(); if (--remaining === 0) console.log("OK"); }); - Bun.gc(true); - Bun.gc(true); + drive(zlib.createBrotliCompress({ chunkSize: 32 * 1024, params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 2 } }), compressible, brotli); + drive(zlib.createZstdCompress({ chunkSize: 32 * 1024, params: { [zlib.constants.ZSTD_c_compressionLevel]: 1 } }), random, zstd); } - Bun.gc(true); + Promise.all(brotli).then(() => { Bun.gc(true); console.log("brotli OK"); }); + Promise.all(zstd).then(() => { Bun.gc(true); console.log("zstd OK"); }); `; -async function runGc(fixture: string) { - await using proc = Bun.spawn({ - cmd: [bunExe(), "-e", fixture], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - return { stdout, stderr, exitCode }; +// Both fixture tests share one child process; a failed group is identified by +// its missing " OK" line in the assertion diff. +let gcRunPromise: Promise<{ stdout: string; stderr: string; exitCode: number }> | undefined; +function gcRun() { + return (gcRunPromise ??= (async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", gcFixture], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + })()); } test.concurrent("brotli: estimatedSize during GC while a stream is live exits cleanly", async () => { - const { stdout, stderr, exitCode } = await runGc(brotliGcFixture); + const { stdout, stderr, exitCode } = await gcRun(); expect(stderr).toBe(""); - expect(stdout.trim()).toBe("OK"); + expect(stdout.split("\n").filter(Boolean).toSorted()).toEqual(["brotli OK", "zstd OK"]); expect(exitCode).toBe(0); }); test.concurrent("zstd: estimatedSize during GC while a stream is live exits cleanly", async () => { - const { stdout, stderr, exitCode } = await runGc(zstdGcFixture); + const { stdout, stderr, exitCode } = await gcRun(); expect(stderr).toBe(""); - expect(stdout.trim()).toBe("OK"); + expect(stdout.split("\n").filter(Boolean).toSorted()).toEqual(["brotli OK", "zstd OK"]); expect(exitCode).toBe(0); }); From 7ad49f4ea334ed7c7b135f6ed5101ed808b465f1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:57:24 +0000 Subject: [PATCH 2/3] test: collapse the duplicate fixture tests into one and correct the incident description The two GC-guard tests asserted identical things on one shared child and could never diverge, so one honestly-named test replaces the memoized pair; -t brotli and -t zstd still match it. Fixture errors now carry their group name so the stderr of a rejection identifies the failing codec. Also drop the timeout claim from the comment: the CI incident this file was flagged for was a parallel-batch worker crash (exit code 9) that passed on solo retry, not a timeout. --- .../node/zlib/zlib-estimated-size-gc.test.ts | 52 +++++++------------ 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/test/js/node/zlib/zlib-estimated-size-gc.test.ts b/test/js/node/zlib/zlib-estimated-size-gc.test.ts index 4c98aed1e6c4..317dede20cfe 100644 --- a/test/js/node/zlib/zlib-estimated-size-gc.test.ts +++ b/test/js/node/zlib/zlib-estimated-size-gc.test.ts @@ -56,62 +56,46 @@ test("zstd decompress: estimated size stays tied to construction mode across clo // ASAN under `bun bd`). // // The guarded race is marking-thread vs work-pool timing, not compression -// effort, so quality/level are set low and both classes share one process: -// spawn + module load dominate the fixture's cost, which matters on slow -// contended CI runners (this file used to time out in parallel batches on -// Windows). +// effort, so quality/level are set low and both classes share one child +// process: spawn + module load dominate the fixture's cost, which matters on +// slow contended CI runners. A failed group names itself, either in the +// stderr of its rejection or as the missing " OK" line. const gcFixture = /* js */ ` const zlib = require("zlib"); const compressible = Buffer.alloc(128 * 1024, "abcdefgh"); const random = require("crypto").randomBytes(128 * 1024); - function drive(z, buf, bucket) { + function drive(z, buf, name, bucket) { bucket.push(new Promise((resolve, reject) => { + const fail = why => reject(new Error(name + ": " + why)); let out = 0; let sampled = false; - z.on("error", reject); + z.on("error", e => fail(e.message || e)); z.on("data", c => { out += c.length; if (!sampled) { sampled = true; Bun.gc(true); } }); - z.on("end", () => (out > 0 ? resolve() : reject(new Error("stream produced no output")))); + z.on("end", () => (out > 0 ? resolve() : fail("stream produced no output"))); z.write(buf, () => z.end()); Bun.gc(true); })); } const brotli = [], zstd = []; for (let i = 0; i < 8; i++) { - drive(zlib.createBrotliCompress({ chunkSize: 32 * 1024, params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 2 } }), compressible, brotli); - drive(zlib.createZstdCompress({ chunkSize: 32 * 1024, params: { [zlib.constants.ZSTD_c_compressionLevel]: 1 } }), random, zstd); + drive(zlib.createBrotliCompress({ chunkSize: 32 * 1024, params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 2 } }), compressible, "brotli", brotli); + drive(zlib.createZstdCompress({ chunkSize: 32 * 1024, params: { [zlib.constants.ZSTD_c_compressionLevel]: 1 } }), random, "zstd", zstd); } Promise.all(brotli).then(() => { Bun.gc(true); console.log("brotli OK"); }); Promise.all(zstd).then(() => { Bun.gc(true); console.log("zstd OK"); }); `; -// Both fixture tests share one child process; a failed group is identified by -// its missing " OK" line in the assertion diff. -let gcRunPromise: Promise<{ stdout: string; stderr: string; exitCode: number }> | undefined; -function gcRun() { - return (gcRunPromise ??= (async () => { - await using proc = Bun.spawn({ - cmd: [bunExe(), "-e", gcFixture], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - return { stdout, stderr, exitCode }; - })()); -} - -test.concurrent("brotli: estimatedSize during GC while a stream is live exits cleanly", async () => { - const { stdout, stderr, exitCode } = await gcRun(); - expect(stderr).toBe(""); - expect(stdout.split("\n").filter(Boolean).toSorted()).toEqual(["brotli OK", "zstd OK"]); - expect(exitCode).toBe(0); -}); - -test.concurrent("zstd: estimatedSize during GC while a stream is live exits cleanly", async () => { - const { stdout, stderr, exitCode } = await gcRun(); +test.concurrent("brotli+zstd: estimatedSize during GC while streams are live exits cleanly", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", gcFixture], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).toBe(""); expect(stdout.split("\n").filter(Boolean).toSorted()).toEqual(["brotli OK", "zstd OK"]); expect(exitCode).toBe(0); From d7df118dbe113398f750194fb1f4df11b81d2c47 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:55:18 +0000 Subject: [PATCH 3/3] ci: retrigger