mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-09 03:22:40 +00:00
perf(plugins): skip fresh boundary plugin compiles
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import path, { dirname, join, resolve } from "node:path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const repoRoot = resolve(import.meta.dirname, "..");
|
||||
@@ -15,6 +23,7 @@ const prepareBoundaryArtifactsBin = resolve(
|
||||
);
|
||||
const extensionPackageBoundaryBaseConfig = "../tsconfig.package-boundary.base.json";
|
||||
const FAILURE_OUTPUT_TAIL_LINES = 40;
|
||||
const COMPILE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".json"]);
|
||||
|
||||
function parseMode(argv) {
|
||||
const modeArg = argv.find((arg) => arg.startsWith("--mode="));
|
||||
@@ -76,6 +85,9 @@ export function formatBoundaryCheckSuccessSummary(params = {}) {
|
||||
if (Number.isInteger(params.compileCount)) {
|
||||
lines.push(`compiled plugins: ${params.compileCount}`);
|
||||
}
|
||||
if (Number.isInteger(params.skippedCompileCount) && params.skippedCompileCount > 0) {
|
||||
lines.push(`skipped plugins: ${params.skippedCompileCount}`);
|
||||
}
|
||||
if (Number.isInteger(params.canaryCount)) {
|
||||
lines.push(`canary plugins: ${params.canaryCount}`);
|
||||
}
|
||||
@@ -148,6 +160,77 @@ function collectCanaryExtensionIds(extensionIds) {
|
||||
];
|
||||
}
|
||||
|
||||
function isRelevantCompileInput(filePath) {
|
||||
const basename = path.basename(filePath);
|
||||
if (
|
||||
basename === "__rootdir_boundary_canary__.ts" ||
|
||||
basename === "tsconfig.rootdir-canary.json"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (basename.endsWith(".tsbuildinfo")) {
|
||||
return false;
|
||||
}
|
||||
return COMPILE_INPUT_EXTENSIONS.has(path.extname(filePath));
|
||||
}
|
||||
|
||||
function collectNewestMtime(entryPath, params = {}) {
|
||||
const includeFile = params.includeFile ?? (() => true);
|
||||
let newestMtimeMs = 0;
|
||||
|
||||
function visit(currentPath) {
|
||||
if (!existsSync(currentPath)) {
|
||||
return;
|
||||
}
|
||||
const stats = statSync(currentPath);
|
||||
if (stats.isDirectory()) {
|
||||
const basename = path.basename(currentPath);
|
||||
if (basename === "dist" || basename === "node_modules") {
|
||||
return;
|
||||
}
|
||||
for (const child of readdirSync(currentPath)) {
|
||||
visit(path.join(currentPath, child));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!includeFile(currentPath)) {
|
||||
return;
|
||||
}
|
||||
newestMtimeMs = Math.max(newestMtimeMs, stats.mtimeMs);
|
||||
}
|
||||
|
||||
visit(entryPath);
|
||||
return newestMtimeMs;
|
||||
}
|
||||
|
||||
function collectOldestMtime(paths) {
|
||||
let oldestMtimeMs = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const entryPath of paths) {
|
||||
if (!existsSync(entryPath)) {
|
||||
return null;
|
||||
}
|
||||
oldestMtimeMs = Math.min(oldestMtimeMs, statSync(entryPath).mtimeMs);
|
||||
}
|
||||
|
||||
return Number.isFinite(oldestMtimeMs) ? oldestMtimeMs : null;
|
||||
}
|
||||
|
||||
export function isBoundaryCompileFresh(extensionId, params = {}) {
|
||||
const rootDir = params.rootDir ?? repoRoot;
|
||||
const extensionRoot = resolve(rootDir, "extensions", extensionId);
|
||||
const newestInputMtimeMs = Math.max(
|
||||
collectNewestMtime(extensionRoot, { includeFile: isRelevantCompileInput }),
|
||||
collectNewestMtime(resolve(rootDir, "extensions", extensionId, "tsconfig.json")),
|
||||
collectNewestMtime(resolve(rootDir, "dist/plugin-sdk")),
|
||||
collectNewestMtime(resolve(rootDir, "packages/plugin-sdk/dist")),
|
||||
);
|
||||
const oldestOutputMtimeMs = collectOldestMtime([
|
||||
resolve(rootDir, "extensions", extensionId, "dist", ".boundary-tsc.tsbuildinfo"),
|
||||
]);
|
||||
return oldestOutputMtimeMs !== null && oldestOutputMtimeMs >= newestInputMtimeMs;
|
||||
}
|
||||
|
||||
function runNodeStep(label, args, timeoutMs) {
|
||||
const startedAt = Date.now();
|
||||
const result = spawnSync(process.execPath, args, {
|
||||
@@ -444,29 +527,43 @@ async function runCompileCheck(extensionIds) {
|
||||
const concurrency = resolveCompileConcurrency();
|
||||
process.stdout.write(`compile concurrency ${concurrency}\n`);
|
||||
const compileStartedAt = Date.now();
|
||||
const steps = extensionIds.map((extensionId, index) => {
|
||||
const tsBuildInfoPath = resolveBoundaryTsBuildInfoPath(extensionId);
|
||||
mkdirSync(dirname(tsBuildInfoPath), { recursive: true });
|
||||
return {
|
||||
label: extensionId,
|
||||
onStart() {
|
||||
process.stdout.write(`[${index + 1}/${extensionIds.length}] ${extensionId}\n`);
|
||||
},
|
||||
args: [
|
||||
tscBin,
|
||||
"-p",
|
||||
resolve(repoRoot, "extensions", extensionId, "tsconfig.json"),
|
||||
"--noEmit",
|
||||
"--incremental",
|
||||
"--tsBuildInfoFile",
|
||||
tsBuildInfoPath,
|
||||
],
|
||||
timeoutMs: 120_000,
|
||||
};
|
||||
});
|
||||
await runNodeStepsWithConcurrency(steps, concurrency);
|
||||
let skippedCompileCount = 0;
|
||||
const steps = extensionIds
|
||||
.map((extensionId, index) => {
|
||||
const tsBuildInfoPath = resolveBoundaryTsBuildInfoPath(extensionId);
|
||||
mkdirSync(dirname(tsBuildInfoPath), { recursive: true });
|
||||
if (isBoundaryCompileFresh(extensionId)) {
|
||||
skippedCompileCount += 1;
|
||||
process.stdout.write(
|
||||
`[${index + 1}/${extensionIds.length}] ${extensionId} (fresh; skipping)\n`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label: extensionId,
|
||||
onStart() {
|
||||
process.stdout.write(`[${index + 1}/${extensionIds.length}] ${extensionId}\n`);
|
||||
},
|
||||
args: [
|
||||
tscBin,
|
||||
"-p",
|
||||
resolve(repoRoot, "extensions", extensionId, "tsconfig.json"),
|
||||
"--noEmit",
|
||||
"--incremental",
|
||||
"--tsBuildInfoFile",
|
||||
tsBuildInfoPath,
|
||||
],
|
||||
timeoutMs: 120_000,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (steps.length > 0) {
|
||||
await runNodeStepsWithConcurrency(steps, concurrency);
|
||||
}
|
||||
return {
|
||||
prepElapsedMs,
|
||||
compileCount: steps.length,
|
||||
skippedCompileCount,
|
||||
compileElapsedMs: Date.now() - compileStartedAt,
|
||||
};
|
||||
}
|
||||
@@ -532,13 +629,16 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
const releaseBoundaryLock = acquireBoundaryCheckLock();
|
||||
const teardownCanaryCleanup = installCanaryArtifactCleanup(cleanupExtensionIds);
|
||||
let prepElapsedMs;
|
||||
let compileCount = 0;
|
||||
let skippedCompileCount = 0;
|
||||
let compileElapsedMs;
|
||||
let canaryElapsedMs;
|
||||
|
||||
try {
|
||||
cleanupCanaryArtifactsForExtensions(cleanupExtensionIds);
|
||||
if (mode === "all" || mode === "compile") {
|
||||
({ prepElapsedMs, compileElapsedMs } = await runCompileCheck(optInExtensionIds));
|
||||
({ prepElapsedMs, compileCount, skippedCompileCount, compileElapsedMs } =
|
||||
await runCompileCheck(optInExtensionIds));
|
||||
}
|
||||
if (shouldRunCanary) {
|
||||
({ canaryElapsedMs } = runCanaryCheck(canaryExtensionIds));
|
||||
@@ -546,7 +646,8 @@ export async function main(argv = process.argv.slice(2)) {
|
||||
process.stdout.write(
|
||||
formatBoundaryCheckSuccessSummary({
|
||||
mode,
|
||||
compileCount: mode === "canary" ? 0 : optInExtensionIds.length,
|
||||
compileCount,
|
||||
skippedCompileCount,
|
||||
canaryCount: shouldRunCanary ? canaryExtensionIds.length : 0,
|
||||
prepElapsedMs,
|
||||
compileElapsedMs,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
formatBoundaryCheckSuccessSummary,
|
||||
formatStepFailure,
|
||||
installCanaryArtifactCleanup,
|
||||
isBoundaryCompileFresh,
|
||||
resolveBoundaryCheckLockPath,
|
||||
resolveCanaryArtifactPaths,
|
||||
runNodeStepAsync,
|
||||
@@ -138,7 +139,8 @@ describe("check-extension-package-tsc-boundary", () => {
|
||||
expect(
|
||||
formatBoundaryCheckSuccessSummary({
|
||||
mode: "all",
|
||||
compileCount: 97,
|
||||
compileCount: 84,
|
||||
skippedCompileCount: 13,
|
||||
canaryCount: 12,
|
||||
prepElapsedMs: 12_345,
|
||||
compileElapsedMs: 54_321,
|
||||
@@ -149,7 +151,8 @@ describe("check-extension-package-tsc-boundary", () => {
|
||||
[
|
||||
"extension package boundary check passed",
|
||||
"mode: all",
|
||||
"compiled plugins: 97",
|
||||
"compiled plugins: 84",
|
||||
"skipped plugins: 13",
|
||||
"canary plugins: 12",
|
||||
"prep elapsed: 12345ms",
|
||||
"compile elapsed: 54321ms",
|
||||
@@ -165,6 +168,7 @@ describe("check-extension-package-tsc-boundary", () => {
|
||||
formatBoundaryCheckSuccessSummary({
|
||||
mode: "compile",
|
||||
compileCount: 97,
|
||||
skippedCompileCount: 0,
|
||||
canaryCount: 0,
|
||||
prepElapsedMs: 12_345,
|
||||
compileElapsedMs: 54_321,
|
||||
@@ -185,6 +189,62 @@ describe("check-extension-package-tsc-boundary", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("treats a plugin compile as fresh only when its outputs are newer than plugin and sdk inputs", () => {
|
||||
const { rootDir, extensionRoot } = createTempExtensionRoot();
|
||||
const extensionSourcePath = path.join(extensionRoot, "index.ts");
|
||||
const extensionTsconfigPath = path.join(extensionRoot, "tsconfig.json");
|
||||
const buildInfoPath = path.join(extensionRoot, "dist", ".boundary-tsc.tsbuildinfo");
|
||||
const rootSdkBuildInfoPath = path.join(rootDir, "dist", "plugin-sdk", ".tsbuildinfo");
|
||||
const packageSdkBuildInfoPath = path.join(
|
||||
rootDir,
|
||||
"packages",
|
||||
"plugin-sdk",
|
||||
"dist",
|
||||
".tsbuildinfo",
|
||||
);
|
||||
const entryShimStampPath = path.join(
|
||||
rootDir,
|
||||
"dist",
|
||||
"plugin-sdk",
|
||||
".boundary-entry-shims.stamp",
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(extensionSourcePath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(buildInfoPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(rootSdkBuildInfoPath), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(packageSdkBuildInfoPath), { recursive: true });
|
||||
|
||||
fs.writeFileSync(extensionSourcePath, "export const demo = 1;\n", "utf8");
|
||||
fs.writeFileSync(
|
||||
extensionTsconfigPath,
|
||||
'{ "extends": "../tsconfig.package-boundary.base.json" }\n',
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(buildInfoPath, "ok\n", "utf8");
|
||||
fs.writeFileSync(rootSdkBuildInfoPath, "ok\n", "utf8");
|
||||
fs.writeFileSync(packageSdkBuildInfoPath, "ok\n", "utf8");
|
||||
fs.writeFileSync(entryShimStampPath, "ok\n", "utf8");
|
||||
|
||||
fs.utimesSync(extensionSourcePath, new Date(1_000), new Date(1_000));
|
||||
fs.utimesSync(extensionTsconfigPath, new Date(1_000), new Date(1_000));
|
||||
fs.utimesSync(rootSdkBuildInfoPath, new Date(2_000), new Date(2_000));
|
||||
fs.utimesSync(packageSdkBuildInfoPath, new Date(2_000), new Date(2_000));
|
||||
fs.utimesSync(entryShimStampPath, new Date(2_000), new Date(2_000));
|
||||
fs.utimesSync(buildInfoPath, new Date(3_000), new Date(3_000));
|
||||
|
||||
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true);
|
||||
|
||||
fs.utimesSync(rootSdkBuildInfoPath, new Date(500), new Date(500));
|
||||
fs.utimesSync(packageSdkBuildInfoPath, new Date(500), new Date(500));
|
||||
fs.utimesSync(entryShimStampPath, new Date(500), new Date(500));
|
||||
|
||||
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(true);
|
||||
|
||||
fs.utimesSync(rootSdkBuildInfoPath, new Date(4_000), new Date(4_000));
|
||||
|
||||
expect(isBoundaryCompileFresh("demo", { rootDir })).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps full failure output on the thrown error for canary detection", async () => {
|
||||
await expect(
|
||||
runNodeStepAsync(
|
||||
|
||||
Reference in New Issue
Block a user