From e9ed7193d8d58085e8a8ce1307efec2d9bd4477e Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:24:37 +0800 Subject: [PATCH] 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. --- scripts/coverage-exempt.ts | 41 ++++++++++++++++++++++++++++++++++++++ scripts/run-gates.ts | 38 +++++++++++++++++++++++++---------- vitest.config.ts | 13 +++++++++++- 3 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 scripts/coverage-exempt.ts diff --git a/scripts/coverage-exempt.ts b/scripts/coverage-exempt.ts new file mode 100644 index 0000000000..8f20f54424 --- /dev/null +++ b/scripts/coverage-exempt.ts @@ -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' }, +] diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 46bc72c833..0289625959 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -9,6 +9,7 @@ import { spawn } from 'node:child_process' import { availableParallelism } from 'node:os' import { resolve } from 'node:path' import { performance } from 'node:perf_hooks' +import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './coverage-exempt.ts' /** A named aggregate exposed by the gate runner. */ export type Mode = @@ -202,7 +203,7 @@ export function gatesForMode(selected: Mode): Gate[] { pnpmScript('duplication', 'duplication'), ] case 'ci-coverage': - return [coverageGate()] + return coverageGates() case 'ci-snapshot': return [pnpmScript('build', 'build'), snapshotGate()] case 'ci-artifacts': @@ -248,7 +249,7 @@ function ciPrimaryGates(): Gate[] { pnpmScript('typecheck', 'typecheck'), lintGate(), pnpmScript('duplication', 'duplication'), - coverageGate(), + ...coverageGates(), ...nodeCompatSmokeGates(), snapshotGate(), ...docSyncLeafGates(), @@ -407,15 +408,30 @@ function lintGate(): Gate { : { displayCommand: `DSH_OXLINT_THREADS=${raw} pnpm run lint` }) } -function coverageGate(): Gate { - return pnpmExec('coverage', [ - 'vitest', - 'run', - '--coverage', - ...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'), - ], { - label: 'test:coverage', - }) +// The heavy suites run uninstrumented beside the thresholded gate: their +// compiler- and subprocess-bound fixtures pay a multiple of their runtime +// under v8 instrumentation while contributing nothing the thresholds need +// (membership contract in scripts/coverage-exempt.ts). +function coverageGates(): Gate[] { + return [ + pnpmExec('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, diff --git a/vitest.config.ts b/vitest.config.ts index c49f55bea8..d31dca5d3f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,5 +1,6 @@ import tsconfigPaths from 'vite-tsconfig-paths' 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 // has no include, which vite-tsconfig-paths treats as match-all, so its paths @@ -37,6 +38,12 @@ const testIncludes = [ '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 // 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. @@ -73,6 +80,7 @@ export default defineConfig({ exclude: [ ...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), ...processBoundTests, + ...coverageExemptExcludes, ], }, }, @@ -83,7 +91,10 @@ export default defineConfig({ pool: 'forks', setupFiles: ['./scripts/test-invariants.ts'], include: processBoundTests, - exclude: windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), + exclude: [ + ...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), + ...coverageExemptExcludes, + ], }, }, ],