Files
MobileGL/docs/Disaggregated/P5D-INPROC-PERFORMANCE.md
T
swung0x48 2fde70341d [Docs] (Disaggregated): P5d round 3 close-out - the report, the measurements section and the roadmap row
- P5D-INPROC-PERFORMANCE.md: the round-2 diagnosis is corrected against the
  symbolized call graph (the apply thread's mutex time was the idle poll, not
  the backend pendingMutex), the four round-3 packages and the two race fixes are
  described, the numbers table carries the two device caveats (monolith is
  120 Hz vsync-capped in most runs; the kernel ignores thread affinity, so the
  client thread runs on a mid core), and the lockstep-feasibility research says
  why the deferred draw barrier waits for P3b/P4b and P11.
- MEASUREMENTS.md section 9: the per-arm table with per-thread CPU ms/frame as
  the primary metric, the interleaved session and the wait/park counters.
- ROADMAP.md: the P5d row is the three-round shape; P11 gains the gPipeInputs
  versioning item the research points at.
- CURRENT_STAGE_PROGRESS.md, README.md, ARCHITECTURE.md (env table: the
  generate_mipmap exception to batched waits, the calibrated spin, the affinity
  readback): head 1f8de61b, the gate numbers and ruling ID-79.
2026-09-18 04:30:38 -04:00

