Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .github/OPENCLAW_CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 all 10,000 scripts
after a separate 5,000-script cache and allocator warmup. 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.

## Windows release qualification

The separate [`openclaw-release.yml`](workflows/openclaw-release.yml) keeps
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,5 @@
- Publish Darwin/Linux prereleases while Windows signing is unconfigured; require an explicit signed-Windows release switch before including either Windows architecture, while preserving test-only Windows CI.

- Avoid requesting executable access when renaming files on Windows, preventing unnecessary synchronous antivirus scans during compile-cache publication.

- Check live `vm.Script` counts and full-workload RSS growth after warmup 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!
45 changes: 28 additions & 17 deletions test/js/node/vm/script-leak.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
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 warmupIterations = 5_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;
// The measured source payload exceeds the RSS bound if native sources are retained.
const source = `/*\n${Buffer.alloc(32_768, " * 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 < warmupIterations + iterations; ++i) {
go(i);
// Bound temporary garbage before it inflates the allocator's resident pages.
if ((i + 1) % batch === 0) Bun.gc(true);
// Measure all 10,000 scripts after a separate cache and allocator warmup.
if (i + 1 === warmupIterations) 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);
});
});
Loading