diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 35dc9fe8..ef2e8783 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -551,16 +551,19 @@ namespace MobileGL::MG_Config { // MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the // client waits for one. 1 = every create waits, which is the shape R-5 shipped with AND THE // DEFAULT, because the deferral was measured on the device and does not work: the reply pool - // is 8 slots and one load frame posts 59,673 replies (create 4,536 + respecify 20,633 + - // params 18,056 + sub-data 12,671 + the rest), so an answer is overwritten after 8 replies - - // 0.61 of a create - and the window's drain can never read one back. All 4,536 creates took + // is 8 slots and one load frame posts 55,903 replies (create 4,536 + respecify 20,633 + + // params 18,056 + sub-data 12,671 + 7), so an answer is overwritten after 8 replies - + // 0.65 of a create - and the window's drain can never read one back. All 4,536 creates took // the blocking path at 4 and at 1 alike (28.64 s both ways). Worse than inert: the // provisional accept latches an object the applier may have refused, and the un-latch that // corrects it lives in that same unreadable drain. WireTables.cpp's section comment has the // counters and the arithmetic, and the gates in RemoteClientControls.inc still pin the // mechanism, which is sound and simply unusable against this much other reply traffic. - // N > 1 is reachable for the experiment that would make it usable (a deeper pool, or a way - // to tell the server not to answer a fire-and-forget row). + // N > 1 is reachable for the experiment that would make it usable (a deeper pool, or a + // per-record "this answer will never be read" bit). NOTE THE PREDICATE: NOT + // "fire-and-forget" - the window's own creates ARE fire-and-forget and the drain reads + // them, so a flag keyed on wantReply would kill the very thing it is meant to enable + // (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md section 7). Uint32 CreateWindow = 1; // MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which // is the only tier P5 implements and the reason persistent-map-push can be non-zero diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 0784eb68..bd7db550 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -413,7 +413,7 @@ namespace MobileGL::MG_ConfigLoader { ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1); ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1); // The create window. DEFAULT 1 - OFF - because the deferral was measured on the device and - // cannot be collected (Config.h has the numbers, WireTables.cpp the arithmetic: 59,673 + // cannot be collected (Config.h has the numbers, WireTables.cpp the arithmetic: 55,903 // replies per frame into 8 pool slots). Capped at 4 rather than the pool's 8 for the reason // the mechanism is sized that way at all. ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 1, 1, 4); diff --git a/MobileGL/MG_Remote/CONTRACT-P5E.md b/MobileGL/MG_Remote/CONTRACT-P5E.md index a57de110..88a28021 100644 --- a/MobileGL/MG_Remote/CONTRACT-P5E.md +++ b/MobileGL/MG_Remote/CONTRACT-P5E.md @@ -231,9 +231,9 @@ handle", and it is what `MGPipeResourceRespecifyWantsItsReply` (`PipeRoute.h`) a does not hold. It was given a WINDOW instead of a rule - `MOBILEGL_IPC_CREATE_WINDOW`, answers deferred and collected in order, a late DECLINE un-latching the object - and **the window is OFF BY DEFAULT because the deferral was measured and cannot be collected**: the reply pool is - `kDefaultReplySlotCount` = 8 slots deep and one load frame posts 59,673 replies (the server + `kDefaultReplySlotCount` = 8 slots deep and one load frame posts 55,903 replies (the server answers every reply-owning record whether or not the client wants the answer), so an answer is - overwritten after 8 replies - 0.61 of a create. On the device all 4,536 creates took the blocking + overwritten after 8 replies - 0.65 of a create. On the device all 4,536 creates took the blocking path at window 4 and at window 1 alike (28.64 s both ways, against the handoff's 31.3 s pre-window baseline on the same device). `WireTables.cpp`'s section comment carries the counters and the arithmetic. The client-visible consequence is worth stating here: with the window ON, the diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index ee49d704..f2e130ce 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -347,13 +347,14 @@ namespace MobileGL::MG_Remote::Client { // slot is reused, and the pool is kDefaultReplySlotCount = 8 slots deep. The frame posts a // reply for EVERY reply-owning record - not only for the rows that wait, because the server // cannot know which those are (ClientSession.cpp:1897-1900: "The server posts either way") - // - and that is 59,673 replies in this one frame: + // - and that is 55,903 replies in this one frame (op 4 ResourceDestroy is NOT counted: its + // catalogue flags are kNone, so PostReply would Fatal on it and it never answers): // // create 4,536 + respecify 20,633 + params 18,056 + sub-data 12,671 + 3,770 + 6 + 1 // - // 8 slots against 59,673 replies is a pool that churns 7,459 times per frame, i.e. 13.2 + // 8 slots against 55,903 replies is a pool that churns 6,988 times per frame, i.e. 12.3 // replies between two creates: an answer posted at create N is overwritten after EIGHT - // replies, which is 0.61 of a create. There is no window depth for which the deferred + // replies, which is 0.65 of a create. There is no window depth for which the deferred // answer still exists when the client comes back for it - the deferral is not slow, it is // UNREADABLE. (A 1-deep "window" is the blocking path, which is what the row does.) // @@ -364,8 +365,13 @@ namespace MobileGL::MG_Remote::Client { // prevent. So the default is 1: the row is the pre-window shape unless someone sets // MOBILEGL_IPC_CREATE_WINDOW explicitly, and the machinery below is kept as the record of // what was tried and what it cost (a fix would have to make a deferred answer outlive - // 59,673 intervening replies, which means a deeper pool or a way for the client to tell - // the server "do not answer this one" - both are wire changes, not tuning). + // 12.3 intervening replies, which means a deeper pool, or NOT sending the ~51,000 answers + // nobody will read - and that second one needs its predicate stated exactly, because + // "fire-and-forget" is the WRONG one: the window's own creates are fire-and-forget and + // the drain above reads them, so a bit keyed on wantReply disables the mechanism it is + // meant to enable. The bit has to mean "this answer will never be read" - which is true + // of respecify/params/sub-data and false of a windowed create. See + // docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md section 7, reviewed). // // The gates that pin it are in MG_Test/Wire/RemoteClientControls.inc, and they still hold: // the window is a coherent mechanism, it is simply unusable on a frame with this much diff --git a/docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md b/docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md index 40b18c79..bce7b5f0 100644 --- a/docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md +++ b/docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md @@ -9,8 +9,8 @@ - **窗口开了等于没开**:同一台设备、同一帧、同一 101,421,396 B staged,`MOBILEGL_IPC_CREATE_WINDOW=4` 与 `=1` 的 create 行都是 **4,536 次等待**,墙钟都是 **28.64 s**(handoff 的 pre-window 基线是 31.3 s)。 临时计数器给出的形态是:`pushed=24 blocked=4,476 took=20 readfail=8,951 pending=4`——**drain 读不回来**。 -- **不是调参问题**:回复池 8 槽,而这一帧发出 **59,673 条带回复的记录**(服务端对每条 kReplySlot 记录都回, - 不管客户端要不要),所以一条答案在 **8 条回复**之后就被覆盖,等于 **0.61 个 create**。任何深度的窗口都读不回来。 +- **不是调参问题**:回复池 8 槽,而这一帧发出 **55,903 条带回复的记录**(服务端对每条 kReplySlot 记录都回, + 不管客户端要不要),所以一条答案在 **8 条回复**之后就被覆盖,等于 **0.65 个 create**。任何深度的窗口都读不回来。 - **顺带修掉一个真 bug**:`StreamLink::ReadReply` 只认「最新一条」(单槽),违背 `ILink::ReadReply(seq)` 的按 seq 读取语义。已改成一个 8 槽环 + 用例。 - **默认关掉**:`MOBILEGL_IPC_CREATE_WINDOW` 默认 1。窗口开着不只是白费,它会让**被拒绝的 create 留在「已发布」状态** @@ -41,25 +41,27 @@ P12diag create=4501 cfg=4 eff=4 suspects=0 refusals=0 pending=4 pushed=24 blocke `pending` 从第 500 条起**一直钉在 4**,`refusals=0`(一条答案都没读到过),`readfail` 约每条 create 涨 2 (非阻塞 poll + 阻塞 drain 各失败一次)。 -## 2. 根因:回复池 8 槽 vs 一帧 59,673 条回复 +## 2. 根因:回复池 8 槽 vs 一帧 55,903 条回复 答案只在**它的槽没被复用**之前可读(`ReplySlotPool`,`kDefaultReplySlotCount = 8`, -`Transport/ReplySlot.h:111`)。而这一帧发回复的记录数是: +`Transport/ReplySlot.h:111`)。而这一帧发回复的记录数是(**只数带 `kReplySlot` 的行**—— +`MG_Pipe/generated/PipeWire.inc:177-179` 的 flags 列,op 4 `ResourceDestroy` 是 `kNone`, +`PostReply` 对它直接 Fatal,从来不回;本文第一版把它的 3,770 条算了进来,审查纠正为下表): | 行 | 条数 | 为什么回 | |---|---|---| | 2 `ResourceCreate` | 4,536 | 等 | | 3 `ResourceRespecify` | 20,633 | **不等也给**(首用确认后 fire-and-forget) | | 47 `SetTextureParams` | 18,056 | **不等也给** | -| 48 `ResourceSubData` + 4 | 16,441 | **不等也给** | +| 48 `ResourceSubData` | 12,671 | **不等也给** | | 5 / 9 等 | 7 | 等 | -| **合计** | **59,673** | | +| **合计** | **55,903** | | 服务端**无法知道**客户端要不要这条答案——`ClientSession.cpp:1897-1900` 原文:「The server posts either way, -so the wire is unchanged and the row keeps its flag」。于是池子一帧churn **7,459 次**,两条 create 之间平均夹 -**13.2 条回复**,而一条答案只活得下 **8 条**: +so the wire is unchanged and the row keeps its flag」。于是池子一帧 churn **6,988 次**,两条 create 之间平均夹 +**12.3 条回复**,而一条答案只活得下 **8 条**: - 8 / 13.2 = 0.61 个 create + 8 / 12.3 = 0.65 个 create **所以窗口的失败不是「慢」,是「读不到」。** 1 深度的窗口就是阻塞路径本身。 @@ -115,10 +117,36 @@ if (x.replySeq != seq) return ... MOBILEGL_ERR_PROTOCOL_MISMATCH; // 等的是 要让延后的答案活下来,只有两条路,**都是协议面而不是调参**: 1. **加深回复池**:`ReplySlotCount` 在 Hello 里协商(`ClientSession.cpp:957` / `ServerSession.cpp:745`), - 两端一致即可改。需要的深度 \>= 两条 create 之间的回复数(本帧 13.2)乘以窗口深度,即 4 深度要 ~53 槽; - 而 `ReplyBytes` 固定 16 MiB 时槽会变小,`ReadPixels` 的 `ReplyCanHold` 会先撞墙,所以要一起抬 `ReplyBytes`。 -2. **让客户端能说「这条别回」**:四个 Bool 行(create/respecify/params/sub-data)目前服务端一律回; - 一个 record flag 就能把 59,673 砍到 ~4,543,那时 8 槽够用。这是 wire 改动,两端都要动。 + 两端一致即可改。需要的深度 \>= 两条 create 之间的回复数(本帧 12.3)乘以窗口深度,即 4 深度要 ~49 槽; + 而 `ReplyBytes` 固定 16 MiB 时槽会变小(16 MiB / 64 ≈ 256 KiB),`ReadPixels` 的 `ReplyCanHold` + 会先撞墙,所以要一起抬 `ReplyBytes`(W=4 保 2 MiB 上限约需 128 MiB)。**注意这是 8× 常驻内存**, + 而且评审指出服务端 SEG_REPLY 的实际分配路径**没人读过**——走这条之前先读它。 +2. **让服务端别回那些没人会读的答案**:四个 Bool 行(create/respecify/params/sub-data)目前一律回, + 其中约 51,000 条的答案是**没有人会读**的。砍到 ~4,543 之后两条 create 之间只剩 ~1 条回复,8 槽够用。 + + **判据必须是「这条的答案永远不会被读」,不是「fire-and-forget」。** 这两者今天在 4 个站点里 3 个重合, + **在最重要的那一个上正好相反**:window 延后的那些 create **正是** `wantReply=false`,而 §1 的 drain + 读的就是它们。按字面实现会让窗口从「读不回来」变成「根本没得读」,而且连 `readfail` 这个唯一能诊断它的 + 计数器一起消失——不会崩,只会静默退化。(这条是独立评审抓到的;本页 §7 的第一版写法就是那个错判据。) + + 三件已经核过的事: + + - **位有空位**:`MGPWireRecHeader::Flags` 是链路 framing 空间 `RingRecordFlags`(**不是** `MGPipeCallFlags`), + `Ring.h:281-285` 用掉 bit 0-4,**bit 5 空**(`PipeWire.inc:35-65` 的对照表自己写着「bit 5 kOptional vs(无)」)。 + - **wire fingerprint 不动**:`WireFingerprint()` 混的是 payload 布局 digest + PipeCalls.def 的 catalogue + digest + 渲染状态 + OpCount,加一个 ring 位三个都不动 ⇒ **不必 bump `MOBILEGL_PROTOCOL_CONTROL_REVISION`**。 + 但 `PipeWireCodec.cpp:106` / `:142-145` 与 `RingTest.cpp:740` 三条 `static_assert` **会红**——那是故意的 + 绊线(新 ring 位正好别名 `kOptional`)。 + - **两个方向都退化成今天的行为**(旧端忽略 bit 5 照旧回包;旧客户端从不设位)⇒ 它**不是** handoff-2 §3.2 + 那种「不升级就会坏」的协议面,而是**「不两端都升就不会生效」**的协议面——便宜一档,客户端可以先落。 + + 还有一处**必须同改**:`LinkMetricsBeginReply` 计的是 `ownsReplySlot`(`ClientSession.cpp:1883`),window + 生效后 **`by_op 2` 会掉到 ~0**,而 §1 正是教人用 `by_op 2` 判断窗口有没有生效——它会变成**假绿**。 + 验证这个提案要另加「服务端为这条 op 回了多少次」的计数。 + + (本节的两条路都经过一次独立评审。评审指出一处**未实测**:`StreamLink::PostReply` 把 `SendProgress()` + 搭在回包上(`StreamLink.cpp:577`),跳过回包会把进度粒度从「每条一次」降到「>= 64 条或 >= 1 ms 一次」 + (`:564-571`)——水位仍独立推进,但 barrier 的平均多等量没人算过。) ## 8. 复算