perf(ci): run the two heaviest fixtures in distributed mode (#2825)

* perf(ci): run the two heaviest fixtures in distributed mode

* test(ci): pin distributed-mode fixtures to harness support
This commit is contained in:
James Russo
2026-07-26 22:42:12 -07:00
committed by GitHub
parent d8a8f8e044
commit 3a0590925c
4 changed files with 141 additions and 9 deletions
+3 -1
View File
@@ -113,15 +113,17 @@ jobs:
# concurrent exports cannot exhaust the Actions cache service. # concurrent exports cannot exhaust the Actions cache service.
cache-to: ${{ github.event_name == 'push' && 'type=gha,mode=max,scope=regression-test-image' || '' }} cache-to: ${{ github.event_name == 'push' && 'type=gha,mode=max,scope=regression-test-image' || '' }}
- name: "Run regression shard: ${{ matrix.shard }}" - name: "Run regression shard: ${{ matrix.shard }} (${{ matrix.mode }})"
run: | run: |
echo "Shard: ${{ matrix.shard }}" echo "Shard: ${{ matrix.shard }}"
echo "Mode: ${{ matrix.mode }}"
echo "Args: ${{ matrix.args }}" echo "Args: ${{ matrix.args }}"
docker run --rm \ docker run --rm \
--security-opt seccomp=unconfined \ --security-opt seccomp=unconfined \
--shm-size=4g \ --shm-size=4g \
-v ${{ github.workspace }}/packages/producer/tests:/app/packages/producer/tests \ -v ${{ github.workspace }}/packages/producer/tests:/app/packages/producer/tests \
hyperframes-producer:test \ hyperframes-producer:test \
--mode=${{ matrix.mode }} \
${{ matrix.args }} ${{ matrix.args }}
- name: Upload failure artifacts - name: Upload failure artifacts
@@ -31,6 +31,13 @@ const SCHEDULE_FILE = join(TESTS_DIR, "shard-schedule.json");
export const DEFAULT_SHARD_COUNT = 8; export const DEFAULT_SHARD_COUNT = 8;
/**
* Shards for fixtures run via `--mode=distributed-simulated`. Chunked renders
* are fast enough that one shard absorbs all of them; raise it in the schedule
* if that stops being true.
*/
export const DEFAULT_DISTRIBUTED_SHARD_COUNT = 1;
/** /**
* A fixture with no recorded timing still has to land somewhere. Assume it is * A fixture with no recorded timing still has to land somewhere. Assume it is
* on the expensive side so an unmeasured newcomer cannot quietly overload the * on the expensive side so an unmeasured newcomer cannot quietly overload the
@@ -100,7 +107,10 @@ export function planShards({
const schedule = JSON.parse(readFileSync(scheduleFile, "utf-8")); const schedule = JSON.parse(readFileSync(scheduleFile, "utf-8"));
const timings = schedule.timings ?? {}; const timings = schedule.timings ?? {};
const excluded = schedule.excluded ?? {}; const excluded = schedule.excluded ?? {};
const distributed = schedule.distributed ?? {};
const resolvedShardCount = shardCount ?? schedule.shardCount ?? DEFAULT_SHARD_COUNT; const resolvedShardCount = shardCount ?? schedule.shardCount ?? DEFAULT_SHARD_COUNT;
const resolvedDistributedShardCount =
schedule.distributedShardCount ?? DEFAULT_DISTRIBUTED_SHARD_COUNT;
const onDisk = discoverFixtures(testsDir); const onDisk = discoverFixtures(testsDir);
const onDiskSet = new Set(onDisk); const onDiskSet = new Set(onDisk);
@@ -139,17 +149,47 @@ export function planShards({
); );
} }
// A distributed fixture must also be scheduled — the mode says *how* to run
// it, not *whether*. Listing one that is excluded or absent is a typo, and a
// silent one, since the mode map is not consulted when building the shard set.
const misdeclared = Object.keys(distributed).filter((name) => !(name in timings));
if (misdeclared.length > 0) {
throw new Error(
`Fixtures marked "distributed" are not scheduled: ${misdeclared.join(", ")}.\n` +
`Add each to "timings" in ${scheduleFile}, or drop it from "distributed".`,
);
}
const scheduled = onDisk.filter((name) => !(name in excluded)); const scheduled = onDisk.filter((name) => !(name in excluded));
const bins = packShards(scheduled, timings, resolvedShardCount);
// Harness mode is a per-invocation flag, so a shard cannot mix modes. Pack
// each mode into its own shards rather than trying to interleave them.
const inProcess = scheduled.filter((name) => !(name in distributed));
const chunked = scheduled.filter((name) => name in distributed);
const bins = [
...packShards(inProcess, timings, resolvedShardCount).map((bin) => ({
...bin,
mode: "in-process",
})),
...(chunked.length > 0
? packShards(chunked, timings, resolvedDistributedShardCount).map((bin) => ({
...bin,
mode: "distributed-simulated",
}))
: []),
];
return { return {
include: bins.map((bin, index) => ({ include: bins.map((bin, index) => ({
shard: `shard-${index + 1}`, shard: `shard-${index + 1}`,
args: bin.fixtures.join(" "), args: bin.fixtures.join(" "),
mode: bin.mode,
})), })),
// Diagnostics for the workflow log — not consumed by the matrix. // Diagnostics for the workflow log — not consumed by the matrix.
plan: bins.map((bin, index) => ({ plan: bins.map((bin, index) => ({
shard: `shard-${index + 1}`, shard: `shard-${index + 1}`,
mode: bin.mode,
fixtures: bin.fixtures.length, fixtures: bin.fixtures.length,
estimatedMinutes: Math.round((bin.seconds / 60) * 10) / 10, estimatedMinutes: Math.round((bin.seconds / 60) * 10) / 10,
})), })),
@@ -171,7 +211,7 @@ function main() {
const worst = Math.max(...plan.map((row) => row.estimatedMinutes)); const worst = Math.max(...plan.map((row) => row.estimatedMinutes));
const best = Math.min(...plan.map((row) => row.estimatedMinutes)); const best = Math.min(...plan.map((row) => row.estimatedMinutes));
for (const row of plan) { for (const row of plan) {
console.log(`${row.shard}\t${row.fixtures} fixtures\t~${row.estimatedMinutes}m`); console.log(`${row.shard}\t${row.mode}\t${row.fixtures} fixtures\t~${row.estimatedMinutes}m`);
} }
console.log( console.log(
`\nworst shard ~${worst}m, lightest ~${best}m, spread ~${ `\nworst shard ~${worst}m, lightest ~${best}m, spread ~${
@@ -11,6 +11,7 @@ import { describe, expect, it } from "bun:test";
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { checkDistributedSupport } from "./regression-harness-distributed.js";
import { discoverTestSuites } from "./regression-harness.js"; import { discoverTestSuites } from "./regression-harness.js";
import { import {
discoverFixtures, discoverFixtures,
@@ -21,7 +22,11 @@ import {
const TESTS_DIR = join(import.meta.dir, "..", "tests"); const TESTS_DIR = join(import.meta.dir, "..", "tests");
function readSchedule(): { timings?: Record<string, number>; excluded?: Record<string, string> } { function readSchedule(): {
timings?: Record<string, number>;
excluded?: Record<string, string>;
distributed?: Record<string, string>;
} {
return JSON.parse(readFileSync(join(TESTS_DIR, "shard-schedule.json"), "utf-8")); return JSON.parse(readFileSync(join(TESTS_DIR, "shard-schedule.json"), "utf-8"));
} }
@@ -72,6 +77,85 @@ describe("shard planner fixture discovery", () => {
expect(() => planShards({ scheduleFile: tainted })).toThrow(/both scheduled and excluded/); expect(() => planShards({ scheduleFile: tainted })).toThrow(/both scheduled and excluded/);
}); });
it("never mixes harness modes within a shard", () => {
// `--mode` is a per-invocation flag, so a shard carrying both kinds would
// silently run half of them in the wrong mode.
const { include } = planShards();
const distributed = new Set(Object.keys(readSchedule().distributed ?? {}));
for (const row of include) {
const fixtures = row.args.split(" ");
const chunked = fixtures.filter((f) => distributed.has(f));
expect(chunked.length === 0 || chunked.length === fixtures.length).toBe(true);
expect(row.mode).toBe(chunked.length > 0 ? "distributed-simulated" : "in-process");
}
});
it("gives every shard a mode the harness accepts", () => {
// Guards against a typo reaching the workflow, where `--mode=<bad>` throws
// at parse time inside the container after the image has already built.
for (const row of planShards().include) {
expect(["in-process", "distributed-simulated", "lambda-local"]).toContain(row.mode);
}
});
it("rejects a distributed fixture that is not scheduled", () => {
const schedule = readSchedule();
const tainted = join(mkdtempSync(join(tmpdir(), "hf-shard-schedule-")), "shard-schedule.json");
writeFileSync(
tainted,
JSON.stringify({
...schedule,
distributed: { ...schedule.distributed, "not-a-real-fixture": "typo" },
}),
);
expect(() => planShards({ scheduleFile: tainted })).toThrow(/are not scheduled/);
});
it("only assigns distributed mode to fixtures the harness can actually run that way", () => {
// The blocking gap: `checkDistributedSupport` refuses HDR, non-integer fps,
// and fps outside {24,30,60}, and the harness records a refusal as
// `passed: true` with `skipped`. Skipping was safe while in-process also
// ran the fixture. It is not safe now — these fixtures run in distributed
// mode and nowhere else, so a later `hdr: true` or fps edit would turn
// their only coverage into a green no-op with every other planner
// invariant still passing. Membership and reason-text checks cannot see
// that; runtime support has to be part of the committed contract.
const suites = new Map(discoverTestSuites(TESTS_DIR, []).map((s) => [s.id, s]));
for (const fixture of Object.keys(readSchedule().distributed ?? {})) {
const suite = suites.get(fixture);
expect(
suite,
`${fixture} is marked distributed but the harness cannot load it`,
).toBeDefined();
const support = checkDistributedSupport(
(suite as { meta: { renderConfig: Parameters<typeof checkDistributedSupport>[0] } }).meta
.renderConfig,
);
expect(
support.supported,
`${fixture} is scheduled distributed-only but distributed mode refuses it: ` +
`${support.supported ? "" : support.reason}`,
).toBe(true);
}
});
it("would reject a distributed fixture the harness refuses to run chunked", () => {
// Proves the guard above has teeth rather than passing vacuously.
const hdr = checkDistributedSupport({ fps: { num: 30, den: 1 }, hdr: true });
expect(hdr.supported).toBe(false);
const ntsc = checkDistributedSupport({ fps: { num: 30000, den: 1001 } });
expect(ntsc.supported).toBe(false);
const odd = checkDistributedSupport({ fps: { num: 25, den: 1 } });
expect(odd.supported).toBe(false);
});
it("gives every distributed fixture a written reason", () => {
for (const [fixture, reason] of Object.entries(readSchedule().distributed ?? {})) {
expect(typeof reason, `${fixture} needs a reason`).toBe("string");
expect((reason as string).length, `${fixture} needs a real reason`).toBeGreaterThan(20);
}
});
it("gives every excluded fixture a written reason", () => { it("gives every excluded fixture a written reason", () => {
// Exclusions are how a fixture legitimately stays out of CI, so the bar // Exclusions are how a fixture legitimately stays out of CI, so the bar
// is that someone had to type why. This is what stops the excluded list // is that someone had to type why. This is what stops the excluded list
+10 -4
View File
@@ -3,12 +3,14 @@
"Drives the regression workflow's shard matrix via scripts/plan-regression-shards.mjs.", "Drives the regression workflow's shard matrix via scripts/plan-regression-shards.mjs.",
"Every fixture on disk must appear in exactly one of 'timings' or 'excluded', or CI fails.", "Every fixture on disk must appear in exactly one of 'timings' or 'excluded', or CI fails.",
"timings: per-fixture wall-clock seconds, used only for bin-packing \u2014 approximate is fine.", "timings: per-fixture wall-clock seconds, used only for bin-packing \u2014 approximate is fine.",
" Measured on CI run 30223148821 (green, post single-pass PSNR). A newly added fixture can", " Measured on CI run 30223148821; entries in 'distributed' carry their chunked timing.",
" carry a conservative estimate until the next refresh; over-estimating only costs balance.",
" Refresh from a green main run when shards drift apart; no test asserts these values.", " Refresh from a green main run when shards drift apart; no test asserts these values.",
"distributed: fixtures run with --mode=distributed-simulated, mapped to why.",
" Mode is a per-invocation flag, so these get their own shards.",
"excluded: fixtures deliberately not run, each mapped to the reason why." "excluded: fixtures deliberately not run, each mapped to the reason why."
], ],
"shardCount": 8, "shardCount": 8,
"distributedShardCount": 1,
"timings": { "timings": {
"animejs-adapter": 17, "animejs-adapter": 17,
"audio-mux-parity": 32, "audio-mux-parity": 32,
@@ -37,7 +39,7 @@
"style-11-prod": 324, "style-11-prod": 324,
"style-12-prod": 247, "style-12-prod": 247,
"style-13-prod": 898, "style-13-prod": 898,
"style-15-prod": 1413, "style-15-prod": 114,
"style-16-prod": 401, "style-16-prod": 401,
"style-17-prod": 224, "style-17-prod": 224,
"style-18-prod": 394, "style-18-prod": 394,
@@ -51,7 +53,7 @@
"style-9-prod": 216, "style-9-prod": 216,
"sub-comp-id-selector": 10, "sub-comp-id-selector": 10,
"sub-comp-t0": 18, "sub-comp-t0": 18,
"sub-composition-video": 1346, "sub-composition-video": 99,
"typegpu-adapter": 15, "typegpu-adapter": 15,
"variables-prod": 8, "variables-prod": 8,
"vfr-screen-recording": 7, "vfr-screen-recording": 7,
@@ -89,5 +91,9 @@
"video-hfid-no-id": "Was absent from the hand-written shard matrix, so it has never run in CI and its cost and pass state are unknown. Triage and either schedule it or record a real reason here.", "video-hfid-no-id": "Was absent from the hand-written shard matrix, so it has never run in CI and its cost and pass state are unknown. Triage and either schedule it or record a real reason here.",
"webgl-video-texture-render-compat": "Was absent from the hand-written shard matrix, so it has never run in CI and its cost and pass state are unknown. Triage and either schedule it or record a real reason here.", "webgl-video-texture-render-compat": "Was absent from the hand-written shard matrix, so it has never run in CI and its cost and pass state are unknown. Triage and either schedule it or record a real reason here.",
"wysiwyg-subcomp-css": "Rejected by the harness at load time (meta.json: maxAudioLagWindows must be >= 1), so it has never run despite looking scheduled-able. Fix the metadata, then schedule it." "wysiwyg-subcomp-css": "Rejected by the harness at load time (meta.json: maxAudioLagWindows must be >= 1), so it has never run despite looking scheduled-able. Fix the metadata, then schedule it."
},
"distributed": {
"style-15-prod": "Renders 12.4x faster chunked (1413s -> 114s at 4 CPUs) because in-process capture is pinned to one worker by the auto-worker derate. Passes the same golden at the same 30dB bar, 0/100 failed frames, psnr min 39.3 vs 40.1 in-process, so the sensitivity cost is 0.8dB.",
"sub-composition-video": "Renders 13.6x faster chunked (1346s -> 99s at 4 CPUs). Slow in-process for a different reason to style-15-prod (video decode rather than feTurbulence), which is why chunking helps both. Passes the same golden, 0/100 failed."
} }
} }