mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
test: add capability-based local thread expansion
This commit is contained in:
110
scripts/test-parallel-pool-policy.mjs
Normal file
110
scripts/test-parallel-pool-policy.mjs
Normal file
@@ -0,0 +1,110 @@
|
||||
const parseTruthyEnv = (value) => {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "1" || normalized === "true";
|
||||
};
|
||||
|
||||
export function resolveThreadPoolPolicy({
|
||||
env = process.env,
|
||||
isCI = false,
|
||||
isWindows = false,
|
||||
hostCpuCount = 0,
|
||||
hostMemoryGiB = 0,
|
||||
loadRatio = 0,
|
||||
testProfile = "normal",
|
||||
} = {}) {
|
||||
const forceThreads = parseTruthyEnv(env.OPENCLAW_TEST_FORCE_THREADS);
|
||||
const forceForks =
|
||||
parseTruthyEnv(env.OPENCLAW_TEST_FORCE_FORKS) ||
|
||||
parseTruthyEnv(env.OPENCLAW_TEST_DISABLE_THREAD_EXPANSION);
|
||||
|
||||
if (isCI) {
|
||||
return {
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "threads",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: isWindows ? 1 : 3,
|
||||
reason: "ci-preserves-current-policy",
|
||||
};
|
||||
}
|
||||
|
||||
if (forceForks) {
|
||||
return {
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "forks",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 1,
|
||||
reason: "forced-forks",
|
||||
};
|
||||
}
|
||||
|
||||
if (forceThreads) {
|
||||
return {
|
||||
threadExpansionEnabled: true,
|
||||
defaultUnitPool: "threads",
|
||||
defaultBasePool: "threads",
|
||||
unitFastLaneCount: hostCpuCount >= 12 && hostMemoryGiB >= 96 && loadRatio < 0.5 ? 2 : 1,
|
||||
reason: "forced-threads",
|
||||
};
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
return {
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "forks",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 1,
|
||||
reason: "windows-local-conservative",
|
||||
};
|
||||
}
|
||||
|
||||
if (testProfile === "serial" || testProfile === "low" || testProfile === "macmini") {
|
||||
return {
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "forks",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 1,
|
||||
reason: "profile-conservative",
|
||||
};
|
||||
}
|
||||
|
||||
if (hostMemoryGiB < 64) {
|
||||
return {
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "forks",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 1,
|
||||
reason: "memory-below-thread-threshold",
|
||||
};
|
||||
}
|
||||
|
||||
if (hostCpuCount < 10) {
|
||||
return {
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "forks",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 1,
|
||||
reason: "cpu-below-thread-threshold",
|
||||
};
|
||||
}
|
||||
|
||||
if (loadRatio >= 0.9) {
|
||||
return {
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "forks",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 1,
|
||||
reason: "host-under-load",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
threadExpansionEnabled: true,
|
||||
defaultUnitPool: "threads",
|
||||
defaultBasePool: "threads",
|
||||
unitFastLaneCount: hostCpuCount >= 12 && hostMemoryGiB >= 96 && loadRatio < 0.5 ? 2 : 1,
|
||||
reason: "strong-local-host",
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
parseCompletedTestFileLines,
|
||||
sampleProcessTreeRssKb,
|
||||
} from "./test-parallel-memory.mjs";
|
||||
import { resolveThreadPoolPolicy } from "./test-parallel-pool-policy.mjs";
|
||||
import {
|
||||
appendCapturedOutput,
|
||||
formatCapturedOutputTail,
|
||||
@@ -105,6 +106,21 @@ const disableIsolation =
|
||||
!forceIsolation &&
|
||||
process.env.OPENCLAW_TEST_NO_ISOLATE !== "0" &&
|
||||
process.env.OPENCLAW_TEST_NO_ISOLATE !== "false";
|
||||
const loadAwareDisabledRaw = process.env.OPENCLAW_TEST_LOAD_AWARE?.trim().toLowerCase();
|
||||
const loadAwareDisabled = loadAwareDisabledRaw === "0" || loadAwareDisabledRaw === "false";
|
||||
const loadRatio =
|
||||
!isCI && !loadAwareDisabled && process.platform !== "win32" && hostCpuCount > 0
|
||||
? os.loadavg()[0] / hostCpuCount
|
||||
: 0;
|
||||
const threadPoolPolicy = resolveThreadPoolPolicy({
|
||||
env: process.env,
|
||||
isCI,
|
||||
isWindows,
|
||||
hostCpuCount,
|
||||
hostMemoryGiB,
|
||||
loadRatio,
|
||||
testProfile,
|
||||
});
|
||||
const includeGatewaySuite = process.env.OPENCLAW_TEST_INCLUDE_GATEWAY === "1";
|
||||
const includeChannelsSuite = process.env.OPENCLAW_TEST_INCLUDE_CHANNELS === "1";
|
||||
const includeExtensionsSuite = process.env.OPENCLAW_TEST_INCLUDE_EXTENSIONS === "1";
|
||||
@@ -119,6 +135,7 @@ const parsePoolOverride = (value, fallback) => {
|
||||
// Even on low-memory hosts, keep the isolated lane split so files like
|
||||
// git-commit.test.ts still get the worker/process isolation they require.
|
||||
const shouldSplitUnitRuns = testProfile !== "serial";
|
||||
const defaultBasePool = threadPoolPolicy.defaultBasePool;
|
||||
let runs = [];
|
||||
const shardOverride = Number.parseInt(process.env.OPENCLAW_TEST_SHARDS ?? "", 10);
|
||||
const configuredShardCount =
|
||||
@@ -283,7 +300,10 @@ const channelIsolatedFiles = dedupeFilesPreserveOrder([
|
||||
),
|
||||
]);
|
||||
const channelIsolatedFileSet = new Set(channelIsolatedFiles);
|
||||
const defaultUnitPool = parsePoolOverride(process.env.OPENCLAW_TEST_UNIT_DEFAULT_POOL, "threads");
|
||||
const defaultUnitPool = parsePoolOverride(
|
||||
process.env.OPENCLAW_TEST_UNIT_DEFAULT_POOL,
|
||||
threadPoolPolicy.defaultUnitPool,
|
||||
);
|
||||
const isTargetedIsolatedUnitFile = (fileFilter) =>
|
||||
unitForkIsolatedFiles.includes(fileFilter) || unitMemoryIsolatedFiles.includes(fileFilter);
|
||||
const inferTarget = (fileFilter) => {
|
||||
@@ -459,7 +479,7 @@ const channelIsolatedEntries = channelIsolatedFiles.map((file) => ({
|
||||
name: `${path.basename(file, ".test.ts")}-channels-isolated`,
|
||||
args: ["vitest", "run", "--config", "vitest.channels.config.ts", "--pool=forks", file],
|
||||
}));
|
||||
const defaultUnitFastLaneCount = testProfile === "low" ? 8 : isCI && !isWindows ? 3 : 1;
|
||||
const defaultUnitFastLaneCount = testProfile === "low" ? 8 : threadPoolPolicy.unitFastLaneCount;
|
||||
const unitFastLaneCount = Math.max(
|
||||
1,
|
||||
parseEnvNumber("OPENCLAW_TEST_UNIT_FAST_LANES", defaultUnitFastLaneCount),
|
||||
@@ -619,7 +639,7 @@ const baseRuns = [
|
||||
"run",
|
||||
"--config",
|
||||
"vitest.unit.config.ts",
|
||||
"--pool=forks",
|
||||
`--pool=${defaultUnitPool}`,
|
||||
...noIsolateArgs,
|
||||
],
|
||||
},
|
||||
@@ -671,7 +691,11 @@ const baseRuns = [
|
||||
runs = baseRuns;
|
||||
const formatEntrySummary = (entry) => {
|
||||
const explicitFilters = countExplicitEntryFilters(entry.args) ?? 0;
|
||||
return `${entry.name} filters=${String(explicitFilters || "all")} maxWorkers=${String(
|
||||
const poolArg =
|
||||
entry.args.find((arg) => arg.startsWith("--pool=")) ??
|
||||
(entry.args.includes("--pool") ? (entry.args[entry.args.indexOf("--pool") + 1] ?? null) : null);
|
||||
const pool = typeof poolArg === "string" ? poolArg.replace(/^--pool=/u, "") : "config-default";
|
||||
return `${entry.name} filters=${String(explicitFilters || "all")} pool=${pool} maxWorkers=${String(
|
||||
maxWorkersForRun(entry.name) ?? "default",
|
||||
)}`;
|
||||
};
|
||||
@@ -801,7 +825,7 @@ const createTargetedEntry = (owner, isolated, filters) => {
|
||||
"--config",
|
||||
"vitest.config.ts",
|
||||
...noIsolateArgs,
|
||||
...(forceForks ? ["--pool=forks"] : []),
|
||||
`--pool=${forceForks ? "forks" : defaultBasePool}`,
|
||||
...filters,
|
||||
],
|
||||
};
|
||||
@@ -1047,12 +1071,6 @@ const serialRuns = keepGatewaySerial ? runs.filter((entry) => entry.name === "ga
|
||||
const serialPrefixRuns = parallelRuns.filter((entry) => entry.serialPhase);
|
||||
const deferredParallelRuns = parallelRuns.filter((entry) => !entry.serialPhase);
|
||||
const baseLocalWorkers = Math.max(4, Math.min(16, hostCpuCount));
|
||||
const loadAwareDisabledRaw = process.env.OPENCLAW_TEST_LOAD_AWARE?.trim().toLowerCase();
|
||||
const loadAwareDisabled = loadAwareDisabledRaw === "0" || loadAwareDisabledRaw === "false";
|
||||
const loadRatio =
|
||||
!isCI && !loadAwareDisabled && process.platform !== "win32" && hostCpuCount > 0
|
||||
? os.loadavg()[0] / hostCpuCount
|
||||
: 0;
|
||||
// Keep the fast-path unchanged on normal load; only throttle under extreme host pressure.
|
||||
const extremeLoadScale = loadRatio >= 1.1 ? 0.75 : loadRatio >= 1 ? 0.85 : 1;
|
||||
const localWorkers = Math.max(4, Math.min(16, Math.floor(baseLocalWorkers * extremeLoadScale)));
|
||||
@@ -1612,6 +1630,19 @@ process.on("SIGINT", () => shutdown("SIGINT"));
|
||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||
process.on("exit", cleanupTempArtifacts);
|
||||
|
||||
if (
|
||||
process.env.OPENCLAW_TEST_SHOW_POOL_DECISION === "1" ||
|
||||
process.env.OPENCLAW_TEST_LIST_LANES === "1"
|
||||
) {
|
||||
console.log(
|
||||
`[test-parallel] pool-policy unit=${defaultUnitPool} base=${defaultBasePool} threadExpansion=${String(
|
||||
threadPoolPolicy.threadExpansionEnabled,
|
||||
)} unitFastLanes=${String(threadPoolPolicy.unitFastLaneCount)} reason=${threadPoolPolicy.reason} load=${loadRatio.toFixed(2)} cpu=${String(
|
||||
hostCpuCount,
|
||||
)} memGiB=${String(hostMemoryGiB)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.OPENCLAW_TEST_LIST_LANES === "1") {
|
||||
const entriesToPrint = targetedEntries.length > 0 ? targetedEntries : runs;
|
||||
for (const entry of entriesToPrint) {
|
||||
|
||||
99
test/scripts/test-parallel-pool-policy.test.ts
Normal file
99
test/scripts/test-parallel-pool-policy.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveThreadPoolPolicy } from "../../scripts/test-parallel-pool-policy.mjs";
|
||||
|
||||
describe("scripts/test-parallel-pool-policy", () => {
|
||||
it("keeps constrained local hosts on forks", () => {
|
||||
expect(
|
||||
resolveThreadPoolPolicy({
|
||||
isCI: false,
|
||||
isWindows: false,
|
||||
hostCpuCount: 8,
|
||||
hostMemoryGiB: 32,
|
||||
loadRatio: 0.2,
|
||||
testProfile: "normal",
|
||||
}),
|
||||
).toMatchObject({
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "forks",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 1,
|
||||
reason: "memory-below-thread-threshold",
|
||||
});
|
||||
});
|
||||
|
||||
it("enables threads for strong idle local hosts", () => {
|
||||
expect(
|
||||
resolveThreadPoolPolicy({
|
||||
isCI: false,
|
||||
isWindows: false,
|
||||
hostCpuCount: 16,
|
||||
hostMemoryGiB: 128,
|
||||
loadRatio: 0.2,
|
||||
testProfile: "normal",
|
||||
}),
|
||||
).toMatchObject({
|
||||
threadExpansionEnabled: true,
|
||||
defaultUnitPool: "threads",
|
||||
defaultBasePool: "threads",
|
||||
unitFastLaneCount: 2,
|
||||
reason: "strong-local-host",
|
||||
});
|
||||
});
|
||||
|
||||
it("disables thread expansion for saturated local hosts", () => {
|
||||
expect(
|
||||
resolveThreadPoolPolicy({
|
||||
isCI: false,
|
||||
isWindows: false,
|
||||
hostCpuCount: 16,
|
||||
hostMemoryGiB: 128,
|
||||
loadRatio: 1.05,
|
||||
testProfile: "normal",
|
||||
}),
|
||||
).toMatchObject({
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "forks",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 1,
|
||||
reason: "host-under-load",
|
||||
});
|
||||
});
|
||||
|
||||
it("honors explicit force-threads overrides", () => {
|
||||
expect(
|
||||
resolveThreadPoolPolicy({
|
||||
env: { OPENCLAW_TEST_FORCE_THREADS: "1" },
|
||||
isCI: false,
|
||||
isWindows: false,
|
||||
hostCpuCount: 8,
|
||||
hostMemoryGiB: 32,
|
||||
loadRatio: 1.2,
|
||||
testProfile: "normal",
|
||||
}),
|
||||
).toMatchObject({
|
||||
threadExpansionEnabled: true,
|
||||
defaultUnitPool: "threads",
|
||||
defaultBasePool: "threads",
|
||||
reason: "forced-threads",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps CI on the current policy", () => {
|
||||
expect(
|
||||
resolveThreadPoolPolicy({
|
||||
isCI: true,
|
||||
isWindows: false,
|
||||
hostCpuCount: 32,
|
||||
hostMemoryGiB: 128,
|
||||
loadRatio: 0,
|
||||
testProfile: "normal",
|
||||
}),
|
||||
).toMatchObject({
|
||||
threadExpansionEnabled: false,
|
||||
defaultUnitPool: "threads",
|
||||
defaultBasePool: "forks",
|
||||
unitFastLaneCount: 3,
|
||||
reason: "ci-preserves-current-policy",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user