Files
MobileGL/MobileGL/MG_Remote/Client/WireTables.h
T
BZLZHH b1ef484116 [Perf] (MG_Remote): the residency budget does not make the window usable either - the answer is gone before the client next looks, so a deferral needs RETENTION, not a budget
Steps 1 and 2 of the plan the design review set out, landed and measured on the
device. Step 1 answers the question step 2 depended on; step 2 answers the one the
whole line depended on, and the answer is that the window's shape is wrong.

STEP 1 - CAN AN ANSWER BE LOST IN DELIVERY? NO. ReplyPool now counts what the link
took (PostedReplies) and what it refused (FailedReplies), and a non-OK result is
NAMED instead of swallowed: it checked only BUFFER_TOO_SMALL and let
TRANSPORT_CLOSED vanish with no line and no counter, so "the server never sent it"
and "it was sent and the buffer overwrote it" were the same symptom with opposite
fixes. Measured: posted=4501 failed=0. The twenty-answer gap between the server's
tally and the client's was a sampling difference, not a drop - and the server's
m_answered counts ATTEMPTS (incremented before PostReply), which is why it could
never have settled it.

STEP 2 - DOES A RESIDENCY BUDGET FIX THE WINDOW? NO.
Wire_ResourceCreate now measures how many reply-owning records the client has
emitted since the oldest window entry was pushed (ClientSession::ReplyPostings, a
new clock) and takes the oldest answer back as soon as that approaches the reply
buffer's depth, instead of deferring past the point where the answer can still
exist. On the device:

    P12win call=4501 cfg=2 eff=2 pending=2 taken=63 pushed=65 blocked=4435
           budget=4451 readfail=8872 posts=54377
    kind=frame-op frame=14 wait_replies=9078 by_op=2=9071   wall 27.87 s

budget=4451: it fires on essentially EVERY create, the window stops deferring
(pushed=65), and the drain still reads nothing back. THE REASON IS STRUCTURAL, not
a constant:

  * the window only gets control WHEN A CREATE ARRIVES. Between two creates this
    client emits on the order of a thousand records - at the FIRST failed read the
    reply ring already spanned [1175..1504] while the head was seq 310 - and nothing
    looks at the window in that gap;
  * what displaces a deferred answer is not what this client emits, it is WHAT THE
    SERVER ANSWERS. The server is the one behind (one answer per ~5 ms wakeup while
    the client runs ahead), so the replies arriving during the gap are its LATE
    ANSWERS TO THE EARLIER CREATES - they fill the 8 slots first;
  * so the check fires too late by construction. Tightening it does not fix that; it
    only removes the deferral entirely, which is what pushed=65 is.

WHAT THAT LEAVES. A deferral is readable only if the client KEEPS the answers it
deferred. The buffer is bounded by COUNT and the client cannot bound how many
answers the server will post between two of its creates, so hoping the buffer still
holds one is a race the client loses - which is also why the earlier two diagnoses
(reply-pool churn, then FIFO-with-no-way-out) were both describing symptoms of the
same thing. The shape that can work is retention: the reader keeps the answers for
the seqs still outstanding (a handful), drops them when read, and reports a LOSS
when a read asks for one it no longer has. That is a transport change - StreamLink's
ring becomes a retention map with a cap and a lost counter - and it is the next step.

BOTH STEPS STAY: step 1 is a net gain on its own, and step 2 is the safe direction
(it can only degrade the window to no-deferral, i.e. the pre-window shape) and its
clock is the one the retention design needs. MOBILEGL_IPC_CREATE_WINDOW stays at 1.

172/172 and 13/13 after the probes were removed. The measurements and the revised
conclusion are in notes/p12/CREATE-WINDOW-MEASURED.md section 9.
2026-09-26 02:35:05 +08:00

150 lines
9.3 KiB
C++

// MobileGL - MobileGL/MG_Remote/Client/WireTables.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// THE CLIENT ARM OF R-17's ROUTING: the encode twin of `gMGPipeWireRecordApply`. Owner: c1.
//
// Thirty-eight thin emitters over `ClientSession::EmitAndWait`, installed over the two
// generated tables and the escape table that `MG_Pipe/PipeRoute.h` declares, so that under
// split every resource, CSO, texture and program record leaves the GL thread as a WIRE RECORD
// instead of executing synchronously against a context the apply thread now owns.
//
// WHAT ARMS `integration-split`. The 21 `DirectGLES.Split.*` entries skip on
// `ClientSession::Active() == nullptr`. `Install()` below is called from the END of a
// successful `ClientSession::Start()`, on the GL thread (the thread that called
// `MG_Backend::Init()`), AFTER the Hello/Welcome handshake, after the first CapsSnapshot has
// been adopted, and after the in-process server role's apply thread has been started. That
// order is not a preference:
// - after the handshake, because an emitter that published before Welcome would be writing
// into a ring the peer has not mapped;
// - after the first snapshot, because R-8's liveness gates read the caps mirror and a
// placeholder mirror consumes nothing, so a record emitted before it would be emitted to a
// server this client has not yet been told consumes that family;
// - after the apply thread exists, because `EmitAndWait` BLOCKS on `appliedSeq` and nothing
// would advance it - a barrier wait with no applier is the 30-second Fatal, not a hang;
// - on the GL thread, because that is the only thread that may touch `gPipeInputs` while the
// barrier holds (table 3), and installing from the apply thread would publish the table to
// the GL thread with no synchronisation at all.
// `Uninstall()` runs at the TOP of `Stop()`, before the rings go away, so the last thing any
// straggling GL-thread call reaches is a named refusal rather than a dangling session.
#pragma once
#include <Includes.h>
#include <MG_Pipe/MGPipeHandles.h>
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Remote::Client {
// Installs the thirty-eight wire emitters over gMGPipeScreen / gMGPipeContext /
// gMGPipeRouteEscapes and records the arm. Idempotent.
void InstallClientWireTables();
// Marks the routed tables uninstalled so a routed call refuses by name
// (Fatal{ClientTablesUninstalled, "<row>"}) rather than running the applier on the caller
// (codex 4). It does NOT restore the monolith adapters - that is ReinstallMonolithAfterTeardown
// below, run only once the session's rings are freed. Idempotent.
void UninstallClientWireTables();
// Shared by routed rows and the five class-B emitters, before session/ring access.
void RequireClientTablesInstalled(const char* row);
// The LAST step of ClientSession::Stop: after the rings, segments and transport are gone,
// puts the monolith adapters back and clears the refusal flag, so the at-exit ~BufferObject
// deletes that reach a process with no session run the applier as they do under monolith.
void ReinstallMonolithAfterTeardown();
// How many records the thirty-eight emitters have published. It counts the ROUTED rows
// only - a resource_create, a set_vertex_buffers, a create_shader_state - and never the
// five class-B verbs, so it is the one number that says "the resource/CSO/state path really
// ran" as opposed to "a Clear crossed".
//
// WHAT IT IS NOT (M7, corrected). This is NOT what arms the split lane. WireTables.h round 2
// claimed it was "t1's FOURTH arming fact", and that was wrong: the harness reads the
// encoder's EmitSeq (Harness/SplitRuntimePeek.cpp:50, Harness/ScenarioFixture.h:85), which
// moves for the class-B verbs too. So an armed Clear-only lane is green on EmitSeq while this
// counter stays 0. Making the harness read THIS instead is t1's file (SplitRuntimePeek), so
// the honest statement is the one here: c1 counts the routed ordinal at the only place that
// can, PipeFill's InitialBytesNotCarried self-check reads it (PipeFill.cpp:755), and the
// lane's own arming remains EmitSeq until t1 re-points it. c1-v3.md M7 has the full note.
Uint64 ClientWireRecordsEmitted();
// How many of those were refused by the server, by acceptance row. Counted rather than
// inferred, R-8's rule one level out.
Uint64 ClientWireRecordsDeclined();
// ---- P12: the CREATE WINDOW (MOBILEGL_IPC_CREATE_WINDOW) ---------------------------------
//
// The resource_create row defers its answer instead of dropping it: up to N creates go out
// fire-and-forget, the caller reads a provisional accept, and the answers are taken in order.
// The four numbers below are what a test can say about that without reaching into the row:
// how many answers are outstanding, the ceiling the code actually applies (the config value
// clamped to the array), the array's own size, and the two tallies - objects whose create came
// back DECLINED and are therefore out of the window, and how many refusals there have been.
Uint32 ClientCreateWindowPending();
// How many deferred answers the window has read back. THE NUMBER THAT SAYS IT WORKED: a
// drain that never collects leaves Pending at the ceiling and every other reading unchanged,
// which is exactly what the device measured before the stream link learned to answer by seq.
Uint64 ClientCreateWindowTaken();
// How many times the RESIDENCY BUDGET - not a full window - forced the blocking take: the
// oldest answer was about to outlive its reply slot. Non-zero is the budget doing its job;
// a value near the record count says the budget is too tight for this workload and the
// window is deferring almost nothing, which is the honest way to read that.
Uint64 ClientCreateWindowBudgetStops();
Uint32 ClientCreateWindowEffective();
Uint32 ClientCreateWindowArraySize();
Uint32 ClientCreateWindowSuspects();
Uint64 ClientCreateWindowRefusals();
// TRUE ON THE SERVER ROLE's OWN THREAD (the apply thread), false everywhere else. It is
// v1's ServerLoop::OnApplyThread(), exposed here because it is table 3's role split made
// into one predicate and TWO packages read it: the wire emitters below (a routed call that
// finds itself on the apply thread runs the monolith adapter, because on that thread this
// process IS the server), and MG_Impl/Pipe/PipeFill.cpp's split-only respecify/flush
// branches (M5: those branches emit CLIENT wire records and must not run on the server's
// apply thread, which produces none - the InitialBytesNotCarried self-check would abort the
// server otherwise). Declared here so PipeFill does not have to include a server header.
Bool RunsAsTheServerRole();
// ---- P5c (ct), CONTRACT-P5C.md §5: the two control records' client halves --------------
//
// NOT ROUTED ROWS - the two are appended to PipeCalls.def with no MGPipeApply* entry point
// and no table slot at all, so their producers call these directly. PipeFill.cpp's
// FreshlyPrimed arm calls the first; DirectGLES' OnFrontendStateObjectDestroyed calls the
// second.
// applier_reset (§5.1). Emitted on the GL thread at the tracker.FreshlyPrimed() edge,
// BEFORE the block's client-side resets, so the record's barrier orders the server's
// MGPipeApplierReset() against every verb that follows. ContextSerial is the client's own
// count of applier_reset emissions (0 = the first make-current): nothing in the frontend
// numbers a make-current today, and the sink asserts the value against its own count of
// the same records rather than dispatching on it (P5c: one context per session; P6 reads
// it for real). Returns false - nothing emitted - when no live session could carry the
// record (the bring-up window before Start, a server-role-only fixture, teardown); the
// caller then makes the direct call the record replaced.
Bool EmitApplierResetRecord();
// P5c (rv), CONTRACT-P5C.md §5.3: whether set_context_values can cross right now (a live,
// started session, tables not being torn down). PipeFill.cpp gates the record's emission
// AND the residual-fill skip for its eight fields on this one answer, so the two halves
// can never disagree about who supplies them.
Bool ContextValuesWireLive();
// The three answers a death notice can produce (§5.2): the record crossed; the client's
// own allocator cannot resolve the dying object (the server never saw it, so there is no
// twin to kill and NOTHING crossed); or no live session could carry the record. The
// third is NOT folded into the second: a server role with no wire at all - a ServerLoop
// fixture's shape, never a real split's - still has a server loop the caller delivers to,
// where a handle that never existed has nothing to deliver.
enum class ObjectDeathEmit : Uint8 { Emitted, NoHandle, NoSession };
ObjectDeathEmit EmitObjectDeathRecord(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId);
} // namespace MobileGL::MG_Remote::Client
#endif // MOBILEGL_BUILD_DISAGGREGATED