GitNexus/gitnexus/test/unit/perf-sequencer.test.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

113 lines
4.4 KiB
TypeScript

import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { TestSpecification, Vitest } from 'vitest/node';
import PerfSequencer from '../helpers/perf-sequencer.js';
const roots: string[] = [];
afterEach(() => {
vi.unstubAllEnvs();
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
function fixture() {
const root = mkdtempSync(path.join(tmpdir(), 'gitnexus-shards-'));
roots.push(root);
mkdirSync(path.join(root, '.vitest/timings'), { recursive: true });
const specs = ['a', 'b', 'c', 'd'].map((name) => {
const moduleId = path.join(root, `${name}.test.ts`);
writeFileSync(moduleId, '// same size\n');
return {
moduleId,
project: { name: 'native', config: { fileParallelism: false } },
} as unknown as TestSpecification;
});
const history = (profile: string, weights: number[]) =>
writeFileSync(
path.join(root, '.vitest/timings', `${profile}.json`),
JSON.stringify(
Object.fromEntries(
specs.map((spec, i) => [`native:${path.basename(spec.moduleId)}`, weights[i]]),
),
),
);
const split = async () =>
Promise.all(
[1, 2].map(async (index) => {
const ctx = { config: { root, shard: { index, count: 2 } } } as Vitest;
return new PerfSequencer(ctx).shard(specs);
}),
);
return { root, specs, history, split };
}
describe('timing-based CI sharding', () => {
it('learns from measured durations instead of putting both slow files on one shard', async () => {
const { specs, history, split } = fixture();
history('coverage', [10000, 10, 9000, 10]);
const bins = await split();
expect(bins.some((bin) => bin.includes(specs[0]) && bin.includes(specs[2]))).toBe(false);
expect(
bins
.flat()
.map((spec) => spec.moduleId)
.sort(),
).toEqual(specs.map((spec) => spec.moduleId).sort());
});
it('keeps platform histories separate and gives independent runners identical partitions', async () => {
const { specs, history, split } = fixture();
history('windows-latest', [10000, 10, 9000, 10]);
history('macos-latest', [9000, 10000, 10, 10]);
vi.stubEnv('GITNEXUS_SHARD_PROFILE', 'windows-latest');
const windows = await split();
expect(await split()).toEqual(windows);
expect(windows.some((bin) => bin.includes(specs[0]) && bin.includes(specs[2]))).toBe(false);
vi.stubEnv('GITNEXUS_SHARD_PROFILE', 'macos-latest');
const mac = await split();
expect(mac.some((bin) => bin.includes(specs[0]) && bin.includes(specs[1]))).toBe(false);
expect(mac).not.toEqual(windows);
});
it('still schedules new files and cold starts as complete disjoint partitions', async () => {
const { specs, history, split } = fixture();
const cold = await split();
expect(cold.map((bin) => bin.length)).toEqual([2, 2]);
history('coverage', [10000]);
const warm = await split();
expect(new Set(warm.flat()).size).toBe(specs.length);
expect(warm.flat().length).toBe(specs.length);
});
it('spreads the parallel cohort across all cold shards when sequential counts differ', async () => {
const { root } = fixture();
const specs = Array.from({ length: 1414 }, (_, i) => {
const moduleId = path.join(root, `${i}.test.ts`);
writeFileSync(moduleId, ' '.repeat(10 * 1024));
const sequential = i < 94;
return {
moduleId,
project: {
name: sequential ? 'native' : 'default',
config: { fileParallelism: !sequential },
},
} as unknown as TestSpecification;
});
const bins = await Promise.all(
Array.from({ length: 8 }, (_, i) => {
const ctx = { config: { root, shard: { index: i + 1, count: 8 } } } as Vitest;
return new PerfSequencer(ctx).shard(specs);
}),
);
const parallelCounts = bins.map(
(bin) => bin.filter((spec) => spec.project.name === 'default').length,
);
// A one-file difference in sequential work must not concentrate the entire
// parallel cohort on two runners (660 files each in the failing CI run).
expect(Math.min(...parallelCounts)).toBeGreaterThan(100);
expect(Math.max(...parallelCounts)).toBeLessThan(250);
expect(new Set(bins.flat()).size).toBe(specs.length);
expect(bins.flat()).toHaveLength(specs.length);
});
});