[Perf] (MG_Remote/Client): The create window's shipped depth is 6, and its ceiling 8.

This commit is contained in:
BZLZHH
2026-09-26 11:26:54 +08:00
parent 1f1ece8a33
commit 102f2ca7d5
8 changed files with 123 additions and 40 deletions
+19 -4
View File
@@ -572,14 +572,29 @@ namespace MobileGL::MG_Config {
// at 1), which is the measurement that says the ~5 ms is a WAKEUP the server batches away
// rather than its apply throughput - so amortising four creates per wakeup is a real 2.6x.
//
// 4 IS REACHABLE AND BETTER (11.02 s against 17.27 s) and is NOT the default only because
// the wider window holds more answers at once; raise it after a run that reads `by_op 2`
// and the wall clock rather than the config. NOTE THE PREDICATE the whole mechanism rests
// 6 IS THE DEFAULT, AND IT IS WHERE THE CURVE FLATTENS - the sweep that says so is the
// one the paragraph above asked for, and it moved the ceiling too (kCreateWindowMax is 8
// now; the old 4 was derived from a reply pool the window does not run on). Three
// interleaved runs per arm on one binary, load frame, create 4,536 sent in every arm:
//
// window 4 1,503 blocking takes 13.23 s median (spread 11.37-16.29)
// window 5 1,277 11.31 s (10.61-12.12)
// window 6 1,155 10.79 s ( 9.69-10.81)
// window 8 859 10.96 s ( 8.92-11.03)
//
// 4->5 buys 1.92 s, 5->6 buys 0.53 s, 6->8 buys NOTHING (-0.18 s, inside a session noise
// of ~1.5-2 s) - so 6 is the smallest window on the flat part and the extra answers a
// deeper one holds buy no time. AND PAST THIS POINT THE FRAME IS NOT WAIT-BOUND ANY MORE:
// window 8 pays 296 fewer blocking takes than window 6 and the wall clock does not move.
// The store is not the constraint either - its peak is logged as it crosses (StreamLink's
// P12 line) and reads 6..11 against a cap of 24 at every arm from 5 up. What is left is
// the constant ~350-400 creates that block for a reason depth does not reach, and the
// 101 MB / 103k records the SERVER has to apply. NOTE THE PREDICATE the whole mechanism rests
// on: an answer is retained because the wire DECLARED it will be read - NOT because its
// 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 = 2;
Uint32 CreateWindow = 6;
// 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
+14 -7
View File
@@ -412,12 +412,15 @@ namespace MobileGL::MG_ConfigLoader {
// xxHash64 changed since the last push are shipped.
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 2, and it works because a link that declares RetainsReplies
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
// was inert before and the device numbers (28.66 s at 1, 17.27 s at 2, 11.02 s at 4).
// Capped at 4: the retained set is bounded by the window's own depth, and 4 is what the
// declared-answer store's capacity is written against.
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 2, 1, 4);
// was inert before, the device numbers, and the sweep that put the default at 6 rather
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
// 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);
// 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
@@ -514,12 +517,16 @@ namespace MobileGL::MG_ConfigLoader {
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// One line, on the arm where these numbers decide behaviour, because every one of
// them is a number a bug report has to quote.
// CREATE-WINDOW IS IN THIS LINE BECAUSE A RUN HAS TO SAY WHICH WINDOW IT USED. It was
// missing while the value was a number nobody varied, and the moment it became one that
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
// log that does not name it makes every run ambiguous about its own configuration.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
"persistent-block=%uKiB "
"persistent-block=%uKiB create-window=%u "
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
ipc.PersistentBlockKb, ipc.AdoptTier,
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.AdoptTier,
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
+34 -12
View File
@@ -54,6 +54,9 @@
// P12: kDefaultReplySlotCount, because the create window's ceiling is derived from the pool it
// shares rather than from a number written twice (see kCreateWindowMax below).
#include <MG_Remote/Transport/ReplySlot.h>
// P12: kRetainedAnswersMax, the store capacity the window's own ceiling is derived from - the
// constant itself rather than a second literal, so the two cannot drift.
#include <MG_Remote/Transport/StreamLink.h>
// P12: LinkMetricsBeginReply/ReplyApplied, so the window's blocking take is visible in by_op 2.
#include <MG_Remote/Transport/LinkMetrics.h>
@@ -392,18 +395,37 @@ namespace MobileGL::MG_Remote::Client {
// 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
// other reply traffic.
constexpr Uint32 kCreateWindowMax = 4;
// THE WINDOW MAY NOT OWN MORE THAN HALF THE POOL, and this is where that stops being a
// sentence in a comment. The pool is kDefaultReplySlotCount slots addressed seq % count, so
// an answer is readable only while its slot has not been reused; every other reply-owning
// row - fence, query, and the blocking rows themselves - needs a slot too. The change the
// handoff names as the one worth pinning ("someone raises kCreateWindowMax to 8 and steps on
// the pool") goes red HERE, by name, at compile time, instead of as a wrong picture on a
// device. Raised to 8 it would take the whole pool and leave nothing for the answer the
// blocking rows are waiting on.
static_assert(kCreateWindowMax <= Transport::kDefaultReplySlotCount / 2,
"the create window may hold at most half the reply pool: the other half "
"belongs to the rows that take their answers immediately");
// (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;
// 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
// reused, and a window owning half of them would starve the rows that take their answers
// immediately - the change the handoff named as the one worth pinning, "someone raises
// kCreateWindowMax to 8 and steps on the pool". That is true of a link whose answers live
// in a SLOT POOL, and the window does not run on one: CreateWindowEffective() is gated on
// ILink::RetainsReplies, which is false for ShmLink, so on the pool-addressed transport
// the window is 1 and holds nothing. On the link where it does run - StreamLink - an
// answer is kept in the declared-answer store until its reader takes it, so depth costs
// STORE ENTRIES, not pool slots.
//
// SO THE SAME RULE IS RE-DERIVED AGAINST THE RIGHT OBJECT: at most half of StreamLink's
// kRetainedCap = 24, the other half being the one blocking row in flight and whatever a
// retired window has stranded. The store's overflow is a named Fatal and never an
// eviction, so exceeding this costs a run rather than a wrong picture - and the store
// logs its own peak as it crosses, which is what a future raise must be argued from.
//
// MEASURED, device load frame, three interleaved runs per arm: the curve had NOT flattened
// at the old cap (1->2 saved 10.29 s, 2->3 3.74 s, 3->4 1.83 s), which is why this
// constant moved at all.
static_assert(kCreateWindowMax <= Transport::StreamLink::kRetainedAnswersMax / 2,
"the create window may hold at most half of StreamLink's declared-answer "
"store: the other half belongs to the one blocking row in flight and to "
"whatever a retired window has stranded. The store's overflow is a named "
"Fatal rather than an eviction, so this bound is about losing a run, not "
"about a wrong picture");
// AND THE ENVIRONMENT MAY NOT RAISE IT EITHER. The configured value is clamped to the
// array's own size, so the fixed array below cannot be overrun by a config bound and this
// constant having drifted apart. 1 (and anything below it) is the pre-window shape.
+22 -1
View File
@@ -5,6 +5,9 @@
#include "Doorbell.h"
#include "ReplySlot.h"
#include "Framing.h"
// P12: the declared-answer store's crossing log. Same include as LinkMetrics.cpp's, rather than
// <Includes.h>, because this file is transport-layer and pulls nothing else from the tree.
#include <MG_Util/Debug/Log.h>
#include <algorithm>
#include <array>
#include <chrono>
@@ -138,7 +141,8 @@ namespace MobileGL::MG_Remote::Transport {
// cleared its dirty flags at EMISSION (PipeApply.h:298-304), so nobody re-sends those
// texels. The capacity is sized so that cannot be reached, and reaching it is a bug that
// says its own name.
static constexpr std::size_t kRetainedCap = 24;
// ONE DEFINITION, IN THE HEADER, because the client's window is bounded by this number.
static constexpr std::size_t kRetainedCap = StreamLink::kRetainedAnswersMax;
struct StoredReply {
std::uint64_t seq = 0;
std::int32_t status = 0;
@@ -153,6 +157,10 @@ namespace MobileGL::MG_Remote::Transport {
std::size_t wantCursor = 0;
std::vector<StoredReply> retained;
std::uint64_t skippedUnwanted = 0, unansweredWanted = 0, retainedTotal = 0;
// P12: the most declared answers this store ever held at once. The cap is an overflow
// Fatal rather than an eviction, so the MAXIMUM is the number that says whether a given
// window depth has room - a sample at teardown answers a different question.
std::uint64_t retainedPeak = 0;
std::uint64_t lastProgressSeq = 0;
std::chrono::steady_clock::time_point lastProgress = std::chrono::steady_clock::now();
LinkArena records{}, events{};
@@ -391,6 +399,19 @@ namespace MobileGL::MG_Remote::Transport {
slot.status = static_cast<std::int32_t>(static_cast<std::uint32_t>(b));
slot.bytes.assign(payload, payload + size);
++retainedTotal;
// LOGGED AS IT CROSSES EVERY QUARTER OF THE CAP, not sampled: at most
// four lines per run, and they are the ones a "how deep may the window
// go" question is answered from without a rebuild (P12's
// MOBILEGL_IPC_CREATE_WINDOW sweep reads exactly these).
if (retained.size() > retainedPeak) {
retainedPeak = retained.size();
if (retainedPeak % (kRetainedCap / 4) == 0) {
MGLOG_I("P12 store: peak=%llu of cap=%llu declared answers "
"outstanding; the window's depth is what is spent here",
static_cast<unsigned long long>(retainedPeak),
static_cast<unsigned long long>(kRetainedCap));
}
}
} else {
// NOBODY DECLARED IT: drop it. This is what keeps the store bounded
// against a server that answers records this client never reads.
@@ -38,6 +38,14 @@ namespace MobileGL::MG_Remote::Transport {
MobileGLResult ReadReply(std::uint64_t, std::int32_t*, const void**, std::uint64_t*) override;
void DeclareReplyRead(std::uint64_t) override;
// THE DECLARED-ANSWER STORE'S CAPACITY, and it is a PUBLIC NUMBER rather than a private
// one because the client's create window is bounded by it: WireTables.cpp's
// kCreateWindowMax static_assert and RemoteClientControls' ceiling case both read it, and
// a bound that is restated in prose in three places is a bound that drifts. The argument
// for the VALUE (why 24, and why overflow is a Fatal and never an eviction) is in the
// .cpp beside the store it sizes.
static constexpr std::size_t kRetainedAnswersMax = 24;
// P12 diagnostics for the declared-answer store. RetainedOutstanding is what the capacity
// is about (it must stay near the window's depth, not grow with the server's answers);
// RetainedTotal counts the answers kept for their readers; SkippedUnwanted counts the ones
+17 -14
View File
@@ -1232,24 +1232,27 @@ TEST(RemoteClientControls, TheWindowRefusesToDeferOnALinkThatCannotRetainAnswers
Uint8 pixels[4 * 4 * 4];
std::memset(pixels, 0x21, sizeof(pixels));
// ---- the clamp and the ceiling, before a single record goes out ----
// THE WINDOW IS OPT-IN AND THE DEFAULT IS OFF (Config.h: the deferral was measured on the
// device and the drain cannot collect it), so both arms of this case SET the knob rather
// than inheriting it - the ON half below is the mechanism being exercised, not a claim
// about what ships.
// BOTH ARMS SET THE KNOB rather than inheriting it: this case is about what the code does
// with a configured window, and not a claim about which value ships (the default is 2 -
// Config.h carries the device numbers).
// WHAT THE OPERATOR WROTE IS NOT WHAT THE CODE MAY USE. With the clamp gone a configured
// window of 99 is compared against a count that the ring keeps below its own size, so the
// fallback never fires and the push index runs off the array.
MG_Config::Ipc.CreateWindow = 99;
if (ClientCreateWindowEffective() != ClientCreateWindowArraySize()) ::_exit(170);
MG_Config::Ipc.CreateWindow = 4;
if (ClientCreateWindowEffective() != 4u) ::_exit(171);
// AND THE ARRAY IS SIZED FROM THE POOL IT SHARES, not from a second literal: the pool is
// Transport::kDefaultReplySlotCount slots addressed seq % count, and every reply-owning row
// (fence, query, the blocking rows themselves) needs one. Raising the window to the whole
// pool is the change the handoff names, and it is red here as well as at the static_assert
// in WireTables.cpp - deliberately twice, because the compile-time one is the guard and
// this one is the statement of what the guard is for.
if (ClientCreateWindowArraySize() > Transport::kDefaultReplySlotCount / 2) ::_exit(172);
MG_Config::Ipc.CreateWindow = 8;
if (ClientCreateWindowEffective() != 8u) ::_exit(171);
// AND THE ARRAY IS SIZED FROM THE STORE IT SHARES, not from a second literal (P12). This
// line used to read "at most half the REPLY POOL", which was true while an answer was read
// back out of a slot pool addressed seq % slotCount - and the window does not run on such
// a link: CreateWindowEffective() is gated on ILink::RetainsReplies, false for ShmLink, so
// the pool-addressed transport holds a window of 1. On the link where it does run, an
// answer waits in the declared-answer store instead, so the bound that matters is the
// store's - read from the constant rather than restated, for the reason the pool version
// read kDefaultReplySlotCount. WireTables.cpp's static_assert is the compile-time half of
// the same sentence, deliberately twice: that one is the guard, this one is what it is for.
if (ClientCreateWindowArraySize() > Transport::StreamLink::kRetainedAnswersMax / 2)
::_exit(172);
// TWELVE OBJECTS, ONE CREATE EACH, AND THE INVARIANT READ AFTER EVERY ONE. This is the
// reading a silent overwrite cannot fake: the count stays at the ceiling while an entry is
@@ -1268,7 +1271,7 @@ TEST(RemoteClientControls, TheWindowRefusesToDeferOnALinkThatCannotRetainAnswers
};
// ---- THE KNOB IS AS HIGH AS IT GOES, AND THE ROW MUST STILL BLOCK ----
MG_Config::Ipc.CreateWindow = 4;
MG_Config::Ipc.CreateWindow = ClientCreateWindowArraySize();
const Uint64 recordsBefore = Transport::LinkMetricsRecordsFor(createOp);
const Uint64 waitsBefore = Transport::LinkMetricsReplyWaitsFor(createOp);
burst(12);
@@ -37,6 +37,9 @@
#include <MG_Remote/Client/CapsMirror.h>
#include <MG_Remote/Transport/ReplySlot.h>
#include <MG_Remote/Transport/LinkMetrics.h>
// P12: StreamLink::kRetainedAnswersMax, the store capacity RemoteClientControls' window-ceiling
// case reads instead of restating it.
#include <MG_Remote/Transport/StreamLink.h>
#include <MG_Pipe/PipeRoute.h>
#include <MG_Remote/Client/EmitTables.h>
#include <MG_Remote/Client/WireTables.h>
@@ -262,7 +262,11 @@ namespace {
TEST_F(StreamClientWindow, EveryDeferredCreateAnswerComesBackOnTheShippingArm) {
const Uint32 savedWindow = MG_Config::Ipc.CreateWindow;
MG_Config::Ipc.CreateWindow = 4;
// THE DEEPEST WINDOW THE CODE SUPPORTS, so the case exercises the ceiling rather than a
// comfortable middle: more deferred answers outstanding means more of the store spent, and
// the crowding below is what has to survive it. (The CAP itself is pinned by
// RemoteClientControls' window-ceiling case; this one pins that the answers come back.)
MG_Config::Ipc.CreateWindow = 8;
StartServo();
const std::uint64_t takenBefore = Client::ClientCreateWindowTaken();
constexpr Int32 kCreates = 24;
@@ -331,7 +335,7 @@ namespace {
EXPECT_LE(pending, Client::ClientCreateWindowEffective());
// AND THE ROW REALLY IS DEFERRING HERE, so the case cannot pass by taking the blocking
// path on every create (which is what a link the window refuses to run on looks like).
EXPECT_EQ(Client::ClientCreateWindowEffective(), 4u);
EXPECT_EQ(Client::ClientCreateWindowEffective(), 8u);
EXPECT_TRUE(client.LinkRetainsReplies());
MG_Config::Ipc.CreateWindow = savedWindow;
}