Files
orca/config/scripts/ci-shard-timing-import.mjs
T
Neil 403b62a8d8 ci: balance existing unit and E2E shards using recorded timings (#20367)
* ci: balance existing unit and E2E shards using recorded timings

* ci: fix timing refresh units and deferred-menu test traversal

* ci: preserve isolated E2E window launch policy

* ci: keep diagnostic artifact outages from failing tests
2026-09-12 03:10:05 -07:00

87 lines
3.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { stripVTControlCharacters } from 'node:util'
import { pathToFileURL } from 'node:url'
export function parseTimingLog(text) {
const clean = stripVTControlCharacters(text)
const unit = {}
const e2e = {}
for (const match of clean.matchAll(
/[✓×❯] ([\w./-]+\.test\.(?:ts|tsx|mjs)) \([^\n]*?\)\s+([\d.]+)ms/g
)) {
unit[match[1]] = Number(match[2])
}
for (const match of clean.matchAll(
/[✓✘]\s+\d+ \[electron-headless\] (tests\/e2e\/[^:]+):\d+:\d+ .*? \(([\d.]+)(ms|s|m)\)/g
)) {
e2e[match[1]] = (e2e[match[1]] ?? 0) + Number(match[2]) * { ms: 1, s: 1000, m: 60000 }[match[3]]
}
const summary = clean.match(
/Duration\s+[\d.]+(?:ms|s) \(transform ([\d.]+(?:ms|s)), setup ([\d.]+(?:ms|s)), import ([\d.]+(?:ms|s)), tests [\d.]+(?:ms|s), environment ([\d.]+(?:ms|s))\)/
)
if (Object.keys(unit).length && !summary) {
throw new Error('Unit timing log has no supported Duration summary')
}
return {
unit,
e2e,
overheadMs: summary
? summary
.slice(1)
.reduce(
(sum, value) => sum + Number.parseFloat(value) * (value.endsWith('ms') ? 1 : 1000),
0
)
: 0
}
}
export function importTimingLogs(directory, unitRun, e2eRun) {
const baseline = {
unit: { runId: unitRun, jobIds: [], overheadMs: 0, timings: {} },
e2e: { runId: e2eRun, jobIds: [], overheadMs: 0, timings: {} }
}
for (const file of readdirSync(directory)
.filter((file) => /^log-\d+\.txt$/.test(file))
.sort()) {
const parsed = parseTimingLog(readFileSync(join(directory, file), 'utf8'))
for (const suite of ['unit', 'e2e']) {
if (!Object.keys(parsed[suite]).length) {
continue
}
baseline[suite].jobIds.push(file.match(/\d+/)[0])
for (const [name, duration] of Object.entries(parsed[suite])) {
if (suite === 'unit' && name in baseline.unit.timings) {
throw new Error(`Duplicate unit timing: ${name}`)
}
baseline[suite].timings[name] = (baseline[suite].timings[name] ?? 0) + duration
}
}
baseline.unit.overheadMs += parsed.overheadMs
}
for (const suite of ['unit', 'e2e']) {
if (!baseline[suite].jobIds.length) {
throw new Error(`No ${suite} timing evidence`)
}
baseline[suite].timings = Object.fromEntries(
Object.entries(baseline[suite].timings).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
)
}
baseline.unit.overheadMs = Math.ceil(
baseline.unit.overheadMs / Object.keys(baseline.unit.timings).length
)
return baseline
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const [directory, unitRun, e2eRun, output] = process.argv.slice(2)
if (!directory || !unitRun || !e2eRun || !output) {
throw new Error('Usage: ci-shard-timing-import.mjs LOG_DIRECTORY UNIT_RUN E2E_RUN OUTPUT')
}
writeFileSync(
output,
`${JSON.stringify(importTimingLogs(directory, unitRun, e2eRun), null, 2)}\n`
)
}