mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
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:
parent
6ed426e3a8
commit
912b21b985
4 changed files with 24 additions and 36 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(() => {});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue