Skip to content

ci: linux 加一条 llvm 腿,并给它一个装了系统 ffmpeg 的宿主机 - #184

Merged
Sunrisepeak merged 1 commit into
mainfrom
ci/llvm-leg-on-linux
Aug 8, 2026
Merged

ci: linux 加一条 llvm 腿,并给它一个装了系统 ffmpeg 的宿主机#184
Sunrisepeak merged 1 commit into
mainfrom
ci/llvm-leg-on-linux

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

#183 修的两个包在这份 CI 里一直是绿的,不是因为它们对,而是因为这份 CI 结构上看不见那类问题:

  • mcpp 在 linux 的默认工具链是 gcc,它通过 --sysroot 进 xlings subos 编译 —— 那里的 /usr/include 干干净净,宿主机的头根本不在搜索路径上。
  • 就算换了工具链,GitHub runner 的 /usr/include 里也没有 libav*,-idirafter 没有东西可输。

所以这条腿必须同时改两件事才有意义:换 llvm(无 sysroot),并真的把 ffmpeg 的 dev 头装上。只做前者,#183 那个 bug 照样照不出来。

矩阵

emit() 多一个 toolchain 维度,linux 发两次(default + llvm),macos / windows 保持 default。job 名只在非 default 时才带工具链后缀,所以既有 job 名不变。

成本是实打实的:linux 从 3 个 shard 变 6 个,而实测 linux runner 并发是 3 —— 第二条腿排在第一条后面跑,full run 的 linux 墙钟大致翻倍。要调的话杠杆在 shards_for 上面那段注释里。

三处必须跟着改的地方

改动 不改会怎样
registry / toolstore 缓存键加 matrix.toolchain 两条腿的 runner.os 都是 Linux,缓存装的是工具链和编译好的 compat 包 —— 共用一个条目会让 gcc 的产物替 llvm 回答,正好抹掉这条腿存在的理由
timings artifact 名加 toolchain upload-artifact@v4 拒绝重名,两条腿会抢 timings-linux-0,第二个直接把 job 弄失败
member-timings.tsv 只吃 default 腿 llvm 腿跑同一批成员,全 glob 进去会让每个 (platform, member) 出现两行(sort -u 留不住,秒数不同),而 shards_for 把匹配行全加起来 —— linux 工作量读成约两倍,shard 数被永久顶到上限

step summary 里则按分别列(linux-default / linux-llvm / …),两条腿是不同的构建,平均它们谁也不描述。

验证

本地在一台装了 ffmpeg 6.1.1 dev 头和 Catch2 v3 的机器上,llvm@22.1.8 取样跑了 19/60 个成员(C compat / header-only / 编译型 C++ / module 包 / 测试框架 / install() 钩子包各若干):

矩阵生成逻辑单独跑过:10 个 job(linux×2 腿×3 shard + macos×2 + windows×2),JSON 合法,step 顺序为 Download mcpp → 装头 → 选工具链 → 取成员 → 刷索引 → test。

已知会红 / 未覆盖

catch2-v2-main 在装了系统 Catch2 v3 的机器上失败:catch2_main.cpp__has_include(<catch2/catch_all.hpp>) 判 v2/v3,该探测同样落到系统目录。GitHub runner 不装 catch2,所以这条腿上它是绿的 —— 实测 #183 的 CI,linux / macos / windows 三平台 catch2-v2-main 全 ok。真要修得等 per-version build blocks(mcpp#290)。

其余 41 个成员(grpc / protobuf / godot / llamacpp / openssl 等重型)受本机磁盘所限没在 llvm 下跑过 —— 这条腿的第一轮就是它们第一次在 linux 上被 llvm 编译,可能还有别的既有问题被照出来。这是新增覆盖必然的第一轮代价,不是本 PR 引入的回归;但也意味着第一次跑很可能不是全绿。

刻意不加 continue-on-error:一条非阻塞的腿很容易被永久无视,和一条长期红的腿是同一种病。如果第一轮红得太散,更好的处理是逐个修或临时缩小成员集,而不是把腿静音。

#183 修的两个包(compat.ffmpeg 的 -idirafter 排在系统目录之后、compat.catch2
漏 #include <new>)在这份 CI 里一直是绿的,不是因为它们对,而是因为这份 CI
结构上看不见它们:

  * mcpp 在 linux 的默认工具链是 gcc,而它通过 --sysroot 进 xlings subos 编
    译 —— 那里的 /usr/include 干干净净,宿主机的头根本不在搜索路径上。
  * 就算换了工具链,GitHub runner 的 /usr/include 里也没有 libav*,-idirafter
    没有东西可输。

所以这条腿要同时改两件事才有意义:换 llvm(无 sysroot),并真的把 ffmpeg 的
dev 头装上。只做前者,本次这个 bug 照样照不出来。

