feat(gateway): profile watched gateway startup

This commit is contained in:
Peter Steinberger
2026-05-02 17:05:26 +01:00
parent bd83f8a844
commit 20bb52e42c
5 changed files with 190 additions and 3 deletions

View File

@@ -7,6 +7,8 @@ const TMUX_DISABLE_VALUES = new Set(["0", "false", "no", "off"]);
const TMUX_ATTACH_DISABLE_VALUES = new Set(["0", "false", "no", "off"]);
const TMUX_ATTACH_FORCE_VALUES = new Set(["1", "true", "yes", "on"]);
const DEFAULT_PROFILE_NAME = "main";
const DEFAULT_BENCHMARK_PROFILE_DIR = ".artifacts/gateway-watch-profiles";
const RUN_NODE_CPU_PROF_DIR_ENV = "OPENCLAW_RUN_NODE_CPU_PROF_DIR";
const RAW_WATCH_SCRIPT = "scripts/watch-node.mjs";
const TMUX_CWD_ENV_KEY = "OPENCLAW_GATEWAY_WATCH_CWD";
const TMUX_CWD_OPTION_KEY = "@openclaw.gateway_watch.cwd";
@@ -16,6 +18,7 @@ const TMUX_CHILD_ENV_KEYS = [
"OPENCLAW_GATEWAY_PORT",
"OPENCLAW_HOME",
"OPENCLAW_PROFILE",
RUN_NODE_CPU_PROF_DIR_ENV,
"OPENCLAW_SKIP_CHANNELS",
"OPENCLAW_STATE_DIR",
];
@@ -46,6 +49,54 @@ const readArgValue = (args, flag) => {
return null;
};
const resolveGatewayWatchBenchmarkArgs = ({ args = [], env = process.env } = {}) => {
const passthroughArgs = [];
let benchmarkDir = null;
let benchmarkFlagSeen = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--benchmark") {
benchmarkFlagSeen = true;
benchmarkDir ??= DEFAULT_BENCHMARK_PROFILE_DIR;
continue;
}
if (typeof arg === "string" && arg.startsWith("--benchmark=")) {
benchmarkFlagSeen = true;
benchmarkDir = arg.slice("--benchmark=".length) || DEFAULT_BENCHMARK_PROFILE_DIR;
continue;
}
if (arg === "--benchmark-dir") {
benchmarkFlagSeen = true;
const next = args[index + 1];
if (typeof next === "string" && !next.startsWith("-")) {
benchmarkDir = next;
index += 1;
} else {
benchmarkDir ??= DEFAULT_BENCHMARK_PROFILE_DIR;
}
continue;
}
if (typeof arg === "string" && arg.startsWith("--benchmark-dir=")) {
benchmarkFlagSeen = true;
benchmarkDir = arg.slice("--benchmark-dir=".length) || DEFAULT_BENCHMARK_PROFILE_DIR;
continue;
}
passthroughArgs.push(arg);
}
const nextEnv = { ...env };
if (benchmarkFlagSeen) {
nextEnv[RUN_NODE_CPU_PROF_DIR_ENV] =
benchmarkDir || nextEnv[RUN_NODE_CPU_PROF_DIR_ENV] || DEFAULT_BENCHMARK_PROFILE_DIR;
}
return {
args: passthroughArgs,
benchmarkProfileDir: nextEnv[RUN_NODE_CPU_PROF_DIR_ENV] || null,
env: nextEnv,
};
};
export const resolveGatewayWatchTmuxSessionName = ({ args = [], env = process.env } = {}) => {
const profile =
env.OPENCLAW_PROFILE ||
@@ -168,10 +219,14 @@ const setTmuxSessionMetadata = ({ cwd, sessionName, spawnSyncImpl, stderr }) =>
};
export const runGatewayWatchTmuxMain = (params = {}) => {
const deps = {
const resolvedArgs = resolveGatewayWatchBenchmarkArgs({
args: params.args ?? process.argv.slice(2),
cwd: params.cwd ?? process.cwd(),
env: params.env ? { ...params.env } : { ...process.env },
});
const deps = {
args: resolvedArgs.args,
cwd: params.cwd ?? process.cwd(),
env: resolvedArgs.env,
nodePath: params.nodePath ?? process.execPath,
spawnSync: params.spawnSync ?? spawnSync,
stderr: params.stderr ?? process.stderr,
@@ -180,6 +235,10 @@ export const runGatewayWatchTmuxMain = (params = {}) => {
stdoutIsTTY: params.stdoutIsTTY ?? process.stdout.isTTY,
};
if (resolvedArgs.benchmarkProfileDir) {
log(deps.stderr, `gateway:watch benchmark CPU profiles: ${resolvedArgs.benchmarkProfileDir}`);
}
if (TMUX_DISABLE_VALUES.has(String(deps.env.OPENCLAW_GATEWAY_WATCH_TMUX ?? "").toLowerCase())) {
return runForegroundWatcher({
args: deps.args,

View File

@@ -432,6 +432,7 @@ const isSignalKey = (signal) => Object.hasOwn(SIGNAL_EXIT_CODES, signal);
const getSignalExitCode = (signal) => (isSignalKey(signal) ? SIGNAL_EXIT_CODES[signal] : 1);
const RUN_NODE_OUTPUT_LOG_ENV = "OPENCLAW_RUN_NODE_OUTPUT_LOG";
const RUN_NODE_CPU_PROF_DIR_ENV = "OPENCLAW_RUN_NODE_CPU_PROF_DIR";
const RUN_NODE_BUILD_LOCK_TIMEOUT_ENV = "OPENCLAW_RUN_NODE_BUILD_LOCK_TIMEOUT_MS";
const RUN_NODE_BUILD_LOCK_POLL_ENV = "OPENCLAW_RUN_NODE_BUILD_LOCK_POLL_MS";
const RUN_NODE_BUILD_LOCK_STALE_ENV = "OPENCLAW_RUN_NODE_BUILD_LOCK_STALE_MS";
@@ -504,6 +505,35 @@ const logRunner = (message, deps) => {
deps.outputTee?.write(line);
};
const sanitizeCpuProfileNamePart = (value) => {
const normalized = String(value ?? "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9_.-]+/g, "-")
.replace(/^-+|-+$/g, "");
return normalized || "command";
};
const resolveRunNodeCpuProfileArgs = (deps) => {
const profileDir = deps.env[RUN_NODE_CPU_PROF_DIR_ENV]?.trim();
if (!profileDir) {
return [];
}
const absoluteProfileDir = path.resolve(deps.cwd, profileDir);
deps.fs.mkdirSync(absoluteProfileDir, { recursive: true });
deps.env[RUN_NODE_CPU_PROF_DIR_ENV] = absoluteProfileDir;
const commandName = sanitizeCpuProfileNamePart(deps.args[0]);
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const pid = Number.isInteger(deps.process.pid) && deps.process.pid > 0 ? deps.process.pid : "pid";
const profileName = `openclaw-${commandName}-${pid}-${timestamp}.cpuprofile`;
const profilePath = path.join(absoluteProfileDir, profileName);
const relativeProfilePath = path.relative(deps.cwd, profilePath) || profilePath;
logRunner(`Writing Node CPU profile to ${relativeProfilePath}.`, deps);
return ["--cpu-prof", `--cpu-prof-dir=${absoluteProfileDir}`, `--cpu-prof-name=${profileName}`];
};
const waitForSpawnedProcess = async (childProcess, deps) => {
let forwardedSignal = null;
let onSigInt;
@@ -574,7 +604,8 @@ const getInterruptedSpawnExitCode = (res) => {
};
const runOpenClaw = async (deps) => {
const nodeProcess = deps.spawn(deps.execPath, ["openclaw.mjs", ...deps.args], {
const cpuProfileArgs = resolveRunNodeCpuProfileArgs(deps);
const nodeProcess = deps.spawn(deps.execPath, [...cpuProfileArgs, "openclaw.mjs", ...deps.args], {
cwd: deps.cwd,
env: deps.env,
stdio: deps.outputTee ? ["inherit", "pipe", "pipe"] : "inherit",