From dbd7966cfdb4c8ffaf8e2cbb74851f19ac39ed8d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 15:41:32 +0100 Subject: [PATCH] ci: remove runner caps after timing review --- .github/workflows/ci.yml | 4 -- docs/ci.md | 3 +- scripts/ci-run-timings.mjs | 109 ++++++++++++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 434c0b4454c..88a3c2df3d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -746,7 +746,6 @@ jobs: timeout-minutes: 60 strategy: fail-fast: false - max-parallel: 3 matrix: ${{ fromJson(needs.preflight.outputs.channel_contracts_matrix) }} steps: - name: Checkout @@ -1119,7 +1118,6 @@ jobs: timeout-minutes: 60 strategy: fail-fast: false - max-parallel: 8 matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }} steps: - name: Checkout @@ -1357,7 +1355,6 @@ jobs: timeout-minutes: 20 strategy: fail-fast: false - max-parallel: 3 matrix: include: - check_name: check-preflight-guards @@ -1498,7 +1495,6 @@ jobs: timeout-minutes: 20 strategy: fail-fast: false - max-parallel: 3 matrix: include: - check_name: check-additional-boundaries diff --git a/docs/ci.md b/docs/ci.md index 536864ae7bf..bee4aca8e6c 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -96,5 +96,6 @@ pnpm test:channels pnpm test:contracts:channels pnpm check:docs # docs format + lint + broken links pnpm build # build dist when CI artifact/build-smoke lanes matter -node scripts/ci-run-timings.mjs # summarize wall time, queue time, and slowest jobs +node scripts/ci-run-timings.mjs # summarize wall time, queue time, and slowest jobs +node scripts/ci-run-timings.mjs --recent 10 # compare recent successful main CI runs ``` diff --git a/scripts/ci-run-timings.mjs b/scripts/ci-run-timings.mjs index 028d1387521..519006b5362 100644 --- a/scripts/ci-run-timings.mjs +++ b/scripts/ci-run-timings.mjs @@ -70,6 +70,28 @@ function getLatestCiRunId() { return String(runId); } +function listRecentSuccessfulCiRuns(limit) { + const raw = execFileSync( + "gh", + [ + "run", + "list", + "--branch", + "main", + "--workflow", + "CI", + "--limit", + String(Math.max(limit * 4, limit)), + "--json", + "databaseId,headSha,status,conclusion", + ], + { encoding: "utf8" }, + ); + return JSON.parse(raw) + .filter((run) => run.status === "completed" && run.conclusion === "success") + .slice(0, limit); +} + function loadRun(runId) { return JSON.parse( execFileSync( @@ -82,6 +104,62 @@ function loadRun(runId) { ); } +function summarizeJobs(run) { + const created = parseTime(run.createdAt); + const updated = parseTime(run.updatedAt); + const jobs = (run.jobs ?? []) + .filter((job) => !job.name?.startsWith("matrix.")) + .map((job) => { + const started = parseTime(job.startedAt); + const completed = parseTime(job.completedAt); + return { + conclusion: job.conclusion ?? "", + durationSeconds: secondsBetween(started, completed), + name: job.name, + queueSeconds: secondsBetween(created, started), + started, + completed, + status: job.status, + }; + }) + .filter((job) => job.started !== null && job.completed !== null); + const successfulDurations = jobs + .filter((job) => job.status === "completed" && job.conclusion === "success") + .map((job) => job.durationSeconds) + .filter((duration) => duration !== null); + const firstStart = Math.min(...jobs.map((job) => job.started)); + const lastComplete = Math.max(...jobs.map((job) => job.completed)); + + return { + avgDurationSeconds: + successfulDurations.length === 0 + ? null + : Math.round( + successfulDurations.reduce((sum, duration) => sum + duration, 0) / + successfulDurations.length, + ), + executionWindowSeconds: + Number.isFinite(firstStart) && Number.isFinite(lastComplete) + ? secondsBetween(firstStart, lastComplete) + : null, + firstQueueSeconds: Number.isFinite(firstStart) ? secondsBetween(created, firstStart) : null, + jobCount: successfulDurations.length, + maxDurationSeconds: successfulDurations.length === 0 ? null : Math.max(...successfulDurations), + p90DurationSeconds: percentile(successfulDurations, 0.9), + p95DurationSeconds: percentile(successfulDurations, 0.95), + wallSeconds: secondsBetween(created, updated), + }; +} + +function percentile(values, percentileValue) { + if (values.length === 0) { + return null; + } + const sorted = [...values].toSorted((left, right) => left - right); + const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * percentileValue) - 1); + return sorted[index]; +} + function printSection(title, jobs, metric) { console.log(title); for (const job of jobs) { @@ -93,12 +171,39 @@ function printSection(title, jobs, metric) { async function main() { const args = process.argv.slice(2); + const recentIndex = args.indexOf("--recent"); const limitIndex = args.indexOf("--limit"); const limit = limitIndex === -1 ? 15 : Math.max(1, Number.parseInt(args[limitIndex + 1] ?? "", 10) || 15); + if (recentIndex !== -1) { + const recentLimit = Math.max(1, Number.parseInt(args[recentIndex + 1] ?? "", 10) || 10); + for (const run of listRecentSuccessfulCiRuns(recentLimit)) { + const summary = summarizeJobs(loadRun(run.databaseId)); + console.log( + [ + `CI run ${run.databaseId}`, + run.headSha.slice(0, 10), + `wall=${formatSeconds(summary.wallSeconds)}`, + `exec=${formatSeconds(summary.executionWindowSeconds)}`, + `firstQueue=${formatSeconds(summary.firstQueueSeconds)}`, + `jobs=${summary.jobCount}`, + `avg=${formatSeconds(summary.avgDurationSeconds)}`, + `p90=${formatSeconds(summary.p90DurationSeconds)}`, + `p95=${formatSeconds(summary.p95DurationSeconds)}`, + `max=${formatSeconds(summary.maxDurationSeconds)}`, + ].join(" "), + ); + } + return; + } const runId = - args.find((arg, index) => index !== limitIndex && index !== limitIndex + 1) ?? - getLatestCiRunId(); + args.find( + (arg, index) => + index !== limitIndex && + index !== limitIndex + 1 && + index !== recentIndex && + index !== recentIndex + 1, + ) ?? getLatestCiRunId(); const summary = summarizeRunTimings(loadRun(runId), limit); console.log(