Files
MobileGL/docs/archive/notes/p7/link-ratchet.md
T

13 KiB
Raw Blame History

链接闭包棘轮(P7 wave 0,ID-P7-7)

脚本 scripts/link_ratchet.py,基线 scripts/data/link_ratchet_baseline.txt, CI 步骤在 .github/workflows/test.yml 的 build-linux-split 作业里。 被量的那件事见 a6-link-experiment.md 与 a6-link-experiment-data.md。

0 这道门归谁

契约 CONTRACT-P6.md §12.1 把 184 记成债并写下一句义务:「一道棘轮在 CI 上重算这 184 并断言它只减不增」。 路线图把它写成**「移交 P6.5」——P6.5 行从未接收**:P6.5 落地的是传输栈两轴、归档 v2、发布边界、半关闭与 AcceptPair/角色修复,没有一行是这道门。scripts/ci/ 下也确实没有任何脚本算这个数 (link_seam_purity.py 量的是源码文本里的所有权/派发纪律,p65_link_metrics.py 量的是帧日志)。

ID-P7-7 把它归 P7 wave 0,并定了两条:基线存符号列表而不是计数;--assert-monotone 只对新增符号变红,符号消失只提示重基线。第二条是这道门的全部意义—— 裸计数的棘轮在「清掉一个、又加一个」的那天是绿的,而那天正是它该红的那天。

1 怎么量

没有模块 target 可链(SOURCE_FILES 是一张平的源文件列表,同时喂 MobileGL 与 MobileGL_s; 建立模块边界本身是 P13 的具名工作),所以 a6 的办法是对单个 MobileGL target 的逐个 object 施加一个纸面上的分区,然后做集合运算:

(SERVER 未定义) − (SERVER 已定义) − (SHARED 已定义)  ∩  (FRONTEND 已定义)

这就是一次真链接会打印的那份未定义符号表。脚本照做,唯一的区别是那张分区表现在写在脚本里 (link_ratchet.py 的 PARTITION),一行一条路径前缀、一个角色、一句理由,最长前缀匹配。 它就是构建系统没有的那道模块边界,只不过写下来了。

两条纪律:

  • 任何 object 匹配不到 PARTITION 的任何一行 = 硬失败,不是静默忽略。 树会长,新目录会出现;一个悄悄被漏掉的 object 会让这个数字悄悄停止度量它声称度量的东西。
  • CMakeCache.txt 里三个选项不全为 ON = 硬失败(--no-require-flags 可关)。 选项为 OFF 时 MG_Remote 根本不编译,SERVER 集少掉整个传输半边,脚本会给出一个 很小、很绿、毫无意义的数。

nm 带 -C,所以集合运算的单位是解构后的名字——a6 发表的就是它,人也只读得懂它。 代价写在脚本的 docstring 里:基线树上 FRONTEND 集有 14974 个不同的 mangling 落在 14880 个不同的 解构拼写上,那 94 处重合几乎全是 C1/C2 构造与 D1/D2 析构对(按定义解构成同一串文本)。 解构棘轮是链接的一个可读的近似,不是链接。 要在 mangling 上较真的工具是 symbol_report.py。

只有 U 算引用;弱未定义(w)在无人定义时解析为 0,不构成对任何人的义务。 基线树上没有任何 w 触及前端定义,所以这条过滤今天不花钱。

2 今天的数字

基座 e8b2c4bd,build-split(Release,-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_PIPE_PUSH=ON),211 个 object:

分区 SERVER 52 / FRONTEND 71 / SHARED 88
SERVER 集未定义符号总数 1579
其中 SERVER + SHARED 自己满足不了的 593
其中由 FRONTEND 定义的 186

分桶(--bucket),与 a6 手做的那份逐个对照:

桶 符号数 a6 delta 含义
p6-core 11 6 +5 没有任何后端 object 引用它:传输/applier 自己的账
p7-magma 101 101 +0 只有 DirectVulkan 引用:P7 的账
p3b-p4b-espryt 14 14 +0 只有 DirectGLES 引用:P3b/P4b 的账
both-backends 60 60 +0 两个后端都引用:两边都迁完才掉
合计 186 184 +2

三个后端桶逐一精确复现 a6 手做的 101 / 14 / 60。 引用方与定义方的分布也逐一对上: UniformManager.cpp 90、VulkanRenderer.cpp 60、DirectGLES/Managers.cpp 53、DirectGLES.cpp 51; TextureObject.cpp 59、SamplerObject.cpp 30、BufferObject.cpp 18、FramebufferObject.cpp 16、 PipeFill.cpp 9。a6 的 §3.1 / §3.2 一个数都没差。

2.1 +2 的来源:两个符号,都是 P6.5 的

对 a6 发表的 184 逐符号取差集:新增 2 个,消失 0 个。

