perf(process): reduce allocation while processing command output (#152261)

* perf(process): reuse the command output idle timer

* test(process): synchronize idle timeout cases on live descendants
This commit is contained in:
Peter Steinberger 2026-09-18 18:37:42 -07:00 • committed by GitHub
parent 6ed426e3a8
commit 912b21b985
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 24 additions and 36 deletions

View file

@ -280,12 +280,6 @@ async function runCommandWithOutputEncoding(
}
});
const clearNoOutputTimer = () => {
if (noOutputTimer) {
clearTimeout(noOutputTimer);
noOutputTimer = undefined;
}
};
const cancel = (reason: Exclude<CommandTerminationReason, "exit">) => {
// Failed roots already own a drain; later deadlines must preserve their exit result.
// Successful POSIX roots retain deadline ownership of inherited descendants.
@ -321,8 +315,9 @@ async function runCommandWithOutputEncoding(
) {
return;
}
clearNoOutputTimer();
noOutputTimer = setTimeout(() => cancel("no-output-timeout"), resolvedNoOutputTimeoutMs);
noOutputTimer =
noOutputTimer?.refresh() ??
setTimeout(() => cancel("no-output-timeout"), resolvedNoOutputTimeoutMs);
};
const timeoutTimer =
@ -336,7 +331,8 @@ async function runCommandWithOutputEncoding(
if (timeoutTimer) {
clearTimeout(timeoutTimer);
}
clearNoOutputTimer();
clearTimeout(noOutputTimer);
noOutputTimer = undefined;
signal?.removeEventListener("abort", onAbort);
};
if (startupReady) {

View file

@ -3,9 +3,11 @@ import fsSync from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { isPidAlive } from "../shared/pid-alive.js";
import {
killPidIfAlive,
waitForPidFile,
@ -466,12 +468,12 @@ describe("secret ref resolver", () => {
let childPid: number | undefined;
let resultPromise: Promise<string> | undefined;
const nativeSetTimeout = globalThis.setTimeout;
const noOutputTimeouts: Array<() => void> = [];
let noOutputTimeout: (() => void) | undefined;
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation((callback, delay, ...args) => {
if (delay === 1_000) {
noOutputTimeouts.push(() => callback(...args));
noOutputTimeout = () => callback(...args);
return nativeSetTimeout(() => undefined, 60_000);
}
return nativeSetTimeout(callback, delay, ...args);
@ -485,13 +487,8 @@ describe("secret ref resolver", () => {
});
const resultErrorPromise = resultPromise.catch((error: unknown) => error);
childPid = await waitForPidFile(pidPath);
await vi.waitFor(
() => {
expect(noOutputTimeouts.length).toBeGreaterThanOrEqual(2);
},
{ timeout: 5_000 },
);
noOutputTimeouts.at(-1)?.();
expect(isPidAlive(childPid)).toBe(true);
expectDefined(noOutputTimeout, "no-output timeout")();
const error = await resultErrorPromise;
expect(isProviderScopedSecretResolutionError(error)).toBe(true);

View file

@ -1,10 +1,12 @@
// Covers install-policy checks for packages and plugin installs.
import fs from "node:fs/promises";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { requireNodeTool } from "../../test/helpers/node-toolchain.js";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isPidAlive } from "../shared/pid-alive.js";
import {
killPidIfAlive,
waitForPidFile,
@ -210,20 +212,21 @@ describe("runInstallPolicy", () => {
const forkScriptPath = await writeForkingNoOutputScript(sourceDir);
const pidPath = path.join(sourceDir, "forked.pid");
let childPid: number | undefined;
let resultPromise: ReturnType<typeof runInstallPolicy> | undefined;
const nativeSetTimeout = globalThis.setTimeout;
const noOutputTimeouts: Array<() => void> = [];
let noOutputTimeout: (() => void) | undefined;
const setTimeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockImplementation((callback, delay, ...args) => {
if (delay === 1_000) {
noOutputTimeouts.push(() => callback(...args));
noOutputTimeout = () => callback(...args);
return nativeSetTimeout(() => undefined, 60_000);
}
return nativeSetTimeout(callback, delay, ...args);
});
try {
const resultPromise = runInstallPolicy({
resultPromise = runInstallPolicy({
config: {
security: {
installPolicy: {
@ -243,13 +246,8 @@ describe("runInstallPolicy", () => {
});
void resultPromise.catch(() => undefined);
childPid = await waitForPidFile(pidPath);
await vi.waitFor(
() => {
expect(noOutputTimeouts.length).toBeGreaterThanOrEqual(2);
},
{ timeout: 5_000 },
);
noOutputTimeouts.at(-1)?.();
expect(isPidAlive(childPid)).toBe(true);
expectDefined(noOutputTimeout, "no-output timeout")();
const result = await resultPromise;
expect(result?.blocked?.reason).toContain("policy command produced no output");
@ -257,6 +255,7 @@ describe("runInstallPolicy", () => {
} finally {
setTimeoutSpy.mockRestore();
killPidIfAlive(childPid);
await resultPromise?.catch(() => {});
}
},
);

View file

@ -4,18 +4,14 @@ import { isPidAlive } from "../shared/pid-alive.js";
export async function writeForkingNoOutputScript(dir: string): Promise<string> {
const scriptPath = path.join(dir, "fork-no-output.sh");
// The readiness byte on stderr re-arms the caller's rolling no-output timer,
// so the silence window that kills the tree starts only after the forked pid
// is on disk; without it, slow spawns under suite load race the first window
// and the test reads a missing/empty pid file.
// The descendant publishes its PID after installing its keepalive, so callers
// can trigger the idle deadline only after a live process tree is ready.
await fs.writeFile(
scriptPath,
[
"#!/bin/sh",
'"$NODE_BINARY" -e "setInterval(() => {}, 1000)" &',
'printf "%s" "$!" > "$PID_FILE"',
"echo ready >&2",
"sleep 30",
'"$NODE_BINARY" -e \'setInterval(() => {}, 1000); require("node:fs").writeFileSync(process.env.PID_FILE, String(process.pid)); process.stderr.write("ready\\n");\' &',
"wait",
].join("\n"),
"utf8",
);