ci: run coverage-exempt heavy suites uninstrumented in parallel

The coverage lane's wall clock was pinned by a few compiler- and
subprocess-bound suites whose v8 instrumentation tax is a multiple of
their runtime while contributing nothing the per-file thresholds need:
typert generator fixtures (whole-workspace compiler analysis; its src is
threshold-excluded) and three scripts/ child-process fixture suites
(scripts/ sources are never coverage-measured; in-process imports are
covered by their owning package tests).

Split ci-coverage into two parallel gates: the instrumented run sets
DSH_COVERAGE_EXEMPT_HEAVY=1 and vitest.config.ts drops the exempt suites
from both projects (CLI --exclude cannot reach per-project include
resolution); a second uninstrumented gate runs exactly those suites, so
the aggregate still executes every test. Membership contract and the
filter/exclude pairs live in scripts/coverage-exempt.ts.

Local 6-worker A/B: instrumented gate 900s -> 260s wall; exempt gate
262s wall runs beside it, so the lane converges near the slower of the
two (~4.4min vs ~7min single-gate). DSH_GATE_CONCURRENCY now has two
schedulable gates in this lane.
This commit is contained in:
imccyu
2026-07-31 02:24:37 +08:00
parent 8a3b47ae5e
commit e9ed7193d8
3 changed files with 80 additions and 12 deletions

View File

@@ -0,0 +1,41 @@
/**
* Heavy suites the coverage aggregate runs uninstrumented in a parallel gate.
* Membership contract: a suite qualifies only when every coverage-measured
* file it executes in-process (`coverage.include` spans package src trees;
* typert generator src is threshold-excluded in vitest.config.ts) is already
* fully covered by other suites, so removing it from the instrumented run
* changes no threshold outcome. The aggregate still runs every listed suite
* plain beside the instrumented gate, so correctness signal is unchanged —
* only the v8 instrumentation tax on compiler- and subprocess-heavy fixtures
* is dropped.
*/
/** One coverage-exempt suite: a Vitest CLI filter and its exclude glob. */
export interface CoverageExemptSuite {
/** Positional file filter selecting the suite in the uninstrumented gate. */
readonly filter: string
/** Exclude glob removing the suite from the instrumented gate. */
readonly exclude: string
}
/**
* Set to `1` by the instrumented coverage gate; vitest.config.ts then drops
* the exempt suites from every project. CLI `--exclude` cannot express this:
* it does not reach per-project include resolution.
*/
export const COVERAGE_EXEMPT_ENV = 'DSH_COVERAGE_EXEMPT_HEAVY'
/** Coverage-exempt heavy suites; keep filter and exclude selecting the same files. */
export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [
// Whole-workspace compiler analysis per case — the lane's longest tail.
// Generator src is threshold-excluded; tools-catalog's registry and
// tool-cordis imports are fully covered by those packages' own tests.
{
filter: 'packages/typert/generator/tests/',
exclude: 'packages/typert/generator/tests/**',
},
// Real child-process fixtures over scripts/ sources, which coverage never measures.
{ filter: 'scripts/install-lefthook.spec.ts', exclude: 'scripts/install-lefthook.spec.ts' },
{ filter: 'scripts/oxlint-contract.spec.ts', exclude: 'scripts/oxlint-contract.spec.ts' },
{ filter: 'scripts/change-scope.spec.ts', exclude: 'scripts/change-scope.spec.ts' },
]

View File

