perf(plugins): report slow boundary compiles

This commit is contained in:
Vincent Koc
2026-04-08 07:49:24 +01:00
parent 680c0f77cb
commit 2e7a0fc7fb
2 changed files with 70 additions and 5 deletions

View File

@@ -23,6 +23,7 @@ const prepareBoundaryArtifactsBin = resolve(
);
const extensionPackageBoundaryBaseConfig = "../tsconfig.package-boundary.base.json";
const FAILURE_OUTPUT_TAIL_LINES = 40;
const SLOW_COMPILE_SUMMARY_LIMIT = 10;
const COMPILE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".json"]);
const ROOTDIR_BOUNDARY_CANARY_IMPORT_PATH =
"../../src/plugins/contracts/rootdir-boundary-canary.ts";
@@ -123,6 +124,23 @@ export function formatSkippedCompileProgress(params = {}) {
return `skipped ${skippedCount} fresh plugin compiles\n`;
}
export function formatSlowCompileSummary(params = {}) {
const compileTimings = Array.isArray(params.compileTimings) ? params.compileTimings : [];
if (compileTimings.length === 0) {
return "";
}
const limit =
Number.isInteger(params.limit) && params.limit > 0 ? params.limit : SLOW_COMPILE_SUMMARY_LIMIT;
const lines = ["slowest plugin compiles:"];
for (const timing of [...compileTimings]
.toSorted((left, right) => right.elapsedMs - left.elapsedMs)
.slice(0, limit)) {
lines.push(`- ${timing.extensionId}: ${timing.elapsedMs}ms`);
}
return `${lines.join("\n")}\n`;
}
export function formatStepFailure(label, params = {}) {
const stdoutSection = summarizeOutputSection("stdout", params.stdout ?? "");
const stderrSection = summarizeOutputSection("stderr", params.stderr ?? "");
@@ -411,7 +429,7 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) {
clearTimeout(timer);
settled = true;
if (code === 0) {
resolvePromise({ stdout, stderr });
resolvePromise({ stdout, stderr, elapsedMs: Date.now() - startedAt });
return;
}
const error = attachStepFailureMetadata(
@@ -454,13 +472,13 @@ export async function runNodeStepsWithConcurrency(steps, concurrency) {
}
const step = steps[index];
step.onStart?.();
await runNodeStepAsync(step.label, step.args, step.timeoutMs, {
const result = await runNodeStepAsync(step.label, step.args, step.timeoutMs, {
abortController,
onFailure(error) {
firstFailure ??= error;
},
});
step.onSuccess?.();
step.onSuccess?.(result);
}
});
await Promise.allSettled(workers);
@@ -573,6 +591,7 @@ async function runCompileCheck(extensionIds) {
process.stdout.write(`compile concurrency ${concurrency}\n`);
const compileStartedAt = Date.now();
let skippedCompileCount = 0;
const compileTimings = [];
const steps = extensionIds
.map((extensionId, index) => {
const tsBuildInfoPath = resolveBoundaryTsBuildInfoPath(extensionId);
@@ -602,8 +621,12 @@ async function runCompileCheck(extensionIds) {
onStart() {
process.stdout.write(`[${index + 1}/${extensionIds.length}] ${extensionId}\n`);
},
onSuccess() {
onSuccess(result) {
writeStampFile(resolveBoundaryTsStampPath(extensionId));
compileTimings.push({
extensionId,
elapsedMs: result.elapsedMs,
});
},
args: [
tscBin,
@@ -634,6 +657,7 @@ async function runCompileCheck(extensionIds) {
compileCount: steps.length,
skippedCompileCount,
compileElapsedMs: Date.now() - compileStartedAt,
compileTimings,
};
}
@@ -709,12 +733,13 @@ export async function main(argv = process.argv.slice(2)) {
let compileCount = 0;
let skippedCompileCount = 0;
let compileElapsedMs;
let compileTimings = [];
let canaryElapsedMs;
try {
cleanupCanaryArtifactsForExtensions(cleanupExtensionIds);
if (mode === "all" || mode === "compile") {
({ prepElapsedMs, compileCount, skippedCompileCount, compileElapsedMs } =
({ prepElapsedMs, compileCount, skippedCompileCount, compileElapsedMs, compileTimings } =
await runCompileCheck(optInExtensionIds));
}
if (shouldRunCanary) {
@@ -732,6 +757,11 @@ export async function main(argv = process.argv.slice(2)) {
elapsedMs: Date.now() - startedAt,
}),
);
process.stdout.write(
formatSlowCompileSummary({
compileTimings,
}),
);
} finally {
releaseBoundaryLock?.();
teardownCanaryCleanup?.();

View File

@@ -7,6 +7,7 @@ import {
acquireBoundaryCheckLock,
cleanupCanaryArtifactsForExtensions,
formatBoundaryCheckSuccessSummary,
formatSlowCompileSummary,
formatSkippedCompileProgress,
formatStepFailure,
installCanaryArtifactCleanup,
@@ -206,6 +207,19 @@ describe("check-extension-package-tsc-boundary", () => {
).toBe("skipped 97 fresh plugin compiles\n");
});
it("formats the slowest plugin compiles in descending order", () => {
expect(
formatSlowCompileSummary({
compileTimings: [
{ extensionId: "quick", elapsedMs: 40 },
{ extensionId: "slow", elapsedMs: 900 },
{ extensionId: "medium", elapsedMs: 250 },
],
limit: 2,
}),
).toBe(["slowest plugin compiles:", "- slow: 900ms", "- medium: 250ms", ""].join("\n"));
});
it("treats a plugin compile as fresh only when its outputs are newer than plugin and shared sdk inputs", () => {
const { rootDir, extensionRoot } = createTempExtensionRoot();
const extensionSourcePath = path.join(extensionRoot, "index.ts");
@@ -331,4 +345,25 @@ describe("check-extension-package-tsc-boundary", () => {
expect(Date.now() - startedAt).toBeLessThan(2_000);
});
it("passes successful step timing metadata to onSuccess handlers", async () => {
const elapsedTimes: number[] = [];
await runNodeStepsWithConcurrency(
[
{
label: "demo-step",
args: ["--eval", "setTimeout(() => process.exit(0), 10)"],
timeoutMs: 5_000,
onSuccess(result: { elapsedMs: number }) {
elapsedTimes.push(result.elapsedMs);
},
},
],
1,
);
expect(elapsedTimes).toHaveLength(1);
expect(elapsedTimes[0]).toBeGreaterThanOrEqual(0);
});
});