Skip to content
Open
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
102 changes: 50 additions & 52 deletions test/js/node/zlib/zlib-estimated-size-gc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,93 +12,91 @@ 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 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 "<group> OK" line.
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, 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", 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() : fail("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", brotli);
drive(zlib.createZstdCompress({ chunkSize: 32 * 1024, params: { [zlib.constants.ZSTD_c_compressionLevel]: 1 } }), random, "zstd", 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) {
test.concurrent("brotli+zstd: estimatedSize during GC while streams are live exits cleanly", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", fixture],
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);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("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);
expect(stderr).toBe("");
expect(stdout.trim()).toBe("OK");
expect(stdout.split("\n").filter(Boolean).toSorted()).toEqual(["brotli OK", "zstd OK"]);
expect(exitCode).toBe(0);
});