Skip to content

Commit 957eb68

Browse files
killaguclaude
andcommitted
perf(ci): promote fork-heavy plugins to own shards + weight-balanced rest
8-way run left only ubuntu rest-5 (redis+watcher) and rest-6 (security) over 60s — both bottlenecked by single fork-heavy plugin dirs riding in rest. - Promote security (→2 slices), redis, multipart out of rest into dedicated heavy shards (with --maxWorkers cap), so no rest shard carries a fork-plugin giant. - Replace rest index-interleave with deterministic greedy bin-packing by per-dir cpu weight, spreading the remaining heavy dirs (watcher, onerror, logrotator, tegg/plugin/tegg, core, logger) one-per-shard. rest now 6 bins of ~17.7 weight each. 22 test shard groups total. Windows failures remain absorbed by --retry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1fc515c commit 957eb68

2 files changed

Lines changed: 59 additions & 12 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,16 @@ jobs:
9292
- schedule-3
9393
- schedule-4
9494
- development
95+
- security-1
96+
- security-2
97+
- redis
98+
- multipart
9599
- rest-1
96100
- rest-2
97101
- rest-3
98102
- rest-4
99103
- rest-5
100104
- rest-6
101-
- rest-7
102-
- rest-8
103105

104106
name: Test (${{ matrix.os }}, ${{ matrix.node }}, ${{ matrix.shard }})
105107
runs-on: ${{ matrix.os }}

scripts/run-shard.js

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,40 @@ const HEAVY_SHARDS = {
5555
'schedule-4': { dir: 'plugins/schedule', shard: '4/4' },
5656
// development ~49s -> single shard is fine
5757
development: { dir: 'plugins/development' },
58+
// Fork-based plugins that dominated rest-* and pushed it >60s on ubuntu.
59+
// Pull them out of rest into their own (sliceable) shards.
60+
'security-1': { dir: 'plugins/security', shard: '1/2' },
61+
'security-2': { dir: 'plugins/security', shard: '2/2' },
62+
redis: { dir: 'plugins/redis' },
63+
multipart: { dir: 'plugins/multipart' },
5864
};
5965

6066
const HEAVY_DIRS = new Set(Object.values(HEAVY_SHARDS).map((v) => v.dir));
6167

62-
// `rest` is everything not in a heavy dir, interleaved into N light shards.
63-
// The rest tests are dominated by module transform/import (full egg + tegg
64-
// decorator stack per file), which barely shrinks with fewer files, so this
65-
// needs more slices than raw cpu-sum suggests to hit ≤60s on ubuntu.
66-
const REST_SHARD_COUNT = 8;
68+
// `rest` is every remaining project dir, balance-packed into N light shards by
69+
// approximate per-dir cpu weight (seconds; from a full local run). Greedy
70+
// largest-first bin-packing keeps the heaviest leftover dirs (onerror,
71+
// logrotator, watcher, tegg/plugin/tegg) on separate shards so no single shard
72+
// dominates. Unlisted dirs default to a small weight.
73+
const REST_SHARD_COUNT = 6;
74+
const REST_DIR_WEIGHTS = {
75+
'plugins/watcher': 9.7,
76+
'packages/core': 7.6,
77+
'plugins/logrotator': 7.7,
78+
'plugins/onerror': 7.3,
79+
'packages/logger': 6.9,
80+
'tegg/core/dynamic-inject': 6.8,
81+
'plugins/session': 5.3,
82+
'plugins/view-nunjucks': 3.2,
83+
'packages/utils': 2.8,
84+
'tools/create-egg': 2.7,
85+
'tegg/standalone/standalone': 2.3,
86+
'tegg/plugin/controller': 2.1,
87+
'packages/koa': 2.0,
88+
'plugins/view': 1.8,
89+
'tegg/plugin/tegg': 11.2,
90+
};
91+
const REST_DEFAULT_WEIGHT = 0.5;
6792

6893
// The vitest projects globs (see root vitest.config.ts):
6994
// packages/*, plugins/*, tools/create-egg, tegg/core/*, tegg/plugin/*, tegg/standalone/*
@@ -123,6 +148,28 @@ const baseArgs = [
123148
'20000',
124149
];
125150

151+
// Greedy largest-first bin-packing of the rest dirs into REST_SHARD_COUNT bins
152+
// by approximate cpu weight, so the heaviest leftover dirs land on separate
153+
// shards. Deterministic: sort by (weight desc, name) then assign each dir to
154+
// the currently-lightest bin.
155+
function packRestDirs() {
156+
const dirs = listProjectDirs().filter((d) => !HEAVY_DIRS.has(d));
157+
dirs.sort((a, b) => {
158+
const wa = REST_DIR_WEIGHTS[a] ?? REST_DEFAULT_WEIGHT;
159+
const wb = REST_DIR_WEIGHTS[b] ?? REST_DEFAULT_WEIGHT;
160+
return wb - wa || (a < b ? -1 : 1);
161+
});
162+
const bins = Array.from({ length: REST_SHARD_COUNT }, () => ({ total: 0, dirs: [] }));
163+
for (const d of dirs) {
164+
const w = REST_DIR_WEIGHTS[d] ?? REST_DEFAULT_WEIGHT;
165+
let min = bins[0];
166+
for (const b of bins) if (b.total < min.total) min = b;
167+
min.total += w;
168+
min.dirs.push(d);
169+
}
170+
return bins;
171+
}
172+
126173
const restMatch = /^rest-(\d+)$/.exec(shardName);
127174

128175
let targetArgs = [];
@@ -132,16 +179,14 @@ if (shardName === 'all') {
132179
// Single combined light shard (local fallback).
133180
targetArgs = listProjectDirs().filter((d) => !HEAVY_DIRS.has(d));
134181
} else if (restMatch) {
135-
// Interleave the remaining dirs into REST_SHARD_COUNT shards so the heavier
136-
// tegg dirs spread evenly across them. No --maxWorkers cap: fast in-process
137-
// tests benefit from full parallelism.
182+
// Weight-balanced bin for this rest shard. No --maxWorkers cap: fast
183+
// in-process tests benefit from full parallelism.
138184
const idx = Number(restMatch[1]) - 1;
139185
if (idx < 0 || idx >= REST_SHARD_COUNT) {
140186
console.error(`Unknown rest shard: ${shardName} (valid: ${restShardNames().join(', ')})`);
141187
process.exit(2);
142188
}
143-
const restDirs = listProjectDirs().filter((d) => !HEAVY_DIRS.has(d));
144-
targetArgs = restDirs.filter((_, i) => i % REST_SHARD_COUNT === idx);
189+
targetArgs = packRestDirs()[idx].dirs;
145190
} else if (HEAVY_SHARDS[shardName]) {
146191
const entry = HEAVY_SHARDS[shardName];
147192
targetArgs = [entry.dir, '--maxWorkers', HEAVY_MAX_WORKERS];

0 commit comments

Comments
 (0)