Files
openclaw/test/vitest-ui-e2e-config.test.ts
Peter Steinberger 733d8f994f perf(ci): shard Control UI E2E by measured duration, not source bytes
checks-ui-e2e now owns the CI wall: in run 32617320781 its seven slowest shards
ran 199-245s while the tallest compact node job was 182s. The sequencer already
does greedy longest-first bin packing -- it was just weighting by source bytes as
a "zero-state duration proxy", and that proxy is too weak. Across 247 suites
bytes correlate with wall time at r=0.79 and mispredict by 3.6x (0.20-0.73 s/KB),
so the packer filled one shard to 182s against a 130s ideal while another took
86s.

Hint the 30 slowest suites with measured seconds and keep the byte proxy for the
long tail, whose 3.5s median file is interchangeable. Replaying the real
per-file durations through the packer: widest shard 171s -> 137s. With ~63s of
per-job overhead that is roughly 245s -> 200s on the tallest ui-e2e job, at which
point checks-ui (199s) becomes the pole, so this is worth ~45s of wall and no
extra runners.

Durations are medians over runs 32617320781 and 32617488420, each normalized by
its own VM speed first; the two runs agreed within p10 0.88 / p90 1.17, so the
per-file numbers are stable rather than one noisy sample.

Unhinted files still rebalance automatically off source size, so new suites need
no table edit. A guard asserts every hint still points at a real suite, because a
renamed file would otherwise rot into a silent byte-proxy fallback.
2026-08-22 23:27:21 -07:00

88 lines
3.6 KiB
TypeScript

// Vitest UI E2E config tests protect complete, duration-balanced browser sharding.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { TestSpecification } from "vitest/node";
import uiE2eConfig from "./vitest/vitest.ui-e2e.config.ts";
import { UiE2eSequencer } from "./vitest/vitest.ui-e2e.sequencer.ts";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { force: true, recursive: true });
}
});
function requireTestConfig(config: unknown): {
sequence?: { sequencer?: unknown };
} {
if (!config || typeof config !== "object" || !("test" in config) || !config.test) {
throw new Error("expected UI E2E Vitest test config");
}
return config.test as { sequence?: { sequencer?: unknown } };
}
async function shardFiles(files: TestSpecification[], index: number, count: number) {
const sequencer = new UiE2eSequencer({ config: { shard: { count, index } } } as never);
return sequencer.shard(files);
}
describe("Control UI E2E Vitest sharding", () => {
it("uses the duration weighted sequencer", () => {
expect(requireTestConfig(uiE2eConfig).sequence?.sequencer).toBe(UiE2eSequencer);
});
it("covers every file once while balancing unhinted files by source bytes", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ui-e2e-shards-"));
tempDirs.push(tempDir);
const files = [600, 500, 400, 300, 200, 100].map((bytes, index) => {
const moduleId = path.join(tempDir, `suite-${index}.e2e.test.ts`);
fs.writeFileSync(moduleId, "x".repeat(bytes));
return { moduleId } as TestSpecification;
});
const shards = await Promise.all([1, 2, 3].map((index) => shardFiles(files, index, 3)));
const assignedFiles = shards.flat().map((file) => file.moduleId);
const assignedBytes = shards.map((shard) =>
shard.reduce((total, file) => total + fs.statSync(file.moduleId).size, 0),
);
expect(assignedFiles.toSorted()).toEqual(files.map((file) => file.moduleId).toSorted());
expect(new Set(assignedFiles).size).toBe(files.length);
expect(assignedBytes).toEqual([700, 700, 700]);
});
it("packs a slow hinted suite ahead of a much larger unhinted one", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ui-e2e-hints-"));
tempDirs.push(tempDir);
// chat-sidebar-panel-contract is hinted at 27s; the byte proxy would score
// this 1 KB file at well under a second and bury it behind the 40 KB suite.
const hinted = path.join(tempDir, "chat-sidebar-panel-contract.e2e.test.ts");
const large = path.join(tempDir, "unhinted-large.e2e.test.ts");
fs.writeFileSync(hinted, "x".repeat(1024));
fs.writeFileSync(large, "x".repeat(40 * 1024));
const files = [{ moduleId: hinted }, { moduleId: large }] as TestSpecification[];
const shards = await Promise.all([1, 2].map((index) => shardFiles(files, index, 2)));
expect(shards.map((shard) => shard.map((file) => file.moduleId))).toEqual([[hinted], [large]]);
});
it("keeps every duration hint pointed at a real Control UI E2E suite", () => {
const sequencerSource = fs.readFileSync(
path.join(import.meta.dirname, "vitest/vitest.ui-e2e.sequencer.ts"),
"utf8",
);
const hintedNames = [...sequencerSource.matchAll(/\["([^"]+\.e2e\.test\.ts)", \d+\]/g)].map(
(match) => match[1]!,
);
expect(hintedNames.length).toBeGreaterThan(0);
const missing = hintedNames.filter(
(name) => !fs.existsSync(path.join(import.meta.dirname, "..", "ui/src/e2e", name)),
);
expect(missing).toEqual([]);
});
});