mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
## Summary Fix several memory leaks in the compression libraries: - **NativeBrotli/NativeZstd reset()** - Each call to `reset()` allocated a new encoder/decoder without freeing the previous one - **NativeBrotli/NativeZstd init() error paths** - If `setParams()` failed after `stream.init()` succeeded, the instance was leaked - **NativeZstd init()** - If `setPledgedSrcSize()` failed after context creation, the context was leaked - **ZlibCompressorArrayList** - After `deflateInit2_()` succeeded, if `ensureTotalCapacityPrecise()` failed with OOM, zlib internal state was never freed - **NativeBrotli close()** - Now sets state to null to prevent potential double-free (defensive) - **LibdeflateState** - Added `deinit()` for API consistency ## Test plan - [x] Added regression test that calls `reset()` 100k times and measures memory growth - [x] Test shows memory growth dropped from ~600MB to ~10MB for Brotli - [x] Verified no double-frees by tracing code paths - [x] Existing zlib tests pass (except pre-existing timeout in debug build) Before fix (system bun 1.3.3): ``` Memory growth after 100000 reset() calls: 624.38 MB (BrotliCompress) Memory growth after 100000 reset() calls: 540.63 MB (BrotliDecompress) ``` After fix: ``` Memory growth after 100000 reset() calls: 11.84 MB (BrotliCompress) Memory growth after 100000 reset() calls: 0.16 MB (BrotliDecompress) ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com>
66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { createBrotliCompress, createBrotliDecompress } from "zlib";
|
|
|
|
// This test verifies that calling reset() on Brotli streams doesn't leak memory.
|
|
// Before the fix, each reset() call would allocate a new Brotli encoder/decoder
|
|
// without freeing the previous one.
|
|
|
|
test("Brotli reset() should not leak memory", { timeout: 30_000 }, async () => {
|
|
const iterations = 100_000;
|
|
|
|
// Get baseline memory
|
|
Bun.gc(true);
|
|
await Bun.sleep(10);
|
|
const baselineMemory = process.memoryUsage.rss();
|
|
|
|
const compressor = createBrotliCompress();
|
|
|
|
// Reset many times - before the fix, each reset leaks ~400KB (brotli encoder state)
|
|
for (let i = 0; i < iterations; i++) {
|
|
compressor.reset();
|
|
}
|
|
|
|
compressor.close();
|
|
|
|
// Force GC and measure
|
|
Bun.gc(true);
|
|
await Bun.sleep(10);
|
|
const finalMemory = process.memoryUsage.rss();
|
|
|
|
const memoryGrowth = finalMemory - baselineMemory;
|
|
const memoryGrowthMB = memoryGrowth / 1024 / 1024;
|
|
|
|
console.log(`Memory growth after ${iterations} reset() calls: ${memoryGrowthMB.toFixed(2)} MB`);
|
|
|
|
// With 100k iterations and ~400KB per leak, we'd expect ~40GB of leakage without the fix.
|
|
// With the fix, memory growth should be minimal (under 50MB accounting for test overhead).
|
|
expect(memoryGrowthMB).toBeLessThan(50);
|
|
});
|
|
|
|
test("BrotliDecompress reset() should not leak memory", { timeout: 30_000 }, async () => {
|
|
const iterations = 100_000;
|
|
|
|
Bun.gc(true);
|
|
await Bun.sleep(10);
|
|
const baselineMemory = process.memoryUsage.rss();
|
|
|
|
const decompressor = createBrotliDecompress();
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
decompressor.reset();
|
|
}
|
|
|
|
decompressor.close();
|
|
|
|
Bun.gc(true);
|
|
await Bun.sleep(10);
|
|
const finalMemory = process.memoryUsage.rss();
|
|
|
|
const memoryGrowth = finalMemory - baselineMemory;
|
|
const memoryGrowthMB = memoryGrowth / 1024 / 1024;
|
|
|
|
console.log(`Memory growth after ${iterations} reset() calls: ${memoryGrowthMB.toFixed(2)} MB`);
|
|
|
|
expect(memoryGrowthMB).toBeLessThan(50);
|
|
});
|