mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-16 06:23:23 +09:00
[Feat] (MG_Remote): a record can tell the server its answer will never be read - the bit works and cuts a frame's replies by 92%, and it proves the window's real blocker is the FIFO drain, not the pool depth
The create window could not collect its deferred answers. The previous commit
diagnosed that as the reply pool being 8 slots deep against 55,903 replies in one
frame, and left a no-reply bit as the way out. This lands that bit, measures it on
the device, and reports that the diagnosis was half wrong.
WHAT LANDED. ClientSession::EmitAndWaitTails takes a second question beside
wantReply: willReadReply. They are not the same question and the window is the case
that separates them - it emits with wantReply=false and READS the answer later, so
its records must still be answered. kRecNoReply (RingRecordFlags bit 5, which
aliases kOptional in the call-flag space; the flag-space asserts were extended and
the alias named) is stamped when willReadReply is false, and the decoder skips
PostReply while keeping every acceptance counter - "nobody reads the answer" is not
"nothing happened". The two illegal combinations are Fatal by name.
Three call sites say it: SetTextureParams for a confirmed object, the respecify
row's fire-and-forget half, and sub-data where MGPipeSubDataWantsItsReply decides.
MEASURED ON THE DEVICE (aikjs4s8cys8ba7l, rebuilt APK + client, both halves new):
server: skipped=50,001 answered=4,521 (a frame's replies: 55,903 -> 4,543)
client: Reply frames received seen=4,501, latest seq=97,181
frame-sent: identical per op, records untouched
create row: by_op 2 = 9,070 (was 4,536) - because the drain's blocking take is
now counted, so the row's number stays honest instead of reading as
"create stopped waiting" while the waiting merely moved
AND THE WINDOW STILL READ BACK NOTHING: readfail 8,951 -> 8,872, i.e. -0.9%.
THE REAL CAUSE, from the miss probe:
P12miss n=1 want=310 replySeq=1504 ring=[1175..1504]
P12miss n=9001 want=310 replySeq=102915 ring=[102866..102915]
want=310 every time. The drain is STRICTLY FIFO and a failed read has no way out -
it retries the same head for ever. Any reply buffer is bounded by design (8 pool
slots, or the 8-frame ring the stream link now keeps), so with 4,536 deferred
answers at least one is overwritten before it is read, and that one wedges the
window permanently. Cutting the churn by two orders of magnitude does not help: it
turns a certainty into a certainty. That is the constraint, not the depth.
THE FIX IS NAMED AND NOT TAKEN (the goal this work ran under stops here on purpose).
On a failed read of the head, abandon it: MGPipeNoteHandleUnpublished +
NoteCreateSuspect + pop - the DECLINE path reused for "unknown". The window then
self-heals through the blocking path and the object merely gets slower. It needs its
own gate: a failed read must let the window ADVANCE.
Gates, all green (172 + 13 + 22 + 103):
PipeWireCodecTest.TheNoReplyBitSkipsTheAnswerAndKeepsTheAcceptanceRecord - the skip,
the kept tally, the answered/skipped split, and the negative control that a record
WITHOUT the bit is still answered (a server that stopped answering everything
would otherwise pass)
RemoteClientControls.ACreateWindowDefersTheAnswersAndTheWindowOffDoesNot - now also
asserts the answers CAME BACK (answers_taken=11, outstanding=1, fallbacks=0 against
12 records). RED-ONCE PROVED: giving the window's creates the literal
"fire-and-forget => skip" predicate makes this case fail.
RingTest - the sixth and last flag alias, and the hand-written exhaustiveness list
extended (with a note that it is only as exhaustive as that list).
Server side: AnsweredRecords()/SkippedAnswers() - the number that made this
debuggable, since a wire change whose effect is invisible is undebuggable.
Client-first is safe: an old server ignores bit 5 and replies as before, so the two
cross-version directions degrade to today's behaviour - this is "both ends must
upgrade to take EFFECT", not "to avoid breaking". Hit that in practice first.
This commit is contained in:
@@ -65,6 +65,10 @@ so the wire is unchanged and the row keeps its flag」。于是池子一帧 chur
|
||||
|
||||
**所以窗口的失败不是「慢」,是「读不到」。** 1 深度的窗口就是阻塞路径本身。
|
||||
|
||||
> **⚠️ 本节后来被 §8 部分更正。** 池深度是**加剧因素**而不是约束:把一帧回包从 55,903 砍到 4,543
|
||||
> 之后,drain 依然一条都读不回来(readfail 8,951 → 8,872)。真正的约束是 §8 的
|
||||
> 「有界缓冲 + 严格 FIFO + 读失败无出路」。本节的数字仍然有效,结论不完整。
|
||||
|
||||
## 3. 顺带发现并修掉:`StreamLink::ReadReply` 违背了自己的接口
|
||||
|
||||
`ILink::ReadReply`(`Transport/ILink.h:285`)的签名是**按 seq 读**;`ShmLink` 用 `ReplySlotPool` 实现它,
|
||||
@@ -148,7 +152,45 @@ if (x.replySeq != seq) return ... MOBILEGL_ERR_PROTOCOL_MISMATCH; // 等的是
|
||||
搭在回包上(`StreamLink.cpp:577`),跳过回包会把进度粒度从「每条一次」降到「>= 64 条或 >= 1 ms 一次」
|
||||
(`:564-571`)——水位仍独立推进,但 barrier 的平均多等量没人算过。)
|
||||
|
||||
## 8. 复算
|
||||
## 8. 更正(同日晚,实现 no-reply 位之后):池深度**不是**约束
|
||||
|
||||
§2 把「8 槽池 vs 55,903 条回复」写成了根因。**实测否掉了这个诊断的一半。** 后来按 §7 的第 2 条路
|
||||
实现了那个 no-reply 位(服务端跳过没人读的答案),真机上服务端日志确认它生效:
|
||||
|
||||
服务端:decisions=50001(跳过) answered=4521(回包)
|
||||
客户端:收到的 Reply 帧 seen=4501, latest seq=97181
|
||||
|
||||
也就是说一帧的回包从 55,903 降到 ~4,543(−92%),池子不再被冲刷。**而窗口的 drain 依然一条都读不回来:**
|
||||
|
||||
P12win call=4501 cfg=2 eff=2 pending=2 taken=62 pushed=64 blocked=4436 waitfail=65 readfail=8872
|
||||
P12miss n=1 want=310 replySeq=1504 ring=[1175..1504] slots=8
|
||||
P12miss n=9001 want=310 replySeq=102915 ring=[102866..102915] slots=8
|
||||
|
||||
**want=310 每一次都是同一个数**:窗口是按序的,队头那条的答案早就被有界缓冲覆盖掉了,而 drain 只会
|
||||
**永远重试同一个队头**。于是 taken 停在 62、pending 钉在 2、readfail 每条 create 涨 2、suspects 恒为 0。
|
||||
|
||||
**真正的根因是「有界缓冲 + 严格 FIFO + 读失败无出路」三者相乘**,不是池子有多深:
|
||||
|
||||
- 回复通道在任何实现下都是有界的(shm 是 8 槽池、stream 是我那轮改的 8 帧环),覆盖是设计的一部分;
|
||||
- 窗口只读队头,且读失败时**只记账不放弃**(`if (!session.ReadReply(...)) return;`);
|
||||
- 于是 4,536 条延后答案里**只要有任意一条**在被读之前被覆盖,窗口就永久卡死,之后每条 create 都走阻塞路径;
|
||||
- 概率上这是必然事件,而不是小概率——这也解释了为什么去掉 50,001 条回复只让 readfail 从 8,951 动到 8,872(−0.9%):
|
||||
冲刷速率降了两个数量级,但「至少丢一条」的概率仍然是 1。
|
||||
|
||||
**要修的话是一个明确的形状**(本轮按 goal 的止损条件停在这里,没有实现):队头的答案读不回来时
|
||||
**放弃它**——`MGPipeNoteHandleUnpublished` + `NoteCreateSuspect` + 弹出该条目——也就是把 DECLINE 的
|
||||
处理路径复用到「不知道」这个结果上。这样窗口自愈(该对象回到阻塞路径,下一次 create 把真相带回来),
|
||||
代价是丢过答案的对象变慢,而不再是把整行拖死。它需要一个自己的门:**读失败必须让窗口继续前进**。
|
||||
|
||||
顺带记两条本轮得到的事实:
|
||||
|
||||
1. **no-reply 位本身是好的,而且两端都验证过**:服务端跳过 50,001 条、回 4,521 条;客户端的 `by_op 2`
|
||||
随之变成「drain 的阻塞次数 + fallback 次数」,`frame-sent` 逐 op 不变。它救不了窗口,但它把
|
||||
「服务端为每条 kReplySlot 记录都回」这件事变成了可关的——每帧少 ~102,000 次 `sendmsg`。
|
||||
2. **两端必须都升才生效**:客户端先落、服务端不升时,行为与改前逐字节相同(旧端忽略 bit 5 照旧回包)——
|
||||
这正是 B6 说的那一档,本轮在真机上先撞到了一次。
|
||||
|
||||
## 9. 复算
|
||||
|
||||
~~~sh
|
||||
# 两臂的帧读数(handoff-2 §1 的读法)
|
||||
|
||||
Reference in New Issue
Block a user