[Perf] (MG_Pipe, MG_Remote/Client): a texture respecify for a CONFIRMED object stops waiting - the row pays its round trip once, at first use - and the frame drops from 130.9 s to 31.3 s

- The predicate is one function again: MGPipeResourceRespecifyWantsItsReply (PipeRoute.h) is read
  by the wire emitter, so the two halves of this row cannot disagree about whether an answer is
  owed. The buffer half never owes one (both call sites discard the Bool, PipeFill.cpp:1105/:1142);
  the texture half owes one exactly until MGPipeHandleIsPublished, and that latch is set only by an
  ACCEPTED create - which EmitResourceRespecify already sends, blocking, when it is clear
  (TextureEmit.h:869-879). So the object pays once, at first use, and every record after it is
  fire-and-forget.
- MEASURED on the device, same frame, same 101,421,396 B staged, stock power policy: waits
  25,154 -> 4,543 and wall 130.9 s -> 31.3 s. The op split is the proof of what changed and what
  did not: waits by_op 3=20611 -> absent (only 2=4536, 5=6, 9=1 remain), while the RECORDS sent
  are unchanged - frame-sent still shows 3=20633 and 103,094 records total. The round trips went
  away; the work did not.
- It also lands on an independent prediction: an audit run beside this work (mgl-device/scratch/
  scheme-review.md) computed 4,543 remaining waits and a 24-37 s credible range (midpoint ~30 s)
  from the archived logs, a host-inproc round trip of 35.4 us and a measured 43.1 MiB/s link.
  Measured: 31.3 s, at the midpoint.
- Cumulative on that frame since this work started: 43,232 waits / 221.5 s -> 4,543 / 31.3 s (7.1x).
  What is left is the create row (4,536 waits, ~23 s) - the confirmation itself, not waste - plus
  hard floors of ~13.4 s client CPU and ~2.2 s of transfer. Going below that means batching the
  create row, which keeps real answers.
- TWO DEBTS, STATED: (1) the invalidation guard for the one silent-loss case is NOT wired; the
  audit confirmed independently that no production path can drop a texture record today, and if
  one appears the notice it needs is protocol-surface (a tenth reverse callback). (2) No SSIM or
  visual check has been taken since these rows stopped asking - frame-sent agreement is strong
  evidence but it is not a picture.
- Two stale controls in older cases were fixed rather than left: the item-B gate now names the
  CREATE row (its old total-based control only moved because a respecify was still waiting), and
  the by-op measurement asserts records-sent for the rows that deliberately stopped waiting.
  RemoteClientTest 169/169; red-once for the predicate is forcing it back to "always wait".
