From 2e7a0fc7fb434f435c646bd5382f4c9e545c15b8 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 8 Apr 2026 07:49:24 +0100 Subject: [PATCH] perf(plugins): report slow boundary compiles --- .../check-extension-package-tsc-boundary.mjs | 40 ++++++++++++++++--- ...eck-extension-package-tsc-boundary.test.ts | 35 ++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/scripts/check-extension-package-tsc-boundary.mjs b/scripts/check-extension-package-tsc-boundary.mjs index 936124f87d8..293c3536a0b 100644 --- a/scripts/check-extension-package-tsc-boundary.mjs +++ b/scripts/check-extension-package-tsc-boundary.mjs @@ -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?.(); diff --git a/test/scripts/check-extension-package-tsc-boundary.test.ts b/test/scripts/check-extension-package-tsc-boundary.test.ts index fc8ad41fb4a..597860d96d7 100644 --- a/test/scripts/check-extension-package-tsc-boundary.test.ts +++ b/test/scripts/check-extension-package-tsc-boundary.test.ts @@ -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); + }); });