mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(perf): keep abort leak thresholds active
This commit is contained in:
@@ -19,9 +19,9 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as v8 from "node:v8";
|
||||
import { abortable as productionAbortable } from "../src/agents/embedded-agent-runner/run/abortable.js";
|
||||
|
||||
type Mode = "production" | "closure-extracted" | "closure-inline" | "synthetic-leak";
|
||||
type Abortable = <T>(signal: AbortSignal, promise: Promise<T>) => Promise<T>;
|
||||
|
||||
type Options = {
|
||||
iters: number;
|
||||
@@ -50,11 +50,11 @@ function parseArgs(argv: string[]): Options {
|
||||
const next = argv[i + 1];
|
||||
switch (arg) {
|
||||
case "--iters":
|
||||
opts.iters = Number.parseInt(next ?? "", 10);
|
||||
opts.iters = parsePositiveInt(next, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--batches":
|
||||
opts.batches = Number.parseInt(next ?? "", 10);
|
||||
opts.batches = parsePositiveInt(next, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--snap-dir":
|
||||
@@ -77,15 +77,15 @@ function parseArgs(argv: string[]): Options {
|
||||
i += 1;
|
||||
break;
|
||||
case "--max-rss-growth-mb":
|
||||
opts.maxRssGrowthMb = Number.parseInt(next ?? "", 10);
|
||||
opts.maxRssGrowthMb = parseNonNegativeInt(next, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--max-tracked-retention":
|
||||
opts.maxTrackedRetention = Number.parseInt(next ?? "", 10);
|
||||
opts.maxTrackedRetention = parseNonNegativeInt(next, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--scope-bytes":
|
||||
opts.scopeBytes = Number.parseInt(next ?? "", 10);
|
||||
opts.scopeBytes = parsePositiveInt(next, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--quiet":
|
||||
@@ -109,6 +109,38 @@ function parseArgs(argv: string[]): Options {
|
||||
return opts;
|
||||
}
|
||||
|
||||
function parsePositiveInt(raw: string | undefined, flag: string): number {
|
||||
const value = parseStrictInt(raw, flag, "positive");
|
||||
if (value <= 0) {
|
||||
fail(`${flag} must be a positive integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseNonNegativeInt(raw: string | undefined, flag: string): number {
|
||||
const value = parseStrictInt(raw, flag, "non-negative");
|
||||
if (value < 0) {
|
||||
fail(`${flag} must be a non-negative integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseStrictInt(
|
||||
raw: string | undefined,
|
||||
flag: string,
|
||||
label: "positive" | "non-negative",
|
||||
): number {
|
||||
const text = (raw ?? "").trim();
|
||||
if (!/^\d+$/u.test(text)) {
|
||||
fail(`${flag} must be a ${label} integer`);
|
||||
}
|
||||
const value = Number(text);
|
||||
if (!Number.isSafeInteger(value)) {
|
||||
fail(`${flag} must be a ${label} integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function printUsage(): void {
|
||||
process.stderr.write(
|
||||
[
|
||||
@@ -137,6 +169,14 @@ const FINALIZED = { count: 0 };
|
||||
const finalizer = new FinalizationRegistry<number>(() => {
|
||||
FINALIZED.count += 1;
|
||||
});
|
||||
let productionAbortable: Abortable | null = null;
|
||||
|
||||
async function loadProductionAbortable(): Promise<void> {
|
||||
const module = (await import("../src/agents/embedded-agent-runner/run/abortable.js")) as {
|
||||
abortable: Abortable;
|
||||
};
|
||||
productionAbortable = module.abortable;
|
||||
}
|
||||
|
||||
function abortableExtracted<T>(signal: AbortSignal, promise: Promise<T>): Promise<T> {
|
||||
if (signal.aborted) {
|
||||
@@ -179,6 +219,9 @@ function runOnce(mode: Mode, scopeBytes: number, iter: number): void {
|
||||
KEEP_ALIVE.push(neverSettling);
|
||||
|
||||
if (mode === "production") {
|
||||
if (!productionAbortable) {
|
||||
throw new Error("production abortable is not loaded");
|
||||
}
|
||||
void productionAbortable(ac.signal, neverSettling).catch(() => {});
|
||||
} else if (mode === "closure-extracted") {
|
||||
void abortableExtracted(ac.signal, neverSettling).catch(() => {});
|
||||
@@ -243,6 +286,9 @@ function fmtBytes(bytes: number): string {
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const opts = parseArgs(process.argv.slice(2));
|
||||
if (opts.mode === "production") {
|
||||
await loadProductionAbortable();
|
||||
}
|
||||
if (typeof globalThis.gc !== "function") {
|
||||
fail("--expose-gc is required (run with: node --expose-gc ...)");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
function makeTempRoot(): string {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "openclaw-embedded-abort-leak-test-"));
|
||||
tempRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
function runHarness(args: string[]) {
|
||||
return spawnSync(
|
||||
process.execPath,
|
||||
["--import", "tsx", "--expose-gc", "scripts/embedded-run-abort-leak.ts", ...args],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("scripts/embedded-run-abort-leak", () => {
|
||||
it("rejects loose numeric thresholds before writing heap snapshots", () => {
|
||||
const cases = [
|
||||
["--iters", "1e3", "positive"],
|
||||
["--batches", "2abc", "positive"],
|
||||
["--max-rss-growth-mb", "0x10", "non-negative"],
|
||||
["--max-tracked-retention", "abc", "non-negative"],
|
||||
["--scope-bytes", "1mb", "positive"],
|
||||
] as const;
|
||||
|
||||
for (const [flag, value, label] of cases) {
|
||||
const snapDir = makeTempRoot();
|
||||
const result = runHarness(["--snap-dir", snapDir, flag, value, "--quiet"]);
|
||||
|
||||
expect(result.status).toBe(2);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain(`error: ${flag} must be a ${label} integer`);
|
||||
expect(readdirSync(snapDir)).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user