mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 02:52:15 +00:00
fix(bench): clean timed-out sample process groups
This commit is contained in:
@@ -105,6 +105,8 @@ type CliOptions = {
|
||||
const DEFAULT_RUNS = 5;
|
||||
const DEFAULT_WARMUP = 1;
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
const TIMEOUT_KILL_GRACE_MS = 1_000;
|
||||
const PROCESS_GROUP_EXIT_POLL_MS = 25;
|
||||
const DEFAULT_ENTRY = "openclaw.mjs";
|
||||
const MAX_RSS_MARKER = "__OPENCLAW_MAX_RSS_KB__=";
|
||||
const VALUE_FLAGS = new Set([
|
||||
@@ -708,12 +710,16 @@ async function runSample(params: {
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
let timedOut = false;
|
||||
let forceKillAt: number | null = null;
|
||||
let forceKillTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const maxOutputLength = 32 * 1024 * 1024;
|
||||
|
||||
try {
|
||||
return await new Promise<Sample>((resolve) => {
|
||||
const useProcessGroup = process.platform !== "win32";
|
||||
const proc = spawn(process.execPath, nodeArgs, {
|
||||
cwd: process.cwd(),
|
||||
detached: useProcessGroup,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: runRoot,
|
||||
@@ -733,6 +739,10 @@ async function runSample(params: {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
if (forceKillTimer) {
|
||||
clearTimeout(forceKillTimer);
|
||||
forceKillTimer = null;
|
||||
}
|
||||
const ms = Number(process.hrtime.bigint() - started) / 1e6;
|
||||
resolve({
|
||||
ms,
|
||||
@@ -751,18 +761,11 @@ async function runSample(params: {
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
try {
|
||||
proc.kill("SIGTERM");
|
||||
} catch {
|
||||
// Best-effort timeout cleanup.
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
proc.kill("SIGKILL");
|
||||
} catch {
|
||||
// Best-effort timeout cleanup.
|
||||
}
|
||||
}, 1_000).unref?.();
|
||||
signalSampleProcess(proc, "SIGTERM", useProcessGroup);
|
||||
forceKillAt = Date.now() + TIMEOUT_KILL_GRACE_MS;
|
||||
forceKillTimer = setTimeout(() => {
|
||||
signalSampleProcess(proc, "SIGKILL", useProcessGroup);
|
||||
}, TIMEOUT_KILL_GRACE_MS).unref?.();
|
||||
}, params.timeoutMs);
|
||||
timeout.unref?.();
|
||||
|
||||
@@ -790,16 +793,27 @@ async function runSample(params: {
|
||||
});
|
||||
proc.once("close", (code, signal) => {
|
||||
clearTimeout(timeout);
|
||||
finish({
|
||||
exitCode: code,
|
||||
signal,
|
||||
...(code === 0 && signal == null
|
||||
? {}
|
||||
: {
|
||||
stdoutTail: tailLines(stdout, 20),
|
||||
stderrTail: tailLines(stderr, 20),
|
||||
}),
|
||||
});
|
||||
const complete = () =>
|
||||
finish({
|
||||
exitCode: code,
|
||||
signal,
|
||||
...(code === 0 && signal == null
|
||||
? {}
|
||||
: {
|
||||
stdoutTail: tailLines(stdout, 20),
|
||||
stderrTail: tailLines(stderr, 20),
|
||||
}),
|
||||
});
|
||||
if (timedOut && isSampleProcessGroupAlive(proc, useProcessGroup)) {
|
||||
void finishAfterTimeoutCleanup({
|
||||
complete,
|
||||
forceKillAt,
|
||||
proc,
|
||||
useProcessGroup,
|
||||
});
|
||||
return;
|
||||
}
|
||||
complete();
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
@@ -807,6 +821,80 @@ async function runSample(params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function finishAfterTimeoutCleanup(params: {
|
||||
complete: () => void;
|
||||
forceKillAt: number | null;
|
||||
proc: ReturnType<typeof spawn>;
|
||||
useProcessGroup: boolean;
|
||||
}): Promise<void> {
|
||||
const graceRemainingMs =
|
||||
params.forceKillAt === null
|
||||
? TIMEOUT_KILL_GRACE_MS
|
||||
: Math.max(0, params.forceKillAt - Date.now());
|
||||
if (graceRemainingMs > 0) {
|
||||
await waitForSampleProcessGroupExit(params.proc, params.useProcessGroup, graceRemainingMs);
|
||||
}
|
||||
if (isSampleProcessGroupAlive(params.proc, params.useProcessGroup)) {
|
||||
signalSampleProcess(params.proc, "SIGKILL", params.useProcessGroup);
|
||||
}
|
||||
await waitForSampleProcessGroupExit(params.proc, params.useProcessGroup, TIMEOUT_KILL_GRACE_MS);
|
||||
params.complete();
|
||||
}
|
||||
|
||||
function signalSampleProcess(
|
||||
proc: ReturnType<typeof spawn>,
|
||||
signal: NodeJS.Signals,
|
||||
useProcessGroup: boolean,
|
||||
): void {
|
||||
if (!proc.pid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (useProcessGroup) {
|
||||
process.kill(-proc.pid, signal);
|
||||
} else {
|
||||
proc.kill(signal);
|
||||
}
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
||||
if (code !== "ESRCH" && code !== "EPERM") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isSampleProcessGroupAlive(
|
||||
proc: ReturnType<typeof spawn>,
|
||||
useProcessGroup: boolean,
|
||||
): boolean {
|
||||
if (!useProcessGroup || !proc.pid) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
process.kill(-proc.pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return (error as NodeJS.ErrnoException | undefined)?.code === "EPERM";
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForSampleProcessGroupExit(
|
||||
proc: ReturnType<typeof spawn>,
|
||||
useProcessGroup: boolean,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> {
|
||||
const deadlineAt = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadlineAt) {
|
||||
if (!isSampleProcessGroupAlive(proc, useProcessGroup)) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((resolvePoll) => {
|
||||
setTimeout(resolvePoll, PROCESS_GROUP_EXIT_POLL_MS);
|
||||
});
|
||||
}
|
||||
return !isSampleProcessGroupAlive(proc, useProcessGroup);
|
||||
}
|
||||
|
||||
async function runCase(params: {
|
||||
entry: string;
|
||||
commandCase: CommandCase;
|
||||
|
||||
Reference in New Issue
Block a user