mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
ci: rebalance test workers
This commit is contained in:
119
scripts/ci-run-timings.mjs
Normal file
119
scripts/ci-run-timings.mjs
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
function parseTime(value) {
|
||||
if (!value || value === "0001-01-01T00:00:00Z") {
|
||||
return null;
|
||||
}
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function secondsBetween(start, end) {
|
||||
return start !== null && end !== null ? Math.round((end - start) / 1000) : null;
|
||||
}
|
||||
|
||||
function formatSeconds(value) {
|
||||
return value === null ? "" : `${value}s`;
|
||||
}
|
||||
|
||||
export function summarizeRunTimings(run, limit = 15) {
|
||||
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),
|
||||
status: job.status,
|
||||
};
|
||||
});
|
||||
const byDuration = [...jobs]
|
||||
.filter((job) => job.durationSeconds !== null)
|
||||
.toSorted((left, right) => right.durationSeconds - left.durationSeconds)
|
||||
.slice(0, limit);
|
||||
const byQueue = [...jobs]
|
||||
.filter((job) => job.queueSeconds !== null && (job.durationSeconds ?? 0) > 5)
|
||||
.toSorted((left, right) => right.queueSeconds - left.queueSeconds)
|
||||
.slice(0, limit);
|
||||
const badJobs = jobs.filter(
|
||||
(job) => job.conclusion && !["success", "skipped", "cancelled"].includes(job.conclusion),
|
||||
);
|
||||
|
||||
return {
|
||||
byDuration,
|
||||
byQueue,
|
||||
conclusion: run.conclusion ?? "",
|
||||
status: run.status ?? "",
|
||||
wallSeconds: secondsBetween(created, updated),
|
||||
badJobs,
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestCiRunId() {
|
||||
const raw = execFileSync(
|
||||
"gh",
|
||||
["run", "list", "--branch", "main", "--workflow", "CI", "--limit", "1", "--json", "databaseId"],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const runs = JSON.parse(raw);
|
||||
const runId = runs[0]?.databaseId;
|
||||
if (!runId) {
|
||||
throw new Error("No CI runs found on main");
|
||||
}
|
||||
return String(runId);
|
||||
}
|
||||
|
||||
function loadRun(runId) {
|
||||
return JSON.parse(
|
||||
execFileSync(
|
||||
"gh",
|
||||
["run", "view", runId, "--json", "status,conclusion,createdAt,updatedAt,jobs"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function printSection(title, jobs, metric) {
|
||||
console.log(title);
|
||||
for (const job of jobs) {
|
||||
console.log(
|
||||
`${String(job.name).padEnd(48)} ${formatSeconds(job[metric]).padStart(6)} queue=${formatSeconds(job.queueSeconds).padStart(6)} ${job.status}/${job.conclusion}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const limitIndex = args.indexOf("--limit");
|
||||
const limit =
|
||||
limitIndex === -1 ? 15 : Math.max(1, Number.parseInt(args[limitIndex + 1] ?? "", 10) || 15);
|
||||
const runId =
|
||||
args.find((arg, index) => index !== limitIndex && index !== limitIndex + 1) ??
|
||||
getLatestCiRunId();
|
||||
const summary = summarizeRunTimings(loadRun(runId), limit);
|
||||
|
||||
console.log(
|
||||
`CI run ${runId}: ${summary.status}/${summary.conclusion} wall=${formatSeconds(summary.wallSeconds)}`,
|
||||
);
|
||||
printSection("\nSlowest jobs", summary.byDuration, "durationSeconds");
|
||||
printSection("\nLongest queues", summary.byQueue, "queueSeconds");
|
||||
if (summary.badJobs.length > 0) {
|
||||
console.log("\nFailed jobs");
|
||||
for (const job of summary.badJobs) {
|
||||
console.log(`${job.name} ${job.status}/${job.conclusion}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
await main();
|
||||
}
|
||||
@@ -39,7 +39,7 @@ function resolveContractFileWeight(file) {
|
||||
|
||||
export function createChannelContractTestShards() {
|
||||
const rootDir = "src/channels/plugins/contracts";
|
||||
const suffixes = ["a", "b", "c", "d"];
|
||||
const suffixes = ["a", "b", "c"];
|
||||
const groups = Object.fromEntries(
|
||||
["registry", "core"].flatMap((family) =>
|
||||
suffixes.map((suffix) => [`checks-fast-contracts-channels-${family}-${suffix}`, []]),
|
||||
|
||||
@@ -98,6 +98,7 @@ const SPLIT_NODE_SHARDS = new Map([
|
||||
"test/vitest/vitest.secrets.config.ts",
|
||||
"test/vitest/vitest.logging.config.ts",
|
||||
"test/vitest/vitest.process.config.ts",
|
||||
"test/vitest/vitest.runtime-config.config.ts",
|
||||
],
|
||||
requiresDist: false,
|
||||
},
|
||||
@@ -117,7 +118,6 @@ const SPLIT_NODE_SHARDS = new Map([
|
||||
configs: [
|
||||
"test/vitest/vitest.acp.config.ts",
|
||||
"test/vitest/vitest.cron.config.ts",
|
||||
"test/vitest/vitest.runtime-config.config.ts",
|
||||
"test/vitest/vitest.shared-core.config.ts",
|
||||
"test/vitest/vitest.tasks.config.ts",
|
||||
"test/vitest/vitest.utils.config.ts",
|
||||
|
||||
@@ -3,19 +3,23 @@ import path from "node:path";
|
||||
import { channelTestRoots } from "../../test/vitest/vitest.channel-paths.mjs";
|
||||
import { isAcpxExtensionRoot } from "../../test/vitest/vitest.extension-acpx-paths.mjs";
|
||||
import { isBlueBubblesExtensionRoot } from "../../test/vitest/vitest.extension-bluebubbles-paths.mjs";
|
||||
import { isBrowserExtensionRoot } from "../../test/vitest/vitest.extension-browser-paths.mjs";
|
||||
import { resolveSplitChannelExtensionShard } from "../../test/vitest/vitest.extension-channel-split-paths.mjs";
|
||||
import { isDiffsExtensionRoot } from "../../test/vitest/vitest.extension-diffs-paths.mjs";
|
||||
import { isFeishuExtensionRoot } from "../../test/vitest/vitest.extension-feishu-paths.mjs";
|
||||
import { isIrcExtensionRoot } from "../../test/vitest/vitest.extension-irc-paths.mjs";
|
||||
import { isMatrixExtensionRoot } from "../../test/vitest/vitest.extension-matrix-paths.mjs";
|
||||
import { isMattermostExtensionRoot } from "../../test/vitest/vitest.extension-mattermost-paths.mjs";
|
||||
import { isMediaExtensionRoot } from "../../test/vitest/vitest.extension-media-paths.mjs";
|
||||
import { isMemoryExtensionRoot } from "../../test/vitest/vitest.extension-memory-paths.mjs";
|
||||
import { isMessagingExtensionRoot } from "../../test/vitest/vitest.extension-messaging-paths.mjs";
|
||||
import { isMiscExtensionRoot } from "../../test/vitest/vitest.extension-misc-paths.mjs";
|
||||
import { isMsTeamsExtensionRoot } from "../../test/vitest/vitest.extension-msteams-paths.mjs";
|
||||
import {
|
||||
isProviderExtensionRoot,
|
||||
isProviderOpenAiExtensionRoot,
|
||||
} from "../../test/vitest/vitest.extension-provider-paths.mjs";
|
||||
import { isQaExtensionRoot } from "../../test/vitest/vitest.extension-qa-paths.mjs";
|
||||
import { isTelegramExtensionRoot } from "../../test/vitest/vitest.extension-telegram-paths.mjs";
|
||||
import { isVoiceCallExtensionRoot } from "../../test/vitest/vitest.extension-voice-call-paths.mjs";
|
||||
import { isWhatsAppExtensionRoot } from "../../test/vitest/vitest.extension-whatsapp-paths.mjs";
|
||||
@@ -118,16 +122,20 @@ export function resolveExtensionTestPlan(params = {}) {
|
||||
const splitChannelShard = resolveSplitChannelExtensionShard(relativeExtensionDir);
|
||||
const usesChannelConfig = roots.some((root) => channelTestRoots.includes(root));
|
||||
const usesAcpxConfig = roots.some((root) => isAcpxExtensionRoot(root));
|
||||
const usesBrowserConfig = roots.some((root) => isBrowserExtensionRoot(root));
|
||||
const usesDiffsConfig = roots.some((root) => isDiffsExtensionRoot(root));
|
||||
const usesBlueBubblesConfig = roots.some((root) => isBlueBubblesExtensionRoot(root));
|
||||
const usesFeishuConfig = roots.some((root) => isFeishuExtensionRoot(root));
|
||||
const usesIrcConfig = roots.some((root) => isIrcExtensionRoot(root));
|
||||
const usesMattermostConfig = roots.some((root) => isMattermostExtensionRoot(root));
|
||||
const usesMediaConfig = roots.some((root) => isMediaExtensionRoot(root));
|
||||
const usesMiscConfig = roots.some((root) => isMiscExtensionRoot(root));
|
||||
const usesTelegramConfig = roots.some((root) => isTelegramExtensionRoot(root));
|
||||
const usesVoiceCallConfig = roots.some((root) => isVoiceCallExtensionRoot(root));
|
||||
const usesWhatsAppConfig = roots.some((root) => isWhatsAppExtensionRoot(root));
|
||||
const usesZaloConfig = roots.some((root) => isZaloExtensionRoot(root));
|
||||
const usesMatrixConfig = roots.some((root) => isMatrixExtensionRoot(root));
|
||||
const usesQaConfig = roots.some((root) => isQaExtensionRoot(root));
|
||||
const usesMemoryConfig = roots.some((root) => isMemoryExtensionRoot(root));
|
||||
const usesMsTeamsConfig = roots.some((root) => isMsTeamsExtensionRoot(root));
|
||||
const usesMessagingConfig = roots.some((root) => isMessagingExtensionRoot(root));
|
||||
@@ -139,37 +147,45 @@ export function resolveExtensionTestPlan(params = {}) {
|
||||
? "test/vitest/vitest.extension-channels.config.ts"
|
||||
: usesAcpxConfig
|
||||
? "test/vitest/vitest.extension-acpx.config.ts"
|
||||
: usesDiffsConfig
|
||||
? "test/vitest/vitest.extension-diffs.config.ts"
|
||||
: usesBlueBubblesConfig
|
||||
? "test/vitest/vitest.extension-bluebubbles.config.ts"
|
||||
: usesFeishuConfig
|
||||
? "test/vitest/vitest.extension-feishu.config.ts"
|
||||
: usesIrcConfig
|
||||
? "test/vitest/vitest.extension-irc.config.ts"
|
||||
: usesMattermostConfig
|
||||
? "test/vitest/vitest.extension-mattermost.config.ts"
|
||||
: usesMatrixConfig
|
||||
? "test/vitest/vitest.extension-matrix.config.ts"
|
||||
: usesTelegramConfig
|
||||
? "test/vitest/vitest.extension-telegram.config.ts"
|
||||
: usesVoiceCallConfig
|
||||
? "test/vitest/vitest.extension-voice-call.config.ts"
|
||||
: usesWhatsAppConfig
|
||||
? "test/vitest/vitest.extension-whatsapp.config.ts"
|
||||
: usesZaloConfig
|
||||
? "test/vitest/vitest.extension-zalo.config.ts"
|
||||
: usesMemoryConfig
|
||||
? "test/vitest/vitest.extension-memory.config.ts"
|
||||
: usesBlueBubblesConfig
|
||||
? "test/vitest/vitest.extension-bluebubbles.config.ts"
|
||||
: usesBrowserConfig
|
||||
? "test/vitest/vitest.extension-browser.config.ts"
|
||||
: usesDiffsConfig
|
||||
? "test/vitest/vitest.extension-diffs.config.ts"
|
||||
: usesFeishuConfig
|
||||
? "test/vitest/vitest.extension-feishu.config.ts"
|
||||
: usesIrcConfig
|
||||
? "test/vitest/vitest.extension-irc.config.ts"
|
||||
: usesMattermostConfig
|
||||
? "test/vitest/vitest.extension-mattermost.config.ts"
|
||||
: usesMatrixConfig
|
||||
? "test/vitest/vitest.extension-matrix.config.ts"
|
||||
: usesMediaConfig
|
||||
? "test/vitest/vitest.extension-media.config.ts"
|
||||
: usesMemoryConfig
|
||||
? "test/vitest/vitest.extension-memory.config.ts"
|
||||
: usesMessagingConfig
|
||||
? "test/vitest/vitest.extension-messaging.config.ts"
|
||||
: usesMiscConfig
|
||||
? "test/vitest/vitest.extension-misc.config.ts"
|
||||
: usesMsTeamsConfig
|
||||
? "test/vitest/vitest.extension-msteams.config.ts"
|
||||
: usesMessagingConfig
|
||||
? "test/vitest/vitest.extension-messaging.config.ts"
|
||||
: usesProviderOpenAiConfig
|
||||
? "test/vitest/vitest.extension-provider-openai.config.ts"
|
||||
: usesProviderConfig
|
||||
? "test/vitest/vitest.extension-providers.config.ts"
|
||||
: "test/vitest/vitest.extensions.config.ts";
|
||||
: usesQaConfig
|
||||
? "test/vitest/vitest.extension-qa.config.ts"
|
||||
: usesTelegramConfig
|
||||
? "test/vitest/vitest.extension-telegram.config.ts"
|
||||
: usesVoiceCallConfig
|
||||
? "test/vitest/vitest.extension-voice-call.config.ts"
|
||||
: usesWhatsAppConfig
|
||||
? "test/vitest/vitest.extension-whatsapp.config.ts"
|
||||
: usesZaloConfig
|
||||
? "test/vitest/vitest.extension-zalo.config.ts"
|
||||
: usesProviderOpenAiConfig
|
||||
? "test/vitest/vitest.extension-provider-openai.config.ts"
|
||||
: usesProviderConfig
|
||||
? "test/vitest/vitest.extension-providers.config.ts"
|
||||
: "test/vitest/vitest.extensions.config.ts";
|
||||
const testFileCount = roots.reduce(
|
||||
(sum, root) => sum + countTestFiles(path.join(repoRoot, root)),
|
||||
0,
|
||||
|
||||
Reference in New Issue
Block a user