fix(test): bound extension memory profiler output

This commit is contained in:
Vincent Koc
2026-05-28 13:22:57 +02:00
parent 88c395c83c
commit d64b394537
2 changed files with 113 additions and 13 deletions

View File

@@ -10,6 +10,8 @@ const DEFAULT_CONCURRENCY = 6;
const DEFAULT_TIMEOUT_MS = 90_000;
const DEFAULT_COMBINED_TIMEOUT_MS = 180_000;
const DEFAULT_TOP = 10;
const OUTPUT_CAPTURE_MAX_CHARS = 128 * 1024;
const STDERR_PREVIEW_MAX_CHARS = 8 * 1024;
const RSS_MARKER = "__OPENCLAW_MAX_RSS_KB__=";
function printHelp() {
@@ -120,8 +122,49 @@ function parseMaxRssMb(stderr) {
return last ? Number(last[1]) / 1024 : null;
}
function summarizeStderr(stderr, lines = 8) {
return stderr.trim().split("\n").filter(Boolean).slice(0, lines).join("\n");
function createOutputCapture() {
return { text: "", truncatedChars: 0 };
}
function appendBoundedOutput(capture, chunk, maxChars = OUTPUT_CAPTURE_MAX_CHARS) {
const nextText = capture.text + String(chunk);
if (nextText.length <= maxChars) {
return capture.truncatedChars === 0
? { text: nextText, truncatedChars: 0 }
: { text: nextText, truncatedChars: capture.truncatedChars };
}
const truncatedChars = capture.truncatedChars + nextText.length - maxChars;
return { text: nextText.slice(-maxChars), truncatedChars };
}
function formatCapturedOutput(capture) {
if (capture.truncatedChars === 0) {
return capture.text;
}
return `[output truncated ${capture.truncatedChars} chars; showing tail]\n${capture.text}`;
}
function scanMaxRssMb(tail, chunk, current) {
const text = `${tail}${String(chunk)}`;
const parsed = parseMaxRssMb(text);
const lineBreakIndex = Math.max(text.lastIndexOf("\n"), text.lastIndexOf("\r"));
const openLine = lineBreakIndex === -1 ? text : text.slice(lineBreakIndex + 1);
return {
maxRssMb: parsed ?? current,
tail: openLine.slice(-(RSS_MARKER.length + 32)),
};
}
function summarizeStderr(stderr, lines = 8, maxChars = STDERR_PREVIEW_MAX_CHARS) {
const text = stderr.trim().split("\n").filter(Boolean).slice(0, lines).join("\n");
if (text.length <= maxChars) {
return text;
}
const firstLine = text.split("\n", 1)[0] ?? "";
const prefix = firstLine.startsWith("[output truncated") ? `${firstLine}\n` : "";
return `${prefix}[stderr preview truncated ${text.length - maxChars} chars; showing tail]\n${text.slice(
-maxChars,
)}`;
}
async function runCase({ repoRoot, env, hookPath, name, body, timeoutMs }) {
@@ -136,8 +179,10 @@ async function runCase({ repoRoot, env, hookPath, name, body, timeoutMs }) {
},
);
let stdout = "";
let stderr = "";
let stdout = createOutputCapture();
let stderr = createOutputCapture();
let stderrRssTail = "";
let maxRssMb = null;
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
@@ -145,21 +190,25 @@ async function runCase({ repoRoot, env, hookPath, name, body, timeoutMs }) {
}, timeoutMs);
child.stdout.on("data", (chunk) => {
stdout += String(chunk);
stdout = appendBoundedOutput(stdout, chunk);
});
child.stderr.on("data", (chunk) => {
stderr += String(chunk);
const rssScan = scanMaxRssMb(stderrRssTail, chunk, maxRssMb);
stderrRssTail = rssScan.tail;
maxRssMb = rssScan.maxRssMb;
stderr = appendBoundedOutput(stderr, chunk);
});
child.on("close", (code, signal) => {
clearTimeout(timer);
const stderrText = formatCapturedOutput(stderr);
resolve({
name,
code,
signal,
timedOut,
stdout,
stderr,
maxRssMb: parseMaxRssMb(stderr),
stdout: formatCapturedOutput(stdout),
stderr: stderrText,
maxRssMb: maxRssMb ?? parseMaxRssMb(stderrText),
});
});
});
@@ -213,9 +262,10 @@ async function main() {
writeFileSync(
hookPath,
[
"import { writeSync } from 'node:fs';",
"process.on('exit', () => {",
" const usage = typeof process.resourceUsage === 'function' ? process.resourceUsage() : null;",
` if (usage && typeof usage.maxRSS === 'number') console.error('${RSS_MARKER}' + String(usage.maxRSS));`,
` if (usage && typeof usage.maxRSS === 'number') writeSync(2, '${RSS_MARKER}' + String(usage.maxRSS) + '\\n');`,
"});",
"",
].join("\n"),

View File

@@ -1,9 +1,14 @@
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
function runProfileExtensionMemory(args: string[]) {
return spawnSync(process.execPath, ["scripts/profile-extension-memory.mjs", ...args], {
cwd: process.cwd(),
const SCRIPT_PATH = path.resolve("scripts/profile-extension-memory.mjs");
function runProfileExtensionMemory(args: string[], cwd = process.cwd()) {
return spawnSync(process.execPath, [SCRIPT_PATH, ...args], {
cwd,
encoding: "utf8",
});
}
@@ -35,4 +40,49 @@ describe("scripts/profile-extension-memory", () => {
expect(result.stderr).not.toContain("at ");
}
});
it("bounds noisy child output without losing RSS samples", () => {
const root = mkdtempSync(path.join(tmpdir(), "openclaw-extension-memory-test-"));
try {
const extensionDir = path.join(root, "dist", "extensions", "noisy");
const reportPath = path.join(root, "report.json");
mkdirSync(extensionDir, { recursive: true });
writeFileSync(
path.join(extensionDir, "index.js"),
[
`const fs = require("node:fs");`,
`fs.writeSync(2, "old stderr " + "x".repeat(160000) + "\\n");`,
`fs.writeSync(1, "old stdout " + "y".repeat(160000) + "\\n");`,
`process.on("exit", () => fs.writeSync(2, "exit tail\\n"));`,
].join("\n"),
"utf8",
);
const result = runProfileExtensionMemory(
[
"--extension",
"noisy",
"--skip-combined",
"--concurrency",
"1",
"--json",
reportPath,
],
root,
);
expect(result.status, result.stderr).toBe(0);
const report = JSON.parse(readFileSync(reportPath, "utf8"));
expect(report.results).toHaveLength(1);
expect(report.results[0].status).toBe("ok");
expect(report.results[0].maxRssMb).toEqual(expect.any(Number));
expect(report.results[0].stderrPreview).toContain("[output truncated");
expect(report.results[0].stderrPreview).toContain("[stderr preview truncated");
expect(report.results[0].stderrPreview).toContain("exit tail");
expect(report.results[0].stderrPreview).not.toContain("old stderr");
expect(report.results[0].stderrPreview.length).toBeLessThan(9_000);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});