An independent review of the one design door the measurement left open (stop sending
the ~51,000 answers nobody reads, so the 8-slot pool stops churning and the create
window becomes collectable). Verdict: feasible, worth doing, and the proposal AS
WRITTEN would disable the thing it exists to enable.
THE CATCH. The windowed create is wantReply=false AND its answer is read -
WireTables.cpp's drain reads exactly those seqs. A bit keyed on "fire-and-forget"
therefore stops the server answering the creates the window is waiting for: the
window goes from "cannot be collected" to "there is nothing to collect", and the
readfail counter that is the only way to diagnose it disappears with it. It fails
silently, not loudly. The predicate has to be per-record "this answer will never be
read" - true of params / respecify-escape / sub-data, false of a windowed create -
carried as a separate input from wantReply (willReadReply), with
willReadReply && !rowCarriesReplySlot rejected rather than ignored.
VERIFIED HERE, not taken on trust (the reviewer's file:line claims, checked):
- the flag space is RingRecordFlags (Ring.h:281-285), bits 0-4 taken, BIT 5 FREE,
and the header's own table (PipeWire.inc:35-65) says "bit 5 kOptional vs (nothing)
no ring counterpart" - so a new ring bit aliases kOptional, which is exactly what
the assert at PipeWireCodec.cpp:142-145 refuses;
- the three static_asserts that will trip are real and deliberate
(PipeWireCodec.cpp:106, :142-145, RingTest.cpp:740);
- WireFingerprint mixes payload-layout, catalogue, render-state digests + OpCount,
none of which a ring bit moves - so no protocol revision bump is required, and
both cross-version directions degrade to today's behaviour. That makes this
"must upgrade both ends to take EFFECT", not "must upgrade both ends not to
BREAK" - one notch cheaper than handoff-2 section 3.2's protocol surface;
- StreamLink.cpp:564-571 is the independent progress flush (>= 64 records or
>= 1 ms) and :577 is PostReply piggybacking SendProgress on the reply - so
skipping replies coarsens progress granularity. Bounded, but unmeasured.
AND IT CORRECTED TWO OF MY OWN NUMBERS. The frame's reply count is 55,903, not
59,673: op 4 ResourceDestroy carries kNone in the catalogue (PipeWire.inc:179) and
PostReply Fatals on a row with no kReplySlot, so it never answers - I had added its
3,770 records to the total. The derived figures move with it (12.3 replies per create,
0.65 of a create of survival, 6,988 pool churns), and every copy is fixed here:
Config.h, ConfigLoader.cpp, WireTables.cpp's section comment, CONTRACT-P5E.md,
CREATE-WINDOW-MEASURED.md.
TWO MORE THINGS THE FOLLOW-UP MUST CARRY, both recorded in the note's section 7:
- LinkMetricsBeginReply counts ownsReplySlot, so once the flag lands by_op 2 falls
to ~0 - and section 1 of the note tells the reader to use by_op 2 to check whether
the window engaged. That check becomes a FALSE GREEN; the flag needs a
server-side "how many times did this op answer" counter beside it.
- the ~112,000 sendmsg/frame the flag removes are on the apply thread, so the
flag's own value is a measurable claim that nobody has measured.
172/172 and 13/13 after the comment corrections.
handoff-2 section 3.3 asks for a gate on the one scope that empties the applier's
object records while the frontend objects live on (MGPipeApplierReleaseObjectRecords,
"latent" because its only caller in the tree is MG_Test). The gate was expected green
for a CHANGED descriptor: the respecify is refused, RespecifyOnce turns the refusal
into a create plus one retry. It is not green.
MEASURED:
dropped-records probe: creates=0 applier_records=0 published=1
Nothing is emitted at all. The refusal can only reach the emitter through an answer
the row READS, and P12 made both rows answer themselves for a "confirmed" object:
the respecify is fire-and-forget (MGPipeResourceRespecifyWantsItsReply) and the
params row writes a PROVISIONAL kStatusOk (Wire_SetTextureParams). "Confirmed" is
MGPipeHandleIsPublished, which answers "did a create GO OUT" - and it stays true
after the records are dropped, which is the scope's whole definition. The only row
left that reads the real answer is create, and create is only sent when the latch is
clear - the one thing that stops being true.
So section 3.1's "clearing the latch is not enough" is the smaller half: nothing
clears the latch either, and nothing can until the applier can say "I hold nothing for
this handle" - section 3.2's third option, the reverse channel, i.e. the protocol
surface the handoff hoped to avoid paying for. That hope was priced on the trigger
being unreachable; it is still unreachable (no production caller), so the call stands,
but the cost of a production caller is now known to be the whole thing.
THE CASE IS WRITTEN AS A DEBT RATHER THAN A SPEC, and says so in its name and its
comment: it pins today's three numbers, so whoever lands the signal sees THIS case go
red and knows the repair started working. Removing the heal from RespecifyOnce would
not make it red - nothing reaches the heal - which is exactly why the assertion is on
the applier's table and the latch rather than on a count (handoff-2 section 3.3: "a
count cannot show this class of error").
Also rewritten: notes/p12/UNPUBLISH-GUARD-SIGNALS.md sections 4 and 6 now carry this
as 4.2 (sections 2 and 3 stand: neither caps generation nor FreshlyPrimed is a signal).
172/172.
The window was the handoff's remaining item: let up to N resource_create answers be
outstanding, read a provisional accept, collect the answers in order, and un-latch an
object whose late answer is DECLINED. It is a sound mechanism and the gates below pin
it - and on the device it does nothing at all, which is why the default is 1.
MEASURED, same device, same frame, same 101,421,396 B staged, same binary:
MOBILEGL_IPC_CREATE_WINDOW=4 by_op 2 = 4,536 wall 28.64 s
MOBILEGL_IPC_CREATE_WINDOW=1 by_op 2 = 4,536 wall 28.64 s
(the handoff's pre-window baseline: 31.3 s)
by_op 2 counts the creates that took the BLOCKING path (LinkMetricsBeginReply is
called with ownsReplySlot = rowCarriesReplySlot && wantReply), so it not moving means
no create deferred. A temporary counter in the row said why: pushed=24, blocked=4,476,
took=20, readfail=8,951, pending pinned at 4 - the drain READ the answers back and
failed every time.
WHY, IN THE PROTOCOL'S OWN ARITHMETIC. An answer is readable only until its pool slot
is reused, and the pool is kDefaultReplySlotCount = 8. The server answers EVERY
reply-owning record, not only the rows that wait, because it cannot know which those
are (ClientSession.cpp:1897-1900: "The server posts either way"). This frame posts
59,673 of them: create 4,536 + respecify 20,633 + params 18,056 + sub-data 12,671 +
the rest. 8 slots against 59,673 replies churns 7,459 times per frame, i.e. 13.2
replies between two creates, so an answer is overwritten after 8 replies - 0.61 of a
create. The deferral is not slow, it is UNREADABLE, and no window depth fixes that.
AND IT IS NOT NEUTRAL: with the window on, the provisional accept latches an object
the applier may have refused, and the un-latch that corrects it lives in that same
unreadable drain. A refusal would become an object the client believes is published -
the silent divergence the suspect list exists to prevent. So the machinery is kept
(one experiment away from usable: a deeper pool, or a "do not answer me" bit, both
wire changes) and the default is off.
Worktree note: the window implementation itself was uncommitted in the tree at
82ed1df9 (MOBILEGL-CS-HANDOFF-2.md section 2, "can compile, gates mostly green, never
run on a device"). This commit lands it in the state the measurement supports. The
row also gains the heal the suspect list was missing - a BLOCKING create that lands
takes its object back out of it, or kCreateSuspectMax recoveries would retire the
window for the rest of the process - and the clamp that ties the environment's value
to the array, with a static_assert tying the array to half the reply pool so "someone
raises kCreateWindowMax to 8 and steps on the pool" is a compile error.
Gates (RemoteClientControls.inc), all green, 171/171 RemoteClientTest:
ACreateWindowDefersTheAnswersAndTheWindowOffDoesNot - window on = 12 records 0 waits;
the SAME CHILD turns the knob to 1 and it is 12 records 12 waits (the negative
control); pending <= effective after every create; 99 clamps to the array; the
array is <= half the pool
ARefusedCreateUnlatchesItsObjectAndItsNextCreateBlocks - a real refusal (the
server's consumed mask narrowed) clears the latch, +1 suspect, the object's next
create BLOCKS (2 records 2 waits), and restoring the consumer HEALS it (latch
back, suspect -1)
Docs: CONTRACT-P5E.md section 2.5 rewritten (it still said all three rows keep
kWaitReply), plus
docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md (the numbers, the arithmetic,
the picture evidence: both arms SSIM 0.977) and
docs/Disaggregated/notes/p12/UNPUBLISH-GUARD-SIGNALS.md (handoff section 3: neither
caps generation nor FreshlyPrimed is a signal for "the applier dropped its records",
and the residual gap is one line - the dedup at TextureEmit.h:838 does not read the
latch, so an UNCHANGED descriptor after a drop produces no record and therefore no
refusal to self-heal from).
- The predicate is one function again: MGPipeResourceRespecifyWantsItsReply (PipeRoute.h) is read
by the wire emitter, so the two halves of this row cannot disagree about whether an answer is
owed. The buffer half never owes one (both call sites discard the Bool, PipeFill.cpp:1105/:1142);
the texture half owes one exactly until MGPipeHandleIsPublished, and that latch is set only by an
ACCEPTED create - which EmitResourceRespecify already sends, blocking, when it is clear
(TextureEmit.h:869-879). So the object pays once, at first use, and every record after it is
fire-and-forget.
- MEASURED on the device, same frame, same 101,421,396 B staged, stock power policy: waits
25,154 -> 4,543 and wall 130.9 s -> 31.3 s. The op split is the proof of what changed and what
did not: waits by_op 3=20611 -> absent (only 2=4536, 5=6, 9=1 remain), while the RECORDS sent
are unchanged - frame-sent still shows 3=20633 and 103,094 records total. The round trips went
away; the work did not.
- It also lands on an independent prediction: an audit run beside this work (mgl-device/scratch/
scheme-review.md) computed 4,543 remaining waits and a 24-37 s credible range (midpoint ~30 s)
from the archived logs, a host-inproc round trip of 35.4 us and a measured 43.1 MiB/s link.
Measured: 31.3 s, at the midpoint.
- Cumulative on that frame since this work started: 43,232 waits / 221.5 s -> 4,543 / 31.3 s (7.1x).
What is left is the create row (4,536 waits, ~23 s) - the confirmation itself, not waste - plus
hard floors of ~13.4 s client CPU and ~2.2 s of transfer. Going below that means batching the
create row, which keeps real answers.
- TWO DEBTS, STATED: (1) the invalidation guard for the one silent-loss case is NOT wired; the
audit confirmed independently that no production path can drop a texture record today, and if
one appears the notice it needs is protocol-surface (a tenth reverse callback). (2) No SSIM or
visual check has been taken since these rows stopped asking - frame-sent agreement is strong
evidence but it is not a picture.
- Two stale controls in older cases were fixed rather than left: the item-B gate now names the
CREATE row (its old total-based control only moved because a respecify was still waiting), and
the by-op measurement asserts records-sent for the rows that deliberately stopped waiting.
RemoteClientTest 169/169; red-once for the predicate is forcing it back to "always wait".
- The two halves of this row are not the same row. PipeFill.cpp:1105 and :1142 both call
MGPipeRouteResourceRespecify(desc, nullptr) and drop the Bool, so for a buffer the wait bought a
value that was thrown away. It goes fire-and-forget with the provisional-accept shape
resource_subdata has had since item B.
- The texture half keeps its answer ON PURPOSE: there the Bool is the self-heal trigger AND the
gate on the acceptance mirror, and TextureEmit.h:885-888 (ID-18 M3) says advancing that mirror on
an unconfirmed answer is how the next identical call gets suppressed "against a record that was
never stored". That half needs the provisional/really-accepted distinction plumbed through the
reply mailbox before it can stop waiting; it is not a flag flip.
- MEASURED, same frame, same 101,421,396 B staged: waits 25,176 -> 25,154 and by_op 3=20633 ->
3=20611. So the buffer half is worth 22 waits (0.1%) and the remaining 20,611 are texture
respecifies - which is what says the next step is the mirror design, not another flag. Wall
134.4 s -> 130.9 s.
- The ERROR-is-not-a-decline case moves to the texture half: a row that reads no answer cannot fold
ERROR into a decision, so the rule it exists for belongs to the half that still waits (the
transport fault is caught at the next row that does). New case
ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes pins both directions; red-once with the
branch removed is exit 155. RemoteClientTest 169/169.
- 2.9 established that a record's ~5 ms is mostly the DEVICE waking out of a deep idle state.
That points at a knob that is already wired: how long the server's apply thread spins before
parking. Raised from 50 us to 2000 us (via the Activity's existing `env` extra, which
ServerEnvironment.apply parses as KEY=VALUE;KEY=VALUE and which does not scrub
MOBILEGL_IPC_SPIN_US), the same 43,232-record frame goes 221.5 s -> 182.8 s (-17%), with
rtt_mean 5,040 -> 4,145 us. Pinning the device and disabling its deep idle states is worth
166.5 s (-25%) but needs root; the spin needs neither root nor a rebuild.
- The server log proves the knob landed (`mgl-srv-apply started ... spin 2000 us`) rather than
leaving the reader to trust an env var, and the note carries the exact am start line.
- So the load stall now has three ordered levers with numbers behind them: batch the waits
(client-only, not built), keep the server hot (this commit, usable today, trades device CPU for
wall clock on a frame that is 94% waiting), and pin the device for measurement discipline.
- Three one-variable experiments on the same device, same workload (the 43k-record frame): a 40x
larger client spin moves the per-wait mean by ~10% (so it is not the client park/wake); the
same frames over adb forward - a link whose raw RTT is several times lower - by ~25-30% (so it
is not the link, which is what the handoff saw in 3.6 and could not account for); and pinning
the device (governor performance on all eight cores, cpuoff_l / clusteroff_l / mcusysoff /
s2idle disabled) by 26% per wait and 25% of the whole frame: 221.5 s -> 166.5 s for 43,218
records and 100.5 MB staged, against 43,232 and 101.4 MB in the baseline.
- The mechanism: the server's apply thread parks between records, the SoC drops into a deep idle
state, and every record pays the exit latency - milliseconds, once per record, which is the
entire shape of the frame. It also matches the server profile the handoff took (65% socket
syscalls, ~15% doorbell spin: waiting and waking, not working).
- Stated where it matters: the handoff's 249 s was taken on an UNPINNED device, so its absolute
numbers carry this idle tax - the project has a pinning discipline for exactly this reason.
- The lever list is now ordered: batch the waits (K <= 8 by the reply-slot pool, client-only),
keep the server hot between records (its own spin budget, which the APK does not wire yet), and
measure split numbers on a pinned device. The device was restored to stock afterwards.
- What was open: the handoff's 4.3 called ResourceCreate and SetTextureParams "a second problem of
the same kind, needing its own analysis" and left it there; 5.4 said the per-op split of the
55,428 waits was never quantified. The measurement committed beside this answers both.
- The ruling (CONTRACT-P5E.md 2.5): ResourceCreate, ResourceRespecify and SetTextureParams stay
kWaitReply, because each answer carries a fact about the SERVER's record table that the client's
own tracker provably cannot know. RespecifyOnce says it outright - "the applier's REFUSAL is the
only signal that says 'I hold nothing for this handle'", which is the scope
MGPipeApplierReleaseObjectRecords leaves for a served context whose frontend objects live on;
EmitTextureParams uses its own refusal as the same self-heal trigger; PublishCreate may not take
the publication latch on a refused create (D-I1/c0b) or the death path emits a resource_destroy
for a record that does not exist. Unlike the upload half these are not "the emission gate already
answered it" rows, so removing their waits removes the signal.
- The numbers, both shapes a stitcher produces: an atlas stitch goes 64 -> 0 reply waits; a texture
per sprite goes 384 -> 320 with ResourceSubData now zero and the remainder all three of those
rows. So item B hits exactly the shape it was aimed at, and what is left is per-resource /
per-call rather than per-chunk.
- notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md gains 2.4 with the table and the ruling, and its "second
problem of the same kind" entry is closed rather than carried; MEASUREMENTS.md records the two
shapes beside the 1 -> 0 single-upload number.
- CONTRACT-P5E.md 2.5 said "the TEXTURE half keeps its wait because DrainTextureSubData clears the
level's dirty flag on the accepted reply (D-D5; trailing)". The trailing is paid, so the clause
now states the three facts that make that answer DERIVABLE on the wire arm (the emission gate IS
the consumer question the applier's own acceptance belt asks; what remains is a loud bug class;
a client-cancelled record still answers "not accepted"), where the wait moves to (SEG_STAGE),
that MGPipeSubDataWantsItsReply stays the one place that decides, and the gate with its red-once.
ResourceCreate and SetTextureParams are named as analyzed separately rather than swept in.
- notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md: 2.3 now carries the G1 A/B this machine ran - .text
13872002 bytes on both sides, 31746 symbols, 0 added / 0 removed, no per-symbol difference -
instead of deferring it to CI, and 3 trades its "G1 not run" item for the device tooling
(mgl-device/: launch-mc.py, gl-aliases.sh, start-phone-server.sh) the next round needs.
- notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md: the two items, their root causes with file:line anchors,
the five cases that pin them, the red each one produced when the fix was taken back out, and the
four things this round did NOT do (device end-to-end, local G1, the ResourceCreate /
SetTextureParams pair, and the sandbox's missing /dev/dri - which reds the two EventForfeitPeer
lanes for reasons that have nothing to do with the change).
- p12/README.md: a line in the summary and the new file in this directory's table.
- MEASUREMENTS.md: the P12 conclusion and row now carry the number the gate pins - one reply wait
per pure texture upload becomes zero - with the device wall clock named as the measurement still
owed rather than left implied.
- README, ARCHITECTURE, ROADMAP, MEASUREMENTS and CURRENT_STAGE_PROGRESS are rewritten as indexes
(84 KB -> 15 KB): what the project is, where it stands, and where each detail lives. ROADMAP is one
plain sentence per phase; MEASUREMENTS is the five conclusions that matter plus one line per phase;
CURRENT_STAGE_PROGRESS is the P12 goal, a checklist and the next steps.
- ARCHITECTURE's body moves unchanged into ten design/ files, keeping §1-§17 and appendices A/B, so
`ARCHITECTURE.md §N` references in code resolve through the table in ARCHITECTURE.md.
- guide/ holds build-and-run, code map, glossary and the engineering discipline (from README and
ROADMAP), and the device pinning record that used to sit in devices/.
- The open-debt table moves to notes/DEBTS.md; the detailed current-stage tables (with decision IDs,
the open line items and evidence locations) move into notes/p12/README.md. notes/README.md maps
every moved path and section.
- The documentation citation lint in test.yml also covers design/ and guide/, which describe the
current tree as the top-level files did.
- ROADMAP, MEASUREMENTS and ARCHITECTURE are rewritten as summaries (333 KB of index text -> 84 KB).
ROADMAP's phase table is one line per phase plus a link; MEASUREMENTS keeps headline numbers under
its old §1-§13 numbering (plus §14-§16 for P6.5, P7 and P12); ARCHITECTURE keeps every decided rule
under the same §1-§17 / appendix numbering, so the `ARCHITECTURE.md §N` references in code still
land, and §17 becomes an index of the phase landing shapes.
- CURRENT_STAGE_PROGRESS now describes the current stage (the P12 on-screen-server subset) and absorbs
CURRENT_STAGE_STATUS, which is removed. The P7 status tables, the P6 closing snapshot and the P6.5
first-wave snapshot move to notes/p7, notes/p6 and notes/p65.
- Each phase gets notes/<phase>/README.md: a short summary, then the verbatim material that left
the index documents (its ROADMAP row, its MEASUREMENTS section with subsection numbers kept, its
ARCHITECTURE §17 part, milestones) and a file index. P8-P11 and P13 get their planned scope;
the full open-question list moves to notes/OPEN-QUESTIONS.md.
- The phase reports that sat at the top level move into their phase folders with unchanged names:
P5D-INPROC-PERFORMANCE, P5E-RUNAHEAD, P5F-WIRE-COMPLETENESS, P6-SPAWN-PLAN, P6-CONTRACT-DRAFT,
P6-ENDSTATE-REVIEW. Their relative links, CONTRACT-P6.md's links to them and one tool docstring
are updated; notes/README.md maps every old path and section number to its new place.
- 71 files under notes/recovered/ that were byte-identical copies of phase-folder files are removed.
- Corrections made while condensing: the server process reads Transport as Spawn (CONTRACT-P6 §3.1
amended the old "child forces monolith" rule); both backends now publish kCapRunAheadApply;
MOBILEGL_IPC_RESPAWN is a named refusal and MOBILEGL_IPC_IDLE_EXIT_S is not parsed; the Redmi
pin row is in tools/device_bench/pin_device.sh.
check_doc_citations over docs/Disaggregated/*.md: 0 problems. No relative link is newly broken.
- unit 2605/2605, integration-spawn/tcp and the three Magma two-process lanes green, hygiene 12/12, census 79 / ratchet 171 unchanged, revision 3 pinned.
- Major 1 red without its fix ("Detach answered TimedOut in 3000 ms after 200 of 200 streamed records"), major 2 red without its fix (all four expectations), both green restored.
- SplitLogPaths / spawn_lane_parity / integration-gpu reds are registration artifacts of this container's CMake 3.28 (lane environment missing) and 4.4 (lane environment flattened into one variable; gate-review-d04ac7d1-cmake4.log); the four PersistentCoherentMapScenario and three F1WireScenario cases behave identically at cb0404f2 and at this head when the binary is run directly with the same environment. No CMake file changed since cb0404f2.
gate/gate-review-3c342c97.log: the P12 gate at head 3c342c97 (dirty=0) in a 4-core container with clang-19, lavapipe (mesa 25.2) and ctest 4.4.3 - unit 2605/2605 (the four new review cases included), integration-spawn/tcp and the three Magma two-process lanes green, hygiene 12/12, fatal census 79 / link ratchet 171 unchanged, protocol revision 3 holds. Still being attributed: SplitLogPaths and spawn_lane_parity (the container's ctest tooling; no CMake file changed since cb0404f2), four PersistentCoherentMapScenario and three F1WireScenario cases in integration-gpu (being run at base cb0404f2 with the same flags), and IterationRPProgram203Scenario (out of scope, ID-P12-12). gate-container.sh is gate.sh with the host paths removed and G1 run separately.
- notes/p12/INTEGRATOR-DECISIONS-P12.md (new): ID-P12-1..4 the subset's shape (the user rulings, one surface mode per session, the lost-window order, the in-process thread hand-off), ID-P12-5..11 the review round's ten fixes (two majors: the lost window answered before every ring record, the ended session's Espryt unit shadows emptied with its twins), ID-P12-12 IterationRP out of scope, ID-P12-13 the device stage not re-run in this environment.
- ARCHITECTURE.md §15.2: the deleted Surface -> AIDL window path is struck through, and the server-owned window path, the lost-window contract and the in-process session boundary are described.
- ROADMAP.md: the P12 row's status names the review round and that the device checks were not repeated.