perf: parallelize local check gate

This commit is contained in:
Peter Steinberger
2026-04-20 13:55:48 +01:00
parent a1bd02fdfd
commit 46ae3d314a
14 changed files with 246 additions and 72 deletions

View File

@@ -1,57 +1,3 @@
import { spawnSync } from "node:child_process";
import { performance } from "node:perf_hooks";
import { main } from "./check.mjs";
const includeArchitecture = process.argv.includes("--include-architecture");
const stages = [
{ name: "conflict markers", args: ["check:no-conflict-markers"] },
{ name: "tool display", args: ["tool-display:check"] },
{ name: "host env policy", args: ["check:host-env-policy:swift"] },
{ name: "typecheck", args: ["tsgo:all"] },
{ name: "lint", args: ["lint"] },
{ name: "webhook body guard", args: ["lint:webhook:no-low-level-body-read"] },
{ name: "pairing store guard", args: ["lint:auth:no-pairing-store-group"] },
{ name: "pairing account guard", args: ["lint:auth:pairing-account-scope"] },
{ name: "runtime import cycles", args: ["check:import-cycles"] },
];
if (includeArchitecture) {
stages.push({ name: "architecture import cycles", args: ["check:madge-import-cycles"] });
}
const timings = [];
let exitCode = 0;
for (const { name, args } of stages) {
const startedAt = performance.now();
console.error(`\n[check:timed] ${name}`);
const result = spawnSync("pnpm", args, {
stdio: "inherit",
shell: process.platform === "win32",
});
const durationMs = performance.now() - startedAt;
timings.push({ name, durationMs, status: result.status ?? 1 });
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
exitCode = result.status ?? 1;
break;
}
}
console.error("\n[check:timed] summary");
for (const timing of timings) {
const status = timing.status === 0 ? "ok" : `failed:${timing.status}`;
console.error(`${formatMs(timing.durationMs).padStart(8)} ${status.padEnd(9)} ${timing.name}`);
}
process.exitCode = exitCode;
function formatMs(durationMs) {
if (durationMs < 1000) {
return `${Math.round(durationMs)}ms`;
}
return `${(durationMs / 1000).toFixed(2)}s`;
}
await main([...process.argv.slice(2), "--timed"]);

125
scripts/check.mjs Normal file
View File

@@ -0,0 +1,125 @@
import { spawn } from "node:child_process";
import { performance } from "node:perf_hooks";
export async function main(argv = process.argv.slice(2)) {
const timed = argv.includes("--timed");
const includeArchitecture = argv.includes("--include-architecture");
const tailChecks = [
{ name: "webhook body guard", args: ["lint:webhook:no-low-level-body-read"] },
{ name: "pairing store guard", args: ["lint:auth:no-pairing-store-group"] },
{ name: "pairing account guard", args: ["lint:auth:pairing-account-scope"] },
includeArchitecture
? { name: "architecture import cycles", args: ["check:architecture"] }
: { name: "runtime import cycles", args: ["check:import-cycles"] },
];
const stages = [
{
name: "preflight guards",
parallel: false,
commands: [
{ name: "conflict markers", args: ["check:no-conflict-markers"] },
{ name: "tool display", args: ["tool-display:check"] },
{ name: "host env policy", args: ["check:host-env-policy:swift"] },
],
},
{
name: "typecheck",
parallel: false,
commands: [{ name: "typecheck", args: ["tsgo:all"] }],
},
{
name: "lint",
parallel: false,
commands: [{ name: "lint", args: ["lint"] }],
},
{
name: "policy guards",
parallel: true,
commands: tailChecks,
},
];
const timings = [];
let exitCode = 0;
for (const stage of stages) {
console.error(`\n[check] ${stage.name}`);
const results = stage.parallel
? await Promise.all(stage.commands.map((command) => runCommand(command)))
: await runSerial(stage.commands);
timings.push(...results);
const failed = results.find((result) => result.status !== 0);
if (failed) {
exitCode = failed.status;
break;
}
}
if (timed || exitCode !== 0) {
printSummary(timings);
}
process.exitCode = exitCode;
}
async function runSerial(commands) {
const results = [];
for (const command of commands) {
const result = await runCommand(command);
results.push(result);
if (result.status !== 0) {
break;
}
}
return results;
}
async function runCommand(command) {
const startedAt = performance.now();
const child = spawn("pnpm", command.args, {
stdio: "inherit",
shell: process.platform === "win32",
});
return await new Promise((resolve) => {
child.once("error", (error) => {
console.error(error);
resolve({
name: command.name,
durationMs: performance.now() - startedAt,
status: 1,
});
});
child.once("close", (status) => {
resolve({
name: command.name,
durationMs: performance.now() - startedAt,
status: status ?? 1,
});
});
});
}
function printSummary(timings) {
console.error("\n[check] summary");
for (const timing of timings) {
const status = timing.status === 0 ? "ok" : `failed:${timing.status}`;
console.error(
`${formatMs(timing.durationMs).padStart(8)} ${status.padEnd(9)} ${timing.name}`,
);
}
}
function formatMs(durationMs) {
if (durationMs < 1000) {
return `${Math.round(durationMs)}ms`;
}
return `${(durationMs / 1000).toFixed(2)}s`;
}
if (import.meta.main) {
await main();
}

