mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 08:38:30 +09:00
[Test] (MG_IntegrationTest): exit gate E3(e) drives the small ring past its own capacity and asserts the wrap, and both split arms of TriangleScenario publish R-10's maximum record bytes against the ring's cap (ID-65)
This commit is contained in:
@@ -47,7 +47,17 @@ namespace MGITest {
|
||||
if (session != nullptr) {
|
||||
// Encoder() returns the member; EmitSeq() returns m_emitSeq. Neither is a stub, and
|
||||
// neither emits anything - this is a read.
|
||||
state.emitSeq = session->Encoder().EmitSeq();
|
||||
const MobileGL::MG_Remote::Wire::PipeWireEncoder& encoder = session->Encoder();
|
||||
state.emitSeq = encoder.EmitSeq();
|
||||
// The producer's ledger. Every one of these is a plain member read on the encoder
|
||||
// or on the RingProducer it holds; none of them emits, publishes or waits, so a
|
||||
// case may read them between two GL calls without changing what the next record is.
|
||||
state.maxRecordBytes = encoder.MaxRecordBytesSeen();
|
||||
state.maxRecordBytesCap = encoder.MaxRecordBytesCap();
|
||||
state.cmdWraps = encoder.CmdWraps();
|
||||
state.cmdWrapPads = encoder.CmdWrapPads();
|
||||
state.cmdBytesWritten = encoder.CmdBytesWritten();
|
||||
state.stageReclaimWaits = encoder.StageReclaimWaits();
|
||||
}
|
||||
#endif
|
||||
return state;
|
||||
|
||||
@@ -70,6 +70,36 @@ namespace MGITest {
|
||||
unsigned int totalVerbSlots = 0;
|
||||
// The encoder's highest produced record ordinal, or 0 when there is no session.
|
||||
unsigned long long emitSeq = 0;
|
||||
|
||||
// ---- the wire producer's ledger, for exit gates E3(e) and R-10's proof obligation ---
|
||||
//
|
||||
// All four are 0 when there is no session, which is why every case that reads them has
|
||||
// to have passed SplitRuntimeSkipReason() first: 0 wraps in a process that never had a
|
||||
// ring and 0 wraps in a process whose ring never filled are the same number and
|
||||
// completely different facts.
|
||||
//
|
||||
// maxRecordBytes / maxRecordBytesCap: R-10 says P5 does no chunking and must prove it
|
||||
// needs none. The cap is RingProducer::MaxRecordBytes() == MOBILEGL_IPC_RING_MB / 2,
|
||||
// read from the ring this process actually got rather than recomputed from the
|
||||
// environment.
|
||||
//
|
||||
// cmdWraps / cmdWrapPads / cmdBytesWritten: SEG_CMD cannot go round until more bytes
|
||||
// have been written than the ring holds, so the byte count is the denominator without
|
||||
// which the wrap count means nothing - "0 wraps" is a defect after 1.25 MiB through a
|
||||
// 1 MiB ring and a tautology after 40 KiB. cmdWrapPads is the narrower R-9 event (a
|
||||
// record STRADDLED the boundary and needed a kRecPad filler) and is recorded rather
|
||||
// than asserted: a uniform record stride over a power-of-two ring lands on the
|
||||
// boundary exactly and never straddles it.
|
||||
//
|
||||
// stageReclaimWaits: SEG_STAGE allocations that only fitted after the encoder reclaimed
|
||||
// what the server had retired - P5's one real producer wait (see PipeWireCodec.h for
|
||||
// why the command ring has none while the verb barrier is armed).
|
||||
unsigned long long maxRecordBytes = 0;
|
||||
unsigned long long maxRecordBytesCap = 0;
|
||||
unsigned long long cmdWraps = 0;
|
||||
unsigned long long cmdWrapPads = 0;
|
||||
unsigned long long cmdBytesWritten = 0;
|
||||
unsigned long long stageReclaimWaits = 0;
|
||||
};
|
||||
|
||||
SplitRuntimeState PeekSplitRuntime();
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireLedgerChecks.h
|
||||
// Copyright (c) 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 TWO ASSERTIONS THE WIRE PRODUCER'S LEDGER MAKES POSSIBLE, in one place so the scenarios
|
||||
// that carry them cannot drift apart on what the numbers mean.
|
||||
//
|
||||
// R-10 (ExpectMaxRecordBytesUnderCap). P5 does no chunking and must instead PROVE it never
|
||||
// needs any: no record may exceed RingProducer::MaxRecordBytes() == MOBILEGL_IPC_RING_MB / 2.
|
||||
// Half of that proof is already a Fatal - PipeWireCodec.cpp aborts Fatal{RingOverrun} on a
|
||||
// record ABOVE the cap - and it is the loud half. The quiet half is the one this assertion
|
||||
// covers: the phase has to publish the MAXIMUM ACTUALLY SEEN on a real workload, so that a
|
||||
// record creeping towards the cap is visible before the day it crosses. Until this landed the
|
||||
// only readers of PipeWireEncoder::MaxRecordBytesSeen() were codec unit cases over synthetic
|
||||
// records, and the joint gate recorded BRIEF 8 item 3 as "Maximum record bytes: NO MEASUREMENT"
|
||||
// (joint-v1.md 5).
|
||||
//
|
||||
// R-9 / exit gate E3(e) (ExpectSmallRingWrappedAtLeastOnce). The SmallRing lane exists to run
|
||||
// the reduced path over a ring small enough to force the wrap path - the kRecPad filler that
|
||||
// both sides must SKIP WITHOUT ADVANCING seq, which is R-9's last clause and the one piece of
|
||||
// ring behaviour no other lane reaches. It ran green from the day it was registered and proved
|
||||
// nothing, because nobody counted: measured on the joint head, a whole split scenario writes
|
||||
// about 40 KiB into SEG_CMD (emitseq 22-71, maxrec 784 bytes), so a 1 MiB "small" ring is 25
|
||||
// times larger than the traffic and the head never comes near its wrap boundary. The lane and
|
||||
// the default lane were the same run under two names - which is exactly the shape ID-53's own
|
||||
// comment warned about and t1-v1.md carried as a debt ("WHAT THIS LANE DOES NOT YET ASSERT").
|
||||
//
|
||||
// So the lane now DRIVES ENOUGH WORK to overrun its own ring before it asserts, and the
|
||||
// assertion prints the denominator with the count: "0 wraps" is a defect after 1.25 MiB through
|
||||
// a 1 MiB ring and a tautology after 40 KiB, and a bare `EXPECT_GE(wraps, 1)` cannot tell those
|
||||
// apart. The driving is ordinary GL - clears and draws through the scenario's own objects - and
|
||||
// not a hand-built record: R-16 forbids an assertion that constructs the state it observes, and
|
||||
// a test that called Reserve directly would be asserting that the ring wraps, not that the
|
||||
// WORKLOAD makes it wrap.
|
||||
//
|
||||
// WHAT IS ASSERTED IS THE HEAD GOING ROUND, NOT THE kRecPad FILLER, and that distinction was
|
||||
// forced by a measurement rather than chosen: the first cut of this assertion read the pad
|
||||
// count, and 1310824 bytes of clears and draws through a 1 MiB SEG_CMD produced one and a half
|
||||
// trips round the ring and ZERO pads. The reason is arithmetic, not a defect - a workload whose
|
||||
// records repeat at a uniform stride that divides a power-of-two capacity lands on the boundary
|
||||
// exactly, every time - so a gate written against the pad count would have been red for the
|
||||
// sizes in the record catalogue and green the day one of them changed. The pad count is
|
||||
// RECORDED beside the wrap count (ringpads= on the stats line, in the JUnit properties, and in
|
||||
// the session's teardown ledger) so the number is available without being load-bearing.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "SplitLane.h"
|
||||
#include "SplitRuntimePeek.h"
|
||||
|
||||
namespace MGITest::WireLedger {
|
||||
|
||||
// The SmallRing lane declares MOBILEGL_IPC_RING_MB=1 (MG_IntegrationTest/CMakeLists.txt;
|
||||
// 1 MiB is ConfigLoader's floor for it). The byte target below is that size plus a
|
||||
// quarter: enough that the head MUST have crossed the wrap boundary, and far enough under
|
||||
// the DEFAULT 8 MiB ring that the same workload cannot wrap there - which is what makes
|
||||
// "raise the ring back to the default and this assertion goes red" a real control rather
|
||||
// than a description.
|
||||
inline constexpr unsigned long long kSmallRingLaneCmdByteTarget = (5ull << 20) / 4; // 1.25 MiB
|
||||
|
||||
// A bound on the drive loop, so a lane whose records shrank cannot spin forever. It is
|
||||
// generous on purpose: the loop's exit condition is the BYTE COUNT, and this only turns an
|
||||
// infinite loop into a named failure.
|
||||
inline constexpr unsigned int kSmallRingLaneMaxIterations = 200000u;
|
||||
|
||||
// R-10's reading, for any split lane. `where` names the case, because the number is a
|
||||
// MEASUREMENT this phase has to publish and a reader needs to know which workload produced
|
||||
// it.
|
||||
inline void ExpectMaxRecordBytesUnderCap(const char* where) {
|
||||
const SplitRuntimeState state = PeekSplitRuntime();
|
||||
ASSERT_TRUE(state.sessionActive)
|
||||
<< "the wire ledger was read in a process with no client session; the caller must "
|
||||
"pass SplitLane::SkipReasonForSplitOnlyAssertions() first, because every field of "
|
||||
"this ledger is 0 there and 0 is also a legal measurement";
|
||||
// Not merely "under the cap": a maximum of ZERO means the case emitted no record at all,
|
||||
// which satisfies `< cap` perfectly and is the exact shape of an emit table that
|
||||
// resolved the transport and then fell through to the driver.
|
||||
EXPECT_GT(state.maxRecordBytes, 0u)
|
||||
<< where << ": the largest record this session wrote is 0 bytes, so nothing crossed "
|
||||
"SEG_CMD. R-10's proof obligation has no subject and the lane did not go "
|
||||
"through the wire";
|
||||
EXPECT_LT(state.maxRecordBytes, state.maxRecordBytesCap)
|
||||
<< where << ": R-10 - the largest record this session wrote is " << state.maxRecordBytes
|
||||
<< " bytes and RingProducer::MaxRecordBytes() is " << state.maxRecordBytesCap
|
||||
<< " (half of a " << (state.maxRecordBytesCap * 2)
|
||||
<< " byte SEG_CMD, i.e. MOBILEGL_IPC_RING_MB). P5 does NOT chunk: a record at or "
|
||||
"above the cap is Fatal{RingOverrun} at the encoder, and a maximum that has "
|
||||
"climbed to it is the proof obligation failing. Report it to the integrator, who "
|
||||
"decides between early chunking (P8) and a bigger default ring";
|
||||
::testing::Test::RecordProperty("max_record_bytes",
|
||||
static_cast<int>(state.maxRecordBytes));
|
||||
::testing::Test::RecordProperty("max_record_bytes_cap",
|
||||
static_cast<int>(state.maxRecordBytesCap));
|
||||
}
|
||||
|
||||
// E3(e)'s reading. The CALLER drives the workload; this only reads the result, so that the
|
||||
// thing being asserted about is the workload and not this header.
|
||||
inline void ExpectSmallRingWrappedAtLeastOnce(const char* where,
|
||||
unsigned long long bytesDriven) {
|
||||
const SplitRuntimeState state = PeekSplitRuntime();
|
||||
ASSERT_TRUE(state.sessionActive)
|
||||
<< "the wire ledger was read in a process with no client session";
|
||||
const unsigned long long capacity = state.maxRecordBytesCap * 2; // MaxRecordBytes == cap/2
|
||||
// THE WRAP FIRST, AND THE DENOMINATOR RIGHT BEHIND IT, both as EXPECT so that a red
|
||||
// carries both sentences. Order matters for what the failure SAYS: the thing this gate
|
||||
// is about is the missing wrap, and "the loop pushed fewer bytes than the ring holds"
|
||||
// is the EXPLANATION for it, not a different failure. An ASSERT on the denominator
|
||||
// would print only the explanation and the reader would have to infer the gate - which
|
||||
// is how the red-once line for this control was measured, and why it is written this
|
||||
// way round.
|
||||
EXPECT_GE(state.cmdWraps, 1u)
|
||||
<< where << ": exit gate E3(e) - " << bytesDriven << " bytes were written into a "
|
||||
<< capacity
|
||||
<< " byte SEG_CMD and the producer's head NEVER WENT ROUND: no wrap, so this entry "
|
||||
"exercised exactly what the default lane exercises and the word SmallRing in its "
|
||||
"name asserts nothing. That is what the joint gate recorded as 'SmallRing entries "
|
||||
"ran, but no back-pressure wait count was measured' (joint-v1.md 6). The usual "
|
||||
"cause is the ring: MOBILEGL_IPC_RING_MB did not reach this process, or the lane "
|
||||
"was given the DEFAULT 8 MiB ring - which is exactly how this assertion was "
|
||||
"proved to be load-bearing (R-16), by re-running this entry's own command with "
|
||||
"MOBILEGL_IPC_RING_MB=8 and nothing else changed";
|
||||
EXPECT_GT(bytesDriven, capacity)
|
||||
<< where << ": and the reason is the denominator - the drive loop pushed only "
|
||||
<< bytesDriven << " bytes through a " << capacity
|
||||
<< " byte SEG_CMD, which cannot reach a wrap boundary at all. The loop stops at "
|
||||
"kSmallRingLaneCmdByteTarget, which is sized for the 1 MiB ring this lane "
|
||||
"declares (MGL_ITEST_GLES_SPLIT_SMALL_RING_ENVIRONMENT); a larger ring needs a "
|
||||
"larger workload and is not what this lane is for";
|
||||
::testing::Test::RecordProperty("ring_wraps", static_cast<int>(state.cmdWraps));
|
||||
::testing::Test::RecordProperty("ring_wrap_pads", static_cast<int>(state.cmdWrapPads));
|
||||
::testing::Test::RecordProperty("ring_waits", static_cast<int>(state.stageReclaimWaits));
|
||||
::testing::Test::RecordProperty("cmd_bytes_written", static_cast<int>(bytesDriven));
|
||||
}
|
||||
|
||||
// Bytes the producer has written so far, or 0 outside a split process.
|
||||
inline unsigned long long CmdBytesWritten() { return PeekSplitRuntime().cmdBytesWritten; }
|
||||
|
||||
} // namespace MGITest::WireLedger
|
||||
@@ -48,6 +48,8 @@
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitLane.h"
|
||||
#include "../Harness/WireLedgerChecks.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
@@ -163,6 +165,32 @@ void main() { oColor = vec4(vColor, 1.0); }
|
||||
EXPECT_TRUE(RegionIsMostly(image, 0, (w * 5) / 100, 0, (h * 5) / 100, color, 0.0, when));
|
||||
}
|
||||
|
||||
// EXIT GATE E3(e)'s DRIVE LOOP. Ordinary GL through this scenario's own objects -
|
||||
// a clear and a VBO-backed draw per iteration, no readback (a readback is a
|
||||
// SEG_REPLY round trip per iteration and would make this cost seconds rather than
|
||||
// milliseconds) - repeated until the producer has written more bytes into SEG_CMD
|
||||
// than the lane's ring holds. Returns the bytes this loop drove.
|
||||
//
|
||||
// NOTHING HERE TOUCHES THE RING DIRECTLY. The loop's only input is the producer's
|
||||
// own head cursor, read through Harness/SplitRuntimePeek, and its only output is
|
||||
// GL calls the scenario already makes. R-16: an assertion may not construct the
|
||||
// state it observes, and "the workload makes the ring wrap" is a different claim
|
||||
// from "a test can make the ring wrap".
|
||||
unsigned long long DriveUntilSmallRingOverruns() {
|
||||
const unsigned long long before = WireLedger::CmdBytesWritten();
|
||||
unsigned long long driven = 0;
|
||||
for (unsigned int i = 0; i < WireLedger::kSmallRingLaneMaxIterations; ++i) {
|
||||
ClearTo(0.0f, 0.0f, (i & 1u) ? 1.0f : 0.0f, 1.0f);
|
||||
glUseProgram(m_program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
// Cheap: this is a member read on the encoder, not a wire round trip.
|
||||
driven = WireLedger::CmdBytesWritten() - before;
|
||||
if (driven > WireLedger::kSmallRingLaneCmdByteTarget) break;
|
||||
}
|
||||
return driven;
|
||||
}
|
||||
|
||||
unsigned int m_program = 0;
|
||||
unsigned int m_vao = 0;
|
||||
unsigned int m_vbo = 0;
|
||||
@@ -205,6 +233,41 @@ void main() { oColor = vec4(vColor, 1.0); }
|
||||
"re-specification of either");
|
||||
ExpectClearedCorner(second, "black", "frame 1's clear, which is the only thing that changed");
|
||||
Gl().EndFrame();
|
||||
|
||||
// ---- the split lanes' two readings of the wire producer's ledger --------------------
|
||||
//
|
||||
// They are HERE, at the end of the steady-state case, and not in a case of their own,
|
||||
// for a reason that is about the gate and not about tidiness: `integration-split` is a
|
||||
// NAMED census (19 ran / 2 skipped by design) and a new entry moves it, so the phase
|
||||
// would have to re-baseline a number the joint gate just pinned. The measurement wants
|
||||
// this workload anyway - BRIEF 8 item 3 names this case - and a reading taken after the
|
||||
// case's own pixel assertions is a reading over a run that is known to have been
|
||||
// correct.
|
||||
//
|
||||
// Both are skipped, loudly and by the same predicate every other split-only assertion
|
||||
// uses, in the monolith lanes: there is no encoder there, and every field of the
|
||||
// ledger reads 0.
|
||||
const std::string skip = SplitLane::SkipReasonForSplitOnlyAssertions();
|
||||
if (!skip.empty()) {
|
||||
RecordProperty("wire_ledger_skip_reason", skip);
|
||||
return;
|
||||
}
|
||||
|
||||
// R-10's proof obligation over target B. Published in every split lane, small ring
|
||||
// included - the cap moves with MOBILEGL_IPC_RING_MB, so the SmallRing lane is also the
|
||||
// arm where a record closest to its cap would show up first.
|
||||
WireLedger::ExpectMaxRecordBytesUnderCap(
|
||||
"TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary");
|
||||
|
||||
// Exit gate E3(e). Only the small-ring lane drives the overrun: at the default 8 MiB
|
||||
// the same loop would take eight times as long to say the same thing, and the point of
|
||||
// the lane is that IT is the arm with a ring the workload can fill.
|
||||
if (SplitLane::IsSmallRingLane()) {
|
||||
const unsigned long long driven = DriveUntilSmallRingOverruns();
|
||||
Gl().EndFrame();
|
||||
WireLedger::ExpectSmallRingWrappedAtLeastOnce(
|
||||
"TriangleScenario.TheSameVboAndVaoRedrawAcrossAFrameBoundary", driven);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
|
||||
Reference in New Issue
Block a user