新增符号 引用方 是什么
MG_Remote::Client::ClientSession::StartSpawned() MG_Backend/Init.cpp.o P6.5 的 fork-per-session 启动路径
MG_State::GLState::ProgramArtifactsSchemaFingerprint() MG_Remote/CapsCodec.cpp.o P6.5 的归档 v2,caps 里带上 schema 指纹

SERVER 集本身也长了:a6 时 45 个 object,现在 52 个(Transport +5、Server +2,全是 P6.5)。 Magma 各 wave 改了 DirectVulkan 的内容但没有改它要的前端符号集合——这一点本身值得记一笔。

2.2 a6 自己的四个总数加不齐

a6 §3.2 / §5 发表的四个数 101 + 14 + 60 + 6 = 181,而它的头条是 184,差 3。 本脚本的四个桶是严格划分,恒等于头条。差额落在 p6-core:a6 §5 的表只列了它觉得有意思的 6 个,实际「没有任何后端引用」的是 9 个(184 − 175)。今天是 11 个(9 + §2.1 那 2 个)。

对得上的算法是 9 = 6 − 1 + 4:a6 §5 列的 6 个里有 1 个(MGPipeApplierIsUnbarrieredApply()) 今天已经不在 core 桶(见下);漏列的 4 个是 BackendObject_Remote::BackendObject_Remote()、 ClientSession::Start(...)、ClientSession::Stop()、ClientSessionInstance()—— 四个全由 MG_Backend/Init.cpp.o 引用,即后端工厂在挑 remote 后端时调 client 的会话对象。 a6 §5 的叙述(「server 不该问 client 要答案」)本来就该把它们算进去,只是表里没写。

另外 a6 §5 说 Server/PipeApplier.cpp 需要 MGPipeApplierIsUnbarrieredApply()。 今天它落在 p3b-p4b-espryt 桶里:引用它的只剩 DirectGLES/DirectGLES.cpp.o, MG_Remote/Server 已经不引用了。这是 P6.5 之后的真实变化,不是分类差异。

3 怎么读这张表

  • p7-magma 的 101 是出口门 4 的分母。 它只会因为 Magma 真的把功能迁到 server 侧而下降。
  • p6-core 的 11 不是后端的债,推给 P7/P3b/P4b 是错的。它的形状很清楚: 9 个是 server 侧在问 client 要答案(MG_Backend/Init.cpp.o 5 个、 MG_Pipe/PipeApply.cpp + Wire/PipeWireCodec.cpp 2 个、Server/PipeApplier.cpp 2 个), 1 个是需要搬到正确模块的解码助手(DecodeProgramArchive), 1 个是 P6.5 的 schema 指纹(ProgramArtifactsSchemaFingerprint)。
  • both-backends 的 60 是最硬的一块:两个后端都要,所以任何一边单独迁完它都不动。

3.1 给集成者的一条:p3b-p4b-espryt 的 14 个里有 5 个 P3b/P4b 清不掉

基线里带 # P13 标注的是由 MG_Impl/Pipe/SlotAllocator.cpp.o 定义的全部 7 个符号。 规则是机械的、可复核的:要清掉其中任何一个,都得让 slot allocator 不再住在 MG_Impl/ 下面—— 那是一次模块边界搬迁,契约 §12.1 与 a6 §6.4 都把它记在 P13 头上,任何后端迁移都够不到它。

这 7 个的落桶是 p3b-p4b-espryt 5 个 + both-backends 2 个:

符号 桶
MG_Pipe::MGPipeApplierIsUnbarrieredApply() p3b-p4b-espryt
MG_Pipe::MGPipeRefuseAllocatorFromApplyThread(char const*) p3b-p4b-espryt
MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread(char const*) p3b-p4b-espryt
MG_Pipe::MGPipeSlotAllocator::Acquire(...) p3b-p4b-espryt
MG_Pipe::MGPipeSlotAllocator::Free(...) p3b-p4b-espryt
MG_Pipe::MGPipeSlotAllocator::FindByLifetimeId(...) const both-backends
MG_Pipe::MGPipeSlots() both-backends

也就是说:p3b-p4b-espryt 这个桶的地板是 5,不是 0。 计划正文把「D12 的 14 个符号」写成 Tier 1(0.5 天)在棘轮落地后做—— 按今天的数据,其中 5 个不在 P3b/P4b 的能力范围内,那一行的估算应按 9 个重做。 标注只是给人看的,解析器忽略它,门的行为不受影响。

4 red-once(R-16)

在 ~/w7/p7-ratchet(e8b2c4bd,build-split)里往一个被分类为 SERVER 的 object 加一次前端触及,只重编那一个 object,跑门,然后还原。

注意选哪个符号。 计划书里举的例子 SamplerObject s(0); s.GetMinFilter(); 不会让门变红—— GetMinFilter() 已经在基线里(两个后端都在调它)。棘轮只对新增符号红, 所以负控必须挑一个今天不在那 186 个里的符号。挑的是 SamplerObject::GetBorderColorForm() const(在 SamplerObject.cpp.o 里是 T,外部调用, 不会被内联掉;今天没有任何 server object 引用它)。

