

















@@ -1,24 +1,27 @@
11#!/usr/bin/env node
223-import { spawnSync } from "node:child_process";
3+import { spawnSync as defaultSpawnSync } from "node:child_process";
44import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
55import os from "node:os";
66import path from "node:path";
7-8-const isLinux = process.platform === "linux";
9-const isMac = process.platform === "darwin";
10-11-if (!isLinux && !isMac) {
12-console.log(`[startup-memory] Skipping on unsupported platform: ${process.platform}`);
13-process.exit(0);
14-}
7+import { pathToFileURL } from "node:url";
158169const repoRoot = process.cwd();
1710const tmpDir = process.env.TMPDIR || process.env.TEMP || process.env.TMP || os.tmpdir();
1811const MAX_RSS_MARKER = "__OPENCLAW_MAX_RSS_KB__=";
12+const DEFAULT_COMMAND_TIMEOUT_MS = 60_000;
13+const COMMAND_TIMEOUT_MS = readPositiveIntEnv(
14+"OPENCLAW_STARTUP_MEMORY_TIMEOUT_MS",
15+DEFAULT_COMMAND_TIMEOUT_MS,
16+);
1917let tmpHome = null;
2018let rssHookPath = null;
211920+function readPositiveIntEnv(name, fallback) {
21+const value = Number(process.env[name] ?? "");
22+return Number.isInteger(value) && value > 0 ? value : fallback;
23+}
24+2225function parseArgs(argv) {
2326const options = {
2427jsonPath:
@@ -171,16 +174,20 @@ function buildBenchEnv() {
171174return env;
172175}
173176174-function runCase(testCase) {
177+function runCase(testCase, params = {}) {
175178if (!rssHookPath) {
176179throw new Error("RSS hook path is not initialized");
177180}
178181const env = buildBenchEnv();
179-const result = spawnSync(process.execPath, ["--import", rssHookPath, ...testCase.args], {
182+const spawn = params.spawnSync ?? defaultSpawnSync;
183+const timeoutMs = params.timeoutMs ?? COMMAND_TIMEOUT_MS;
184+const result = spawn(process.execPath, ["--import", rssHookPath, ...testCase.args], {
180185cwd: repoRoot,
181186 env,
182187encoding: "utf8",
183188maxBuffer: 20 * 1024 * 1024,
189+timeout: timeoutMs,
190+killSignal: "SIGKILL",
184191});
185192const stderr = result.stderr ?? "";
186193const maxRssMb = parseMaxRssMb(stderr);
@@ -193,12 +200,24 @@ function runCase(testCase) {
193200 maxRssMb,
194201status: "pass",
195202exitCode: result.status,
203+signal: result.signal ?? null,
196204error: null,
197205};
198206207+if (result.error) {
208+const timedOut = result.error.code === "ETIMEDOUT";
209+report.status = "fail";
210+report.error = timedOut
211+ ? `${testCase.label} timed out after ${timeoutMs}ms`
212+ : `${testCase.label} failed to start: ${result.error.message}`;
213+return Object.assign(report, {
214+failureMessage: formatFailure(testCase, report.error, stderr.trim() || result.stdout || ""),
215+});
216+}
199217if (result.status !== 0) {
200218report.status = "fail";
201-report.error = `${testCase.label} exited with ${String(result.status)}`;
219+const exitDetail = result.status ?? result.signal ?? "unknown";
220+report.error = `${testCase.label} exited with ${String(exitDetail)}`;
202221return Object.assign(report, {
203222failureMessage: formatFailure(testCase, report.error, stderr.trim() || result.stdout || ""),
204223});
@@ -271,33 +290,59 @@ function writeReport(options, results) {
271290writeFileSync(options.summaryPath, `${lines.join("\n")}\n`, "utf8");
272291}
273292274-const options = parseArgs(process.argv.slice(2));
275-tmpHome = mkdtempSync(path.join(os.tmpdir(), "openclaw-startup-memory-"));
276-rssHookPath = path.join(tmpHome, "measure-rss.mjs");
277-writeFileSync(
278-rssHookPath,
279-[
280-"process.on('exit', () => {",
281-" const usage = typeof process.resourceUsage === 'function' ? process.resourceUsage() : null;",
282-` if (usage && typeof usage.maxRSS === 'number') console.error('${MAX_RSS_MARKER}' + String(usage.maxRSS));`,
283-"});",
284-"",
285-].join("\n"),
286-"utf8",
287-);
288-const results = [];
289-try {
290-for (const testCase of cases) {
291-results.push(runCase(testCase));
293+function runStartupMemoryCheck(argv = process.argv.slice(2), params = {}) {
294+const platform = params.platform ?? process.platform;
295+if (platform !== "linux" && platform !== "darwin") {
296+console.log(`[startup-memory] Skipping on unsupported platform: ${platform}`);
297+return { skipped: true, results: [] };
292298}
293-} finally {
294-writeReport(options, results);
295-if (tmpHome) {
296-rmSync(tmpHome, { recursive: true, force: true });
299+const options = parseArgs(argv);
300+tmpHome = mkdtempSync(path.join(os.tmpdir(), "openclaw-startup-memory-"));
301+rssHookPath = path.join(tmpHome, "measure-rss.mjs");
302+writeFileSync(
303+rssHookPath,
304+[
305+"process.on('exit', () => {",
306+" const usage = typeof process.resourceUsage === 'function' ? process.resourceUsage() : null;",
307+` if (usage && typeof usage.maxRSS === 'number') console.error('${MAX_RSS_MARKER}' + String(usage.maxRSS));`,
308+"});",
309+"",
310+].join("\n"),
311+"utf8",
312+);
313+const results = [];
314+try {
315+for (const testCase of cases) {
316+results.push(runCase(testCase, params));
317+}
318+} finally {
319+writeReport(options, results);
320+if (tmpHome) {
321+rmSync(tmpHome, { recursive: true, force: true });
322+tmpHome = null;
323+rssHookPath = null;
324+}
297325}
326+327+const failure = results.find((result) => result.status !== "pass");
328+if (failure?.failureMessage) {
329+throw new Error(failure.failureMessage);
330+}
331+return { skipped: false, results };
298332}
299333300-const failure = results.find((result) => result.status !== "pass");
301-if (failure?.failureMessage) {
302-throw new Error(failure.failureMessage);
334+export const testing = {
335+ cases,
336+ parseArgs,
337+ runCase,
338+ runStartupMemoryCheck,
339+};
340+341+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
342+try {
343+runStartupMemoryCheck();
344+} catch (error) {
345+console.error(error instanceof Error ? error.stack : String(error));
346+process.exitCode = 1;
347+}
303348}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。