91 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P5d 报告:inproc 性能专项(2026-09-18)
> 状态:**三轮,均在 `feat/disaggregated`**——一轮 `cb06538c`、二轮 `56a77348`、三轮 `1f8de61b`(本文所在提交的前一个)。
> 本报告回答三个问题:要做什么、做到了什么、什么没完成。数字表在 §"数字",三轮的设备记录在 `MEASUREMENTS.md` §9。
## 定位与目标
**插入点**:P5c 之后、P6 之前。路线图的既有纪律是"性能对着 pull 臂只记录、不作阻塞门,专门的优化阶段排在路线图推完之后"——但 P5c 收官后在真实负载上的实测把这件事提前了:FCL + Minecraft 26.3-rc-3、视距 12、Redmi `2f7cbe2e`,inproc 世界内只有 **7-13 fps**,monolith 同场景 **~116 fps**,约 9 倍差距,inproc 不可用。
**目标**:在不改变 wire 语义、不动正确性门(双车道、G1/G2/G5/G14、双生成器门)的前提下,把 `inproc` 与 `monolith` 的逐帧帧率差拉到尽可能接近 1。性能数字的采集纪律与其他阶段一致:CPU 定频(大核 1958400 / 小核 1555200)、风扇二档、只用 Redmi `2f7cbe2e`;A/B 切换靠 `/sdcard/FCL/mg_transport.txt` 与 `/sdcard/FCL/mg_env.txt`(`MOBILEGL_*` 逐行)两个运行时开关,免 APK 重编。
## 起点的瓶颈链(全部实测,非推测)
- persistent-map 推送:每个 verb 把每个映射的**全范围**推上线(实测 ~25 MB/帧,每 64 KB 块一条记录一次 barrier 往返)。
- 脏检查:每 64 KB 块做内容哈希,**每个 draw 对全部 arena 全范围扫**——XXH64/XXH3 一度占整个进程 73%(绑定过滤后仍 32.8%)。
- barrier:每条 verb 记录一次 `appliedSeq` 往返。
- `Doorbell::Wait` 的自旋每次迭代读一次时钟(该设备上 vDSO 的 `clock_gettime` 仍占进程 ~20%)。
## 做了什么
### 一轮 `cb06538c`(15 文件,+1100/-25)
- **persistent-map 推送改为三级臂,按 buffer 在 map 时挑选**:
- **mprotect 脏页追踪**(主臂):映射范围的页对齐内部注册 `PROT_READ`,应用写入触发 SIGSEGV,handler 在槽位位图上标脏页并放行,推送时只发含脏页的 64 KB 块、按连续段一次 mprotect 重新武装。归属判定三条,任一不满足即原样链给前一个 handler:只认内核 `SEGV_ACCERR`(用户态 raise 与未映射页不算)、必须在存活跟踪区间内、位图位鉴别重放(已置位页上的立即重 fault 只应答一次,再犯即非写访问,诚实崩溃)。对齐**向内收**——外向对齐的 edge 页含 malloc 元数据,信号被屏蔽的驱动/运行时 worker 线程 fault 即进程死亡(ctest 下 SmallRing 必崩、gdb 下必过的根因)。首推全量一次(与哈希臂 "fresh pushes everything once" 同规则)。
- **哈希抑制臂**(`MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS`,默认 1):未追踪 buffer 的逐块 XXH3 对比,只推变化块。
- **绑定过滤推送**(`PushDrawConsumers`):只推当前 draw/dispatch 实际消费的 buffer(VAO attribs/element、索引绑定点、indirect/param 槽、**TexBuffer 背存**——顺手修了高速飞行云的抽动,用户已确认)。
- **barrier 批处理**(`MOBILEGL_IPC_BATCH_WAITS`,默认 1;`MOBILEGL_PIPE_VERIFY` 强制 0):无 reply slot 的 kCtxState/kCtxCso/kCtxObject 值类记录发布即返回,barrier 推迟到下一个拉取类 verb——R-1 的围栏柱不动,只减少往返。两个连带问题被钉死而非口头论证:kCtxObject 类记录依赖同步语义的测试加显式围栏(GenerateMipmap、FramebufferDeath 两条);批处理下才可能出现的"apply 线程成为前端对象最后一个引用"新增**延期销毁队列**(入队 `(kind, lifetimeId)` POD,GL 线程在 verb 钩子重放七个 DestroyAndFree helper)——没有它 CopyImageLayered 两条必炸 `Fatal{RoleViolation, "MGPipeSlots"}`。
- **自旋时钟批处理**:`Doorbell::Wait` 的自旋改为每 64 次 yield 读一次时钟(clock_gettime 家族 ~20% → ~2%)。
### 二轮 `56a77348`(2 文件,+34/-11)
一轮上设备后发现 epoch skip 从未生效:负载里总有子页 persistent map,任何"未追踪"成员都否决跳过,绑定游走照样每 draw 跑(~8%),而直接扫所有未追踪成员的替代方案实测更差(41-43 vs 58-62 fps)。
- **零页注册**:子页范围不再落哈希臂,注册为"空内部 + hasEdges"的追踪槽(不保护任何页),全范围由两个有界的边块哈希服务——不再否决任何跳过。
- **shadow 页对齐分配**(`SHADOW_ALLOCATION_ALIGNMENT = 4096`;`GL_MIN_MAP_BUFFER_ALIGNMENT` 查询仍答 64):shadow 占有的每一页字节全是自己的,追踪器永远不必碰邻居分配的字节;边块与零页形态在真实负载中基本消失。monolith 共用该分配器,A/B 无影响。
### 三轮(`1f8de61b`,40 文件,+2474/-297):按符号化 profile 逐项切
二轮收官报告把 apply 线程 ~48% 的 mutex 时间归给后端 `pendingMutex`,那是猜的。对二轮头 `56a77348` 的 simpleperf 数据(cpu-cycles、整进程、世界内 15 s)符号化后,调用图说的是另一回事,三轮的四个包就按它切:
| 线程 | 热点(self %) | 真因 |
|---|---|---|
| apply | `mutex::lock/unlock` + `pthread_mutex_*` 合计 ~49%,`ApplyThreadMain` 直接调用者占 58-60% | **空转轮询**:`ready` lambda 里的 `ControlIsPending()` 与每圈的 `PumpControlRequest()` 各锁一次 `m_controlMutex`;真正的 `DrainRing` 只占该线程 29% |
| apply | `__emutls_get_address` 7.4%(monolith 渲染线程上 7.7%,是**第一热点**) | `MGPipeFrontendKeyedRegistryScope` / `MGPipeReverseAnnouncementScope` 的 `thread_local` 深度计数器,在 DirectGLES 约 20 个站点(含 `HandleOf` 内)每 draw 多次构造 |
| client | `WaitForApplied` 27.9%(含 34.2%) | R-1 lockstep:每个 draw 等 apply |
| client | persistent-map 推送含 22.5%:`XXH3` 8.5%、`PushBlocksForChecked` 5.6%、`MappedData/IsMapped/GetMappedRange` 2.2% | shadow 分配只对齐了基址、没把**尺寸**补到页倍数,几乎每个 buffer 的映射末尾都不在页界上——**每个 draw 对每个有边的存活映射哈希两块边** |
| client | 时钟读取 6.6%(`steady_clock::now` / `__kernel_clock_gettime` / `clock_gettime`) | `Doorbell::Wait` 每次等待至少读两次时钟;~900 次等待/帧 |
| client | `MGPipeValidateForVerb` 含 10.9%;`OnApplyThread()` 2.66% self;`MarkWritableImageBufferTextures` 3.6% | 残余填充每 verb 对 63 个字段算 7 项合取、每 draw 扫 192 个 image unit;角色守卫 `OnApplyThread` = 函数静态守卫 + 三个原子 + `pthread_equal` |
四个包(每包在自己的 worktree 实现、两视角审查、一轮修复;lockstep 可行性另有一份只读研究):
- **T 传输**:控制邮箱加原子影子位 `m_controlPosted`,空转轮询零锁(邮箱握手、C2 临界区、NOT_INITIALIZED 答复原样);`Doorbell::Wait` 的自旋改为一次性校准的迭代预算,稳态**不读时钟**(park 阶段才算 deadline);client/server 的 wait 与 park 计数进 `MGPipe stats:` 行的 `wait[srv= srvpark= cli= clipark=]` 与 wire ledger 的 `cliwait=/clipark=`。
- **D 角色守卫**:`OnApplyThread()` 内联为"一个 relaxed 原子 key 与调用线程的线程指针比较"(aarch64 走 `__builtin_thread_pointer()`,其余回落 `std::this_thread::get_id()`),删掉 `m_applyThreadId` 与函数静态守卫;三个 scope 深度计数器退掉 `thread_local`——它们唯一的读者是 apply 线程的守卫,所以只在 `OnApplyThread()` 为真时计数(单写者论证写在文件里,并有"GL 线程持有 scope 不豁免 apply 线程"的红一次);守卫链按代价重排。
- **B persistent map**:shadow 分配**尺寸**补到 4096 倍数,`TrackWriteMap` 在 shadow 页粒度可证时**向外**对齐(子页映射 = 一个受保护页),边块与边块哈希在稳态消失;向内臂只作回退。审查抓出一个一轮就存在、被边块哈希掩盖的洞:epoch skip 之前的绑定游走只推消费者,一个 fault 过但本 draw 不消费的映射会被标记为"已消费的 epoch"、之后永不推送——修法是游走前先排空每个有脏页的追踪成员(位图字扫描,稳态零成本),并在注册时推动 epoch 让新映射在 draw 路径上首推一次;`kernel page != 4 KB` 时诚实拒绝 mprotect 臂。
- **C 客户端每 verb**:image unit 高水位(push 构建独有的 `TextureState` 标记,`glBindImageTexture` 喂)替代 192 单元扫;残余填充的 7 项合取按其四个输入 memo 成位掩码;`MGPipeServerStampVerbBoundary` 的 63 字段 per-verb 推导变 constexpr 按 verb 类的掩码表;`ParsePoisonOmissionKnob` 的每 verb 字符串比较改尺寸快路径。
- **两处竞争修复(集成时)**:`generate_mipmap` 目录里是 kCtxObject,但它的 sink 是后端 `GenerateMipmap`,会读 `MGB_CTX->GetActiveTextureUnit()`——批处理下不等 apply,客户端紧接着的 validate 把 serial 与 server stamp 移走,apply 线程读到陈旧输入即 `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@ReadPixels"}`(F1 的两条 GenerateMipmap 用例在门禁树上复现)。现在它与四个 stamping verb 一样等待;规则写进代码:**只有 apply 不读残余填充任何字段的记录才可以免等**。`TextureDeathCrosses…` 测试补上 FramebufferDeath 已有的围栏(`object_death` 免等,计数只在 apply 时动)。
## 数字(VD12,同场景,世界内稳定 30 s 窗口,CPU 定频、风扇二档)
两个这台设备上不受控的变量,读表前先知道:(1) **monolith 的 fps 大多数运行被 120 Hz vsync 封顶在 ~115**(渲染线程只 66-75% 忙),偶有一次未封顶跑到 206(窗口内 110→280 摆动);inproc 各臂 client 线程 97-99% 忙、GPU 342 MHz 空转,是纯 CPU 受限,fps 可信。(2) 内核对 app 线程忽略 `sched_setaffinity` / `taskset`:split 下常驻自旋的 apply 线程占大核 cpu7,client GL 线程被放到中核(cpu2/4/5,1555 MHz),而 monolith 渲染线程在 cpu7。所以配对比较以**逐线程 CPU ms/帧**为主指标,fps 只作参考。GPU 的 `min_pwrlevel` 每次启动被厂商守护进程打回 12,未能钉住。
| 阶段 | split (inproc) | monolith | client / apply 逐线程 CPU ms/帧 | 关键 profile 行 |
|---|---|---|---|---|
| 起点 | 7-13 fps | ~116 | — | XXH64/3 73%→32.8%,clock_gettime ~20% |
| 一轮 `cb06538c` | 60-66 fps | 112-115 | — | WaitForApplied 11.6%,绑定游走 ~8% |
| 二轮 `56a77348`(当日复测 p50) | **64.3** | 116.5 | 15.03 / 12.42 | 见上表;client 线程被调度在中核(cpu2/4/5),apply 常驻大核 |
| 二轮 + `MOBILEGL_IPC_SPIN_US=2000` | 68-73 | — | 14.0 / 13.6 | park/unpark 是二阶成本;`SPIN_US=10000` 反而 63 |
| 三轮(审查前构建) | **114.0** | 115.7 | 8.68 / 6.93 | `spin2000` 113.0,不再有收益 |
| 三轮(审查后 + 竞争修复,本提交) | 103-106 | 115(封顶)/ 206(一次未封顶) | 9.2 / 7.0-7.4(client 仍在中核) | 复跑 106.1 / 103.3 / 101.9(交错三次) |
读法:inproc 从 64 → 103-114 fps(+60-77%),client 线程 CPU 从 15.0 → 8.7-9.2 ms/帧(−40%)、apply 线程从 12.4 → 7.0-7.4(其中大半仍是自旋等待)。对着封顶的 monolith(~115)比值 ~1:1.1;对着那次未封顶的 monolith(206,渲染线程 5.0 ms/帧、大核)比值 ~1:2。按 CPU/帧看:inproc 的 client 线程在中核上做 9.2 ms 的活,monolith 渲染线程在大核上做 5.0-6.2 ms——同核折算后 client 侧仍多约 1.2-1.5 倍工作,加上 lockstep 里每 draw 等 apply 的往返,这就是剩余差距的构成。画面与 monolith 无可见差异。
## 验证
- 三轮门禁(WSL `~/w7/p5d-gate`,split 构建):unit **2203/2203**(tcache_count=0 下)、`integration-split` **111/111**,两条曾 flaky 的用例 `--repeat until-fail:5` 全绿;一/二轮头上 unit 2188、split 111。
- `gen_pipe.py` / `gen_pipe_field_ownership.py` 的 --check/--self-test 绿,include 闭包 0 问题,dirty-surface rc=0,doc 引用 0 问题。
- 每处关键修复都有 red-once 形态的证据(一二轮:SmallRing SEGFAULT、GenerateMipmapFieldsCross exit 101、CopyImageLayered `Fatal{RoleViolation}`、余数块计数、117 MB/帧;三轮:每包报告的 red-once 表——控制影子位的 take-clear 窗口、校准自旋两条、`AThreadCreatedAfterTheLoopStoppedIsNotMistakenForTheApplyThread`、GL 线程 scope 不豁免 apply、子页映射一页保护、fault 过未绑定的映射仍在下一次消费前推送、image unit 高水位两端、残余 memo 重键)。
- 二分定位:四包分别单独上门禁树,`onlyT` 复现 GenerateMipmap abort、`onlyD` 复现 TextureDeath,`onlyB` 全绿——两条都是时序相关的既有竞争(见上),不是包的逻辑错误。
## 什么没完成
1. **R-1 序列化仍在**:每个 draw 仍等 `appliedSeq`。只读研究(`~/w7/notes/p5d/reports/E-lockstep-feasibility.md`)的结论是**不要在 P5d 做"draw barrier 延后一个 verb"**:inproc draw 路径上除 `GetBufferBindingSlot` 外每个对象类行都被 server 活读(texture unit 槽的借用指针、UBO 绑定点表、FBO 槽与附件、memo 未命中时的 VAO 属性、program 内部),且身份路径(五张前端键控 twin 表 + `HandleOfBuffer` 经客户端分配器)只能退役不能快照;`GetBufferBindingPoint` 一族根本没有 wire 形式(`set_shader_buffers` 要到 P4b)。真正的路径是 P3b/P4b(按句柄键控的 twin、`set_shader_buffers`)之后的 P11(`gPipeInputs` 版本化)——届时 barrier 延后只是把一处 `WaitForApplied` 搬到下一次 fill 之前。ROADMAP 的 P11 行还没把"`gPipeInputs` 版本化"写进去,本轮补上。
2. **线程放置**:这台内核对 app 线程的 `sched_setaffinity` / `taskset` 一律接受并静默忽略(`MOBILEGL_IPC_SERVER_AFFINITY=auto` 请求 0xc0、解析 0xff),root 下把 GL 线程 tid 写进 `/dev/cpuset/<x>/tasks`(cpus 6-7)也没有改变它的运行核(仍在 cpu5,115 fps / 8.6 ms/帧,在漂移之内);client GL 线程常被放在中核;无法从库内解决,记录。
3. **小项**:`SetHashSuppressor` 的 XXH3(P5 遗留的 set 记录去重哈希);`ReadDrawBindings` 每 draw 重解析 element buffer 句柄;`wants()` 门先读消费者再测脏位(R-8 计数规则未改);两个 `RunsAsTheServerRole` / `OnServerRole` 出线调用为分层保留。
4. **验证债**:79-trace 全集本机未跑;G1 由 CI 的 pull 符号门核(四包自报 0/0/0/0,PipeStats/PipeFill/TextureState 的改动全在 `MOBILEGL_PIPE_PUSH` 或 `MOBILEGL_BUILD_DISAGGREGATED` 之内);x86 32 位的 `MGHostSpan` 32B 断言与 `__builtin_thread_pointer` 的非 aarch64 回落只按检视;FCL 侧测试接线**未提交**(`settings.gradle.kts` 的 `:MobileGL` 指向、`FCLauncher.java` 的 renderer env 注入含 `mg_transport.txt` / `mg_env.txt` 开关、`MobileGL-disagg/build.gradle` 属性映射与 arm64 abiFilters、gradle wrapper 回退 8.13)。插件 APK 构建路径里的 `MOBILEGL_TRANSPORT=inproc` 行在打插件包前必须拿掉。
5. **平台可移植性备注**:mprotect+SIGSEGV 臂只覆盖 POSIX 且要求 4 KB 内核页(16 KB 页内核诚实回落哈希臂——是正确答案但是性能悬崖)。Windows 桌面若需要,应加 `GetWriteWatch` 实现臂而非 `#ifdef` 信号路径;哈希臂作为全平台兜底永久保留。
## 下一轮的入口
P5d 到此收官。剩余项按收益:P3b/P4b 的 twin 表句柄化(同时消掉 apply 线程剩余的 `HandleOf`/emutls 热点与 `HandleOfBuffer` 探针)→ P11 的 `gPipeInputs` 版本化 → barrier 延后。这些都是路线图既有阶段,不再单开性能轮。