From 3a6c5c7f04e14a06d2e7f3d69bc7191e9c5d6458 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sat, 26 Sep 2026 15:30:48 +0800 Subject: [PATCH] [Perf] (MG_Remote): The create window ships at 10, and the server now reports where its own frame time goes. --- MobileGL/Config.h | 2 +- MobileGL/ConfigLoader.cpp | 2 +- MobileGL/MG_Remote/Client/WireTables.cpp | 36 ++++++++++++++++- MobileGL/MG_Remote/Server/ServerLoop.cpp | 50 ++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 7b1b13d5..5a0fcc3d 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -594,7 +594,7 @@ namespace MobileGL::MG_Config { // caller was fire-and-forget. The window's own creates are fire-and-forget AND read, so a // flag keyed on wantReply would disable the very mechanism it exists to enable // (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11). - Uint32 CreateWindow = 6; + Uint32 CreateWindow = 10; // 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 // at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index f5721381..f71c9b8a 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -420,7 +420,7 @@ namespace MobileGL::MG_ConfigLoader { // declared answers, and the device sweep showed the wall clock still falling at the old // cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and // StreamLink's crossing log are the checks on this number. - ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 6, 1, 8); + ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12); // The verify harness compares the pushed block against the applier per verb; a // batched queue lets the comparer read a supplied field mid-apply, which is a // torn read rather than a divergence. The batch is therefore off whenever the diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index ae4d268a..6d126dcb 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -398,7 +398,7 @@ namespace MobileGL::MG_Remote::Client { // (THE NARRATIVE ABOVE PREDATES RETENTION and ends at "the default is 1". It is kept as // the record of what was tried; what made the window work is the declared-answer store, // and the default is 2 - Config.h carries the device numbers.) - constexpr Uint32 kCreateWindowMax = 8; + constexpr Uint32 kCreateWindowMax = 12; // THE POOL ARGUMENT THAT USED TO PIN THIS AT 4 IS GONE, AND WHY IT IS GONE IS THE ONE // THING TO RE-CHECK BEFORE RAISING IT AGAIN. It read: the pool is kDefaultReplySlotCount // slots addressed seq % count, so an answer is readable only while its slot has not been @@ -451,6 +451,32 @@ namespace MobileGL::MG_Remote::Client { // answers from existing at all - the second is what a no-reply bit keyed on wantReply // would produce, and it would leave every other reading green. Uint64 g_createWindowTaken = 0; + // P12 MEASUREMENT: WHY A CREATE DOES NOT DEFER. The window's own arithmetic says a frame's + // blocking takes should be creates/window (4,536/6 = 756 at the shipped default), and the + // device measures 1,155 - a CONSTANT ~400 that does not move when the window deepens (at + // window 8 the ideal is 567 and the measurement is 859). A constant is a fixed population + // rather than a queueing effect, and the four exits below are the only ways into it, so + // counting them by name is what says which one it is. It matters because the frame turned + // out to be LATENCY-bound, not throughput-bound (the server applies all 103,097 records in + // 1.95 s of a 9.64 s frame, and the client's 1,161 blocking takes at ~4.9 ms are 5.7 s of + // it) - so the way to make it faster is fewer blocks, not a faster apply. + Uint64 g_blockNoWindow = 0, g_blockSuspectsFull = 0, g_blockSuspectObject = 0, + g_blockWindowStillFull = 0, g_blockTotal = 0; + void NoteCreateBlocked(Uint64& reason) { + ++reason; + ++g_blockTotal; + // Once per 256 blocks: a handful of lines per load frame, and the composition is + // readable from any one of them. + if (g_blockTotal % 256 == 0) { + MGLOG_I("P12 create blocks: total=%llu no-window=%llu suspects-full=%llu " + "suspect-object=%llu window-still-full=%llu", + static_cast(g_blockTotal), + static_cast(g_blockNoWindow), + static_cast(g_blockSuspectsFull), + static_cast(g_blockSuspectObject), + static_cast(g_blockWindowStillFull)); + } + } // A HANDLE WHOSE CREATE CAME BACK DECLINED IS NEVER WINDOWED AGAIN. Without this the window // would UNDO the repair it exists to keep: every later use of the object sends another create, @@ -624,6 +650,13 @@ namespace MobileGL::MG_Remote::Client { // all four take the blocking branch, which is the pre-window shape - and on the device // this is the branch the whole row takes, because the deferral above it cannot be // collected. See the section comment for the measurement and the arithmetic. + if (window <= 1) { + NoteCreateBlocked(g_blockNoWindow); + } else if (g_createSuspects.size() >= kCreateSuspectMax) { + NoteCreateBlocked(g_blockSuspectsFull); + } else if (CreateIsSuspect(payload->Resource)) { + NoteCreateBlocked(g_blockSuspectObject); + } if (window <= 1 || g_createSuspects.size() >= kCreateSuspectMax || CreateIsSuspect(payload->Resource)) { // THE WINDOW RETIRES HERE, AND IT MAY BE HOLDING ANSWERS (P12). This branch is @@ -650,6 +683,7 @@ namespace MobileGL::MG_Remote::Client { // against the effective window, which is what the array below is sized for, so // this line IS the overwrite guard. if (g_createWindowCount >= window) { + NoteCreateBlocked(g_blockWindowStillFull); EmitCreateBlocking(session, payload, reply); return; } diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index 158c9025..d24d64e6 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -739,6 +739,30 @@ namespace MobileGL::MG_Remote::Server { ServerSession& session = *m_session; Transport::SessionConsumer& consumer = session.Consumer(); PipeApplier& applier = session.Applier(); + // ---- P65ServerFrame: WHERE THE SERVER'S TIME GOES (P12 measurement) ----------------- + // + // THE CLIENT SIDE IS MEASURED OUT AND THE SERVER'S IS NOT MEASURED AT ALL. Every reading + // this harness produces is the CLIENT's - wall_ns, wait_replies, client_thread_cpu_ns - and + // they now add up to less than the frame: at window 6 the client burns 2.5 s of CPU and + // 1,162 blocking waits, and removing 292 of those waits (window 8) moves the wall clock by + // nothing. The same 101 MB over USB instead of WiFi moves it by nothing either. What is + // left is the one process nobody has timed: the server applying 103,097 records. + // + // THE SPLIT IS THE POINT. apply_ms is the sum of the time spent INSIDE + // PipeApplier::ApplyOne - the GL work, the decode, the reply posts - and wall_ms is Present + // to Present on the apply thread. Their difference is everything that is NOT applying: + // waiting on the ring for the next record, retiring, and the socket. So one line answers + // "is the phone CPU-bound, or is it waiting for the client?" without a profiler, which is + // the question the next optimisation has to be chosen from. + // + // FUNCTION-LOCAL STATIC BECAUSE THE FRAME DOES NOT FIT IN ONE DrainRing CALL: records + // trickle in, so a frame spans many drains, and state that reset per drain would measure a + // batch. There is exactly one apply thread (ServerLoopInstance's), which is what makes a + // static safe here; the same reasoning the drain tally above uses. + static auto sFrameWallStart = std::chrono::steady_clock::now(); + static std::uint64_t sFrameApplyNs = 0; + static std::uint64_t sFrameRecords = 0; + static std::uint64_t sFrameIndex = 0; Uint64 applied = 0; for (;;) { if (SessionLatched()) break; @@ -749,7 +773,33 @@ namespace MobileGL::MG_Remote::Server { bool corrupt = false; const bool popped = consumer.ApplyOne( [this, &applier](const Transport::RingRecordView& record) { + const auto applyStarted = std::chrono::steady_clock::now(); applier.ApplyOne(record); + // P65ServerFrame: timed around the ONE call that is the server's work, and + // summed across the drains a frame spans. See the block above DrainRing's loop. + sFrameApplyNs += static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - applyStarted) + .count()); + ++sFrameRecords; + if (record.kind == static_cast(MG_Pipe::MGPWireOp::Present)) { + const auto now = std::chrono::steady_clock::now(); + const auto wallNs = static_cast( + std::chrono::duration_cast(now - sFrameWallStart) + .count()); + const double applyMs = static_cast(sFrameApplyNs) / 1e6; + const double wallMs = static_cast(wallNs) / 1e6; + MGLOG_I("P65ServerFrame frame=%llu records=%llu apply_ms=%.1f wall_ms=%.1f " + "outside_ms=%.1f - apply_ms is the sum of PipeApplier::ApplyOne, " + "wall_ms is Present to Present on the apply thread, and the " + "difference is waiting for work plus retirement plus the socket", + static_cast(++sFrameIndex), + static_cast(sFrameRecords), applyMs, wallMs, + wallMs - applyMs); + sFrameWallStart = now; + sFrameApplyNs = 0; + sFrameRecords = 0; + } // THE TALLY MOVES HERE, INSIDE THE CALLBACK, AND NOT AFTER THE BATCH. s1's // SessionConsumer::ApplyOne publishes appliedSeq the instant this callback // returns, and publishing appliedSeq is what releases the client from the