View File

@@ -0,0 +1,62 @@
import { spawn, spawnSync } from "node:child_process";
import path from "node:path";
const extraArgs = process.argv.slice(2);
const runner = path.resolve("scripts", "run-oxlint.mjs");
const prepareResult = spawnSync(
process.execPath,
[path.resolve("scripts", "prepare-extension-package-boundary-artifacts.mjs")],
{
stdio: "inherit",
env: process.env,
},
);
if (prepareResult.error) {
throw prepareResult.error;
}
if ((prepareResult.status ?? 1) !== 0) {
process.exit(prepareResult.status ?? 1);
}
const shards = [
{
name: "core",
args: ["--tsconfig", "tsconfig.oxlint.core.json", "src", "ui", "packages"],
},
{
name: "extensions",
args: ["--tsconfig", "tsconfig.oxlint.extensions.json", "extensions"],
},
{
name: "scripts",
args: ["--tsconfig", "tsconfig.oxlint.scripts.json", "scripts"],
},
];
const results = await Promise.all(shards.map((shard) => runShard(shard)));
process.exitCode = results.find((status) => status !== 0) ?? 0;
async function runShard(shard) {
console.error(`[oxlint:${shard.name}] starting`);
const child = spawn(process.execPath, [runner, ...shard.args, ...extraArgs], {
stdio: "inherit",
env: {
...process.env,
OPENCLAW_OXLINT_SKIP_LOCK: "1",
OPENCLAW_OXLINT_SKIP_PREPARE: "1",
},
});
return await new Promise((resolve) => {
child.once("error", (error) => {
console.error(error);
resolve(1);
});
child.once("close", (status) => {
console.error(`[oxlint:${shard.name}] finished`);
resolve(status ?? 1);
});
});
}

View File

@@ -54,19 +54,25 @@ function prepareExtensionPackageBoundaryArtifacts(env) {
export function main(argv = process.argv.slice(2), runtimeEnv = process.env) {
const { args: finalArgs, env } = applyLocalOxlintPolicy(argv, runtimeEnv);
const releaseLock = shouldAcquireLocalHeavyCheckLockForOxlint(finalArgs, {
cwd: process.cwd(),
env,
})
? acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env,
toolName: "oxlint",
})
: () => {};
const releaseLock =
env.OPENCLAW_OXLINT_SKIP_LOCK === "1"
? () => {}
: shouldAcquireLocalHeavyCheckLockForOxlint(finalArgs, {
cwd: process.cwd(),
env,
})
? acquireLocalHeavyCheckLockSync({
cwd: process.cwd(),
env,
toolName: "oxlint",
})
: () => {};
try {
if (shouldPrepareExtensionPackageBoundaryArtifacts(finalArgs)) {
if (
env.OPENCLAW_OXLINT_SKIP_PREPARE !== "1" &&
shouldPrepareExtensionPackageBoundaryArtifacts(finalArgs)
) {
prepareExtensionPackageBoundaryArtifacts(env);
}