CI green on a loaded shared machine: browser suites (now also glyph-ligatures and feature-fit, through the shared launch helper) in one dedicated shard started first at concurrency 2; timeouts scaled by load ÷ cores (tests/helpers/load.ts, max(1-, 5-min average), capped at 5x, logged) for browser tests, waits, launch/close, the other shards' default and the desktop boot check; one retry for a browser test that timed out while the factor was above 2, listed as 'retried under load' in the CI summary, never for an assertion failure; the six-type App Privacy walk no longer dumps every step (the --debug-dump check walks two types), wait timeouts print per-line times; play-edit-lock checks reads in flight instead of wall time; tests/load-retry.test.ts

This commit is contained in:
CRTkafa 2026-10-14 05:21:15 +08:00
parent 21fa91afd1
commit 918146d4a6
13 changed files with 261 additions and 96 deletions

View file

@ -9,17 +9,24 @@ export const ROOT = join(import.meta.dir, '..');
/** Recorded per-file test durations (seconds), written after every run. Gitignored (node_modules/). */
export const TIMINGS = join(ROOT, 'node_modules/.cache/storebound-ci/test-timings.json');
/** Suites that drive a real Chromium (or a heavy child process): each gets a shard of its own, never another heavy
* suite next to it, so they don't fight for CPU and time out. */
/** Suites that drive a real Chromium (or a heavy child process). They all run in BROWSER_SHARDS dedicated shards,
* never next to the light tests, and those shards start first: eight browsers at once (one per shard) on a machine
* other sessions already load to 100+ made the walks time out. */
export const HEAVY = [
'tests/console-fill.test.ts', 'tests/site-overflow.test.ts', 'tests/local-fonts.test.ts', 'tests/glyphs.test.ts',
'tests/capture-web.test.ts', 'tests/console-session.test.ts', 'tests/console-persist.test.ts', 'tests/console-flow.test.ts',
'tests/glyph-ligatures.test.ts', 'tests/feature-fit.test.ts',
];
/** Browser tests of one file at the same time (bun test --max-concurrency for the shards with a HEAVY file; the
* tests opt in with testScope().concurrent, tests/helpers/browser.ts): enough to overlap their waits, few enough that
* a loaded machine does not stretch each one towards its timeout. */
export const BROWSER_CONCURRENCY = 4;
/** Browser tests of one file at the same time (bun test --max-concurrency for the browser shards; the tests opt in
* with testScope().concurrent, tests/helpers/browser.ts): enough to overlap their waits, few enough that a loaded
* machine does not stretch each one towards its timeout. */
export const BROWSER_CONCURRENCY = 2;
/** Shards for the HEAVY suites (STOREBOUND_CI_BROWSER_SHARDS=n overrides). */
export function browserShardCount(env = process.env): number {
const n = Number(env.STOREBOUND_CI_BROWSER_SHARDS);
return Number.isInteger(n) && n > 0 ? n : 1;
}
/** Changes that can affect every test: run the whole suite. */
export const GLOBAL = /^(package\.json|bun\.lock|bunfig\.toml|tsconfig\.json|tests\/helpers\/preload\.ts)$/;
@ -81,28 +88,29 @@ export function cost(file: string, timings: Record<string, number>, size: (f: st
}
/**
* Longest-processing-time-first split into at most n shards. Heavy files each open a shard of their own and never
* share it with another heavy file; light files fill the least loaded shard. Empty shards are dropped.
* Longest-processing-time-first split into at most n shards. The heavy files go to `browser` dedicated shards (first
* in the result, so they start first) and never share one with a light file while n leaves room; light files fill the
* least loaded of the others. Empty shards are dropped.
*/
export function balance(files: string[], n: number, costOf: (f: string) => number, heavy: string[] = HEAVY): string[][] {
const shards = Array.from({ length: Math.max(1, n) }, () => ({ files: [] as string[], load: 0, heavy: false }));
const sorted = [...files].sort((a, b) => costOf(b) - costOf(a) || a.localeCompare(b));
export function balance(files: string[], n: number, costOf: (f: string) => number, heavy: string[] = HEAVY, browser = 1): string[][] {
const isHeavy = (f: string) => heavy.includes(f);
for (const f of sorted.filter(isHeavy)) {
const free = shards.filter(s => !s.heavy);
const s = (free.length ? free : shards).reduce((a, b) => (b.load < a.load ? b : a));
s.files.push(f); s.load += costOf(f); s.heavy = true;
}
for (const f of sorted.filter(f => !isHeavy(f))) {
const s = shards.reduce((a, b) => (b.load < a.load ? b : a));
s.files.push(f); s.load += costOf(f);
}
return shards.filter(s => s.files.length).map(s => s.files);
const sorted = [...files].sort((a, b) => costOf(b) - costOf(a) || a.localeCompare(b));
const heavyFiles = sorted.filter(isHeavy), light = sorted.filter(f => !isHeavy(f));
const total = Math.max(1, n);
const nb = heavyFiles.length ? Math.min(Math.max(1, browser), total) : 0;
const mk = (k: number) => Array.from({ length: k }, () => ({ files: [] as string[], load: 0 }));
const lpt = (fs: string[], shards: ReturnType<typeof mk>) => {
for (const f of fs) { const s = shards.reduce((a, b) => (b.load < a.load ? b : a)); s.files.push(f); s.load += costOf(f); }
};
const heavyShards = mk(nb), lightShards = total > nb ? mk(total - nb) : heavyShards;
lpt(heavyFiles, heavyShards);
lpt(light, lightShards);
return [...heavyShards, ...(lightShards === heavyShards ? [] : lightShards)].filter(s => s.files.length).map(s => s.files);
}
export function planShards(files: string[], n = shardCount(), root = ROOT): string[][] {
const t = readTimings();
return balance(files, n, f => cost(f, t, p => { try { return statSync(join(root, p)).size; } catch { return 0; } }));
return balance(files, n, f => cost(f, t, p => { try { return statSync(join(root, p)).size; } catch { return 0; } }), HEAVY, browserShardCount());
}
// ---- which tests a change touches ----