| 1 | // @effect-diagnostics nodeBuiltinImport:off - vitest loads this sequencer from the config, |
| 2 | // outside any Effect runtime, and only needs path.relative. |
| 3 | import * as NodePath from "node:path"; |
| 4 | |
| 5 | import { BaseSequencer, type TestSpecification } from "vite-plus/test/node"; |
| 6 | |
| 7 | import shardWeights from "./shardWeights.json" with { type: "json" }; |
| 8 | |
| 9 | // What a file costs beyond its recorded test time: CI spends about 0.2s per |
| 10 | // server test file on imports and setup, which the recorded times leave out. |
| 11 | const FILE_OVERHEAD_SECONDS = 0.25; |
| 12 | |
| 13 | const recordedSeconds: Readonly<Record<string, number>> = shardWeights; |
| 14 | |
| 15 | /** |
| 16 | * Splits server test files across `--shard` runs by recorded duration. Vitest's |
| 17 | * default split hashes file paths into equal-count shards, which can put the |
| 18 | * slowest files on one runner. Here the longest file goes to the lightest shard |
| 19 | * first. Every shard derives the same split from the same file list, so each |
| 20 | * file still runs in exactly one shard. Files without a recorded time count as |
| 21 | * fast files. Record fresh times with `node scripts/update-test-shard-weights.ts`. |
| 22 | */ |
| 23 | export class WeightedShardSequencer extends BaseSequencer { |
| 24 | override async shard(files: TestSpecification[]) { |
| 25 | const { index, count } = this.ctx.config.shard ?? { index: 1, count: 1 }; |
| 26 | const weighted = files |
| 27 | .map((spec) => { |
| 28 | const key = NodePath.relative(this.ctx.config.root, spec.moduleId).replaceAll("\\", "/"); |
| 29 | return { spec, key, seconds: (recordedSeconds[key] ?? 0) + FILE_OVERHEAD_SECONDS }; |
| 30 | }) |
| 31 | .toSorted((a, b) => b.seconds - a.seconds || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); |
| 32 | |
| 33 | const loads = Array.from({ length: count }, () => 0); |
| 34 | const picked: TestSpecification[] = []; |
| 35 | for (const file of weighted) { |
| 36 | const least = Math.min(...loads); |
| 37 | const lightest = loads.indexOf(least); |
| 38 | loads[lightest] = least + file.seconds; |
| 39 | if (lightest === index - 1) picked.push(file.spec); |
| 40 | } |
| 41 | return picked; |
| 42 | } |
| 43 | } |