加在 MG_Backend/DirectGLES/Utils.cpp(SERVER,espryt 族):

    unsigned P7RatchetRedOnceProbe() {
        MG_State::GLState::SamplerObject s(0);
        return static_cast<unsigned>(s.GetBorderColorForm());
    }
$ ninja -C build-split CMakeFiles/MobileGL.dir/MobileGL/MG_Backend/DirectGLES/Utils.cpp.o
$ python3 scripts/link_ratchet.py --build-dir build-split \
      --baseline scripts/data/link_ratchet_baseline.txt --assert-monotone

实测红线,逐字:

link-ratchet:   OF WHICH defined by FRONTEND: 187  (a6 measured 184)
link-ratchet: baseline scripts/data/link_ratchet_baseline.txt: 186 symbol(s), 7 annotated
link-ratchet: 1 symbol(s) are NEW frontend reach: a server object now needs a frontend definition the baseline did not record. This is what the gate exists for - either stop reaching, or land the reach with the reason in the commit and re-baseline deliberately:
link-ratchet:   NEW: MobileGL::MG_State::GLState::SamplerObject::GetBorderColorForm() const
link-ratchet:        defined by:  MobileGL/MG_State/GLState/SamplerState/SamplerObject.cpp.o
link-ratchet:        referred by: MobileGL/MG_Backend/DirectGLES/Utils.cpp.o
link-ratchet: FAIL --assert-monotone: 1 new frontend symbol(s) (named above)
$ echo $?
1

三项计数同步动了:1579 → 1580 未定义、593 → 594 未满足、186 → 187 前端定义。 还原并重编那一个 object 之后门重新为绿(ratchet: unchanged at 186 symbol(s),退出 0)。

这条红线的价值在第三行和第四行:它不是「数字变了」,它点名了哪个符号、 谁定义的、谁在够它。红的那一刻就知道该去看哪个文件。

5 日常用法

# 看今天的数和分桶
python3 scripts/link_ratchet.py --build-dir build-split --bucket

# 门(CI 就是这一行)
python3 scripts/link_ratchet.py --build-dir build-split \
    --baseline scripts/data/link_ratchet_baseline.txt --assert-monotone \
    --json link-ratchet.json

# 清掉符号之后,在同一个提交里重基线(标注会被带过去)
python3 scripts/link_ratchet.py --build-dir build-split \
    --baseline scripts/data/link_ratchet_baseline.txt \
    --write-baseline scripts/data/link_ratchet_baseline.txt

# 不需要构建
python3 scripts/link_ratchet.py --self-test

符号消失不是失败,脚本会把它们逐个列出来并要求在同一个提交里重基线。 符号新增是失败,脚本会对每一个打印它的定义方 object 与全部引用方 object, 所以红出来的那一刻就知道是谁在够谁。

6 没有验证的(读之前先读这一条)

基线是在参考构建 ~/w7/pipe/build-split 上生成的,不是在 CI 的机器上。 两边都是 clang + libstdc++(CI 装了 libc++-20-dev 但 CMakeLists.txt 与工作流都没有传 -stdlib=libc++,所以 Linux 上落的是 libstdc++;基线里的 std::__cxx11::basic_string 拼写 就是这么来的),配置三选项与 CMAKE_BUILD_TYPE=Release 也一致——但 clang 版本不同 (参考构建是 Arch 的 clang,CI 是 clang++-20)。

内联决策随版本变:某个方法在一边被内联、在另一边留成外部调用,就会让那个符号在 reach 集里 出现或消失。所以这道门在 CI 上的第一次运行有可能是红的,且那种红不是代码变坏。 处理办法是明确的,不要绕过门:看第一次运行上传的 link-ratchet.json artifact, 逐个核对 new_since_baseline / cleared_since_baseline,确认它们全都是内联差异 (同一个类、同一族方法、没有新的引用方 object),然后用一个单独的提交重基线并在提交信息里 写明是工具链差异。如果里面混着一个新的引用方 object,那就不是内联差异。

其余:

  • Android NDK 构建没试过,而且会不一样:NDK 用 libc++,解构拼写变成 std::__1::…, 整份基线都会算作新增。这道门今天只对 Linux 主机构建有意义。
  • MobileGL_s(同源的静态库 target)没量;--target 可以指过去,但没跑过。
  • 解构名重合(§1 那 94 处 C1/C2)今天不影响结果,但脚本没有为此加断言—— 要加的话应当是「reach 集里的每个解构名在 FRONTEND 侧只对应一个 mangling」。
  • check_flags 的「选项为 OFF」分支是手工用一个假的 CMakeCache.txt 验的, 不在 --self-test 覆盖范围内。