## 矩阵

emit() 多一个 toolchain 维度,linux 发两次(default + llvm),macos / windows
保持 default。job 名只在非 default 时才带工具链后缀,所以既有 job 名不变。

成本是实打实的:linux 从 3 个 shard 变 6 个,而实测 linux runner 并发是 3,
所以第二条腿是排在第一条后面跑,full run 的 linux 墙钟大致翻倍。要调的话
杠杆在 shards_for 上面那段注释里。

## 三处必须跟着改的地方

  * registry / toolstore 缓存键加 matrix.toolchain。两条腿的 runner.os 都是
    Linux,而缓存装的是工具链和编译好的 compat 包 —— 共用一个条目会让 gcc
    的产物替 llvm 回答,正好抹掉这条腿存在的理由。
  * timings artifact 名加 toolchain。upload-artifact@v4 拒绝重名,不加的话两
    条腿会抢 `timings-linux-0`,第二个直接把 job 弄失败。
  * member-timings.tsv 只吃 default 腿。llvm 腿跑的是同一批成员,把每条腿都
    glob 进去会让每个 (platform, member) 出现两行(sort -u 留不住,秒数不
    同),而 shards_for 是把匹配行全加起来的 —— linux 的工作量会读成约两倍,
    shard 数被永久顶到上限。step summary 里则按腿分别列,两条腿是不同的构建,
    平均它们谁也不描述。

## 已知会红

