mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
perf(gates): cap local default worker counts
os.availableParallelism() reports host CPUs rather than a container's CPU quota (256 against a 64-CPU quota here), so local defaults spawned 255 Vitest forks (55 GiB peak RSS) and oversubscribed every scheduler. scripts/parallelism.ts keeps the CPU count in CI and caps other runs at 12, or 48 for Vitest forks and the local coverage budget; documentation and hygiene aggregates run up to 8 gates instead of 4. Explicit DSH_GATE_CONCURRENCY, DSH_OXLINT_THREADS, DSH_COVERAGE_MAX_WORKERS, and --maxWorkers still win. Refs #5852
This commit is contained in:
@@ -146,6 +146,8 @@ Apart from the scoped staged-record verification, the hooks intentionally do not
|
||||
|
||||
Contributors can opt into the comprehensive local gate set with `pnpm run check:all`. The command is independent of the Git hooks and is not an agent instruction.
|
||||
|
||||
Outside CI (`CI` other than `true`), default worker counts follow the [local worker budget](../scripts/parallelism.ts): 12 for gate schedulers, `build`, `typecheck`, Oxlint, and publint; 48 for Vitest forks and local coverage; and 8 concurrent gates for `doc-sync`, `test:docs`, `hygiene`, and `check:all`. `os.availableParallelism()` reports host CPUs rather than a container's CPU quota, so uncapped defaults oversubscribe shared hosts. CI keeps the runner's CPU count. `DSH_GATE_CONCURRENCY`, `DSH_OXLINT_THREADS`, `DSH_COVERAGE_MAX_WORKERS`, and Vitest `--maxWorkers` override the defaults.
|
||||
|
||||
### CI gates
|
||||
|
||||
The keyless [CI workflow](../.github/workflows/ci.yml) groups independent gates into broad lanes and runs a smaller compatibility signal across supported Node versions. Artifact consumers wait for one build within their lane. Coverage partitions reuse file timings scoped to their platform and runner pool to balance work within the configured worker limit. Required benchmarks run separately on standard GitHub-hosted Linux; the [benchmark runner reference](../benchmarks/AGENTS.md) owns routing and the job timeout. The separate real-API workflow runs `pnpm run test:e2e` with its configured worker bound. See [scripts/run-gates.ts](../scripts/run-gates.ts) and the workflow files for the current gate and job inventory.
|
||||
|
||||
@@ -151,6 +151,8 @@ vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `v
|
||||
|
||||
贡献者可以选择运行 `pnpm run check:all`,执行全面的本地门禁集。该命令独立于 Git 钩子,也不是对 agent 的指令。
|
||||
|
||||
在 CI 之外(`CI` 不为 `true`),默认 worker 数遵循[本地 worker 预算](../scripts/parallelism.ts):门禁调度器、`build`、`typecheck`、Oxlint 与 publint 为 12;Vitest fork 与本地覆盖率为 48;`doc-sync`、`test:docs`、`hygiene` 与 `check:all` 最多并发 8 个门禁。`os.availableParallelism()` 报告的是宿主 CPU 数而不是容器的 CPU 配额,因此不设上限的默认值会在共享主机上超额订阅。CI 仍使用 runner 的 CPU 数。`DSH_GATE_CONCURRENCY`、`DSH_OXLINT_THREADS`、`DSH_COVERAGE_MAX_WORKERS` 与 Vitest 的 `--maxWorkers` 可覆盖这些默认值。
|
||||
|
||||
### CI 门禁
|
||||
|
||||
keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。覆盖率分区复用按平台和运行器池隔离的文件耗时,在配置的 worker 上限内均衡工作量。必需 benchmark 在标准 GitHub 托管 Linux 上独立运行;[benchmark 运行器说明](../benchmarks/AGENTS.md)拥有路由及 job 超时。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。
|
||||
|
||||
+5
-5
@@ -10,7 +10,6 @@
|
||||
*/
|
||||
|
||||
import { rmSync } from 'node:fs'
|
||||
import { availableParallelism } from 'node:os'
|
||||
import { resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import {
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
import { aggregateBuildArgs, aggregateCheckArgs, contractSteps, libraryBuildSteps } from './compile-referenced-projects.ts'
|
||||
import type { LibraryStep } from './compile-referenced-projects.ts'
|
||||
import { pnpmCommand } from './release/process.ts'
|
||||
import { defaultParallelism, describeDefaultParallelism } from './parallelism.ts'
|
||||
import { runAggregate } from './run-gates.ts'
|
||||
import type { Gate } from './run-gates.ts'
|
||||
|
||||
@@ -155,11 +155,11 @@ async function main(): Promise<number> {
|
||||
}
|
||||
const { scope, profile } = parseBuildArguments(process.argv.slice(2))
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const name = scope === 'complete' ? 'build' : `build:${scope}`
|
||||
const available = availableParallelism()
|
||||
const name = scope === 'complete' ? 'build' : scope === 'typecheck' ? 'typecheck' : `build:${scope}`
|
||||
const budget = defaultParallelism()
|
||||
const workers = (gates: readonly Gate[]) => ({
|
||||
workers: Math.min(gates.length, available),
|
||||
source: `${String(available)} available CPU(s)`,
|
||||
workers: Math.min(gates.length, budget),
|
||||
source: describeDefaultParallelism(),
|
||||
})
|
||||
if (scope === 'check' || scope === 'typecheck') {
|
||||
const gates = buildGates(root, scope)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { LOCAL_PARALLELISM_CAP, LOCAL_TEST_PARALLELISM_CAP, defaultParallelism, describeDefaultParallelism } from './parallelism.ts'
|
||||
|
||||
describe('default worker budget', () => {
|
||||
it('uses every reported CPU in CI', () => {
|
||||
expect(defaultParallelism({ CI: 'true' }, 256)).toBe(256)
|
||||
expect(describeDefaultParallelism({ CI: 'true' }, 16)).toBe('16 available CPU(s)')
|
||||
})
|
||||
|
||||
it('caps local runs without exceeding the reported CPUs', () => {
|
||||
expect(LOCAL_PARALLELISM_CAP).toBe(12)
|
||||
expect(defaultParallelism({}, 256)).toBe(12)
|
||||
expect(defaultParallelism({ CI: 'false' }, 256)).toBe(12)
|
||||
expect(defaultParallelism({}, 4)).toBe(4)
|
||||
expect(describeDefaultParallelism({}, 256)).toBe('12 local default worker(s) of 256 CPU(s)')
|
||||
})
|
||||
|
||||
it('lets Vitest forks use a larger local cap', () => {
|
||||
expect(LOCAL_TEST_PARALLELISM_CAP).toBe(48)
|
||||
expect(defaultParallelism({}, 256, LOCAL_TEST_PARALLELISM_CAP)).toBe(48)
|
||||
expect(defaultParallelism({ CI: 'true' }, 256, LOCAL_TEST_PARALLELISM_CAP)).toBe(256)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Default worker budget shared by repository gates, builds, linters, and Vitest configurations.
|
||||
*
|
||||
* In CI (`CI=true`) the budget is the CPU count Node reports for the runner. Elsewhere it is capped at
|
||||
* {@link LOCAL_PARALLELISM_CAP}, or {@link LOCAL_TEST_PARALLELISM_CAP} for Vitest forks, because
|
||||
* `os.availableParallelism()` reports host CPUs rather than a container's CPU quota and local runs share
|
||||
* the workstation. Each tool's explicit override
|
||||
* (`DSH_GATE_CONCURRENCY`, `DSH_OXLINT_THREADS`, `DSH_COVERAGE_MAX_WORKERS`, `--maxWorkers`) still wins.
|
||||
*/
|
||||
|
||||
import { availableParallelism } from 'node:os'
|
||||
|
||||
/** Largest default worker count outside CI. */
|
||||
export const LOCAL_PARALLELISM_CAP = 12
|
||||
|
||||
/** Largest default Vitest fork count outside CI, for unit and coverage runs. */
|
||||
export const LOCAL_TEST_PARALLELISM_CAP = 48
|
||||
|
||||
/**
|
||||
* Resolve the default worker budget for one process.
|
||||
* @param env - Environment whose `CI` value selects the CI budget.
|
||||
* @param available - CPU count Node reports for this process.
|
||||
* @param cap - Local cap; {@link LOCAL_TEST_PARALLELISM_CAP} for Vitest forks.
|
||||
* @returns `available` in CI, otherwise `available` capped at `cap`.
|
||||
*/
|
||||
export function defaultParallelism(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
available: number = availableParallelism(),
|
||||
cap: number = LOCAL_PARALLELISM_CAP,
|
||||
): number {
|
||||
return env.CI === 'true' ? available : Math.min(available, cap)
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe a default worker budget for scheduler and tool start lines.
|
||||
* @param env - Environment whose `CI` value selects the CI budget.
|
||||
* @param available - CPU count Node reports for this process.
|
||||
* @returns The budget and where it came from, for example `12 local default worker(s) of 256 CPU(s)`.
|
||||
*/
|
||||
export function describeDefaultParallelism(env: NodeJS.ProcessEnv = process.env, available: number = availableParallelism()): string {
|
||||
return env.CI === 'true'
|
||||
? `${String(available)} available CPU(s)`
|
||||
: `${String(defaultParallelism(env, available))} local default worker(s) of ${String(available)} CPU(s)`
|
||||
}
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
readFileSync,
|
||||
statSync,
|
||||
} from 'node:fs'
|
||||
import { availableParallelism } from 'node:os'
|
||||
import { dirname, posix, relative, resolve, sep } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { publint, type Message, type PackFile } from 'publint'
|
||||
import { formatMessage } from 'publint/utils'
|
||||
import ts from 'typescript'
|
||||
import { defaultParallelism } from './parallelism.ts'
|
||||
|
||||
const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY'
|
||||
const repositoryRoot = resolve(import.meta.dirname, '..')
|
||||
@@ -71,7 +71,7 @@ function publintConcurrency(total: number): number {
|
||||
return Math.min(total, parsed)
|
||||
}
|
||||
|
||||
return Math.min(total, availableParallelism())
|
||||
return Math.min(total, defaultParallelism())
|
||||
}
|
||||
|
||||
function publicationFiles(target: PackageTarget): PackFile[] {
|
||||
|
||||
@@ -165,17 +165,17 @@ function withNodeVersion<T>(version: string, action: () => T): T {
|
||||
|
||||
describe('CI worker allocation', () => {
|
||||
it.each([1, 2, 4, 8, 16, 64])('shares a %i CPU coverage budget without multiplying pools', (cpus) => {
|
||||
const env = ciWorkerEnvironment('ci-coverage', {}, cpus)
|
||||
const env = ciWorkerEnvironment('ci-coverage', { CI: 'true' }, cpus)
|
||||
const exempt = Math.max(1, Math.floor(cpus / 3))
|
||||
const instrumented = Number(env.DSH_COVERAGE_PARTITIONS ?? 1)
|
||||
expect(Number(env.DSH_COVERAGE_MAX_WORKERS)).toBe(cpus)
|
||||
expect(instrumented + exempt).toBe(Math.max(2, cpus))
|
||||
expect(defaultConcurrency('ci-coverage', 3, cpus).workers).toBe(Math.min(3, cpus))
|
||||
expect(defaultConcurrency('ci-coverage', 3, cpus, { CI: 'true' }).workers).toBe(Math.min(3, cpus))
|
||||
if (cpus <= 2) expect(env.DSH_COVERAGE_PARTITIONS).toBeUndefined()
|
||||
})
|
||||
|
||||
it('bounds overlapping readers and lets the isolated browser pool use the runner', () => {
|
||||
const env = ciWorkerEnvironment('ci-consumers', {}, 16)
|
||||
const env = ciWorkerEnvironment('ci-consumers', { CI: 'true' }, 16)
|
||||
expect(env).toMatchObject({
|
||||
DSH_OXLINT_THREADS: '8',
|
||||
DSH_PUBLINT_CONCURRENCY: '8',
|
||||
@@ -185,6 +185,20 @@ describe('CI worker allocation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('caps local defaults below the CPU count Node reports', () => {
|
||||
expect(ciWorkerEnvironment('ci-coverage', {}, 256)).toMatchObject({
|
||||
DSH_OXLINT_THREADS: '6',
|
||||
DSH_COVERAGE_MAX_WORKERS: '48',
|
||||
DSH_COVERAGE_PARTITIONS: '32',
|
||||
DSH_WEB_SNAPSHOT_WORKERS: '12',
|
||||
})
|
||||
expect(defaultConcurrency('ci-static', 60, 256, {})).toEqual({
|
||||
workers: 12,
|
||||
source: '12 local default worker(s) of 256 CPU(s)',
|
||||
})
|
||||
expect(defaultConcurrency('ci-static', 60, 8, {}).workers).toBe(8)
|
||||
})
|
||||
|
||||
it('preserves serial reference overrides on large hosts', () => {
|
||||
const inherited = {
|
||||
DSH_GATE_CONCURRENCY: '1',
|
||||
@@ -362,12 +376,16 @@ describe('gate graph validation', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('caps the local hygiene aggregate at four workers', () => {
|
||||
it('caps the local hygiene aggregate at eight workers', () => {
|
||||
const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id))
|
||||
|
||||
expect(defaultConcurrency('hygiene', ids.length, 8)).toEqual({
|
||||
workers: 4,
|
||||
source: '8 available CPU(s), hygiene cap 4',
|
||||
expect(defaultConcurrency('hygiene', ids.length, 16, { CI: 'true' })).toEqual({
|
||||
workers: 8,
|
||||
source: '16 available CPU(s), hygiene cap 8',
|
||||
})
|
||||
expect(defaultConcurrency('doc-sync', 43, 256, {})).toEqual({
|
||||
workers: 8,
|
||||
source: '12 local default worker(s) of 256 CPU(s), doc-sync cap 8',
|
||||
})
|
||||
})
|
||||
|
||||
@@ -911,7 +929,7 @@ describe('Node 24 lane ownership', () => {
|
||||
it('owns the build and orders its artifact consumers', () => {
|
||||
const subject = withPnpmEntrypoint(() => gatesForMode('ci-consumers'))
|
||||
|
||||
expect(defaultConcurrency('ci-consumers', subject.length, 4)).toEqual({
|
||||
expect(defaultConcurrency('ci-consumers', subject.length, 4, { CI: 'true' })).toEqual({
|
||||
workers: 4,
|
||||
source: '4 available CPU(s)',
|
||||
})
|
||||
|
||||
+22
-14
@@ -12,6 +12,7 @@ import { performance } from 'node:perf_hooks'
|
||||
import { CLIENT_BUILD_PROFILE_SELECTOR } from './client-build-environment.ts'
|
||||
import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './coverage-exempt.ts'
|
||||
import { COVERAGE_PARTITIONS_ENV, parseCoveragePartitionCount } from './coverage-partitions.ts'
|
||||
import { LOCAL_TEST_PARALLELISM_CAP, defaultParallelism, describeDefaultParallelism } from './parallelism.ts'
|
||||
import { pnpmInvocation } from './pnpm-invocation.ts'
|
||||
|
||||
/** A named aggregate exposed by the gate runner. */
|
||||
@@ -91,6 +92,9 @@ export interface ConcurrencyDefault {
|
||||
type GateExecutor = (gate: Gate, signal?: AbortSignal) => Promise<GateResult>
|
||||
type ResultObserver = (result: GateResult) => void
|
||||
|
||||
/** Worker cap of the local documentation and hygiene aggregates, whose leaves each build a full ts.Program. */
|
||||
const LOCAL_PROGRAM_MODE_CAP = 8
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
if (import.meta.main) {
|
||||
process.exitCode = await main(process.argv.slice(2))
|
||||
@@ -175,41 +179,43 @@ function parseMode(raw: string | undefined): Mode {
|
||||
* Resolve the default worker count for one aggregate.
|
||||
* @param selectedMode - aggregate whose resource posture applies.
|
||||
* @param total - number of gates in the aggregate.
|
||||
* @param available - host CPU availability for ordinary modes.
|
||||
* @param cpus - CPU count Node reports for this process.
|
||||
* @param env - environment whose `CI` value selects the CI or local [worker budget](./parallelism.ts).
|
||||
* @returns the default worker count and its diagnostic source.
|
||||
*/
|
||||
export function defaultConcurrency(
|
||||
selectedMode: Mode,
|
||||
total: number,
|
||||
available = availableParallelism(),
|
||||
cpus = availableParallelism(),
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): ConcurrencyDefault {
|
||||
const budget = defaultParallelism(env, cpus)
|
||||
const budgetSource = describeDefaultParallelism(env, cpus)
|
||||
// Local modes cap workers: several doc gates each build a full ts.Program,
|
||||
// so an uncapped default on a large host trades wall clock for memory blowups.
|
||||
const localCap = selectedMode === 'check-all'
|
||||
|| selectedMode === 'hygiene'
|
||||
|| selectedMode === 'doc-sync'
|
||||
|| selectedMode === 'doc-quick'
|
||||
const modeLimit = localCap ? Math.min(4, available) : available
|
||||
const modeLimit = localCap ? Math.min(LOCAL_PROGRAM_MODE_CAP, budget) : budget
|
||||
return {
|
||||
workers: Math.min(total, modeLimit),
|
||||
source: localCap
|
||||
? `${available} available CPU(s), ${selectedMode} cap 4`
|
||||
: `${available} available CPU(s)`,
|
||||
source: localCap ? `${budgetSource}, ${selectedMode} cap ${String(LOCAL_PROGRAM_MODE_CAP)}` : budgetSource,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill CI worker defaults from the CPU allocation without replacing explicit overrides.
|
||||
* Fill CI worker defaults from the [worker budget](./parallelism.ts) without replacing explicit overrides.
|
||||
* Coverage shares one budget; artifact readers share CPUs with sibling commands.
|
||||
* @param mode - aggregate selected by the caller.
|
||||
* @param env - inherited worker overrides, including serial reference settings.
|
||||
* @param available - CPUs available to this runner process.
|
||||
* @param env - inherited worker overrides, including serial reference settings, and the `CI` value that selects the budget.
|
||||
* @param cpus - CPU count Node reports for this runner process.
|
||||
* @returns environment additions for this aggregate and its children.
|
||||
*/
|
||||
export function ciWorkerEnvironment(
|
||||
mode: Mode,
|
||||
env: NodeJS.ProcessEnv,
|
||||
available = availableParallelism(),
|
||||
cpus = availableParallelism(),
|
||||
): Record<string, string> {
|
||||
// ci-unit's gates read none of these settings, while the inventory it runs
|
||||
// reads the same variables (run-gates.spec.ts builds coverage gates from
|
||||
@@ -217,18 +223,20 @@ export function ciWorkerEnvironment(
|
||||
// reads DSH_OXLINT_THREADS), so the aggregate leaves the environment as
|
||||
// `pnpm run test` finds it.
|
||||
if (!mode.startsWith('ci-') || mode === 'ci-unit') return {}
|
||||
const budget = defaultParallelism(env, cpus)
|
||||
const testBudget = defaultParallelism(env, cpus, LOCAL_TEST_PARALLELISM_CAP)
|
||||
const additions: Record<string, string> = {}
|
||||
const setDefault = (name: string, value: number): void => {
|
||||
if (env[name] === undefined || env[name] === '') additions[name] = String(value)
|
||||
}
|
||||
const shared = Math.max(1, Math.floor(available / 2))
|
||||
const shared = Math.max(1, Math.floor(budget / 2))
|
||||
setDefault('DSH_OXLINT_THREADS', shared)
|
||||
setDefault('DSH_PUBLINT_CONCURRENCY', shared)
|
||||
setDefault('DSH_SNAPSHOT_MAX_WORKERS', 1)
|
||||
setDefault('DSH_SNAPSHOT_MAX_CONCURRENCY', shared)
|
||||
setDefault('DSH_WEB_SNAPSHOT_WORKERS', env.DSH_GATE_CONCURRENCY === '1' ? 1 : available)
|
||||
setDefault('DSH_COVERAGE_MAX_WORKERS', available)
|
||||
const coverageBudget = env.DSH_COVERAGE_MAX_WORKERS || String(available)
|
||||
setDefault('DSH_WEB_SNAPSHOT_WORKERS', env.DSH_GATE_CONCURRENCY === '1' ? 1 : budget)
|
||||
setDefault('DSH_COVERAGE_MAX_WORKERS', testBudget)
|
||||
const coverageBudget = env.DSH_COVERAGE_MAX_WORKERS || String(testBudget)
|
||||
const total = Number(coverageBudget)
|
||||
if (!Number.isSafeInteger(total) || total < 1) {
|
||||
throw new Error(`run-gates: DSH_COVERAGE_MAX_WORKERS must be a positive integer, got ${JSON.stringify(coverageBudget)}.`)
|
||||
|
||||
@@ -2,9 +2,24 @@ import { describe, expect, it } from 'vitest'
|
||||
import { resolveOxlintInvocation } from './run-oxlint.ts'
|
||||
|
||||
describe('Oxlint invocation', () => {
|
||||
it('preserves the ordinary default invocation', () => {
|
||||
expect(resolveOxlintInvocation(['.'], { PATH: '/bin' })).toEqual({
|
||||
args: ['.'],
|
||||
it('keeps Oxlint\'s own worker default in CI', () => {
|
||||
expect(resolveOxlintInvocation(['.'], { CI: 'true', PATH: '/bin' }, 256)).toEqual({
|
||||
args: ['.', '--format=default'],
|
||||
env: { CI: 'true', PATH: '/bin' },
|
||||
})
|
||||
})
|
||||
|
||||
it('bounds local invocations by the local worker budget', () => {
|
||||
expect(resolveOxlintInvocation(['.'], { PATH: '/bin' }, 256)).toEqual({
|
||||
args: ['.', '--threads=12'],
|
||||
env: { PATH: '/bin', GOMAXPROCS: '12' },
|
||||
})
|
||||
expect(resolveOxlintInvocation(['.'], {}, 4).args).toEqual(['.', '--threads=4'])
|
||||
})
|
||||
|
||||
it('passes a caller\'s own worker bound through without the environment setting', () => {
|
||||
expect(resolveOxlintInvocation(['.', '--threads=2'], { PATH: '/bin' }, 256)).toEqual({
|
||||
args: ['.', '--threads=2'],
|
||||
env: { PATH: '/bin' },
|
||||
})
|
||||
})
|
||||
|
||||
+22
-8
@@ -1,6 +1,8 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { availableParallelism } from 'node:os'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defaultParallelism } from './parallelism.ts'
|
||||
|
||||
const oxlintCli = fileURLToPath(new URL('../node_modules/oxlint/bin/oxlint', import.meta.url))
|
||||
const MAX_CAPTURED_OUTPUT_BYTES = 64 * 1024 * 1024
|
||||
@@ -26,25 +28,37 @@ export interface OxlintInvocation {
|
||||
|
||||
/**
|
||||
* Apply the repository worker bound to both Oxlint backends.
|
||||
* `DSH_OXLINT_THREADS` selects the bound; without it, CI keeps Oxlint's own default and other runs use the
|
||||
* [local worker budget](./parallelism.ts). A caller's own `--threads` without `DSH_OXLINT_THREADS` passes through.
|
||||
* @param args - Oxlint CLI arguments requested by the caller.
|
||||
* @param env - Environment inherited by the Oxlint process.
|
||||
* @param cpus - CPU count Node reports for this process.
|
||||
* @returns the complete CLI arguments and child environment.
|
||||
*/
|
||||
export function resolveOxlintInvocation(args: readonly string[], env: NodeJS.ProcessEnv): OxlintInvocation {
|
||||
export function resolveOxlintInvocation(
|
||||
args: readonly string[],
|
||||
env: NodeJS.ProcessEnv,
|
||||
cpus: number = availableParallelism(),
|
||||
): OxlintInvocation {
|
||||
const resolvedArgs = [...args]
|
||||
if (env.CI === 'true' && !hasOutputFormat(args)) resolvedArgs.push('--format=default')
|
||||
const raw = env.DSH_OXLINT_THREADS
|
||||
if (raw === undefined || raw === '') return { args: resolvedArgs, env: { ...env } }
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
|
||||
throw new Error(`run-oxlint: DSH_OXLINT_THREADS must be a positive integer, got ${JSON.stringify(raw)}.`)
|
||||
const explicit = raw !== undefined && raw !== ''
|
||||
if (explicit) {
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
|
||||
throw new Error(`run-oxlint: DSH_OXLINT_THREADS must be a positive integer, got ${JSON.stringify(raw)}.`)
|
||||
}
|
||||
}
|
||||
if (args.some(arg => arg === '--threads' || arg.startsWith('--threads='))) {
|
||||
throw new Error('run-oxlint: use DSH_OXLINT_THREADS instead of passing --threads directly.')
|
||||
if (explicit) throw new Error('run-oxlint: use DSH_OXLINT_THREADS instead of passing --threads directly.')
|
||||
return { args: resolvedArgs, env: { ...env } }
|
||||
}
|
||||
if (!explicit && env.CI === 'true') return { args: resolvedArgs, env: { ...env } }
|
||||
const threads = explicit ? raw : String(defaultParallelism(env, cpus))
|
||||
return {
|
||||
args: [...resolvedArgs, `--threads=${raw}`],
|
||||
env: { ...env, GOMAXPROCS: raw },
|
||||
args: [...resolvedArgs, `--threads=${threads}`],
|
||||
env: { ...env, GOMAXPROCS: threads },
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { defineConfig } from 'vitest/config'
|
||||
import { standardDecoratorPlugin, vitestExecArgv } from './vitest.shared.ts'
|
||||
import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './scripts/coverage-exempt.ts'
|
||||
import { COVERAGE_PARTITION_MODE_ENV, COVERAGE_TEST_TIMEOUT_ENV, coverageTestTimeoutOptions } from './scripts/coverage-partitions.ts'
|
||||
import { LOCAL_TEST_PARALLELISM_CAP, defaultParallelism } from './scripts/parallelism.ts'
|
||||
|
||||
// Prints exact `path:line:col` records for every uncovered statement, branch
|
||||
// path, and function when a file misses the per-file 100% gate — the built-in
|
||||
@@ -151,6 +152,8 @@ const coveragePartitionMode = coveragePartitionRaw === '1'
|
||||
// project below because CLI flags do not reach them (coverageTestTimeoutOptions
|
||||
// owns the rule and its reach).
|
||||
const laneTestBudget = coverageTestTimeoutOptions(process.env[COVERAGE_TEST_TIMEOUT_ENV])
|
||||
// Outside CI, forks follow the local worker budget; CI keeps Vitest's own default for its runner.
|
||||
const localWorkerBudget = process.env.CI === 'true' ? {} : { maxWorkers: defaultParallelism(process.env, undefined, LOCAL_TEST_PARALLELISM_CAP) }
|
||||
|
||||
// These suites exercise process-global state, process APIs, or timing-sensitive process I/O
|
||||
// that worker threads cannot isolate reliably under aggregate gate contention.
|
||||
@@ -176,6 +179,7 @@ export default defineConfig({
|
||||
plugins: [pathsPlugin(), standardDecoratorPlugin()],
|
||||
resolve: { alias: claudeCodeTestingAliases },
|
||||
test: {
|
||||
...localWorkerBudget,
|
||||
setupFiles: ['./scripts/test-proxy-environment.ts', './scripts/test-dom-environment.ts'],
|
||||
// .tsx: client component specs (jsdom via per-file @vitest-environment pragma).
|
||||
include: testIncludes,
|
||||
|
||||
Reference in New Issue
Block a user