feat(FN-5002): complete Step 4 — implement best-fit shard planning and tests

Fusion-Task-Id: FN-5002
Fusion-Task-Lineage: 76c054fc-dbca-40dd-bd59-dd795946472b
This commit is contained in:
Fusion (runfusion.ai)
2026-05-18 01:34:52 -07:00
committed by gsxdsm
parent c87e0fc4f2
commit 6d48098417
2 changed files with 139 additions and 7 deletions

View File

@@ -78,7 +78,7 @@ test("planShardAssignments: trailing shards are empty when shard count exceeds p
assert.deepEqual(result[2], []);
});
test("planShardAssignments: greedy balancing keeps shard totals within 1", () => {
test("planShardAssignments: best-fit balancing keeps shard totals within 1", () => {
const packages = [
{ name: "a", testFileCount: 5 },
{ name: "b", testFileCount: 4 },
@@ -124,6 +124,96 @@ test("planShardAssignments: preserves total input weight via computeSplitPlan-de
assert.equal(assignedWeight, inputWeight);
});
test("planShardAssignments: FN-5002 regression fixture keeps 4-shard variance below 2%", () => {
const packages = [
{ name: "@fusion/dashboard", testFileCount: 606 },
{ name: "@fusion/engine", testFileCount: 365 },
{ name: "@fusion/core", testFileCount: 200 },
{ name: "@runfusion/fusion", testFileCount: 71 },
{ name: "filler-a", testFileCount: 39 },
{ name: "filler-b", testFileCount: 35 },
{ name: "filler-c", testFileCount: 31 },
{ name: "filler-d", testFileCount: 19 },
{ name: "filler-e", testFileCount: 18 },
{ name: "filler-f", testFileCount: 18 },
{ name: "filler-g", testFileCount: 17 },
{ name: "filler-h", testFileCount: 16 },
{ name: "filler-i", testFileCount: 15 },
{ name: "filler-j", testFileCount: 12 },
];
const shards = planShardAssignments(packages, 4);
const totals = shards.map((entries) => entries.reduce((sum, entry) => sum + entry.weight, 0));
const totalWeight = totals.reduce((sum, weight) => sum + weight, 0);
const perShardBudget = totalWeight / 4;
const varianceRatio = (Math.max(...totals) - Math.min(...totals)) / perShardBudget;
// FN-5002: this fixture previously peaked at 382 on one shard under lightest-shard placement.
assert.ok(varianceRatio < 0.02, `expected <2% variance but got ${(varianceRatio * 100).toFixed(2)}% (${totals.join("/")})`);
});
test("planShardAssignments: best-fit places unsplit large package on tightest under-budget shard", () => {
const packages = [
{ name: "anchor", testFileCount: 290 },
{ name: "preload", testFileCount: 220 },
{ name: "x-large-unsplit", testFileCount: 200 },
{ name: "near-budget", testFileCount: 170 },
{ name: "small", testFileCount: 40 },
{ name: "tiny", testFileCount: 30 },
];
const shards = planShardAssignments(packages, 3, { threshold: Number.POSITIVE_INFINITY });
const targetIndex = shards.findIndex((entries) => entries.some((entry) => entry.name === "x-large-unsplit"));
assert.equal(targetIndex, 2, "best-fit should place 200-weight package onto the empty shard");
});
test("planShardAssignments: uses minimum overshoot and deterministic tie-break when all candidates exceed budget", () => {
const packages = [
{ name: "gamma", testFileCount: 80 },
{ name: "beta", testFileCount: 70 },
{ name: "alpha", testFileCount: 60 },
{ name: "overshoot", testFileCount: 100 },
];
const first = planShardAssignments(packages, 2, { threshold: Number.POSITIVE_INFINITY });
const second = planShardAssignments(packages, 2, { threshold: Number.POSITIVE_INFINITY });
const overshootShard = first.findIndex((entries) => entries.some((entry) => entry.name === "overshoot"));
assert.equal(overshootShard, 0);
assert.deepEqual(first, second);
});
test("planShardAssignments: keeps split slices isolated across distinct shards", () => {
const packages = [
{ name: "split-me", testFileCount: 16 },
{ name: "small", testFileCount: 2 },
];
const shards = planShardAssignments(packages, 2, { threshold: 0.5 });
const splitSliceShardIndices = shards
.map((entries, shardIndex) => ({ entries, shardIndex }))
.filter(({ entries }) => entries.some((entry) => entry.name === "split-me"))
.map(({ shardIndex }) => shardIndex);
assert.deepEqual(splitSliceShardIndices, [0, 1]);
});
test("planShardAssignments: deterministic output for repeated calls", () => {
const packages = [
{ name: "p1", testFileCount: 41 },
{ name: "p2", testFileCount: 39 },
{ name: "p3", testFileCount: 27 },
{ name: "p4", testFileCount: 12 },
{ name: "p5", testFileCount: 8 },
];
assert.deepEqual(
planShardAssignments(packages, 3, { threshold: Number.POSITIVE_INFINITY }),
planShardAssignments(packages, 3, { threshold: Number.POSITIVE_INFINITY }),
);
});
test("selectShardPackages: returns the same shard assignment as planShardAssignments", () => {
const packages = [
{ name: "a", testFileCount: 5 },

View File

@@ -6,9 +6,9 @@
* Packages are weighted by discovered test-file count. Oversized packages are
* rewritten into virtual shard entries `{ name, shardIndex, shardCount }` so
* one package can execute across multiple CI shards via `vitest --shard`.
* The planner then greedily bin-packs weighted entries into the lightest shard
* while keeping slices of the same package on different shards whenever
* possible.
* The planner then uses a best-fit-decreasing strategy that packs each entry
* toward the per-shard budget (or minimizes overshoot when necessary), while
* keeping slices of the same package on different shards whenever possible.
*/
import { spawnSync } from "node:child_process";
@@ -127,6 +127,11 @@ export function computeSplitPlan(packages, total, options = {}) {
}
/**
* Best-fit-decreasing assignment (FN-5002): iterate entries in descending
* weight order and place each entry into the shard that is closest to the
* per-shard budget without exceeding it; if all candidates would exceed budget,
* choose the minimum overshoot shard. Split-slice isolation rules are preserved.
*
* @param {Array<{name:string, testFileCount:number}>} packages
* @param {number} total
* @param {{ threshold?: number }} [options]
@@ -134,6 +139,8 @@ export function computeSplitPlan(packages, total, options = {}) {
*/
export function planShardAssignments(packages, total, options = {}) {
const splitPlan = computeSplitPlan(packages, total, options);
const totalWeight = splitPlan.reduce((sum, entry) => sum + entry.weight, 0);
const perShardBudget = total > 0 ? totalWeight / total : 0;
const shardAssignments = Array.from({ length: total }, () => []);
const shardWeights = Array.from({ length: total }, () => 0);
const sorted = [...splitPlan].sort((a, b) => {
@@ -162,9 +169,44 @@ export function planShardAssignments(packages, total, options = {}) {
}
let targetIndex = candidates[0] ?? 0;
for (const index of candidates) {
if (shardWeights[index] < shardWeights[targetIndex]) {
targetIndex = index;
if (entry.shardCount) {
for (const index of candidates) {
if (shardWeights[index] < shardWeights[targetIndex]) {
targetIndex = index;
}
}
} else {
let bestUnderBudgetIndex = null;
let bestUnderBudgetProjected = Number.NEGATIVE_INFINITY;
let bestOvershootIndex = null;
let bestOvershootProjected = Number.POSITIVE_INFINITY;
for (const index of candidates) {
const projected = shardWeights[index] + entry.weight;
if (projected <= perShardBudget) {
if (
projected > bestUnderBudgetProjected ||
(projected === bestUnderBudgetProjected && (bestUnderBudgetIndex === null || index < bestUnderBudgetIndex))
) {
bestUnderBudgetIndex = index;
bestUnderBudgetProjected = projected;
}
continue;
}
if (
projected < bestOvershootProjected ||
(projected === bestOvershootProjected && (bestOvershootIndex === null || index < bestOvershootIndex))
) {
bestOvershootIndex = index;
bestOvershootProjected = projected;
}
}
if (bestUnderBudgetIndex !== null) {
targetIndex = bestUnderBudgetIndex;
} else if (bestOvershootIndex !== null) {
targetIndex = bestOvershootIndex;
}
}