mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 12:07:58 +00:00
perf(ci): overlap preflight's serial tail; parallel ratchets; allow 2-10 unit shards (#33887)
- Plan unit selection starts after setup (overlaps lint) instead of last. - Localization extraction starts with the typecheck; test-file-runners is background. - run-ratchets.mjs runs ratchets in a pool (19s serial -> 8s locally). - pnpm lint uses CI's single-parse check-import-boundaries.mjs. - ORCA_UNIT_FULL_SHARD_COUNT accepts 2-10 (default stays 5). Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
+46
-35
@@ -220,6 +220,22 @@ jobs:
|
||||
with:
|
||||
bun-version-file: config/.bun-version
|
||||
|
||||
# Plan first: its ~10s single-threaded import-graph walk overlaps lint instead of trailing the job.
|
||||
- name: Plan unit selection
|
||||
if: '!cancelled()'
|
||||
id: unit-plan
|
||||
background: true
|
||||
env:
|
||||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.typecheck == 'true' }}
|
||||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||||
ORCA_UNIT_SELECTION_MODE: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }}
|
||||
ORCA_PULL_REQUEST_DRAFT: ${{ github.event.pull_request.draft }}
|
||||
# Five shards use fewer runner minutes; the repository variable can request 2 to 10.
|
||||
ORCA_UNIT_FULL_SHARD_COUNT: ${{ vars.ORCA_UNIT_FULL_SHARD_COUNT || '5' }}
|
||||
run: |
|
||||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||||
node config/scripts/ci-unit-plan.mjs
|
||||
|
||||
# Run gate scripts directly to avoid repeated package-manager startup.
|
||||
- name: Check process-host imports
|
||||
if: '!cancelled()'
|
||||
@@ -343,6 +359,27 @@ jobs:
|
||||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||||
pnpm run typecheck:ci --bun-concurrency=2 --bun-threads=2
|
||||
|
||||
# Starts with the compiler so its ~16s pass overlaps the serial checks below.
|
||||
# Extract sorted evidence to isolated temp paths with one normalized platform-independent AST pass; use preflight’s pinned Bun.
|
||||
- name: Verify localization extraction
|
||||
if: '!cancelled()'
|
||||
id: localization-extraction
|
||||
background: true
|
||||
env:
|
||||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||||
# Detection failures run the full check; renames retain the removed input path.
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/localization-changes" &&
|
||||
scope="$(node config/scripts/localization-extraction-change-scope.mjs "$RUNNER_TEMP/localization-changes")" && [ "$scope" = false ]; then
|
||||
echo "Localization extraction inputs are unchanged."
|
||||
else
|
||||
bun config/scripts/verify-localization-extraction.mjs
|
||||
fi
|
||||
|
||||
- name: Check Zustand selector fan-out budget
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/zustand-selector-fanout-benchmark.mjs --check
|
||||
@@ -388,33 +425,21 @@ jobs:
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/check-node-runtime-pin.mjs
|
||||
|
||||
# A ~7s single-threaded tree walk; it overlaps the remaining checks.
|
||||
- name: Check every test file has a runner
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/check-test-file-runners.mjs
|
||||
|
||||
- name: Check root and mobile lock the same shared dependencies
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/check-mobile-shared-dependency-versions.mjs
|
||||
|
||||
# Extract sorted evidence to isolated temp paths with one normalized platform-independent AST pass; use preflight’s pinned Bun.
|
||||
- name: Verify localization extraction
|
||||
if: '!cancelled()'
|
||||
id: localization-extraction
|
||||
id: test-file-runners
|
||||
background: true
|
||||
env:
|
||||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
|
||||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||||
# Detection failures run the full check; renames retain the removed input path.
|
||||
DIFF_BASE="$(node config/scripts/git-pull-request-diff-base.mjs "$BASE_SHA")"
|
||||
if git diff --name-only --no-renames -z "$DIFF_BASE" HEAD > "$RUNNER_TEMP/localization-changes" &&
|
||||
scope="$(node config/scripts/localization-extraction-change-scope.mjs "$RUNNER_TEMP/localization-changes")" && [ "$scope" = false ]; then
|
||||
echo "Localization extraction inputs are unchanged."
|
||||
else
|
||||
bun config/scripts/verify-localization-extraction.mjs
|
||||
fi
|
||||
node config/scripts/check-test-file-runners.mjs
|
||||
|
||||
- name: Check root and mobile lock the same shared dependencies
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/check-mobile-shared-dependency-versions.mjs
|
||||
|
||||
- name: Decide whether the orcad terminal smoke runs
|
||||
id: orcad-smoke-scope
|
||||
@@ -498,22 +523,6 @@ jobs:
|
||||
if: needs.code_paths.outputs.static_analysis == 'true'
|
||||
run: node config/scripts/verify-macos-entitlements.mjs
|
||||
|
||||
# Planning shares setup and stays off the compiler's critical path.
|
||||
- name: Plan unit selection
|
||||
if: '!cancelled()'
|
||||
id: unit-plan
|
||||
background: true
|
||||
env:
|
||||
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.typecheck == 'true' }}
|
||||
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
|
||||
ORCA_UNIT_SELECTION_MODE: ${{ vars.ORCA_UNIT_SELECTION_MODE || 'shadow' }}
|
||||
ORCA_PULL_REQUEST_DRAFT: ${{ github.event.pull_request.draft }}
|
||||
# Five shards use fewer runner minutes; the repository variable can still request ten.
|
||||
ORCA_UNIT_FULL_SHARD_COUNT: ${{ vars.ORCA_UNIT_FULL_SHARD_COUNT || '5' }}
|
||||
run: |
|
||||
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
|
||||
node config/scripts/ci-unit-plan.mjs
|
||||
|
||||
- wait: typecheck
|
||||
|
||||
- wait: process-host-imports
|
||||
@@ -524,6 +533,8 @@ jobs:
|
||||
|
||||
- wait: unit-plan
|
||||
|
||||
- wait: test-file-runners
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: needs.code_paths.outputs.typecheck == 'true'
|
||||
with:
|
||||
|
||||
@@ -8,8 +8,9 @@ import { readTimingBaseline, writeAssignment } from './ci-shard-assignment.mjs'
|
||||
|
||||
export function prepareUnitPlan(env = process.env) {
|
||||
const requestedCount = env.ORCA_UNIT_FULL_SHARD_COUNT ?? String(FULL_SHARD_COUNT)
|
||||
if (!['5', '10'].includes(requestedCount)) {
|
||||
throw new Error('ORCA_UNIT_FULL_SHARD_COUNT must be 5 or 10')
|
||||
// Every consumer reads the count from the plan; ten bounds runner-slot use.
|
||||
if (!/^([2-9]|10)$/.test(requestedCount)) {
|
||||
throw new Error('ORCA_UNIT_FULL_SHARD_COUNT must be an integer from 2 to 10')
|
||||
}
|
||||
const fullShardCount = Number(requestedCount)
|
||||
const files = discoverUnitFiles()
|
||||
|
||||
@@ -86,7 +86,7 @@ it.each([
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['5', '10', 'invalid'])(
|
||||
it.each(['5', '6', '10', 'invalid', '11'])(
|
||||
'validates the requested full shard count and keeps fallback coverage: %s',
|
||||
(requestedCount) => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-unit-count-'))
|
||||
@@ -105,9 +105,11 @@ it.each(['5', '10', 'invalid'])(
|
||||
ORCA_SHARD_SOURCE_SHA: 'full-count-fixture'
|
||||
}
|
||||
})
|
||||
if (requestedCount === 'invalid') {
|
||||
if (['invalid', '11'].includes(requestedCount)) {
|
||||
expect(result.code).not.toBe(0)
|
||||
expect(result.stderr).toContain('ORCA_UNIT_FULL_SHARD_COUNT must be 5 or 10')
|
||||
expect(result.stderr).toContain(
|
||||
'ORCA_UNIT_FULL_SHARD_COUNT must be an integer from 2 to 10'
|
||||
)
|
||||
return
|
||||
}
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
|
||||
@@ -8,12 +8,11 @@ import { pathToFileURL } from 'node:url'
|
||||
|
||||
// package.json script names, slowest first so short steps fill in around them.
|
||||
export const LINT_STEPS = [
|
||||
'check:process-host-imports',
|
||||
'check:import-boundaries',
|
||||
'verify:localization-extraction',
|
||||
'audit:anti-slop',
|
||||
'audit:code-quality:type-aware',
|
||||
'check:test-file-runners',
|
||||
'check:compat-boundary-imports',
|
||||
'verify:localization-catalogs',
|
||||
'verify:localization-coverage',
|
||||
'check:flaky-test-pattern-ratchet',
|
||||
@@ -48,7 +47,11 @@ export const LINT_STEPS = [
|
||||
]
|
||||
|
||||
// Steps with no package script of their own run as the bare command.
|
||||
const BARE_COMMANDS = { oxlint: 'oxlint' }
|
||||
const BARE_COMMANDS = {
|
||||
oxlint: 'oxlint',
|
||||
// process-host and compat-boundary imports from one TypeScript parse, as CI runs them.
|
||||
'check:import-boundaries': 'bun config/scripts/check-import-boundaries.mjs'
|
||||
}
|
||||
|
||||
function parseMaxWorkers(argv) {
|
||||
const argument = argv.find((value) => /^--max-?workers=\d+$/i.test(value))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { availableParallelism } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
@@ -19,26 +20,41 @@ export function listRatchetScripts(scriptsDir) {
|
||||
.sort()
|
||||
}
|
||||
|
||||
function main(root) {
|
||||
function runRatchet(scriptsDir, name, root) {
|
||||
return new Promise((resolve) => {
|
||||
let output = ''
|
||||
const child = spawn(process.execPath, [path.join(scriptsDir, name)], { cwd: root })
|
||||
child.stdout.on('data', (chunk) => (output += chunk))
|
||||
child.stderr.on('data', (chunk) => (output += chunk))
|
||||
child.on('error', (error) => resolve({ name, ok: false, output: `${output}${error.message}` }))
|
||||
child.on('close', (code) => resolve({ name, ok: code === 0, output }))
|
||||
})
|
||||
}
|
||||
|
||||
// Each ratchet is a single-threaded scan; half the cores leaves room for the overlapping typecheck.
|
||||
async function main(root) {
|
||||
const scriptsDir = path.join(root, 'config', 'scripts')
|
||||
const pending = listRatchetScripts(scriptsDir)
|
||||
const failed = []
|
||||
for (const name of listRatchetScripts(scriptsDir)) {
|
||||
console.log(`\n▶ ${name}`)
|
||||
const result = spawnSync(process.execPath, [path.join(scriptsDir, name)], {
|
||||
cwd: root,
|
||||
stdio: 'inherit'
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.max(2, Math.floor(availableParallelism() / 2)) }, async () => {
|
||||
while (pending.length > 0) {
|
||||
const result = await runRatchet(scriptsDir, pending.shift(), root)
|
||||
// Whole per-script blocks, so concurrent ratchets never interleave their lines.
|
||||
process.stdout.write(`\n▶ ${result.name}\n${result.output}`)
|
||||
if (!result.ok) {
|
||||
failed.push(result.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (result.error || result.status !== 0) {
|
||||
failed.push(name)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (failed.length > 0) {
|
||||
console.error(`\nFailed ratchets: ${failed.join(', ')}`)
|
||||
console.error(`\nFailed ratchets: ${failed.sort().join(', ')}`)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
process.exit(main(process.cwd()))
|
||||
process.exitCode = await main(process.cwd())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user