This commit is contained in:
BZLZHH
2026-09-25 23:29:08 +08:00
parent f2b55b9d2d
commit 82ed1df90e
5 changed files with 119 additions and 29 deletions
+34
View File
@@ -88,6 +88,8 @@
#include "MGPipe.h" #include "MGPipe.h"
#include "PipeApply.h" #include "PipeApply.h"
// P12: MGPipeHandleIsPublished, the latch the respecify predicate below reads.
#include "PipeMutation.h"
namespace MobileGL::MG_Pipe { namespace MobileGL::MG_Pipe {
@@ -271,6 +273,38 @@ namespace MobileGL::MG_Pipe {
MGP_ResourceCreate(&desc, &reply); MGP_ResourceCreate(&desc, &reply);
return MGPipeTakeReplyBool(reply, "resource_create"); return MGPipeTakeReplyBool(reply, "resource_create");
} }
// P12: WHICH RESPECIFIES STILL WAIT - one predicate, read by the wire emitter, so the two
// halves of this row cannot disagree about whether an answer is owed.
//
// THE BUFFER HALF NEVER DOES. Both call sites discard its Bool outright
// (PipeFill.cpp:1105 and :1142: `MGPipeRouteResourceRespecify(desc, nullptr);`), so the wait
// bought a value that was thrown away - on the device run this row was the second largest
// source of round trips in one frame, at ~5 ms each.
//
// THE TEXTURE HALF WAITS EXACTLY UNTIL THE OBJECT IS CONFIRMED, and "confirmed" is
// MGPipeHandleIsPublished: the latch is set only when the object's create was ACCEPTED, and
// the applier keeps texture records across a make-current (PipeFill.cpp:3756-3759), so for a
// published object the acceptance this row asks for is a fact the client already holds. The
// confirmation itself is paid once, at first use: EmitResourceRespecify sends a create when
// the latch is clear (TextureEmit.h:869-879), and that create is blocking.
//
// AND WHEN THE LATCH IS CLEAR THIS RETURNS TRUE, which is the whole fallback. It also covers
// the one place the two ends' consumer gates are worded differently - the client asks
// "any bit of the family" (PipeFill.cpp:1577-1595) while the applier asks "bit 10"
// (PipeApply.cpp:1408-1419) - because a create refused by the narrower server gate leaves the
// latch clear, so every later row for that object keeps taking the real answer. The cost of
// that case is throughput, never silence.
inline Bool MGPipeResourceRespecifyWantsItsReply(const MGPResourceDesc& desc) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MGPipeInstalledArm() != MGPipeRouteArm::kClientWire) return true;
if (static_cast<MGPipeResourceTarget>(desc.Target) == MGPipeResourceTarget::Buffer)
return false;
return !MGPipeHandleIsPublished(MGPipeKind::Texture, desc.Resource);
#else
(void)desc;
return true;
#endif
}
inline Bool MGPipeRouteResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes, inline Bool MGPipeRouteResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes,
const MGPRespecifiedLevel* level = nullptr) { const MGPRespecifiedLevel* level = nullptr) {
return gMGPipeRouteEscapes.ResourceRespecify(&desc, initialBytes, level); return gMGPipeRouteEscapes.ResourceRespecify(&desc, initialBytes, level);
+17 -17
View File
@@ -470,25 +470,25 @@ namespace MobileGL::MG_Remote::Client {
} else { } else {
MG_Pipe::MGPipeClearRespecifiedLevel(record); MG_Pipe::MGPipeClearRespecifiedLevel(record);
} }
// P12: THE BUFFER HALF'S ANSWER IS READ BY NOBODY. Both call sites discard this // P12: WHETHER THIS RECORD OWES AN ANSWER IS ONE PREDICATE, and it lives beside the
// record's Bool outright (PipeFill.cpp:1105 and :1142: `MGPipeRouteResourceRespecify( // route (PipeRoute.h, MGPipeResourceRespecifyWantsItsReply) so the emitter and the wire
// desc, nullptr);`), so for a buffer the wait buys a value that is thrown away - pure // cannot disagree about it. The two halves are argued there; the short version is that the
// cost, and on the device run this row was the second largest source of round trips in // buffer half's Bool is discarded at both call sites, and the texture half's is owed
// the frame. It goes fire-and-forget, with the same provisional-accept shape // until the object's create has been accepted - the confirmation it pays at first use.
// resource_subdata has had since item B.
// //
// THE TEXTURE HALF KEEPS ITS ANSWER, and that is deliberate rather than left over: // WHAT A PROVISIONAL ACCEPT DOES TO THE TEXTURE HALF, stated here because it is a
// there the Bool is the self-heal trigger AND the gate on the acceptance mirror, and // deliberate trade rather than an oversight: the emitter's acceptance mirror
// advancing that mirror on an answer nobody waited for is exactly ID-18 M3 // (entry.LastDesc) now advances on an answer nobody waited for. ID-18 M3
// (TextureEmit.h:885-888: "a refused respecify must leave LastDesc naming the // (TextureEmit.h:885-888) forbids exactly that for a record whose verdict is UNKNOWN -
// descriptor that actually landed, or the next identical call is suppressed against a // "the next identical call is suppressed against a record that was never stored" - and
// record that was never stored"). A texture respecify still waits until that mirror // what makes it safe here is that the object is PUBLISHED: the client is not guessing,
// can be advanced on something better than a provisional accept. // it is reading a latch the server set by accepting this handle's create. The residue
const Bool bufferHalf = // is the one case where the server loses a record it had, which today has no production
static_cast<MG_Pipe::MGPipeResourceTarget>(record.Target) == // path (PipeApply.cpp:1593's only callers are tests), and whose guard is the
MG_Pipe::MGPipeResourceTarget::Buffer; // invalidation notice the route's notes call ①.
const Bool wantsReply = MG_Pipe::MGPipeResourceRespecifyWantsItsReply(record);
Int32 status = Wire::ReplySink::kStatusError; Int32 status = Wire::ReplySink::kStatusError;
if (bufferHalf) { if (!wantsReply) {
const Uint64 seq = session.EmitAndWaitTails( const Uint64 seq = session.EmitAndWaitTails(
MGPWireOp::ResourceRespecify, &record, sizeof(record), nullptr, 0, nullptr, 0, MGPWireOp::ResourceRespecify, &record, sizeof(record), nullptr, 0, nullptr, 0,
&status, nullptr, /*wantReply=*/false); &status, nullptr, /*wantReply=*/false);
+42 -11
View File
@@ -894,24 +894,32 @@ TEST(RemoteClientControls, TextureUploadsStopOwningAReplyOnTheWireArm) {
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>(); MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
Uint8 pixels[4 * 4 * 4]; Uint8 pixels[4 * 4 * 4];
std::memset(pixels, 0x21, sizeof(pixels)); std::memset(pixels, 0x21, sizeof(pixels));
// THE CONTROL ROW, NAMED RATHER THAN TOTALLED, AND MEASURED ACROSS THE WHOLE SETUP. The
// object's create is the one record an object always pays for (kWaitReply in
// PipeCalls.def), so ITS counter is the live control - and the baseline goes in before
// GenTextures, because which call publishes the twin (generate, bind or the storage
// definition) is the emitter's business and not this case's. The previous version of this
// control read the session TOTAL and only moved because a respecify was still waiting;
// that row is fire-and-forget for a confirmed object now, which is what made the stale
// control visible instead of silently wrong.
const Uint32 uploadOp = static_cast<Uint32>(MGPWireOp::ResourceSubData);
const Uint32 createOp = static_cast<Uint32>(MGPWireOp::ResourceCreate);
const Uint64 createWaitsBefore = Transport::LinkMetricsReplyWaitsFor(createOp);
GLuint texture = 0; GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture); MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(0x0DE1, texture); MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
// THE CONTROL ROW. resource_create is kWaitReply (PipeCalls.def), so it owns a reply
// slot and this counter MUST move on it.
const auto beforeCreate = Transport::LinkMetricsReplyWaits();
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 4, 4, 0, 0x1908, 0x1401, pixels); MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 4, 4, 0, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays); MGP_FILL(DrawArrays);
if (Transport::LinkMetricsReplyWaits() <= beforeCreate) ::_exit(140); if (Transport::LinkMetricsReplyWaitsFor(createOp) <= createWaitsBefore) ::_exit(140);
// THE ROW UNDER TEST. A second write into the same level is a pure resource_subdata: the // THE ROW UNDER TEST. A second write into the same level is a pure resource_subdata: the
// resource is created, so no create crosses, and the level is dirty again - the only // resource is created, so no create crosses, and the level is dirty again - the only
// record it can produce is the one item B is about. // record it can produce is the one item B is about.
const auto beforeRecords = ClientWireRecordsEmitted(); const Uint64 uploadRecordsBefore = Transport::LinkMetricsRecordsFor(uploadOp);
const auto beforeWaits = Transport::LinkMetricsReplyWaits(); const Uint64 uploadWaitsBefore = Transport::LinkMetricsReplyWaitsFor(uploadOp);
MG_Impl::GLImpl::TexSubImage2D(0x0DE1, 0, 0, 0, 2, 2, 0x1908, 0x1401, pixels); MG_Impl::GLImpl::TexSubImage2D(0x0DE1, 0, 0, 0, 2, 2, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays); MGP_FILL(DrawArrays);
if (ClientWireRecordsEmitted() <= beforeRecords) ::_exit(141); if (Transport::LinkMetricsRecordsFor(uploadOp) <= uploadRecordsBefore) ::_exit(141);
if (Transport::LinkMetricsReplyWaits() != beforeWaits) ::_exit(142); if (Transport::LinkMetricsReplyWaitsFor(uploadOp) != uploadWaitsBefore) ::_exit(142);
ClientSessionInstance().Stop(); ClientSessionInstance().Stop();
}); });
ExpectChildSuccess(child); ExpectChildSuccess(child);
@@ -1038,7 +1046,11 @@ TEST(RemoteClientControls, ReplyWaitsByOpForAnAtlasShapedLoad) {
::_exit(148); ::_exit(148);
if (Transport::LinkMetricsReplyWaitsFor(static_cast<Uint32>(MGPWireOp::ResourceCreate)) == 0) if (Transport::LinkMetricsReplyWaitsFor(static_cast<Uint32>(MGPWireOp::ResourceCreate)) == 0)
::_exit(144); ::_exit(144);
if (Transport::LinkMetricsReplyWaitsFor(static_cast<Uint32>(MGPWireOp::ResourceRespecify)) == 0) // AND ResourceRespecify FOLLOWS SetTextureParams OFF THAT LIST FOR THE SAME REASON (P12/R3):
// for a confirmed object it is fire-and-forget too. What must still hold is that its RECORDS
// go out - a zero on both lines would mean the emitter stopped sending, which is a different
// and much worse change than this one.
if (Transport::LinkMetricsRecordsFor(static_cast<Uint32>(MGPWireOp::ResourceRespecify)) == 0)
::_exit(145); ::_exit(145);
// AND SetTextureParams IS DELIBERATELY NOT ON THAT LIST ANY MORE (P12). It was the control // AND SetTextureParams IS DELIBERATELY NOT ON THAT LIST ANY MORE (P12). It was the control
// that proved this counter was live; the first-use confirmation moved the row to // that proved this counter was live; the first-use confirmation moved the row to
@@ -1138,7 +1150,21 @@ TEST(RemoteClientControls, ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes
MGP_FILL(DrawArrays); MGP_FILL(DrawArrays);
if (Transport::LinkMetricsRecordsFor(respecifyOp) <= recordsBefore) ::_exit(154); if (Transport::LinkMetricsRecordsFor(respecifyOp) <= recordsBefore) ::_exit(154);
if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) != waitsBefore) ::_exit(155); if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) != waitsBefore) ::_exit(155);
// ---- the TEXTURE half: the deliberate exception ---- // ---- the TEXTURE half, part 1: the PREDICATE both halves read (P12/R3) ----
// An unpublished texture still owes its answer - that is the first-use confirmation - and a
// buffer never does. Asserted directly rather than through a record, because the only way to
// produce an unpublished texture on the wire is to make the server REFUSE its create, and a
// malformed descriptor would trip the applier's wire report instead of testing this.
if (MG_Pipe::MGPipeInstalledArm() != MG_Pipe::MGPipeRouteArm::kClientWire) ::_exit(157);
MGPResourceDesc unpublished{};
unpublished.Target = static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D);
if (!MG_Pipe::MGPipeResourceRespecifyWantsItsReply(unpublished)) ::_exit(158);
MGPResourceDesc bufferDesc{};
if (MG_Pipe::MGPipeResourceRespecifyWantsItsReply(bufferDesc)) ::_exit(159);
// ---- the TEXTURE half, part 2: CONFIRMED, end to end ----
// GenTextures + a storage definition publishes the object (its create is accepted, and the
// create is blocking). A SECOND storage definition on that same object is a real change, so
// the record must go out - and must not take a reply.
Uint8 pixels[8 * 8 * 4]; Uint8 pixels[8 * 8 * 4];
std::memset(pixels, 0x21, sizeof(pixels)); std::memset(pixels, 0x21, sizeof(pixels));
GLuint texture = 0; GLuint texture = 0;
@@ -1146,7 +1172,12 @@ TEST(RemoteClientControls, ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes
MG_Impl::GLImpl::BindTexture(0x0DE1, texture); MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 8, 8, 0, 0x1908, 0x1401, pixels); MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 8, 8, 0, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays); MGP_FILL(DrawArrays);
if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) == waitsBefore) ::_exit(156); const Uint64 confirmedRecords = Transport::LinkMetricsRecordsFor(respecifyOp);
const Uint64 confirmedWaits = Transport::LinkMetricsReplyWaitsFor(respecifyOp);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 16, 16, 0, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays);
if (Transport::LinkMetricsRecordsFor(respecifyOp) <= confirmedRecords) ::_exit(160);
if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) != confirmedWaits) ::_exit(161);
ClientSessionInstance().Stop(); ClientSessionInstance().Stop();
}); });
ExpectChildSuccess(child); ExpectChildSuccess(child);
+3 -1
View File
@@ -14,7 +14,9 @@
单条纯上传占用的回包等待 **1 → 0**(`wait_replies`,正是 §3.2 里"一帧 55,428"的那个计数器)。 单条纯上传占用的回包等待 **1 → 0**(`wait_replies`,正是 §3.2 里"一帧 55,428"的那个计数器)。
按形状实测(`RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad`,主机 inproc,按 op 计数):**图集拼接**(一张 level 上 64 次 `glTexSubImage2D`)**64 → 0** 次回包等待; 按形状实测(`RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad`,主机 inproc,按 op 计数):**图集拼接**(一张 level 上 64 次 `glTexSubImage2D`)**64 → 0** 次回包等待;
**每 sprite 一张纹理**(64 张纹理 + 256 次 `glTexParameteri`)**384 → 320**,剩下的全是 per-resource / per-call 的三条 row(create 76 / respecify 130 / params 130)—— **每 sprite 一张纹理**(64 张纹理 + 256 次 `glTexParameteri`)**384 → 320**,剩下的全是 per-resource / per-call 的三条 row(create 76 / respecify 130 / params 130)——
它们的拒绝是「服务端没有这个 handle」的唯一客户端可见信号,删掉等待就是删掉信号,裁定见 `CONTRACT-P5E.md` §2.5。 **这两条后来被"首次使用确认"取代了**(见下面那条):对象的存在性由它自己的 create 确认为事实之后,
params 与 texture respecify 都不再逐条买答案,而是只在**首次使用**时付一次阻塞的 create。
同一帧的累计实测:**43,232 waits / 221.5 s → 4,543 waits / 31.3 s(7.1×)**,staged 字节与发出的记录数(103,094)都不变。
真机(TrebleDroid GSI,MC 26.2 经 TCP 入世界)那一帧的构成为 `ResourceRespecify` 20,633 + `SetTextureParams` 18,056 + `ResourceCreate` 4,536 条**各等一次**, 真机(TrebleDroid GSI,MC 26.2 经 TCP 入世界)那一帧的构成为 `ResourceRespecify` 20,633 + `SetTextureParams` 18,056 + `ResourceCreate` 4,536 条**各等一次**,
而 `ResourceSubData` 12,671 条**零等待**(B 的实机证据);这些记录都是真实状态变化,客户端没有可去重的(同一值的 `glTexParameteri` 本来就不产生记录)。 而 `ResourceSubData` 12,671 条**零等待**(B 的实机证据);这些记录都是真实状态变化,客户端没有可去重的(同一值的 `glTexParameteri` 本来就不产生记录)。
- **单次往返那 ~5 ms 是设备侧的深度空闲退出,不是链路也不是客户端**(P12,本轮实测):同一帧、同一批记录, - **单次往返那 ~5 ms 是设备侧的深度空闲退出,不是链路也不是客户端**(P12,本轮实测):同一帧、同一批记录,
@@ -219,6 +219,29 @@ create 被拒 → 闩不置位 → 下一条参数记录照样阻塞取真答案
**下一个 20,633**:`ResourceRespecify` 仍逐条等(在 frame 25 里占 20,633/25,176 = 82%)。它与 params 同形(答案只用于 `NoteRespecified` 与自愈), **下一个 20,633**:`ResourceRespecify` 仍逐条等(在 frame 25 里占 20,633/25,176 = 82%)。它与 params 同形(答案只用于 `NoteRespecified` 与自愈),
"对象已确认"这个前提同样成立——按同一条规则处理,估算落在 **~4.5k waits ≈ 24 s** 量级(create 那 4,536 是确认本身,应当保留)。 "对象已确认"这个前提同样成立——按同一条规则处理,估算落在 **~4.5k waits ≈ 24 s** 量级(create 那 4,536 是确认本身,应当保留)。
### 2.12 `ResourceRespecify` 的两半根本不同:buffer 是白送的,texture 才是那 20,611按"已确认对象不再等"处理这一行时,代码里两半的差别是决定性的:- **buffer 半**:`PipeFill.cpp:1105` 与 `:1142` **两处都把返回值丢掉**(`MGPipeRouteResourceRespecify(desc, nullptr);`), 所以那条等待买到的值当场被扔——改 fire-and-forget 是**纯收益、语义零变化**。- **texture 半**:返回值既是自愈触发器,又是**验收镜像 `LastDesc` 的闸**, `TextureEmit.h:885-888` 明写不许在未确认的答案上前进(ID-18 M3:否则下一次相同调用会被去重掉,而服务端从没存过)。于是本轮只落 buffer 半(提交见下),并用它把两半的占比量了出来——**同一帧的 `by_op`**:| | waits | `by_op` ||---|---|---|| 只做了 params(frame 25) | 25,176 | `2=4536, `**`3=20633`**`, 5=6, 9=1` || 再放开 buffer 半(frame 10) | **25,154** | `2=4536, `**`3=20611`**`, 5=6, 9=1` |**buffer 半只值 22 次等待(0.1%)——20,633 里几乎全是 texture respecify**,也就是带 ID-18 M3 约束的那一半。墙钟 134.4 s → 130.9 s(同一帧、同样 101,421,396 B staged)。**texture 半的正解(下一步,尚未实现)**:让镜像**只在真答案上前进**——fire-and-forget 之后 `NoteRespecified` 收 `accepted && !provisional`,于是最坏情况退化成"多补发一条相同 respecify"(安全方向),而绝不会变成"该重发的被去重掉"。这需要在 MG_Pipe 的回复信箱上把"临时接受"和"真接受"分开(信箱本来就有"最后一次调用"的纪律,`MGPipeTakeReplyBool` 就是这么读的),改动落在 route/mailbox + `EmitResourceRespecify` 一处判断。按同一模型估算:**25,154 → ~4.5k waits ≈ 24 s 量级**(create 那 4,536 是确认本身)。 ### 2.12 `ResourceRespecify` 的两半根本不同:buffer 是白送的,texture 才是那 20,611按"已确认对象不再等"处理这一行时,代码里两半的差别是决定性的:- **buffer 半**:`PipeFill.cpp:1105` 与 `:1142` **两处都把返回值丢掉**(`MGPipeRouteResourceRespecify(desc, nullptr);`), 所以那条等待买到的值当场被扔——改 fire-and-forget 是**纯收益、语义零变化**。- **texture 半**:返回值既是自愈触发器,又是**验收镜像 `LastDesc` 的闸**, `TextureEmit.h:885-888` 明写不许在未确认的答案上前进(ID-18 M3:否则下一次相同调用会被去重掉,而服务端从没存过)。于是本轮只落 buffer 半(提交见下),并用它把两半的占比量了出来——**同一帧的 `by_op`**:| | waits | `by_op` ||---|---|---|| 只做了 params(frame 25) | 25,176 | `2=4536, `**`3=20633`**`, 5=6, 9=1` || 再放开 buffer 半(frame 10) | **25,154** | `2=4536, `**`3=20611`**`, 5=6, 9=1` |**buffer 半只值 22 次等待(0.1%)——20,633 里几乎全是 texture respecify**,也就是带 ID-18 M3 约束的那一半。墙钟 134.4 s → 130.9 s(同一帧、同样 101,421,396 B staged)。**texture 半的正解(下一步,尚未实现)**:让镜像**只在真答案上前进**——fire-and-forget 之后 `NoteRespecified` 收 `accepted && !provisional`,于是最坏情况退化成"多补发一条相同 respecify"(安全方向),而绝不会变成"该重发的被去重掉"。这需要在 MG_Pipe 的回复信箱上把"临时接受"和"真接受"分开(信箱本来就有"最后一次调用"的纪律,`MGPipeTakeReplyBool` 就是这么读的),改动落在 route/mailbox + `EmitResourceRespecify` 一处判断。按同一模型估算:**25,154 → ~4.5k waits ≈ 24 s 量级**(create 那 4,536 是确认本身)。
### 2.13 R3 落地:texture respecify 也只在首次使用时付一次——那一帧 130.9 s → 31.3 s
实现:把"这一行要不要等"做成**唯一一处**判据 `MGPipeResourceRespecifyWantsItsReply`(`MG_Pipe/PipeRoute.h`),wire 端读它决定要不要开回复槽:
buffer 半永远不等(两处返回值都被丢弃);texture 半只在**对象未确认**时等(`MGPipeHandleIsPublished` 为假),确认本身由 `EmitResourceRespecify` 在首次使用时补的那次**阻塞 create** 付掉。
同一台设备、同一份工作量(staged **101,421,396 B**,与改动前逐字节相同)、出厂省电、服务端 spin 默认:
| | 那一帧 waits | `by_op`(等待) | `by_op`(发出的记录) | 墙钟 |
|---|---|---|---|---|
| R1+R2 之后 | 25,154 | `2=4536, 3=20611, 5=6, 9=1` | `2=4536, 3=20633, …` 共 103,088 | 130.9 s |
| **R3 之后** | **4,543** | `2=4536, 5=6, 9=1` | `2=4536, `**`3=20633`**`, …` 共 **103,094** | **31.3 s** |
**respecify 那 20,633 条记录一条没少(照发),少的是它们各自的那次往返**——这正是这条路线与"少干活"的区别。
独立审查给的预测是"4,543 waits、24–37 s(中点 ~30 s)",实测 **31.3 s**,落在中点。
**累计(同一帧)**:43,232 waits / 221.5 s → **4,543 waits / 31.3 s**(7.1×)。剩下的是 create 那 4,536 次等待 ≈ 23 s,
那是确认本身,不是浪费;硬地板还有客户端 CPU ~13.4 s 与传输 ~2.2 s(审查实测 43.1 MiB/s)。**想再往下只剩"批处理 create 行"**(4,536 → ~1.1k,且仍拿真答案)。
**两笔明账**:
1. **① 的守卫尚未接**:审查独立复核"今天不可达"(能清纹理记录的只有 `MGPipeApplierReleaseObjectRecords`,调用者只有测试;`MGPipeApplierReset` 不清它)。
接管时**优先查能否挂在 caps 代次/会话边界这类已有信号上**(一行);否则要新增反向通道第 10 个回调 = 协议面 = 两端重出 APK。
2. **画面未验**:这条路线改的是"客户端还问不问",`frame-sent` 一致是强证据但不是画面证据;R3 之后应补一次 SSIM/目视。
## 3. 剩余(下一轮的直接入口) ## 3. 剩余(下一轮的直接入口)
1. **把记录数压下来(新的头号项)**。§2.5 的真机数字说:那一帧剩下的 43,232 次等待全在「必须等」的三条 row 上, 1. **把记录数压下来(新的头号项)**。§2.5 的真机数字说:那一帧剩下的 43,232 次等待全在「必须等」的三条 row 上,