ci: balance existing unit and E2E shards using recorded timings

This commit is contained in:
Neil
2026-09-12 00:59:59 -07:00
parent 923858e098
commit 3efcf2ad2b
11 changed files with 9396 additions and 1 deletions
+27 -1
View File
@@ -170,8 +170,34 @@ jobs:
# artifact instead of starting five concurrent electron-vite builds.
# ORCA_E2E_FORWARD_APP_LOGS keeps startup failures visible when Electron
# launches but never creates a BrowserWindow.
- name: Balance E2E shard from timing evidence
env:
ORCA_BACKGROUND_LAUNCH: '1'
SKIP_BUILD: '1'
ORCA_E2E_FORWARD_APP_LOGS: '1'
ORCA_E2E_WEB_CLIENT: '1'
ORCA_RELAY_PATH: ${{ github.workspace }}/out/relay
run: |
mkdir -p ci-shards
pnpm exec playwright test --config tests/playwright.config.ts --project=electron-headless --list --reporter=json > ci-shards/discovery.json
export ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)"
node config/scripts/ci-e2e-shard-plan.mjs ci-shards/discovery.json '${{ matrix.shard }}' ci-shards
pnpm exec playwright test --config tests/playwright.config.ts --project=electron-headless --test-list=ci-shards/selected.txt --list --reporter=json > ci-shards/selected-discovery.json
node config/scripts/ci-e2e-shard-plan.mjs --verify ci-shards/assignment.json ci-shards/selected-discovery.json
- name: Run E2E tests (${{ matrix.shard_name }})
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --shard=${{ matrix.shard }}
env:
ORCA_BACKGROUND_LAUNCH: '1'
run: xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 ORCA_E2E_WEB_CLIENT=1 ORCA_RELAY_PATH="$GITHUB_WORKSPACE/out/relay" pnpm run test:e2e --test-list=ci-shards/selected.txt
- name: Upload E2E shard assignment
if: always()
uses: actions/upload-artifact@v7
with:
name: e2e-shard-${{ matrix.shard_name }}-attempt-${{ github.run_attempt }}
path: ci-shards/
retention-days: 14
if-no-files-found: warn
# The frame benchmark needs a mapped window, which the headless shards exclude.
- name: Run worktree first-paint benchmark
+13
View File
@@ -45,7 +45,11 @@ jobs:
npx --yes pnpm@10.24.0 --filter '@orca-cloud/relay^...' build
- name: Test shard
env:
ORCA_BALANCE_UNIT_SHARDS: '1'
ORCA_BACKGROUND_LAUNCH: '1'
run: |
export ORCA_SHARD_SOURCE_SHA="$(git rev-parse HEAD)"
pnpm exec vitest run --config config/vitest.config.ts \
--exclude=src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \
--exclude=src/main/daemon/shell-ready.test.ts \
@@ -65,3 +69,12 @@ jobs:
--exclude=src/shared/posix-command-path-lookup.test.ts \
--exclude=tests/e2e/cross-version-wire/** \
--shard=${{ matrix.shard }}/${{ matrix.shard_total }}
- name: Upload unit shard assignment
if: always()
uses: actions/upload-artifact@v7
with:
name: unit-shard-node-${{ matrix.node }}-${{ matrix.shard }}-attempt-${{ github.run_attempt }}
path: ci-shards/
retention-days: 14
if-no-files-found: warn
+97
View File
@@ -0,0 +1,97 @@
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import {
balanceFiles,
compareIds,
readTimingBaseline,
writeAssignment
} from './ci-shard-assignment.mjs'
export function discoverE2eFiles(report) {
if (report.errors?.length) {
throw new Error('Playwright discovery reported errors')
}
const files = new Map()
function visit(suite) {
for (const spec of suite.specs ?? []) {
const file = spec.file.replaceAll('\\', '/')
if (file.startsWith('/') || file.split('/').includes('..') || /[\n\r>›]/.test(file)) {
throw new Error(`Unsafe test-list path: ${file}`)
}
for (const test of spec.tests) {
const id = `${test.projectName}:${spec.id}`
const ids = files.get(file) ?? []
ids.push(id)
files.set(file, ids)
}
}
for (const child of suite.suites ?? []) {
visit(child)
}
}
for (const suite of report.suites) {
visit(suite)
}
if (!files.size) {
throw new Error('Playwright discovered no tests')
}
const ids = [...files.values()].flat()
if (new Set(ids).size !== ids.length) {
throw new Error('Duplicate discovered test identity')
}
return Object.fromEntries([...files.entries()].sort(([a], [b]) => compareIds(a, b)))
}
export function planE2e(report, count, baseline) {
const testsByFile = discoverE2eFiles(report)
const timings = Object.fromEntries(
Object.entries(baseline.timings).map(([file, duration]) => [
file.replace(/^tests\/e2e\//, ''),
duration
])
)
const assignment = balanceFiles(Object.keys(testsByFile), count, timings)
return { ...assignment, testsByFile, baselineSha256: baseline.baselineSha256 }
}
export function verifyE2eSelection(assignment, report) {
const actual = Object.values(discoverE2eFiles(report)).flat().sort(compareIds)
const expected = assignment.shards[assignment.selectedShard - 1].files
.flatMap((file) => assignment.testsByFile[file])
.sort(compareIds)
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error('Native Playwright selection differs from shard assignment')
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
if (process.argv[2] === '--verify') {
verifyE2eSelection(
JSON.parse(readFileSync(process.argv[3], 'utf8')),
JSON.parse(readFileSync(process.argv[4], 'utf8'))
)
} else {
const [input, shard, directory] = process.argv.slice(2)
const match = shard?.match(/^(\d+)\/(\d+)$/)
if (!input || !directory || !match) {
throw new Error('Usage: ci-e2e-shard-plan.mjs DISCOVERY INDEX/COUNT OUTPUT_DIRECTORY')
}
const index = Number(match[1])
const count = Number(match[2])
if (index < 1 || index > count) {
throw new Error('Invalid shard index')
}
const assignment = planE2e(
JSON.parse(readFileSync(input, 'utf8')),
count,
readTimingBaseline('e2e')
)
const selected = assignment.shards[index - 1].files
if (!selected.length) {
throw new Error('Empty E2E shard')
}
writeAssignment(join(directory, 'assignment.json'), { ...assignment, selectedShard: index })
writeFileSync(join(directory, 'selected.txt'), `${selected.join('\n')}\n`)
}
}
@@ -0,0 +1,73 @@
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { expect, it } from 'vitest'
import { runProcess } from '../../src/shared/child-process/run-process'
import { planE2e, verifyE2eSelection } from './ci-e2e-shard-plan.mjs'
const require = createRequire(import.meta.url)
it('native Playwright test-list preserves full discovery, serial suites, skips and headful filtering', async () => {
const directory = realpathSync(mkdtempSync(join(tmpdir(), 'orca-playwright-shards-')))
const testPackage = JSON.stringify(require.resolve('@stablyai/playwright-test'))
const config = join(directory, 'playwright.config.cjs')
writeFileSync(
config,
`module.exports = { testDir: '.', fullyParallel: true, projects: [{ name: 'electron-headless', grepInvert: /@headful/ }] }`
)
for (let index = 0; index < 17; index++) {
writeFileSync(
join(directory, `file-${index}.spec.cjs`),
`
const { test } = require(${testPackage});
test('normal', () => {});
test.skip('skipped', () => {});
test('visible @headful', () => {});
test.describe.serial('serial', () => {
test('first', () => {});
test('second', () => {});
});
`
)
}
async function discover(extra = []) {
const result = await runProcess({
program: process.execPath,
cwd: directory,
args: [
join(dirname(require.resolve('playwright/package.json')), 'cli.js'),
'test',
'--config',
config,
'--list',
'--reporter=json',
...extra
],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 20000
})
expect(result.code, result.stderr).toBe(0)
return JSON.parse(result.stdout)
}
try {
const full = await discover()
const assignment = planE2e(full, 14, { timings: {} })
const ids = []
for (let index = 0; index < 14; index++) {
const path = join(directory, 'selected.txt')
writeFileSync(path, `${assignment.shards[index].files.join('\n')}\n`)
const selected = await discover(['--test-list', path])
verifyE2eSelection({ ...assignment, selectedShard: index + 1 }, selected)
for (const suite of selected.suites) {
expect(suite.specs.some((spec) => spec.title.includes('@headful'))).toBe(false)
}
ids.push(...assignment.shards[index].files.flatMap((file) => assignment.testsByFile[file]))
}
expect(ids).toHaveLength(17 * 4)
expect(new Set(ids).size).toBe(ids.length)
expect(() => verifyE2eSelection({ ...assignment, selectedShard: 1 }, full)).toThrow('differs')
} finally {
rmSync(directory, { recursive: true, force: true })
}
}, 60000)
+66
View File
@@ -0,0 +1,66 @@
import { createHash } from 'node:crypto'
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname } from 'node:path'
export const compareIds = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
export function balanceFiles(files, count, timings, overheadMs = 0) {
if (!Number.isInteger(count) || count < 1) {
throw new Error('Invalid shard count')
}
if (new Set(files).size !== files.length) {
throw new Error('Duplicate discovered file')
}
const known = Object.values(timings).filter((value) => Number.isFinite(value) && value > 0)
known.sort((a, b) => a - b)
const fallbackMs = known[Math.floor(known.length / 2)] ?? 1000
const weighted = files.map((file) => ({
file,
durationMs:
(Number.isFinite(timings[file]) && timings[file] > 0 ? timings[file] : fallbackMs) +
overheadMs
}))
weighted.sort((a, b) => b.durationMs - a.durationMs || compareIds(a.file, b.file))
const shards = Array.from({ length: count }, () => ({ files: [], durationMs: 0 }))
for (const entry of weighted) {
const target = shards.reduce((best, shard) =>
shard.durationMs < best.durationMs ||
(shard.durationMs === best.durationMs && shard.files.length < best.files.length)
? shard
: best
)
target.files.push(entry.file)
target.durationMs += entry.durationMs
}
for (const shard of shards) {
shard.files.sort(compareIds)
}
const assigned = shards.flatMap((shard) => shard.files).sort(compareIds)
if (JSON.stringify(assigned) !== JSON.stringify([...files].sort(compareIds))) {
throw new Error('Shard coverage differs from discovery')
}
return { algorithm: 'file-lpt-v1', fallbackMs, overheadMs, shards }
}
export function readTimingBaseline(suite) {
const bytes = readFileSync(new URL('./ci-shard-timings.json', import.meta.url), 'utf8')
const baseline = JSON.parse(bytes)
return { ...baseline[suite], baselineSha256: createHash('sha256').update(bytes).digest('hex') }
}
export function writeAssignment(path, assignment) {
mkdirSync(dirname(path), { recursive: true })
writeFileSync(
path,
`${JSON.stringify(
{
sourceSha: process.env.ORCA_SHARD_SOURCE_SHA ?? process.env.GITHUB_SHA ?? null,
runId: process.env.GITHUB_RUN_ID ?? null,
runAttempt: process.env.GITHUB_RUN_ATTEMPT ?? null,
...assignment
},
null,
2
)}\n`
)
}
+113
View File
@@ -0,0 +1,113 @@
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { BaseSequencer } from 'vitest/node'
import { balanceFiles } from './ci-shard-assignment.mjs'
import { discoverE2eFiles, planE2e } from './ci-e2e-shard-plan.mjs'
import { parseTimingLog } from './ci-shard-timing-import.mjs'
import TimingSequencer from './ci-unit-sequencer.mjs'
const directories = []
afterEach(() => {
vi.unstubAllEnvs()
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
describe('timing-weighted shard selection', () => {
it('distributes long files, includes unknowns exactly once, and ignores discovery order', () => {
const files = ['long', 'medium', 'short', 'unknown', 'new', 'zero', 'invalid']
const timings = { long: 100, medium: 80, short: 20, zero: 0, invalid: -1, deleted: 20 }
const plan = balanceFiles(files, 3, timings, 10)
expect(plan).toEqual(balanceFiles(files.toReversed(), 3, timings, 10))
expect(plan.fallbackMs).toBe(80)
expect(plan.shards.flatMap((shard) => shard.files).sort()).toEqual([...files].sort())
expect(Math.max(...plan.shards.map((shard) => shard.durationMs))).toBeLessThan(250)
})
it('has a deterministic cold fallback and permits fewer files than shards', () => {
expect(balanceFiles(['b', 'a'], 3, {}).shards).toEqual([
{ files: ['a'], durationMs: 1000 },
{ files: ['b'], durationMs: 1000 },
{ files: [], durationMs: 0 }
])
expect(() => balanceFiles(['a', 'a'], 8, {})).toThrow('Duplicate')
expect(() => balanceFiles(['a'], 0, {})).toThrow('count')
})
it('uses the post-filter Vitest discovery unchanged across eight shards and retains default sort', async () => {
const directory = mkdtempSync(join(tmpdir(), 'orca-unit-shards-'))
directories.push(directory)
vi.stubEnv('ORCA_SHARD_MANIFEST', join(directory, 'assignment.json'))
const specs = Array.from({ length: 37 }, (_, i) => ({
moduleId: resolve(`src/fixture-${i}.test.ts`)
}))
const selected = []
for (let index = 1; index <= 8; index++) {
const sequencer = new TimingSequencer({
config: { root: process.cwd(), shard: { index, count: 8 } }
})
expect(sequencer.sort).toBe(BaseSequencer.prototype.sort)
selected.push(...(await sequencer.shard(specs)))
const manifest = JSON.parse(readFileSync(join(directory, 'assignment.json'), 'utf8'))
expect(manifest.selectedShard).toBe(index)
expect(manifest.baselineSha256).toMatch(/^[a-f0-9]{64}$/)
}
expect(new Set(selected).size).toBe(specs.length)
expect(selected).toHaveLength(specs.length)
expect(new Set(selected)).toEqual(new Set(specs))
})
it('wires a constructor into the opt-in Vitest config', async () => {
vi.stubEnv('ORCA_BALANCE_UNIT_SHARDS', '1')
const { default: config } = await import('../vitest.config')
expect(config.test.sequence.sequencer).toBe(TimingSequencer)
})
it('keeps nested/serial E2E files atomic and fails closed on discovery errors', () => {
const spec = (id, file) => ({ id, file, tests: [{ projectName: 'electron-headless' }] })
const report = {
suites: [
{
specs: [spec('a', 'one.spec.ts')],
suites: [{ specs: [spec('b', 'one.spec.ts'), spec('c', 'two.spec.ts')] }]
}
]
}
const plan = planE2e(report, 14, { timings: { 'tests/e2e/one.spec.ts': 4000 } })
expect(plan.shards.flatMap((shard) => shard.files).sort()).toEqual([
'one.spec.ts',
'two.spec.ts'
])
expect(plan.testsByFile['one.spec.ts']).toHaveLength(2)
expect(() => discoverE2eFiles({ ...report, errors: [{}] })).toThrow('errors')
expect(() => discoverE2eFiles({ suites: [] })).toThrow('no tests')
expect(() =>
discoverE2eFiles({ suites: [{ specs: [spec('a', '../escape.spec.ts')] }] })
).toThrow('Unsafe')
expect(() =>
discoverE2eFiles({
suites: [{ specs: [spec('a', 'one.spec.ts'), spec('a', 'one.spec.ts')] }]
})
).toThrow('Duplicate')
})
it('imports ANSI unit timings and E2E failures without counting headful reruns', () => {
const parsed = parseTimingLog(
[
'\u001b[32m✓\u001b[39m src/a.test.ts (2 tests) 35ms',
'Duration 1s (transform 0.1s, setup 0.2s, import 0.3s, tests 0.04s, environment 0.4s)',
'✓ 1 [electron-headless] › tests/e2e/a.spec.ts:1:1 › works (2s)',
'✘ 2 [electron-headless] › tests/e2e/a.spec.ts:2:1 › fails (1.2m)',
'✓ 3 [electron-headful] › tests/e2e/a.spec.ts:3:1 › benchmark (9s)'
].join('\n')
)
expect(parsed).toEqual({
unit: { 'src/a.test.ts': 35 },
e2e: { 'tests/e2e/a.spec.ts': 74000 },
overheadMs: 1000
})
})
})
+76
View File
@@ -0,0 +1,76 @@
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.]+s \(transform ([\d.]+)s, setup ([\d.]+)s, import ([\d.]+)s, tests [\d.]+s, environment ([\d.]+)s\)/
)
return {
unit,
e2e,
overheadMs: summary ? summary.slice(1).reduce((sum, value) => sum + Number(value) * 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`
)
}
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
# Timing-based CI shards
The eight unit shards and fourteen general E2E shards use longest-processing-time
assignment of whole files to the currently lightest shard. Ties use file path and
then shard index, independent of filesystem enumeration and locale. Unknown,
zero, or invalid durations use the baseline's positive median (1 second when no
positive evidence exists). Deleted files never enter discovery. Unit weights add
526ms per file for measured transform/setup/import/environment overhead.
Unit assignment runs inside Vitest's sequencer after discovery and CLI exclusions;
Vitest's default sort, workers and isolation remain intact. It is enabled only by
`ORCA_BALANCE_UNIT_SHARDS=1`; ordinary local runs and explicit file filters retain
their existing behavior. E2E uses Playwright's native `--list` and `--test-list`,
retaining project filters, skipped tests and complete serial groups within files.
The workflow verifies selected test IDs against full discovery before executing.
Dedicated SSH, native IME, WSL and first-paint lanes are unchanged.
## Evidence and limits
`ci-shard-timings.json` records run IDs and every contributing job ID:
- Unit run **34675583768**, Node 24, all eight successful shards: 8,484 completed
file durations. The summed transform/setup/import/environment durations divided
by measured file count give a rounded-up **526ms** per-file overhead allowance.
The original shard weighted loads were **764–849 worker-seconds**, versus
**792–792** after balancing the identical measured files. File counts change
from **1,056–1,065** to **1,060–1,061**.
- General E2E run **34652504501**, all fourteen shard logs: 291 files with completed
headless test durations, including failures. Headful benchmark reruns are not
counted. Original completed test loads were **540–1,727 seconds**, versus
**1,083–1,093** after whole-file balancing on the same measured files. The longest
measured file is **528 seconds**, below the balanced shard load.
- Current checkout discovery at validation contained **8,553 unit files** after the
workflow's exact exclusions and **733 headless E2E tests in 340 files**. New and
unmeasured files remain selected. Projected current loads were about **797
worker-seconds** per unit shard (1,068–1,070 files) and **1,190–1,200 seconds** per
E2E shard (22–25 files).
These are scheduling projections, not measured post-change wall-clock gains.
Unit durations overlap across workers and the overhead allowance is an average,
not a per-file import profile. E2E evidence includes failed shards and can omit
unfinished tests; unknowns receive a deterministic estimate. Historical timings
age as specs change. Full CI runs on the existing runner classes are required to
measure elapsed-time and occupancy improvements, including discovery overhead.
No retries, assertions, coverage exclusions, runner classes or shard counts changed.
## Reproduction and refresh
Every shard uploads an artifact named with its shard, Node version where relevant,
and run attempt. `assignment.json` contains the checked-out source SHA, run ID,
attempt, baseline SHA-256, algorithm, fallback, all shard files and chosen shard.
E2E also retains both discovery reports and `selected.txt`. Artifacts live for
14 days. A rerun of the same source uses the same checked-in baseline rather than
mutable timing caches; a GitHub job rerun therefore keeps its assignment.
For E2E reproduction, check out the recorded source and pass the saved list to the
existing command: `pnpm run test:e2e --test-list=/path/to/selected.txt` with the same
CI environment/build inputs. For unit reproduction, use the unchanged workflow
command and exclusions with `ORCA_BALANCE_UNIT_SHARDS=1` and the recorded
`--shard=INDEX/8`. Direct test-file reruns remain supported.
To refresh the baseline, download `log-JOB_ID.txt` files into one directory from
exactly one eight-shard unit run and one fourteen-shard general E2E run. Use the
job IDs from the Actions jobs API and fetch each with
`gh api repos/stablyai/orca/actions/jobs/JOB_ID/logs`. Do not include dedicated
lanes or multiple attempts. Then run:
```sh
node config/scripts/ci-shard-timing-import.mjs LOG_DIRECTORY UNIT_RUN_ID E2E_RUN_ID config/scripts/ci-shard-timings.json
```
The initial source logs are in `/tmp/orca-ci-shard-logs`; two were reused from
`/tmp/orca-ci-audit`, and the remaining twenty were fetched read-only. Reimporting
those logs reproduced the checked-in JSON byte-for-byte. Review file-count and
load projections before adopting a new baseline; no network access is needed to
plan or run shards.
## Validation
- 74 focused tests passed across the two new test files and existing PR
parallelism, E2E gate and release E2E dispatch contracts.
- The pinned Playwright CLI selected the real 733-test suite across all fourteen
saved test lists with exact-once identity coverage and no missing tests.
- A temporary native Playwright fixture checks fourteen shards, serial groups,
skipped cases, headful filtering and mismatch rejection without launching UI.
- Real Vitest discovery with all workflow exclusions yielded 8,553 files; the
sequencer's eight assignments covered each exactly once. An actual opt-in
Vitest shard executed successfully and persisted its manifest.
- Focused TypeScript checking of `config/vitest.config.ts` and imported modules,
oxlint, formatting and baseline reimport checks passed.
All local tests used `ORCA_BACKGROUND_LAUNCH=1` in background tool sessions. No app
windows or full E2E test bodies were launched.
+19
View File
@@ -0,0 +1,19 @@
import { relative } from 'node:path'
import { BaseSequencer } from 'vitest/node'
import { balanceFiles, readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs'
export default class TimingSequencer extends BaseSequencer {
async shard(specs) {
const { index, count } = this.ctx.config.shard
const key = (spec) => relative(this.ctx.config.root, spec.moduleId).replaceAll('\\', '/')
const baseline = readTimingBaseline('unit')
const assignment = balanceFiles(specs.map(key), count, baseline.timings, baseline.overheadMs)
writeAssignment(process.env.ORCA_SHARD_MANIFEST ?? 'ci-shards/unit-assignment.json', {
...assignment,
baselineSha256: baseline.baselineSha256,
selectedShard: index
})
const selected = new Set(assignment.shards[index - 1].files)
return specs.filter((spec) => selected.has(key(spec)))
}
}
+4
View File
@@ -1,5 +1,6 @@
import { resolve } from 'node:path'
import { defineConfig } from 'vitest/config'
import TimingSequencer from './scripts/ci-unit-sequencer.mjs'
const windowsTestWorkerOptions = process.platform === 'win32' ? { maxWorkers: 4 } : {}
@@ -15,6 +16,9 @@ export default defineConfig({
},
test: {
environment: 'node',
...(process.env.ORCA_BALANCE_UNIT_SHARDS === '1'
? { sequence: { sequencer: TimingSequencer } }
: {}),
// Why: Node 26's undefined Web Storage globals prevent Vitest from installing happy-dom's.
// Why --expose-gc: retention tests need a deterministic collection point to measure what a queue really holds.
execArgv: ['--no-experimental-webstorage', '--expose-gc'],