perf(plugins): skip fresh boundary plugin compiles

This commit is contained in:
Vincent Koc
2026-04-07 12:25:57 +01:00
parent c541a9c110
commit 7d2088132d
2 changed files with 188 additions and 27 deletions

View File

@@ -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,

View File

@@ -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(