[Perf] (MG_Remote): The create window ships at 10, and the server now reports where its own frame time goes.

This commit is contained in:
BZLZHH
2026-09-26 15:30:48 +08:00
parent b017ec1c1f
commit 3a6c5c7f04
4 changed files with 87 additions and 3 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+35 -1
View File
@@ -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<unsigned long long>(g_blockTotal),
static_cast<unsigned long long>(g_blockNoWindow),
static_cast<unsigned long long>(g_blockSuspectsFull),
static_cast<unsigned long long>(g_blockSuspectObject),
static_cast<unsigned long long>(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;
}
+50
View File
@@ -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::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - applyStarted)
.count());
++sFrameRecords;
if (record.kind == static_cast<std::uint16_t>(MG_Pipe::MGPWireOp::Present)) {
const auto now = std::chrono::steady_clock::now();
const auto wallNs = static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::nanoseconds>(now - sFrameWallStart)
.count());
const double applyMs = static_cast<double>(sFrameApplyNs) / 1e6;
const double wallMs = static_cast<double>(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<unsigned long long>(++sFrameIndex),
static_cast<unsigned long long>(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