From 43b87ed442e55d4e053b252504ae8ebd981e8d16 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 5 Oct 2026 11:29:00 -0700 Subject: [PATCH] test(vm): check retained growth after warming script caches Adapt the live-cell and batched-collection approach from oven-sh/bun#42474 while preserving 10000 scripts in every profile and the original 200/700 MiB RSS bounds. Compare the second half against an identical warmup workload so allocator and code-cache startup do not masquerade as retained growth. --- .github/OPENCLAW_CI.md | 6 ++++ CHANGELOG.md | 2 ++ test/js/node/vm/script-leak.test.ts | 43 +++++++++++++++++------------ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/.github/OPENCLAW_CI.md b/.github/OPENCLAW_CI.md index 43741a63cf84..412d25e8fbf2 100644 --- a/.github/OPENCLAW_CI.md +++ b/.github/OPENCLAW_CI.md @@ -84,6 +84,12 @@ test/js/node/process/process.test.js test/js/bun/sqlite/column-types.test.js ``` +The `vm.Script` leak regression checks live `Script` cells after collection and +collects between allocation batches. It measures RSS growth over the second half +of the workload, after warming the code cache and allocator with the first half. It retains the 200 MiB release and 700 MiB ASAN limits; a deliberately +retained-script control must fail the live-cell assertion when qualifying changes +to this guard. + ## Caches and artifacts The lane caches the build system's download/prebuilt/ccache directory, Rust diff --git a/CHANGELOG.md b/CHANGELOG.md index f3e73f501a42..9b134fb7197f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -203,3 +203,5 @@ - Add position-preserving `module.stripTypeScriptTypes()` strip mode for tooling that analyzes TypeScript exports. Adapts [oven-sh/bun#35517](https://github.com/oven-sh/bun/pull/35517); thanks @cirospaciari! - Keep query and fragment imports of compiled embedded modules on their canonical module record, including Windows and non-ASCII filenames. + +- Check live `vm.Script` counts and post-warmup RSS growth in the leak regression, preserving the 200 MiB release and 700 MiB ASAN RSS limits. Adapts [oven-sh/bun#42474](https://github.com/oven-sh/bun/pull/42474); thanks @robobun! diff --git a/test/js/node/vm/script-leak.test.ts b/test/js/node/vm/script-leak.test.ts index 586ec3d88b99..2c9161997b64 100644 --- a/test/js/node/vm/script-leak.test.ts +++ b/test/js/node/vm/script-leak.test.ts @@ -1,30 +1,39 @@ -const vm = require("vm"); -const { describe, it, expect } = require("bun:test"); -const { isASAN, rss } = require("harness"); +import { heapStats } from "bun:jsc"; +import { describe, expect, it } from "bun:test"; +import { isASAN, rss } from "harness"; +import vm from "node:vm"; + +const iterations = 10_000; +const batch = 500; describe("vm.Script", () => { it("shouldn't leak memory", () => { - const initialUsage = rss(); - - { - const source = `/*\n${Buffer.alloc(10000, " * aaaaa\n").toString("utf8")}\n*/ Buffer.alloc(10, 'hello');`; - - function go(i) { - const script = new vm.Script(source + "//" + i); - script.runInThisContext(); - } + Bun.gc(true); + const initialCount = heapStats().objectTypeCounts.Script ?? 0; + let initialUsage = 0; + const source = `/*\n${Buffer.alloc(50_000, " * aaaaa\n").toString("utf8")}\n*/ Buffer.alloc(10, 'hello').toString();`; - for (let i = 0; i < 10000; ++i) { - go(i); - } + let result; + function go(i) { + const script = new vm.Script(source + "//" + i); + result = script.runInThisContext(); } + for (let i = 0; i < iterations; ++i) { + go(i); + // Bound temporary garbage before it inflates the allocator's resident pages. + if ((i + 1) % batch === 0) Bun.gc(true); + // Measure growth after warming the code cache and allocator with the same work. + if (i + 1 === iterations / 2) initialUsage = rss(); + } + expect(result).toBe("hellohello"); Bun.gc(true); const finalUsage = rss(); + const finalCount = heapStats().objectTypeCounts.Script ?? 0; const megabytes = Math.round(((finalUsage - initialUsage) / 1024 / 1024) * 100) / 100; - // ASAN's quarantine retains freed allocations (default 256 MB) so RSS - // deltas run far higher under bun-asan; widen the threshold there. + expect(finalCount).toBeLessThanOrEqual(initialCount + 10); + // ASAN's quarantine retains freed allocations (default 256 MB). expect(megabytes).toBeLessThan(isASAN ? 700 : 200); }); });