Skip to content

Decide: Test Core 超 #4859 阈值的是 runner 争用,不是包的固有成本 —— 加分片 / 劈开套件 / 重定基线 #10227

Description

@os-elon

⚠️本卡正文于 2026-08-20 被整体改写。 初版建立在一条已被实测推翻的「结构性事实」上(见 §0)。原阻塞行 Blocked-by: #10152 已移除 —— 该测量已交付(PR #10258 + #10152 的报告评论),本卡现在可以回答。

承接自 #10149(测量卡,已收口)。

0. ⛔ 初版的核心论断是错的

初版写:@objectstack/cli 548.6s、@objectstack/spec 496.4s,各自单独超过 #4859 的 ≤7min(420s),因此「分片数、权重函数、装箱算法都动不了这个数」。

那两个数字是争用后的墙钟,不是包的成本。#10152 在一台空闲 4 核机器上单独跑同样的套件:

CI 墙钟(争用)单独跑(4 worker,空闲机)对 420s 线
@objectstack/cli548.6s337.13s达标
@objectstack/spec496.4s325.31s达标

像对像地比,没有任何一个包固有地超过阈值。 超过阈值的是「最多 4 个套件挤在一个 runner 上」这件事。

机制在 ci.yml 里写得很直白:

  • ci.yml:422pnpm turbo run test $FILTERS --concurrency=4
  • ci.yml:391 注释 — --concurrency=4: turbo's default (10) oversubscribes the 4-vCPU

4 个包的套件共享 4 个 vCPU,每个套件平均只拿到约一个核。六个包的 CI/本地耗时比散布在 0.69×–2.05× —— 争用系数因包而异,所以从 CI 日志读出的每包时长既不是成本,也不是可比的。

⚠️ 由此还废掉了 #10149 §6 路线 2(把 CI 实测时长当分片权重):那等于把「当前放置造成的争用」喂回决定放置的函数,是闭环不是测量。 已由 #10152 单独立卡 #10260

1. cli 的成本长什么样(已测,可作任何方案的输入)

137 文件 / 1498 测试。⚠️不是「测试多」:它的 tests-per-file 是六个包里最低的(10.9,对 13.7 / 16.3 / 25.7 / 26.6),所以按测试归一化,离群从 2.4–5.2× 放大到 3.6–11×

成本不在 import,也不在 setup:

cli 495.81s (transform 29.47s, setup 0ms, import 192.91s, tests 774.95s)
client 23.75s (transform 18.31s, setup 0ms, import 39.72s, tests 2.25s)
turso 38.50s (transform 16.85s, setup 0ms, import 65.98s, tests 3.56s)

cli 的 per-file import 成本 1.41s 是中位(低于 client 1.73s、turso 1.69s),transform 0.215s 是六个里最低

成本高度集中在测试体:中位文件 0.03s,137 个里 105 个低于 2s,前 20 个文件占 87.7% 的墙钟。那 20 个文件把真实 CLI 作为子进程拉起(tsx bin/run-dev.js 打到 mkdtemp 项目),占文件墙钟的 56.1%(300.1s),只承载 1498 个测试里的 177 个。

每次 spawn 的地板(只打印版本号):

tsx bin/run-dev.js --version 6.5-6.8s ← e2e 用的源码入口
node bin/run.js --version 2.9-3.2s ← 构建产物入口
node -e 0 0.031s ← 进程地板

即仓里那条「per-file 模块图重执行」的理论是对的,只是被搬到了 vitest worker 之外的冷进程里,transform 缓存与模块注册表都够不着。

packages/cli 内部没有可去除的部分:NODE_COMPILE_CACHE 实测在噪声内(成本是执行不是编译);maxWorkers 2→4 省 32% 墙钟但 CPU 持平(1291.3s → 1256.2s),那是装箱不是减负;把 spawn 换成构建产物入口能砍一半启动成本,但那正是 check-test-source-alias.mjs 存在来拒绝的 source-vs-dist 交易。

2. 三个选项

A —— 只把 speccli 劈到文件级(包内 vitest --shard,其余保持包级)

