mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 10:28:34 +00:00
fix(test): bound extension memory profiler output
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user