@@ -9,6 +9,7 @@ import { spawn } from 'node:child_process'
import { availableParallelism } from 'node:os' import { availableParallelism } from 'node:os'
import { resolve } from 'node:path' import { resolve } from 'node:path'
import { performance } from 'node:perf_hooks' import { performance } from 'node:perf_hooks'
import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './coverage-exempt.ts'
/** A named aggregate exposed by the gate runner. */ /** A named aggregate exposed by the gate runner. */
export type Mode = export type Mode =
@@ -202,7 +203,7 @@ export function gatesForMode(selected: Mode): Gate[] {
pnpmScript('duplication', 'duplication'), pnpmScript('duplication', 'duplication'),
] ]
case 'ci-coverage': case 'ci-coverage':
return [coverageGate()] return coverageGates()
case 'ci-snapshot': case 'ci-snapshot':
return [pnpmScript('build', 'build'), snapshotGate()] return [pnpmScript('build', 'build'), snapshotGate()]
case 'ci-artifacts': case 'ci-artifacts':
@@ -248,7 +249,7 @@ function ciPrimaryGates(): Gate[] {
pnpmScript('typecheck', 'typecheck'), pnpmScript('typecheck', 'typecheck'),
lintGate(), lintGate(),
pnpmScript('duplication', 'duplication'), pnpmScript('duplication', 'duplication'),
coverageGate(), ...coverageGates(),
...nodeCompatSmokeGates(), ...nodeCompatSmokeGates(),
snapshotGate(), snapshotGate(),
...docSyncLeafGates(), ...docSyncLeafGates(),
@@ -407,15 +408,30 @@ function lintGate(): Gate {
: { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` }) : { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` })
} }
function coverageGate(): Gate { // The heavy suites run uninstrumented beside the thresholded gate: their
return pnpmExec('coverage', [ // compiler- and subprocess-bound fixtures pay a multiple of their runtime
'vitest', // under v8 instrumentation while contributing nothing the thresholds need
'run', // (membership contract in scripts/coverage-exempt.ts).
'--coverage', function coverageGates(): Gate[] {
...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'), return [
], { pnpmExec('coverage', [
label: 'test:coverage', 'vitest',
}) 'run',
'--coverage',
...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'),
], {
label: 'test:coverage',
env: { [COVERAGE_EXEMPT_ENV]: '1' },
}),
pnpmExec('coverage-exempt-heavy', [
'vitest',
'run',
...coverageExemptHeavySuites.map(suite => suite.filter),
...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'),
], {
label: 'test:coverage-exempt-heavy',
}),
]
} }
// Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node, // Example and package snapshots boot their bins in `lib` mode (built artifacts under plain Node,

View File

@@ -1,5 +1,6 @@
import tsconfigPaths from 'vite-tsconfig-paths' import tsconfigPaths from 'vite-tsconfig-paths'
import { defineConfig } from 'vitest/config' import { defineConfig } from 'vitest/config'
import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './scripts/coverage-exempt.ts'
// Resolution facade shared by every plugin instance below: tsconfig.base.json // Resolution facade shared by every plugin instance below: tsconfig.base.json
// has no include, which vite-tsconfig-paths treats as match-all, so its paths // has no include, which vite-tsconfig-paths treats as match-all, so its paths
@@ -37,6 +38,12 @@ const testIncludes = [
'scripts/**/*.spec.ts', 'scripts/**/*.spec.ts',
] ]
// The instrumented coverage gate sets this env; the exempt heavy suites then
// run beside it uninstrumented (membership contract in scripts/coverage-exempt.ts).
const coverageExemptExcludes = process.env[COVERAGE_EXEMPT_ENV] === '1'
? coverageExemptHeavySuites.map(suite => suite.exclude)
: []
// These suites exercise process-global state, process APIs, or timing-sensitive process I/O // These suites exercise process-global state, process APIs, or timing-sensitive process I/O
// that worker threads cannot isolate reliably under aggregate gate contention. // that worker threads cannot isolate reliably under aggregate gate contention.
// Keep the narrow exception in forks while the rest of the inventory avoids per-file processes. // Keep the narrow exception in forks while the rest of the inventory avoids per-file processes.
@@ -73,6 +80,7 @@ export default defineConfig({
exclude: [ exclude: [
...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), ...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`),
...processBoundTests, ...processBoundTests,
...coverageExemptExcludes,
], ],
}, },
}, },
@@ -83,7 +91,10 @@ export default defineConfig({
pool: 'forks', pool: 'forks',
setupFiles: ['./scripts/test-invariants.ts'], setupFiles: ['./scripts/test-invariants.ts'],
include: processBoundTests, include: processBoundTests,
exclude: windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), exclude: [
...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`),
...coverageExemptExcludes,
],
}, },
}, },
], ],