GitNexus/gitnexus/test/helpers/perf-sequencer.ts
Gergő Magyar 13d06d41c4
feat(ci): rebalance test shards from recorded timings (#3748)
* feat(ci): rebalance test shards from recorded timings

* fix(ci): preserve artifact retries for shard timings

Find shard-plan generation by its stable step ID and clarify the native runner cohort count (#3748).

Note: pre-existing reference-oracle parser type failures in two test_oracle_assets cases remain outside this change.

* fix(ci): keep retry timing snapshots immutable

Require the existing snapshot on reruns so a failed download cannot refresh history and change shard assignments. Cover the workflow contract and document recovery when the artifact is unavailable.

* docs(ci): document shard timing profile selection

* fix(ci): balance cold shard estimates consistently

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-28 11:58:29 +00:00

52 lines
2.3 KiB
TypeScript

import { statSync } from 'node:fs';
import path from 'node:path';
import { BaseSequencer, type TestSpecification } from 'vitest/node';
import { partition, readTimings } from 'vitest-shard-balancer';
import { shardTimingKey, shardTimingsFile } from '../../scripts/shard-timings.js';
/**
* Cost-balanced shard sequencer (wired via `sequence.sequencer` in
* vitest.config.ts).
*
* Uses the package's duration partitioner with the frozen CI timing snapshot.
* Only shard() changes; BaseSequencer keeps groupOrder and local sorting.
* On a cold start, spread sequential/native work using a generic size estimate
* instead of the count split that previously clustered the slowest suites.
*/
export default class PerfSequencer extends BaseSequencer {
override async shard(specs: TestSpecification[]): Promise<TestSpecification[]> {
const shard = this.ctx.config.shard;
if (!shard) return specs;
const root = this.ctx.config.root;
const keyed = new Map(
specs.map((spec) => [shardTimingKey(root, spec.moduleId, spec.project.name), spec]),
);
if (keyed.size !== specs.length) throw new Error('Duplicate test specification in shard input');
const keys = [...keyed.keys()];
const timings = readTimings(path.resolve(root, shardTimingsFile()));
let groups = partition(keys, timings, shard.count);
if (!groups) {
const estimates = Object.fromEntries(
[...keyed].map(([key, spec]) => {
let sizeKb = 0;
try {
sizeKb = statSync(spec.moduleId).size / 1024;
} catch {
/* Virtual test module. */
}
const config = spec.project.config;
const sequential = 'fileParallelism' in config && config.fileParallelism === false;
// Keep all cold weights on the same scale: 1,000 points represents
// 20 seconds of sequential work. A millisecond value here would
// dwarf file size and pile the parallel cohort onto fewer shards.
return [key, (sequential ? 1_000 : 1) + sizeKb];
}),
);
groups = partition(keys, estimates, shard.count);
}
const selected = new Set(groups?.[shard.index - 1] ?? []);
return specs.filter((spec) =>
selected.has(shardTimingKey(root, spec.moduleId, spec.project.name)),
);
}
}