diff --git a/test/js/workerd/html-rewriter-leak.test.ts b/test/js/workerd/html-rewriter-leak.test.ts index c51e13d93983..4976410c0cef 100644 --- a/test/js/workerd/html-rewriter-leak.test.ts +++ b/test/js/workerd/html-rewriter-leak.test.ts @@ -47,11 +47,13 @@ test("onEndTag callbacks are released after the rewrite", () => { // LOLHTMLContext.deinit() must destroy those allocations. Previously it only // unprotected the held JSValues and leaked the struct memory. // -// RSS is a high-water mark — Bun.gc(true) collects every wrapper and its -// lol-html builder, but the allocators don't promptly hand pages back to the -// OS. So warmup runs the *same* workload as the measured phase: the allocator -// footprint is established before the baseline, and any growth past that is -// what's actually retained. +// `process.memoryUsage.rss()` is a point sample; mimalloc hands a ~30 MB +// segment back to the OS on its own cadence between passes, so two point +// samples can straddle that release and read as a +31 MB "delta" with nothing +// retained (observed on the 4-vCPU Windows lane). `resourceUsage().maxRSS` is +// the kernel's monotone peak (VmHWM / PeakWorkingSetSize), so it never dips: +// flat when freed memory is reused, strictly rising (~50 MB over 3 passes) +// when it leaks. // // Skipped in debug: at this N a debug pass is ~40s and the extra debug-build // allocation tracking adds enough RSS noise to drown the signal. CI has no @@ -70,19 +72,23 @@ test.skipIf(isDebug)( } const N = 4000; + const samples = []; function pass() { for (let i = 0; i < N; i++) once(); Bun.gc(true); - return process.memoryUsage.rss(); + samples.push(process.memoryUsage.rss()); } - pass(); pass(); - const before = pass(); - pass(); pass(); - const after = pass(); + // ru_maxrss is KB on Linux/Windows, bytes on macOS; normalize to bytes. + const maxRSS = () => process.resourceUsage().maxRSS * (process.platform === "darwin" ? 1 : 1024); + + for (let i = 0; i < 3; i++) pass(); + const before = maxRSS(); + for (let i = 0; i < 3; i++) pass(); + const after = maxRSS(); process.stdout.write( - JSON.stringify({ before, after, deltaMB: (after - before) / 1024 / 1024 }) + "\\n", + JSON.stringify({ samples, before, after, deltaMB: (after - before) / 1024 / 1024 }) + "\\n", ); `; @@ -111,9 +117,11 @@ test.skipIf(isDebug)( .trim(); expect(filteredStderr).toBe(""); + // Logged so a future red carries the per-pass trace without a repro. + console.log(stdout.trim()); const { deltaMB } = JSON.parse(stdout.trim()); - // Unfixed: ~50 MB over 3 measured passes. Fixed: ±1 MB plateau. + // Unfixed: ~50 MB over 3 measured passes. Fixed: <1 MB. // Threshold sits at ~half the unfixed signal. expect(deltaMB).toBeLessThan(25); expect(exitCode).toBe(0);