Files
MobileGL/MobileGL/MG_Remote/Transport/ShmLink.cpp
T
BZLZHH 3a2555805a [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.
2026-09-26 03:31:14 +08:00

139 lines
6.1 KiB
C++

// SPDX-License-Identifier: LGPL-3.0-only
#include "ShmLink.h"
namespace MobileGL::MG_Remote::Transport {
LinkSignals ILink::Signals() {
auto* c = Memory().CmdControl();
return {&c->cmdHead, &c->submittedSeq, &c->consumerParked, &c->producerParked, &c->eventRingFull};
}
std::uint64_t ILink::SegmentSize(LinkSegment segment) {
auto& m = Memory();
switch (segment) {
case LinkSegment::Cmd:
return m.CmdRingCapacity();
case LinkSegment::Stage:
return m.StageBytes();
case LinkSegment::Reply:
return m.ReplyBytes();
case LinkSegment::Event:
return m.AnnouncedSize(SessionSegmentSlot::Event);
default:
return 0;
}
}
std::unique_ptr<ILink> CreateSharedLink(TransportRoleTag role) {
auto link = std::make_unique<ShmLink>();
link->SetRole(role);
return link;
}
void ShmLink::InitializeEndpoints() {
auto* c = m_memory.CmdControl();
m_commandsOut = RingProducer(c, m_memory.CmdRingBase(), m_memory.CmdRingCapacity(), RingCursorSet::Cmd);
m_commandsIn = RingConsumer(c, m_memory.CmdRingBase(), m_memory.CmdRingCapacity(), RingCursorSet::Cmd);
m_eventsOut =
EventRingProducer(m_memory.EventControl(), c, m_memory.EventRingBase(), m_memory.EventRingCapacity());
m_eventsIn = EventRingConsumer(m_memory.EventControl(), c, m_memory.EventRingBase(),
m_memory.EventRingCapacity(), m_memory.EventSegmentBase());
if (m_role == TransportRoleTag::ServerConsumer) {
ReplySlotPool pool(m_memory.ReplyBase(), m_memory.ReplyBytes(), m_memory.ReplySlotCount());
pool.Clear();
}
}
void ShmLink::BindDoorbells(Doorbell* c, Doorbell* p) {
m_consumer = c;
m_producer = p;
}
LinkCapabilities ShmLink::Capabilities() const {
const ReplySlotPool replies(m_memory.ReplyBase(), m_memory.ReplyBytes(), m_memory.ReplySlotCount());
// RetainsReplies stays false: a slot is seq % slotCount and the next answer into it wins.
return {true, true, true, m_memory.CmdRingCapacity() / 2, replies.MaxReplyBytes(),
/*RetainsReplies=*/false};
}
LinkProgress* ShmLink::Progress() {
return m_memory.Valid() ? &m_memory.CmdControl()->Progress : nullptr;
}
LinkEventFlags* ShmLink::EventFlags() {
return m_memory.Valid() ? reinterpret_cast<LinkEventFlags*>(&m_memory.CmdControl()->eventRingFull) : nullptr;
}
LinkArena* ShmLink::RecordArena() {
m_recordArena = {m_memory.CmdRingBase(), m_memory.CmdRingCapacity(), m_memory.CmdRingCapacity() - 1,
m_commandsOut.LocalHead()};
return &m_recordArena;
}
LinkCursor* ShmLink::RecordCursor() {
m_recordCursor = {m_memory.CmdRingBase(), m_memory.CmdRingCapacity(), m_memory.CmdRingCapacity() - 1,
m_commandsIn.LocalTail()};
return &m_recordCursor;
}
LinkArena* ShmLink::EventArena() {
m_eventArena = {m_memory.EventRingBase(), m_memory.EventRingCapacity(), m_memory.EventRingCapacity() - 1,
m_eventsOut.Ring().LocalHead()};
return &m_eventArena;
}
LinkCursor* ShmLink::EventCursor() {
m_eventCursor = {m_memory.EventRingBase(), m_memory.EventRingCapacity(), m_memory.EventRingCapacity() - 1,
m_eventsIn.Ring().LocalTail()};
return &m_eventCursor;
}
MobileGLResult ShmLink::StageBytes(std::uint64_t, LinkSpan*, void**) {
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult ShmLink::ResolveSpan(LinkSegment segment, LinkSpan span, const void** out) {
if (!out || !Attached()) return MOBILEGL_ERR_NOT_INITIALIZED;
const void* base = nullptr;
std::uint64_t size = 0;
switch (segment) {
case LinkSegment::Cmd:
base = m_memory.CmdRingBase();
size = m_memory.CmdRingCapacity();
break;
case LinkSegment::Stage:
base = m_memory.StageBase();
size = m_memory.StageBytes();
break;
case LinkSegment::Reply:
base = m_memory.ReplyBase();
size = m_memory.ReplyBytes();
break;
case LinkSegment::Event:
base = m_memory.EventSegmentBase();
size = m_memory.AnnouncedSize(SessionSegmentSlot::Event);
break;
default:
return MOBILEGL_ERR_UNSUPPORTED;
}
if (span.Offset > size || span.Size > size - span.Offset) return MOBILEGL_ERR_PROTOCOL_MISMATCH;
*out = static_cast<const std::uint8_t*>(base) + span.Offset;
return MOBILEGL_OK;
}
MobileGLResult ShmLink::PostReply(std::uint64_t seq, std::int32_t status, const void* payload, std::uint64_t size) {
ReplySlotPool replies(m_memory.ReplyBase(), m_memory.ReplyBytes(), m_memory.ReplySlotCount());
replies.Post(seq, status, payload, size);
return MOBILEGL_OK;
}
MobileGLResult ShmLink::ReadReply(std::uint64_t seq, std::int32_t* status, const void** payload,
std::uint64_t* size) {
if (!status || !payload || !size) return MOBILEGL_ERR_INVALID_ARGUMENT;
ReplySlotPool replies(m_memory.ReplyBase(), m_memory.ReplyBytes(), m_memory.ReplySlotCount());
return replies.ReadView(seq, status, payload, size) ? MOBILEGL_OK : MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
std::uint64_t ShmLink::EventPublishedHead() const {
return m_memory.Valid() ? m_memory.EventControl()->cmdHead.load(std::memory_order_acquire) : 0;
}
MobileGLResult ShmLink::WaitForEventDelivery(std::uint64_t head, std::uint32_t) {
return EventPublishedHead() >= head ? MOBILEGL_OK : MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
Doorbell& ShmLink::ConsumerBell() {
return *m_consumer;
}
Doorbell& ShmLink::ProducerBell() {
return *m_producer;
}
void ShmLink::Detach() {
m_commandsOut = {};
m_commandsIn = {};
m_eventsOut = {};
m_eventsIn = {};
m_memory.Close();
}
} // namespace MobileGL::MG_Remote::Transport