mirror of https://github.com/openclaw/openclaw.git
196 lines
5.8 KiB
JavaScript
196 lines
5.8 KiB
JavaScript
import { spawnSync } from "node:child_process";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import {
|
|
booleanFlag,
|
|
floatFlag,
|
|
intFlag,
|
|
parseFlagArgs,
|
|
readEnvNumber,
|
|
stringFlag,
|
|
} from "./lib/arg-utils.mjs";
|
|
import { formatMs } from "./lib/vitest-report-cli-utils.mjs";
|
|
import { loadTestRunnerBehavior, loadUnitTimingManifest } from "./test-runner-manifest.mjs";
|
|
|
|
export function parseArgs(argv) {
|
|
const envLimit = readEnvNumber("OPENCLAW_TEST_THREAD_CANDIDATE_LIMIT");
|
|
return parseFlagArgs(
|
|
argv,
|
|
{
|
|
config: "vitest.unit.config.ts",
|
|
limit: Number.isFinite(envLimit) ? Math.max(1, Math.floor(envLimit)) : 20,
|
|
minDurationMs: readEnvNumber("OPENCLAW_TEST_THREAD_CANDIDATE_MIN_DURATION_MS") ?? 250,
|
|
minGainMs: readEnvNumber("OPENCLAW_TEST_THREAD_CANDIDATE_MIN_GAIN_MS") ?? 100,
|
|
minGainPct: readEnvNumber("OPENCLAW_TEST_THREAD_CANDIDATE_MIN_GAIN_PCT") ?? 10,
|
|
json: false,
|
|
files: [],
|
|
},
|
|
[
|
|
stringFlag("--config", "config"),
|
|
intFlag("--limit", "limit", { min: 1 }),
|
|
floatFlag("--min-duration-ms", "minDurationMs", { min: 0 }),
|
|
floatFlag("--min-gain-ms", "minGainMs", { min: 0 }),
|
|
floatFlag("--min-gain-pct", "minGainPct", { min: 0, includeMin: false }),
|
|
booleanFlag("--json", "json"),
|
|
],
|
|
{
|
|
ignoreDoubleDash: true,
|
|
onUnhandledArg(arg, args) {
|
|
if (arg.startsWith("-")) {
|
|
throw new Error(`Unknown option: ${arg}`);
|
|
}
|
|
args.files.push(arg);
|
|
return "handled";
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
export function getExistingThreadCandidateExclusions(behavior) {
|
|
return new Set([
|
|
...(behavior.base?.threadPinned ?? []).map((entry) => entry.file),
|
|
...(behavior.base?.threadSingleton ?? []).map((entry) => entry.file),
|
|
...(behavior.unit?.isolated ?? []).map((entry) => entry.file),
|
|
...(behavior.unit?.threadPinned ?? []).map((entry) => entry.file),
|
|
...(behavior.unit?.threadSingleton ?? []).map((entry) => entry.file),
|
|
]);
|
|
}
|
|
|
|
export function selectThreadCandidateFiles({
|
|
files,
|
|
timings,
|
|
exclude = new Set(),
|
|
limit,
|
|
minDurationMs,
|
|
includeUnknownDuration = false,
|
|
}) {
|
|
return files
|
|
.map((file) => ({
|
|
file,
|
|
durationMs: timings.files[file]?.durationMs ?? null,
|
|
}))
|
|
.filter((entry) => !exclude.has(entry.file))
|
|
.filter((entry) =>
|
|
entry.durationMs === null ? includeUnknownDuration : entry.durationMs >= minDurationMs,
|
|
)
|
|
.toSorted((a, b) => b.durationMs - a.durationMs)
|
|
.slice(0, limit)
|
|
.map((entry) => entry.file);
|
|
}
|
|
|
|
export function summarizeThreadBenchmark({ file, forks, threads, minGainMs, minGainPct }) {
|
|
const forkOk = forks.exitCode === 0;
|
|
const threadOk = threads.exitCode === 0;
|
|
const gainMs = forks.elapsedMs - threads.elapsedMs;
|
|
const gainPct = forks.elapsedMs > 0 ? (gainMs / forks.elapsedMs) * 100 : 0;
|
|
const recommended =
|
|
forkOk &&
|
|
threadOk &&
|
|
gainMs >= minGainMs &&
|
|
gainPct >= minGainPct &&
|
|
threads.elapsedMs < forks.elapsedMs;
|
|
return {
|
|
file,
|
|
forks,
|
|
threads,
|
|
gainMs,
|
|
gainPct,
|
|
recommended,
|
|
};
|
|
}
|
|
|
|
function benchmarkFile({ config, file, pool }) {
|
|
const startedAt = process.hrtime.bigint();
|
|
const run = spawnSync("pnpm", ["vitest", "run", "--config", config, `--pool=${pool}`, file], {
|
|
encoding: "utf8",
|
|
env: process.env,
|
|
maxBuffer: 20 * 1024 * 1024,
|
|
});
|
|
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
|
return {
|
|
pool,
|
|
exitCode: run.status ?? 1,
|
|
elapsedMs,
|
|
stderr: run.stderr ?? "",
|
|
stdout: run.stdout ?? "",
|
|
};
|
|
}
|
|
|
|
function buildOutput(results) {
|
|
return results.map((result) => ({
|
|
file: result.file,
|
|
forksMs: Math.round(result.forks.elapsedMs),
|
|
threadsMs: Math.round(result.threads.elapsedMs),
|
|
gainMs: Math.round(result.gainMs),
|
|
gainPct: Number(result.gainPct.toFixed(1)),
|
|
forksExitCode: result.forks.exitCode,
|
|
threadsExitCode: result.threads.exitCode,
|
|
recommended: result.recommended,
|
|
}));
|
|
}
|
|
|
|
async function main() {
|
|
const opts = parseArgs(process.argv.slice(2));
|
|
const behavior = loadTestRunnerBehavior();
|
|
const timings = loadUnitTimingManifest();
|
|
const exclude = getExistingThreadCandidateExclusions(behavior);
|
|
const inputFiles = opts.files.length > 0 ? opts.files : Object.keys(timings.files);
|
|
const candidates = selectThreadCandidateFiles({
|
|
files: inputFiles,
|
|
timings,
|
|
exclude,
|
|
limit: opts.limit,
|
|
minDurationMs: opts.minDurationMs,
|
|
includeUnknownDuration: opts.files.length > 0,
|
|
});
|
|
|
|
const results = [];
|
|
for (const file of candidates) {
|
|
const forks = benchmarkFile({ config: opts.config, file, pool: "forks" });
|
|
const threads = benchmarkFile({ config: opts.config, file, pool: "threads" });
|
|
results.push(
|
|
summarizeThreadBenchmark({
|
|
file,
|
|
forks,
|
|
threads,
|
|
minGainMs: opts.minGainMs,
|
|
minGainPct: opts.minGainPct,
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (opts.json) {
|
|
console.log(JSON.stringify(buildOutput(results), null, 2));
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
`[test-find-thread-candidates] tested=${String(results.length)} minGain=${formatMs(
|
|
opts.minGainMs,
|
|
0,
|
|
)} minGainPct=${String(opts.minGainPct)}%`,
|
|
);
|
|
for (const result of results) {
|
|
const status = result.recommended
|
|
? "recommend"
|
|
: result.forks.exitCode !== 0
|
|
? "forks-failed"
|
|
: result.threads.exitCode !== 0
|
|
? "threads-failed"
|
|
: "skip";
|
|
console.log(
|
|
`${status.padEnd(14, " ")} ${result.file} forks=${formatMs(
|
|
result.forks.elapsedMs,
|
|
0,
|
|
)} threads=${formatMs(result.threads.elapsedMs, 0)} gain=${formatMs(result.gainMs, 0)} (${result.gainPct.toFixed(1)}%)`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
}
|