CI: add CLI startup memory regression check
This commit is contained in:
parent
a782358c9b
commit
c0e0115b31
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@ -232,6 +232,29 @@ jobs:
|
||||
- name: Enforce safe external URL opening policy
|
||||
run: pnpm lint:ui:no-raw-window-open
|
||||
|
||||
startup-memory:
|
||||
name: "startup-memory"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Build dist
|
||||
run: pnpm build
|
||||
|
||||
- name: Check CLI startup memory
|
||||
run: pnpm test:startup:memory
|
||||
|
||||
# Validate docs (format, lint, broken links) only when docs files changed.
|
||||
check-docs:
|
||||
needs: [docs-scope]
|
||||
|
||||
@ -336,6 +336,7 @@
|
||||
"test:perf:budget": "node scripts/test-perf-budget.mjs",
|
||||
"test:perf:hotspots": "node scripts/test-hotspots.mjs",
|
||||
"test:sectriage": "pnpm exec vitest run --config vitest.gateway.config.ts && vitest run --config vitest.unit.config.ts --exclude src/daemon/launchd.integration.test.ts --exclude src/process/exec.test.ts",
|
||||
"test:startup:memory": "node scripts/check-cli-startup-memory.mjs",
|
||||
"test:ui": "pnpm lint:ui:no-raw-window-open && pnpm --dir ui test",
|
||||
"test:voicecall:closedloop": "vitest run extensions/voice-call/src/manager.test.ts extensions/voice-call/src/media-stream.test.ts src/plugins/voice-call.plugin.test.ts --maxWorkers=1",
|
||||
"test:watch": "vitest",
|
||||
|
||||
112
scripts/check-cli-startup-memory.mjs
Normal file
112
scripts/check-cli-startup-memory.mjs
Normal file
@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const isLinux = process.platform === "linux";
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
if (!isLinux && !isMac) {
|
||||
console.log(`[startup-memory] Skipping on unsupported platform: ${process.platform}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const tmpHome = mkdtempSync(path.join(os.tmpdir(), "openclaw-startup-memory-"));
|
||||
|
||||
const DEFAULT_LIMITS_MB = {
|
||||
help: 500,
|
||||
statusJson: 900,
|
||||
gatewayStatus: 900,
|
||||
};
|
||||
|
||||
const cases = [
|
||||
{
|
||||
id: "help",
|
||||
label: "--help",
|
||||
args: ["node", "openclaw.mjs", "--help"],
|
||||
limitMb: Number(process.env.OPENCLAW_STARTUP_MEMORY_HELP_MB ?? DEFAULT_LIMITS_MB.help),
|
||||
},
|
||||
{
|
||||
id: "statusJson",
|
||||
label: "status --json",
|
||||
args: ["node", "openclaw.mjs", "status", "--json"],
|
||||
limitMb: Number(
|
||||
process.env.OPENCLAW_STARTUP_MEMORY_STATUS_JSON_MB ?? DEFAULT_LIMITS_MB.statusJson,
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "gatewayStatus",
|
||||
label: "gateway status",
|
||||
args: ["node", "openclaw.mjs", "gateway", "status"],
|
||||
limitMb: Number(
|
||||
process.env.OPENCLAW_STARTUP_MEMORY_GATEWAY_STATUS_MB ?? DEFAULT_LIMITS_MB.gatewayStatus,
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
function parseMaxRssMb(stderr) {
|
||||
if (isLinux) {
|
||||
const match = stderr.match(/^\s*Maximum resident set size \(kbytes\):\s*(\d+)\s*$/im);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return Number(match[1]) / 1024;
|
||||
}
|
||||
const match = stderr.match(/^\s*(\d+)\s+maximum resident set size\s*$/im);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
return Number(match[1]) / (1024 * 1024);
|
||||
}
|
||||
|
||||
function runCase(testCase) {
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: tmpHome,
|
||||
XDG_CONFIG_HOME: path.join(tmpHome, ".config"),
|
||||
XDG_DATA_HOME: path.join(tmpHome, ".local", "share"),
|
||||
XDG_CACHE_HOME: path.join(tmpHome, ".cache"),
|
||||
};
|
||||
const timeArgs = isLinux ? ["-v", ...testCase.args] : ["-l", ...testCase.args];
|
||||
const result = spawnSync("/usr/bin/time", timeArgs, {
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
});
|
||||
const stderr = result.stderr ?? "";
|
||||
const maxRssMb = parseMaxRssMb(stderr);
|
||||
const matrixBootstrapWarning = /matrix: crypto runtime bootstrap failed/i.test(stderr);
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`${testCase.label} exited with ${String(result.status)}\n${stderr.trim() || result.stdout || ""}`,
|
||||
);
|
||||
}
|
||||
if (maxRssMb == null) {
|
||||
throw new Error(`${testCase.label} did not report max RSS\n${stderr.trim()}`);
|
||||
}
|
||||
if (matrixBootstrapWarning) {
|
||||
throw new Error(`${testCase.label} triggered Matrix crypto bootstrap during startup`);
|
||||
}
|
||||
if (maxRssMb > testCase.limitMb) {
|
||||
throw new Error(
|
||||
`${testCase.label} used ${maxRssMb.toFixed(1)} MB RSS (limit ${testCase.limitMb} MB)`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[startup-memory] ${testCase.label}: ${maxRssMb.toFixed(1)} MB RSS (limit ${testCase.limitMb} MB)`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
for (const testCase of cases) {
|
||||
runCase(testCase);
|
||||
}
|
||||
} finally {
|
||||
rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user