catch2-v2-main 在装了系统 Catch2 v3 的机器上会失败:catch2_main.cpp 用
__has_include(<catch2/catch_all.hpp>) 判 v2/v3,这个探测同样会落到系统目录。
GitHub runner 不装 catch2,所以这条腿上它是绿的(实测 #183 的 CI:linux /
macos / windows 三平台 catch2-v2-main 全 ok)。真要修得等 per-version build
blocks(mcpp#290)。

本地只在 llvm 下取样跑了 19/60 个成员,其余 41 个(grpc / protobuf / godot /
llamacpp / openssl 等重型)受本机磁盘所限没跑 —— 这条腿的第一轮就是它们第一
次在 linux 上被 llvm 编译,可能还有别的既有问题被照出来。刻意不加
continue-on-error:一条非阻塞的腿很容易被永久无视,和一条长期红的腿是同一种病。
@Sunrisepeak
Sunrisepeak merged commit 13b1fe3 into main Aug 8, 2026
15 checks passed
Sunrisepeak added a commit that referenced this pull request Aug 8, 2026
`Plan the shards` 用平台的矩阵条目数当分片数:

    linux:$(jq -r '[.include[]|select(.platform=="linux")]|length' ...)

在 #184 之前这两个数恰好相等,所以一直是对的。#184 给 linux 加了第二条工具链
腿之后不再相等:平台发 2 x 3 = 6 个条目,而切分仍然是 3 路,每个条目带的
shard 是 0..2。于是 plan 按 6 路切,job 只消费 0/1/2 —— 分到 3/4/5 的成员
一个都没跑。

65 个成员实际只跑了 29 个,而且是**静默**的:一个从未被分配的成员,和一个跑
过并通过的成员,在 CI 界面上长得一模一样。丢掉的里面有 ffmpeg、opencv-module
及其两个 feature 成员、catch2-v2、catch2-main、openssl —— 正是那条新腿被加进
来要测的东西。#184 全绿,但它想验证的路径一次都没执行。

改成读 `.shards`,也就是 emit() 已经写进每个条目、job 自己也在用
(matrix.shards)的那个值。plan 和消费方从此读同一个数,而不是两个碰巧相等
的数。

macos / windows 不受影响也不需要改:它们仍是单腿,条目数正好等于分片数 ——
这正是这个 bug 只咬 linux 的原因,也是它能在 review 里活下来的原因。

验证:拿 #184 那次运行的真实 matrix.json 跑 jq,linux 6 → 3,macos / windows
维持 2;plan_shards 三片合计从 29 回到 65/65,上面点名的成员全部归位。
Sunrisepeak added a commit that referenced this pull request Aug 8, 2026
* ci: 刷新计时表并把 linux 分片上限抬到 4 —— 3 片已经装不下了

`linux default 0/3` 在 run 31266814148 被 90 分钟上限砍掉,成员全绿,死在
cache/artifact 的 post 步骤。查下来既不是冷缓存也不是 runner 争抢,是工作量
真的超了,而**陈旧的计时表把这件事盖住了**。

## 计时表漏了两个重家伙

`tests/member-timings.tsv` 停在 62 行 linux,而 workspace 已经有 67 个成员。
`plan_shards` 对表里没有的成员按**中位数**计价,于是:

    mysql-connector-cpp   实测 881s   被当成 ~60s
    libmysqlclient        实测 329s   被当成 ~60s

两个加起来 20 分钟的工作量,被当轻的塞进了已经扛着 grpc-codegen 的 shard 0。
这两个数是从 run 31266814148 的 `linux default 0/3` 步骤耗时里量的 —— 那一片
正好被砍,timings artifact 从没上传,而它们又正是导致溢出的成员。这个先有鸡还
是先有蛋只能手工破一次。表的其余部分来自 run 31260545520 的 member-timings
artifact(顺带补上 cli11 / cmdline / llmapi,并修正 curl 26→264s、
eui-neo-sdl2 119→591s)。

## 刷完表才看清:3 片本来就不够

刷新后 linux 总量 15891s = 265 分钟。按当前表模拟最慢分片:

    3 片   98 分   ← 超 90 分钟 job 上限
    4 片   74 分
    5 片   60 分

`shards_for` 的公式 `secs / 4200 + 1` 对这个总量算出来就是 4,一直被
`cap 3` 压回 3。所以这不是新问题被引入,是旧上限被工作量长过去了 —— 旧表让
总量看起来只有 13570s,刚好还压得住。

## 上限 3 -> 4

旧注释给这个上限的理由是"linux 实测并发 3,第 4 片会排队"。这个前提已经两头
失效:工作量长大了,而且自 #184 起 linux 每轮发 2 x N 个 job,任何 N 都会排队。
排队是对的取舍 —— 背靠背跑完的分片仍然完成,超过上限的分片不会。

代价:linux 每轮 8 个 job(原 6),模拟确认 `.shards` 读作 4、四片覆盖 67/67。

同时把那段容量注释改成现在为真的样子,并写明"表要保持新鲜"不是打扫卫生:
一个没计时的成员按中位数打包,一个重的新成员就会像轻的一样被塞进任何地方。

* ci: 改计时表不该触发全量运行

tests/member-timings.tsv 在 select 的 case 里一条都不匹配 —— 不是 tests/*.sh,
不是 tests/examples/*,不是 pkgs/*.lua,也不在忽略清单里 —— 于是落进
`*) full "unclassified change"`。

这张表决定活儿怎么在分片间分配,从不决定构建什么:没有任何成员的结果会因为
一个实测数字变了而改变。让它触发全量,是这份 workflow 里"花最大代价测试零
东西"的做法,而下一次真正的全量运行本来就会读到新数字。

代价写在注释里了:这样一来这个文件对 CI 不可见,坏行是静默的,而
plan_shards 对解析不了的东西按中位数计价 —— 正是刚刚撑爆一个分片的那个失效
模式。真要咬到就在 lint 里加校验。

* ci: 计时表只留每平台最重的 10 个,加测试不再需要动它

这张表之前列出每个成员,于是每加一个测试它就过期一次 —— 而这次撑爆分片的
正是"表里没有的成员按中位数计价":mysql-connector-cpp 实测 881s,被当成 60s。

但打包决策其实只由重的那几个做出。linux 上 top10 占总量的 69%,其余 57 个平
均 86s、中位 24s。把它们从表里拿掉,最慢分片一分钟都不动:

    完整表 67 行,4 片    74 / 64 / 63 / 63    最慢 74 分
    top10 表,4 片        74 / 73 / 60 / 58    最慢 74 分

## 兜底价必须是固定常数,不能再取中位数

直接缩表会当场炸:表里只剩 10 个重的,中位数就变成"第 5 重的那个",在 linux
上是 875s,于是 57 个小成员每个都按 875s 计价 ——

    top10 表 + 中位数兜底   90 / 48 / 81 / 46    最慢 90 分  ← 正好撞 job 上限

改成固定 90s(未计时成员的实测均值 86s 取整)。这个值不吃调参:兜底价从 30s
到 300s,最慢分片始终在 72–84 分之间,全都在 90 以下。这种不敏感正是"表可以
长期不动"的依据。

## shards_for 也得跟着改,否则片数会掉回去

它原本直接把表里的行加起来当工作量。表一缩,linux 总量从 15891s 读成 10965s
→ 3 片,而 3 片在这张表下最慢 107 分,直接超时。

给 plan_shards 加一个 `<platform> 0 0` 模式返回总量,shards_for 改调它。这样
"有哪些成员"和"未计时的算多少钱"只有一份定义,而不是 yaml 和 lua 各一份等着
漂移。估算精度:linux 16095s vs 实测 15891s,+1.3%。macOS / windows 的成员比
默认价便宜,总量偏高 —— 两者本来就顶着 cap 2,而且偏多分片是安全方向。

## timings job 同步产出 top10

否则下次刷新又胖回 197 行。

端到端复核:总量 16095s → 4 片,四片覆盖 67/67,最慢 74 分。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant