[Perf] (P12): the create window lands as the default - declared-answer retention replaces the ring, and the load frame goes 28.66 s -> 16.52 s (11.02 s at window 4)

The three things that made the deferral inert are all fixed, in the order they were
found: the reply ring could not hold an answer across the ~1,000 records between two
creates; a residency budget could not help because what displaces an answer is what
the SERVER sends, not what the client emits; and so the mechanism had to be retention.

WHAT LANDED
  * ILink::DeclareReplyRead + LinkCapabilities::RetainsReplies. The wire declares a
    seq will be read BEFORE it publishes, so the declaration cannot race its own
    answer; a link with the capability false ignores it, and the window does not
    defer on one (CreateWindowEffective() is gated on it).
  * StreamLink's ring of the last 8 answers becomes a declared-answer store: a
    monotone whitelist plus a cursor, answers kept until their reader TAKES them
    (take-and-delete under one lock, bytes swapped into the scratch), and a capacity
    of 24 that is FATAL to exceed rather than evicting. An evicted wanted answer is
    unrecoverable - re-latching by sending another create wipes the server's record
    (PipeApply.cpp:2033) and its PendingUploads (PipeApply.h:332) while the client
    cleared its dirty flags at emission (PipeApply.h:298-304) - so the capacity is
    sized so it cannot be reached and reaching it says its own name.
  * An answer nobody declared is DROPPED, which is what keeps the store bounded by the
    client's deferral depth instead of by the server's answer rate. Retain-on-arrival
    grows ~11.3 entries per create against a server that ignores the no-reply bit.
  * A read that fails after WaitForApplied has returned Reached is now Fatal{ReplyMissing}
    naming the seq: on the stream arm the reply frame strictly precedes the progress
    frame that publishes appliedSeq, so that combination is a broken invariant, and
    retrying it (what the row used to do) wedges the window for the rest of the
    process while leaving every deferred object latched on a provisional accept.
  * The residency budget is deleted (WireTables.cpp keeps its history), the window
    retirement branch drains once before falling back - it could otherwise strand the
    answers it was holding - and Detach clears the store.

MEASURED ON THE DEVICE, load frame, both ends rebuilt, frame-sent identical per op
(create 4,536, records 103,097) throughout:

    window 1 (R-5's shape)              4,536 takes   28.66 s
    window 2 explicit                   2,500         17.27 s
    window 2 AS THE SHIPPED DEFAULT     2,563         16.52 s
    window 4                            1,644         11.02 s
    window 2 against an OLD SERVER      2,663         16.38 s   (zero Fatals)

The last row is the version-skew case the design review named as a must: a server
built before the no-reply bit answers ~55,903 records a frame, the whitelist drops
every answer nobody declared, and the deferral still reads its own back.

GATES (172 + 15 green), and the important one is proved to go red:
  * ADeclaredAnswerSurvivesFarMoreInterveningAnswersThanAnyFixedBuffer - reverting the
    store to "keep the last 8 arrivals" fails it immediately with "the declared answer
    did not survive 64 intervening ones". That is the line between the ring-1024
    stand-in and a bounded mechanism.
  * AnAnswerNobodyDeclaredIsNotKeptAtAll - red if everything is retained.
  * TheWindowRefusesToDeferOnALinkThatCannotRetainAnswers - on the in-process (ShmLink)
    arm the knob at 4 must still block every create, red if the capability gate goes.
  * The refused-create case now reads the repair that exists on THAT arm (the latch),
    and asserts the window's own books are empty there.

NOT DONE, and named: the window has no unit gate on the STREAM arm yet - only the
device runs. Three rounds of this feature were green in the lanes while inert on the
device because the lanes and the device run different transports; the transport-level
retention gates close that for the store, and the window-level one should be built on
StreamClientPublicationTest's real stream session next.
This commit is contained in:
BZLZHH
2026-09-26 03:31:14 +08:00
parent 1ef7439625
commit 3a2555805a
13 changed files with 416 additions and 202 deletions
+14 -3
View File
@@ -1898,10 +1898,17 @@ namespace MobileGL::MG_Remote::Client {
// creates or parameter sets, and those three have different fixes.
const auto replyMetricStart =
Transport::LinkMetricsBeginReply(ownsReplySlot, static_cast<std::uint32_t>(op));
// P12: the residency budget's clock. Placed HERE, where the record is certainly
// published, rather than beside rowCarriesReplySlot: a call that declined before
// encoding emits nothing and must not advance it.
// P12: the residency clock AND the declaration, both placed HERE - after EncodeRecord has
// an ordinal and BEFORE the publish, which is the only window in which the declaration is
// guaranteed to precede its own answer. A call that declined before encoding emits nothing
// and must not advance either.
if (rowCarriesReplySlot) ++m_replyPostings;
// THE DECLARATION THAT LETS A LINK RETAIN THIS ANSWER (P12). willReadReply, not wantReply:
// the create window emits without waiting and reads afterwards, so its records must be
// declared even though this call does not read them. Links that cannot retain ignore it,
// and the window does not defer on them (see ClientSession::LinkRetainsReplies).
if (rowCarriesReplySlot && willReadReply && m_link != nullptr)
m_link->DeclareReplyRead(seq);
m_producer.PublishAndNotify(seq);
if (op == MG_Pipe::MGPWireOp::Present) {
// A credit is permission to run ahead, not permission to retain the
@@ -2464,6 +2471,10 @@ namespace MobileGL::MG_Remote::Client {
Uint64 ClientSession::ReplyPostings() const { return m_replyPostings; }
Bool ClientSession::LinkRetainsReplies() const {
return m_link != nullptr && m_link->Capabilities().RetainsReplies;
}
Bool ClientSession::ReadReply(Uint64 seq, void* outBytes, Uint64 outCapacity, Int32* outStatus,
Uint64* outSize) {
const void* bytes = nullptr; Uint64 size = 0; Int32 status = 0;