Files
openclaw/scripts/check-cli-startup-memory.mjs
T
Tabula Myriad a9ae1a6778 feat: Triad development iteration complete
Matrix Protocol:
- docker-compose.matrix.yml: Dendrite homeserver + PostgreSQL + Nginx TLS
- src/channels/plugins/matrix-channel.ts: OpenClaw plugin implementation
- docs/matrix-triad-setup.md: Setup guide with auth scheme (@tm1-4:triad.local)

MCP Server Integration:
- docs/mcp-triad-integration.md: SearXNG, Playwright, GitHub MCP configs
- docs/mcp-curiosity-mapping.md: Gap-to-capability mapping

Node Sync Architecture:
- src/services/node-sync-service.ts: WebSocket peer sync + presence detection
- src/services/node-sync-service.test.ts: Unit tests
- docs/node-sync-architecture.md: Architecture docs

Triad Resilience:
- scripts/triad-corruption-check.mjs: SQLite + log + config + git integrity
- docs/triad-resilience.md: Recovery procedures
- .secure/deployment-logs/README.md: Schema v2
- skills/triad-heartbeat/SKILL.md: Corruption check integration

NPM Publish Workflow:
- scripts/npm-publish.mjs: version, changelog, validate, publish, rollback
- .github/workflows/npm-publish.yml: GitHub Actions with provenance
- docs/npm-publish-guide.md: Complete documentation

All deliverables tested in Docker before production.
2026-03-24 00:44:50 -04:00

185 lines
5.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } 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 tmpDir = process.env.TMPDIR || process.env.TEMP || process.env.TMP || os.tmpdir();
const rssHookPath = path.join(tmpHome, "measure-rss.mjs");
const MAX_RSS_MARKER = "__OPENCLAW_MAX_RSS_KB__=";
writeFileSync(
rssHookPath,
[
"process.on('exit', () => {",
" const usage = typeof process.resourceUsage === 'function' ? process.resourceUsage() : null;",
` if (usage && typeof usage.maxRSS === 'number') console.error('${MAX_RSS_MARKER}' + String(usage.maxRSS));`,
"});",
"",
].join("\n"),
"utf8",
);
const DEFAULT_LIMITS_MB = {
help: 100,
statusJson: 400,
gatewayStatus: 500,
};
const cases = [
{
id: "help",
label: "--help",
args: ["openclaw.mjs", "--help"],
limitMb: Number(process.env.OPENCLAW_STARTUP_MEMORY_HELP_MB ?? DEFAULT_LIMITS_MB.help),
},
{
id: "statusJson",
label: "status --json",
args: ["openclaw.mjs", "status", "--json"],
limitMb: Number(
process.env.OPENCLAW_STARTUP_MEMORY_STATUS_JSON_MB ?? DEFAULT_LIMITS_MB.statusJson,
),
},
{
id: "gatewayStatus",
label: "gateway status",
args: ["openclaw.mjs", "gateway", "status"],
limitMb: Number(
process.env.OPENCLAW_STARTUP_MEMORY_GATEWAY_STATUS_MB ?? DEFAULT_LIMITS_MB.gatewayStatus,
),
},
];
function formatFixGuidance(testCase, details) {
const command = `node ${testCase.args.join(" ")}`;
const guidance = [
"[startup-memory] Fix guidance",
`Case: ${testCase.label}`,
`Command: ${command}`,
"Next steps:",
`1. Run \`${command}\` locally on the built tree.`,
"2. If this is an RSS overage, compare the startup import graph against the last passing commit and look for newly eager imports, bootstrap side effects, or plugin loading on the command path.",
"3. If this is a non-zero exit, inspect the first transitive import/config error in stderr and fix that root cause before re-checking memory.",
"LLM prompt:",
`"OpenClaw startup-memory CI failed for '${testCase.label}'. Analyze this failure, identify the first runtime/import side effect that makes startup heavier or broken, and propose the smallest safe patch. Failure output:\n${details}"`,
];
return `${guidance.join("\n")}\n`;
}
function formatFailure(testCase, message, details = "") {
const trimmedDetails = details.trim();
const sections = [message];
if (trimmedDetails) {
sections.push(trimmedDetails);
}
sections.push(formatFixGuidance(testCase, trimmedDetails || message));
return sections.join("\n\n");
}
function parseMaxRssMb(stderr) {
const matches = [...stderr.matchAll(new RegExp(`^${MAX_RSS_MARKER}(\\d+)\\s*$`, "gm"))];
const lastMatch = matches.at(-1);
if (!lastMatch) {
return null;
}
return Number(lastMatch[1]) / 1024;
}
function buildBenchEnv() {
const env = {
HOME: tmpHome,
USERPROFILE: tmpHome,
XDG_CONFIG_HOME: path.join(tmpHome, ".config"),
XDG_DATA_HOME: path.join(tmpHome, ".local", "share"),
XDG_CACHE_HOME: path.join(tmpHome, ".cache"),
PATH: process.env.PATH ?? "",
TMPDIR: tmpDir,
TEMP: tmpDir,
TMP: tmpDir,
LANG: process.env.LANG ?? "C.UTF-8",
TERM: process.env.TERM ?? "dumb",
};
if (process.env.LC_ALL) {
env.LC_ALL = process.env.LC_ALL;
}
if (process.env.CI) {
env.CI = process.env.CI;
}
if (process.env.NODE_DISABLE_COMPILE_CACHE) {
env.NODE_DISABLE_COMPILE_CACHE = process.env.NODE_DISABLE_COMPILE_CACHE;
} else {
// Keep the regression check focused on app/runtime startup, not Node's
// one-shot compile cache overhead, which varies across runner builds.
env.NODE_DISABLE_COMPILE_CACHE = "1";
}
// Keep the benchmark on a single process so RSS reflects the actual command
// path rather than the warning-suppression respawn wrapper.
env.OPENCLAW_NO_RESPAWN = "1";
return env;
}
function runCase(testCase) {
const env = buildBenchEnv();
const result = spawnSync(process.execPath, ["--import", rssHookPath, ...testCase.args], {
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(
formatFailure(
testCase,
`${testCase.label} exited with ${String(result.status)}`,
stderr.trim() || result.stdout || "",
),
);
}
if (maxRssMb == null) {
throw new Error(formatFailure(testCase, `${testCase.label} did not report max RSS`, stderr));
}
if (matrixBootstrapWarning) {
throw new Error(
formatFailure(testCase, `${testCase.label} triggered Matrix crypto bootstrap during startup`),
);
}
if (maxRssMb > testCase.limitMb) {
throw new Error(
formatFailure(
testCase,
`${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 });
}