比初版credited 的更有余量:cli 最长的单个文件是 74.3s(2 worker),远在线下。代价不变 —— 在一个以「防止静默丢覆盖」为唯一职责的脚本里开手工豁免清单,该脚本头注释逐字论证过为什么不能全局这么做:

vitest 4 hard-fails every package with fewer test files than the shard count — and --passWithNoTests converts the failure into running NOTHING on either shard … a silent-coverage-loss machine, not an option.

B —— 时长权重 LPT + 把 #4859 阈值重定到 ~9–10min

本次测量之后,这条明显变差了。 那个 ~9–10min 反映的是 4 个套件挤 4 个核的结果,不是测试的成本。把验收线重定到那里,等于把一个 runner 装箱参数写进验收标准,而且它会随任何一次分片重排而漂移。B 的权重表输入还正好踩中 #10260 的闭环问题。

C(新增,初版没有这个选项)—— 攻击争用本身:提高分片数 / runner 分配

分片数上去,每个分片分到的 CPU 工作量下降,而每个分片各有自己的 4-vCPU runner —— 总核数随分片数线性增长。这条不新增任何声明面、不动权重函数、不碰 partition-test-shards.mjs 的语义。⚠️ 代价是并发槽位每分片固定开销(checkout + install + build,每片都要付一遍),而队列还会把这笔开销乘以投机构建数。

3. 四维决策分析

os-decision-facets

⚠️ 上面这行是本块的机器可寻标记,字面文本形式。技能模板的 HTML 注释拼写在首次写入时被 sanitizer 整行吞掉(写后回读发现),故改用字面文本 —— 技能对该标记的规定本就是「提取按字面文本 grep,不依赖注释形状」。⛔ 勿改回注释形式。

① 项目长远合理性 — C 只调一个已有参数,零新增声明面;A 在最不该开例外的脚本里开手工例外;B 把一个装箱参数固化进验收标准,是三者里唯一会让「验收线」这个概念本身贬值的。
② 实际业务拉动 — 真实付费的是队列延迟与驱逐放大。测量指出瓶颈是争用:桶已配平到 0.13%,而同一个套件单独跑就达标。C 直接打这一点;A 绕开它;B 承认它并把它写进标准。
③ 防 AI 犯错 — A 最差(手工豁免清单,漂移模式是静默绿,少跑测试而无人变红);B 次差(时长表过期则配平静默退化,且输入本身是 #10260 的闭环);C 无新增可漂移的声明。
④ 创业阶段不扩散 — C 改一个数字;A 新增一个必须随代码演进保持为真的人工契约;B 新增一张表加它的刷新路径。

推荐:C 先行,测完再看要不要 A。⛔ B 按当前论证不应采纳 —— 它把 runner 装箱写进验收线。

4. 置信缺口(本分析看不见什么)

  1. ⚠️337.13s 那个数字来自 dev 容器里的空闲 4 核机 + 热构建,不是 GitHub 的 4-vCPU runner。 像对像没有验证过 —— runner 的 I/O、冷缓存、以及 GitHub 虚拟核与物理核的差异都可能吃掉余量。这是 C 之前欠的第一个读数。
  2. 分片数的算术是跨环境推断,不是实测。 粗算:cli 的 CPU 工作量约 1291 CPU-s,4 核上的地板约 323s(5m23s),再加每分片固定开销(checkout/install/build)之后可能正好压在 420s 线上。所以 C 未必能干净地清过线,只是比初版认为的近得多。
  3. A 的收益上界仍未实测 —— 「劈开就能到线下」是从最长文件 74.3s 推的,没测过包内文件级分片的真实 makespan(worker 启动开销、包内并行度都要吃掉一部分)。
  4. ⚠️≤7min 这个数字本身仍然没有业务依据。 它来自 ci: merge queue 吞吐优化——Test Core / Dogfood 各 2→3 分片 + 队列失败自动分诊评论 #4859 那轮优化的验收段,不是从任何队列延迟目标反推的;没有任何记录写过「队列 p50 必须低于 X」。在选 A/B/C 之前,值得先确认这条线该不该是 7min —— 分析答不了,只有维护者能。

关联

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions