mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Refactor, Test] (docs/ci, docs/Disaggregated, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Remote/Server, MG_Test/Util, MG_Test/Wire, MG_Util/Metrics, scripts/ci, tools/trace_replay): delete the MOBILEGL_TEST_SERVER_COUNTERS knob
- CtWireScenario reads `ar` / `od` off the server's PipeStats line on every arm - the serial == resets assertion could not fail and is dropped - ServerVerbSink's unread BindContexts, Presents and LastPresentSerial go too
This commit is contained in:
@@ -802,11 +802,13 @@ set(MGL_ITEST_GLES_VIEW_FILTER
|
||||
set(MGL_ITEST_SPLIT_ARM_ONLY_FILTER
|
||||
"PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer")
|
||||
|
||||
# The two lists are named so that SplitLogPaths.cmake.in can reach the ambient CtWireScenario
|
||||
# entries, which the integration job's inproc leg runs on the transport it exports.
|
||||
mgl_itest_lane("DirectGLES." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:${MGL_ITEST_GLES_VIEW_FILTER}:${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
|
||||
ENV "${MGL_ITEST_GLES_ENVIRONMENT}")
|
||||
TEST_LIST MGL_AMBIENT_TESTS_GLES ENV "${MGL_ITEST_GLES_ENVIRONMENT}")
|
||||
|
||||
mgl_itest_lane("DirectVulkan." "-${MGL_ITEST_MONOLITH_MECHANISM_FILTER}:${MGL_ITEST_SPLIT_ARM_ONLY_FILTER}"
|
||||
ENV "${MGL_ITEST_VULKAN_ENVIRONMENT}")
|
||||
TEST_LIST MGL_AMBIENT_TESTS_VULKAN ENV "${MGL_ITEST_VULKAN_ENVIRONMENT}")
|
||||
|
||||
# ScenarioIsolationScenario's two cases in ONE process: the first leaves a deleted program current
|
||||
# and an error unread, the second asserts ScenarioTest::SetUp cleared both. The entries above run
|
||||
@@ -3211,10 +3213,10 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
endif()
|
||||
|
||||
# P5c ct (docs/archive/contracts/CONTRACT-P5C.md §5): the two control records, split-only source for the
|
||||
# same reason as f1's - the scenario reads the SERVER sink's tallies, which a monolith build
|
||||
# has no symbol for. One block per case with a LOG PATH OF ITS OWN, the F1 pattern:
|
||||
# SplitLogPaths.PrivateAndDistinct fails a lane whose cases share one file, and the death
|
||||
# case's child truncates whatever path it inherits.
|
||||
# same reason as f1's - the scenario reads the server's control-record counters (`ar`, `od` on the
|
||||
# server role's PipeStats line), which only a build with a server role produces. One block per case
|
||||
# with a LOG PATH OF ITS OWN, the F1 pattern: SplitLogPaths.PrivateAndDistinct fails a lane whose
|
||||
# cases share one file, and the death case's child truncates whatever path it inherits.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
target_sources(MobileGLIntegrationTest PRIVATE Scenarios/CtWireScenario.cpp)
|
||||
# P7 wave 2 package C adds the fifth: the OQ-10 capability read. It belongs in THIS list
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
# CTest appends ENVIRONMENT here; keep all previously discovered lane settings.
|
||||
file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/split-logs")
|
||||
file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/verify-split-logs")
|
||||
# CtWireScenario reads the server's control-record counters (`ar`, `od`) off the server role's
|
||||
# PipeStats line, so its entries turn the counters on with one line per frame.
|
||||
set(mglCtWireStatsEnvironment "MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1")
|
||||
# P5b t2's two lanes ride the same rule: one private log path per entry, or
|
||||
# SplitLogPaths.PrivateAndDistinct is red for them (ID-53).
|
||||
# P5e vi's client-array lane joins the same list for the same reason: its two entries are the
|
||||
@@ -37,37 +40,34 @@ foreach(listName IN ITEMS @MGL_SPLIT_ARM_TEST_LISTS@)
|
||||
# P11 A2: every split arm runs T2, so the arena is declared emulated.
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MGITEST_PERSISTENT_MAP_ARM=emulated")
|
||||
elseif(entry MATCHES "CtWireScenario\\.")
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MOBILEGL_TEST_SERVER_COUNTERS=1")
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "${mglCtWireStatsEnvironment}")
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
# P7 package L tier 3: the whole-suite Magma census. A LIST OF ITS OWN, and the log path is the
|
||||
# ONLY property it gets - the loop above also arms the strict-arming, pmap and server-counter
|
||||
# ONLY property it gets - the loop above also arms the strict-arming, pmap and persistent-map-arm
|
||||
# lane markers by entry name, and a census is here to be READ, not to assert three unrelated
|
||||
# oracles it never opted into. The path itself is not optional: these entries are
|
||||
# DirectVulkan.{Split,Spawn,Tcp}. names, so split_log_paths.py's ownership gate requires exactly
|
||||
# one absolute private path each, and on the spawn and tcp arms two processes append to it.
|
||||
#
|
||||
# ONE EXCEPTION, AND IT IS TELEMETRY RATHER THAN AN ORACLE: CtWireScenario reads the server's
|
||||
# own control-record tallies, and without MOBILEGL_TEST_SERVER_COUNTERS=1 they are not published
|
||||
# at all - the case then reds on `server control-record telemetry missing`, which says nothing
|
||||
# about Magma. Measured: that is exactly what the spawn arm's ApplierReset case did on this
|
||||
# tier's first run. A switch that makes a counter readable is not the same kind of thing as the
|
||||
# strict-arming, pmap or persistent-map-arm markers above, which make a case ASSERT something it
|
||||
# never opted into, and those stay out of the census.
|
||||
# One exception, telemetry rather than an oracle: CtWireScenario's counters. Without them the case
|
||||
# reds on `server control-record telemetry missing`, which says nothing about Magma, and a switch
|
||||
# that makes a counter readable does not make a case assert anything it never opted into, unlike
|
||||
# the markers above.
|
||||
foreach(listName IN ITEMS @MGL_MAGMA_FULL_TEST_LISTS@)
|
||||
foreach(entry IN LISTS ${listName})
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
|
||||
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
|
||||
if(entry MATCHES "CtWireScenario\\.")
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MOBILEGL_TEST_SERVER_COUNTERS=1")
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "${mglCtWireStatsEnvironment}")
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
# P7 wave 3 (V1): the verify comparator on the split arm. A LIST OF ITS OWN, like tier 3's and
|
||||
# for tier 3's reason - the first loop also arms the strict-arming, pmap and server-counter lane
|
||||
# for tier 3's reason - the first loop also arms the strict-arming, pmap and persistent-map-arm lane
|
||||
# markers by entry name, and this lane is here to compare fields, not to opt three unrelated
|
||||
# oracles into a whole-binary set.
|
||||
# oracles into a whole-binary set. CtWireScenario's counters are the same exception as tier 3's.
|
||||
#
|
||||
# THE PATH IS WHAT MAKES THE LANE ARMED RATHER THAN MERELY GREEN. These entries carry no
|
||||
# MGITEST_SPLIT_LANE marker (MG_IntegrationTest/CMakeLists.txt says why: it reds 13 query-only
|
||||
@@ -80,8 +80,20 @@ foreach(listName IN ITEMS @MGL_VERIFY_SPLIT_TEST_LISTS@)
|
||||
foreach(entry IN LISTS ${listName})
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
|
||||
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/verify-split-logs/${entry}.log")
|
||||
if(entry MATCHES "CtWireScenario\\.")
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "${mglCtWireStatsEnvironment}")
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
# The ambient CtWireScenario entries take the counters and a log of their own as well: the
|
||||
# integration job's inproc leg runs them on the inproc transport it exports (under monolith they
|
||||
# skip). The log is outside split-logs/, which holds the split arms' entries.
|
||||
foreach(entry IN LISTS MGL_AMBIENT_TESTS_GLES MGL_AMBIENT_TESTS_VULKAN)
|
||||
if(entry MATCHES "CtWireScenario\\.")
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
|
||||
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/ct-wire-${entry}.log;${mglCtWireStatsEnvironment}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach(entry IN LISTS MGL_SPLIT_PA_STRICT_DRAW_TESTS MGL_SPLIT_PA_STRICT_DISPATCH_TESTS)
|
||||
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
//
|
||||
// applier_reset (§5.1): this process's first validate primes the pipe tracker, and the
|
||||
// FreshlyPrimed edge is the record's producer (MG_Impl/Pipe/PipeFill.cpp). What the scenario
|
||||
// asserts is the SERVER sink's own counters - moved by nobody else - plus the pixels, so a
|
||||
// reset that never crossed is red by the tally and a client that fell through to the driver
|
||||
// is red by ScenarioFixture's emit-ordinal rule.
|
||||
// asserts is the server's own count of the control records it applied (`ar` and `od` on the
|
||||
// server role's PipeStats line, moved by nobody else) plus the pixels, so a reset that never
|
||||
// crossed is red by the count and a client that fell through to the driver is red by
|
||||
// ScenarioFixture's emit-ordinal rule.
|
||||
//
|
||||
// object_death (§5.2): a texture and a framebuffer die, the deaths cross, and the slots
|
||||
// recycle. The picture after recycling is the behavioural half: a stale twin answering for
|
||||
@@ -31,8 +32,6 @@
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <Config.h> // `t6`: the spawn arm cannot observe the server process's counters
|
||||
#include <MG_Remote/Server/ServerSession.h>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <csignal>
|
||||
@@ -52,36 +51,6 @@
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
MobileGL::MG_Remote::Server::ServerVerbSink& ServerVerbs() {
|
||||
return MobileGL::MG_Remote::Server::ServerSessionInstance().Applier().Verbs();
|
||||
}
|
||||
|
||||
// Inproc reads its local sink. A separate-process session snapshots that
|
||||
// same sink on LogFlush, after the client fences its complete command prefix.
|
||||
// TCP forwards the server log; unix+shm reads the server's local role file.
|
||||
struct CounterSnapshot {
|
||||
bool valid = false;
|
||||
MobileGL::Uint64 resets = 0, serial = 0, deaths = 0;
|
||||
};
|
||||
|
||||
CounterSnapshot ServerCounters() {
|
||||
if (MobileGL::MG_Config::Transport != MobileGL::MG_Config::TransportMode::Spawn) {
|
||||
return {true, ServerVerbs().ApplierResets(), ServerVerbs().ExpectedApplierResetSerial(),
|
||||
ServerVerbs().ObjectDeaths()};
|
||||
}
|
||||
MGPipeSyncPeerLog();
|
||||
const std::string log = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::ServerLibraryLogPath());
|
||||
const auto at = log.rfind("MGPipe server counters:");
|
||||
if (at == std::string::npos) return {};
|
||||
const PipeStatsWindow::Window window{true, log.substr(at, log.find('\n', at) - at)};
|
||||
const auto resets = PipeStatsWindow::CounterOrAbsent(window, "resets");
|
||||
const auto serial = PipeStatsWindow::CounterOrAbsent(window, "reset-serial");
|
||||
const auto deaths = PipeStatsWindow::CounterOrAbsent(window, "deaths");
|
||||
if (resets < 0 || serial < 0 || deaths < 0) return {};
|
||||
return {true, static_cast<MobileGL::Uint64>(resets), static_cast<MobileGL::Uint64>(serial),
|
||||
static_cast<MobileGL::Uint64>(deaths)};
|
||||
}
|
||||
|
||||
class CtWireScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
@@ -109,8 +78,8 @@ namespace MGITest {
|
||||
}
|
||||
|
||||
// The texture's level-0 content, read back through a throwaway FBO, as four bytes.
|
||||
// The FBO is bound AND deleted here, so it never leaks a framebuffer death into the
|
||||
// tally a case is watching.
|
||||
// The FBO is bound AND deleted here, so its death lands in whichever window is open
|
||||
// and a case that counts deaths reads it outside the window it counts.
|
||||
void ReadTextureLevel(GLuint texture, GLubyte out[4]) {
|
||||
GLuint fbo = 0;
|
||||
glGenFramebuffers(1, &fbo);
|
||||
@@ -120,6 +89,47 @@ namespace MGITest {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
}
|
||||
|
||||
// The control records the server applied in one window: `ar` (applier_reset records
|
||||
// accepted) and `od` (object_death records dispatched) on the server role's PipeStats
|
||||
// line. The entries set MOBILEGL_PIPE_STATS=1 and MOBILEGL_PIPE_STATS_PERIOD=1, so every
|
||||
// swap ends a window and writes one line: under inproc the apply thread writes it to the
|
||||
// server role's log, under spawn and tcp the server process writes or forwards it there.
|
||||
struct ControlRecords {
|
||||
bool valid = false;
|
||||
long long applierResets = -1;
|
||||
long long objectDeaths = -1;
|
||||
};
|
||||
|
||||
// Ends the current window. The records and the present that ends it are kWaitNone, so
|
||||
// the client moves on and the server counts them when it applies them. A readback is
|
||||
// kWaitReply on every arm, and its answer is posted only after its record and every
|
||||
// earlier one were applied, the present and the line it writes included. The harness's
|
||||
// applied-seq wait is not that boundary off inproc: there the client's progress page
|
||||
// moves with frames rather than with the peer's apply.
|
||||
void EndWindow() {
|
||||
Gl().EndFrame();
|
||||
GLubyte fence[4]{};
|
||||
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, fence);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the window's fence readback left a GL error behind";
|
||||
}
|
||||
|
||||
// Ends the setup's window, so that the next one counts only what follows the mark.
|
||||
PipeStatsWindow::LogMark StartWindow() {
|
||||
EndWindow();
|
||||
return PipeStatsWindow::MarkLaneLog();
|
||||
}
|
||||
|
||||
ControlRecords EndWindowAndReadControlRecords(const PipeStatsWindow::LogMark& mark) {
|
||||
EndWindow();
|
||||
const PipeStatsWindow::Window window = PipeStatsWindow::LastFromServerLogSince(mark);
|
||||
if (window.found) RecordProperty("stats_line_server", window.line);
|
||||
ControlRecords records;
|
||||
records.applierResets = PipeStatsWindow::CounterOrAbsent(window, "ar");
|
||||
records.objectDeaths = PipeStatsWindow::CounterOrAbsent(window, "od");
|
||||
records.valid = records.applierResets >= 0 && records.objectDeaths >= 0;
|
||||
return records;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(CtWireScenario, ApplierResetCrossesAtThePrimedEdgeInSerialOrder) {
|
||||
@@ -127,25 +137,25 @@ namespace MGITest {
|
||||
// The first verb of the process primes the tracker; the edge is the producer. The
|
||||
// clear also gives the case its pixels, so a lane that emitted nothing is red twice
|
||||
// (here and in the fixture's emit-ordinal rule).
|
||||
const PipeStatsWindow::LogMark mark = PipeStatsWindow::MarkLaneLog();
|
||||
glClearColor(0.2f, 0.4f, 0.6f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
const auto counters = ServerCounters();
|
||||
ASSERT_TRUE(counters.valid) << "server control-record telemetry missing";
|
||||
EXPECT_GE(counters.resets, 1u)
|
||||
<< "the FreshlyPrimed edge emitted no applier_reset; the server's g_applier was "
|
||||
"never reset for this session";
|
||||
// The serial sequence is the session's own count (§1: asserted, never dispatched
|
||||
// on): every record the sink accepted carried the serial it expected, or the counter
|
||||
// and the accepted tally would disagree.
|
||||
EXPECT_EQ(counters.serial, counters.resets)
|
||||
<< "an applier_reset record was dropped or replayed on the wire";
|
||||
|
||||
GLubyte pixel[4]{};
|
||||
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "Ct.ApplierReset.error";
|
||||
const int expected[4] = {51, 102, 153, 255};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
EXPECT_NEAR(pixel[i], expected[i], 1) << "Ct.ApplierReset.pixels";
|
||||
|
||||
// No swap precedes this one in the process, so its window holds the edge wherever the
|
||||
// first validate ran (this clear or the fixture's state reset). The serial needs no
|
||||
// check of its own: OnApplierReset counts only a record whose serial it accepted, and
|
||||
// any other latches Fatal{ProtocolCorruption, "ApplierReset.ContextSerial"}.
|
||||
const ControlRecords counters = EndWindowAndReadControlRecords(mark);
|
||||
ASSERT_TRUE(counters.valid) << "server control-record telemetry missing";
|
||||
EXPECT_GE(counters.applierResets, 1)
|
||||
<< "the FreshlyPrimed edge emitted no applier_reset; the server's g_applier was "
|
||||
"never reset for this session";
|
||||
}
|
||||
|
||||
TEST_F(CtWireScenario, TextureDeathCrossesAndTheRecycledSlotAnswersTheNewObject) {
|
||||
@@ -159,7 +169,7 @@ namespace MGITest {
|
||||
// went backend-agnostic and that the INPROC arm failed here with `deaths` 0 vs 0
|
||||
// (docs/archive/notes/p7/magma-two-process-first-run.md §6); the table is what closes the inproc
|
||||
// half, and this case is the gate on it. It is also the RED-ONCE for that table: with
|
||||
// the install site short-circuited, both cases red on the two EXPECT_GT below.
|
||||
// the install site short-circuited, both cases red on their `od` assertions below.
|
||||
const GLubyte red[4] = {255, 0, 0, 255};
|
||||
const GLubyte green[4] = {0, 255, 0, 255};
|
||||
|
||||
@@ -176,45 +186,14 @@ namespace MGITest {
|
||||
ASSERT_NEAR(before[i], redExpected[i], 1) << "Ct.TextureDeath.before";
|
||||
|
||||
// The object dies unbound so the destructor - and the death record - fire at the
|
||||
// delete, and the tally is read AFTER the EmitAndWait the delete blocked on.
|
||||
// delete. The counted window holds the delete alone: the FBO ReadTextureLevel deleted
|
||||
// died in the setup's window, which StartWindow ends.
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
const auto beforeCounters = ServerCounters();
|
||||
ASSERT_TRUE(beforeCounters.valid) << "server object-death telemetry missing";
|
||||
const MobileGL::Uint64 deathsBefore = beforeCounters.deaths;
|
||||
const PipeStatsWindow::LogMark mark = StartWindow();
|
||||
glDeleteTextures(1, &texture);
|
||||
// THE FENCE, AND WHY IT IS NO LONGER A CLEAR.
|
||||
//
|
||||
// object_death is a kCtxObject value-class record whose wait class is kWaitNone, so
|
||||
// under run-ahead the client publishes it and moves on, and the server-side tally below
|
||||
// only moves at APPLY time. The clear this case used to fence on is kWaitNone TOO -
|
||||
// PipeCalls.def's Clear row - so once the wait column governed (P5e's run-ahead), it
|
||||
// waited for nothing and the read below raced the apply thread. Nothing had hidden that
|
||||
// for a while except the per-record barriers the converted rows still took; P12 took
|
||||
// those away on the wire arm (set_texture_params, respecify and the texture sub-data
|
||||
// stop owning an answer), the client now runs further ahead, and the race stopped being
|
||||
// a rarity. MEASURED on this entry under llvmpipe: 2 red of 5 runs (`deaths` 1 vs 1,
|
||||
// the record applied a moment later) and 3 green of 3 with MOBILEGL_IPC_RUN_AHEAD=0. A
|
||||
// timing race, never a lost record: the notice is emitted and applied either way.
|
||||
//
|
||||
// WHAT IS A REAL BOUNDARY, MEASURED ON ALL THREE ARMS RATHER THAN ASSUMED. The
|
||||
// harness's applied-seq fence (WaitForSplitAppliedForTesting) is the right shape for
|
||||
// inproc - and it is NOT usable here: this case is registered on the spawn and tcp arms
|
||||
// too, the client's progress page there is advanced by frames rather than by the
|
||||
// peer's own apply, and the wait times out. MEASURED with that fence in place: this
|
||||
// case red at 5.0 s on DirectGLES.Tcp.Ct.
|
||||
//
|
||||
// A READBACK IS THE ONE BOUNDARY THAT HOLDS ON EVERY ARM. ReadPixels is
|
||||
// kReplySlot|kWaitReply, so the client blocks on its answer, and an answer is posted
|
||||
// only after its record has been APPLIED - and the ring applies in order, so the death
|
||||
// published before it is applied too. (On the spawn and tcp arms the counter below
|
||||
// comes from the peer's log, which ServerCounters() syncs before it reads - and that
|
||||
// sync is itself a round trip the death has already crossed.)
|
||||
GLubyte fencePixel[4]{};
|
||||
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, fencePixel);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the fence readback left a GL error behind";
|
||||
const auto afterCounters = ServerCounters();
|
||||
ASSERT_TRUE(afterCounters.valid) << "server object-death telemetry missing";
|
||||
EXPECT_GT(afterCounters.deaths, deathsBefore)
|
||||
const ControlRecords counters = EndWindowAndReadControlRecords(mark);
|
||||
ASSERT_TRUE(counters.valid) << "server object-death telemetry missing";
|
||||
EXPECT_GE(counters.objectDeaths, 1)
|
||||
<< "the texture's death produced no object_death record; the server's twin was "
|
||||
"never told to let go";
|
||||
|
||||
@@ -323,17 +302,15 @@ namespace MGITest {
|
||||
// Die unbound, framebuffer first so the attachment's own death is a separate record.
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
const auto beforeCounters = ServerCounters();
|
||||
ASSERT_TRUE(beforeCounters.valid) << "server object-death telemetry missing";
|
||||
const MobileGL::Uint64 deathsBefore = beforeCounters.deaths;
|
||||
const PipeStatsWindow::LogMark mark = StartWindow();
|
||||
// THE CLIENT'S OWN ACCOUNTING, read beside the server's (P7 wave 2 package C,
|
||||
// CONTRACT-P7 §5.5's PipeSlotPeek clause). The two numbers answer DIFFERENT halves of
|
||||
// one death and neither implies the other, which is exactly why both are read here:
|
||||
// * `deaths` below says the SERVER was told - the half the missing Magma table
|
||||
// * `od` below says the SERVER was told - the half the missing Magma table
|
||||
// broke, and the only delivery a framebuffer has (no wire delete opcode at all);
|
||||
// * this one says the CLIENT returned the slot. That half has been backend-neutral
|
||||
// since P4a (PipeFill.cpp's NotifyAndFree frees whatever the notice does), so it
|
||||
// is a CONTROL rather than the subject: if it ever regressed, a green `deaths`
|
||||
// is a CONTROL rather than the subject: if it ever regressed, a green `od`
|
||||
// would be measuring an object that never let go of its handle.
|
||||
// `false` means the allocator is out of reach (a pull build, or Android's hidden
|
||||
// symbols), and the peek is then simply not asserted - "could not look" is not
|
||||
@@ -341,20 +318,9 @@ namespace MGITest {
|
||||
unsigned fboSlotsBefore = 0;
|
||||
const bool slotsReadable = PeekPipeSlotLiveCount(PipeSlotKind::Framebuffer, &fboSlotsBefore);
|
||||
glDeleteFramebuffers(1, &fbo);
|
||||
// THE FENCE, the texture case's exactly: object_death is a kCtxObject value-class
|
||||
// record with wait class kWaitNone, so under run-ahead it is published WITHOUT waiting
|
||||
// for its own apply (production-safe: the in-order ring applies it before any record
|
||||
// that recycles the slot), and the server-side counter this assertion reads only moves
|
||||
// at apply time. A clear cannot be that boundary - its own class is kWaitNone too - so
|
||||
// this waits on a READBACK, which is kWaitReply on every arm. The texture case carries
|
||||
// the measurements, including the boundary that is right for inproc and wrong for
|
||||
// spawn/tcp.
|
||||
GLubyte fencePixel[4]{};
|
||||
glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, fencePixel);
|
||||
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the fence readback left a GL error behind";
|
||||
const auto afterCounters = ServerCounters();
|
||||
ASSERT_TRUE(afterCounters.valid) << "server object-death telemetry missing";
|
||||
EXPECT_GT(afterCounters.deaths, deathsBefore)
|
||||
const ControlRecords counters = EndWindowAndReadControlRecords(mark);
|
||||
ASSERT_TRUE(counters.valid) << "server object-death telemetry missing";
|
||||
EXPECT_GE(counters.objectDeaths, 1)
|
||||
<< "the framebuffer's death produced no object_death record - and no other "
|
||||
"opcode can carry it. Backend "
|
||||
<< Gl().BackendName();
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <MG_Util/Metrics/TextureMetrics.h>
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -274,10 +275,10 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// backend->SwapEGLBuffers -> BackendObject::SwapEGLBuffers -> eglSwapBuffers, and never
|
||||
// Present() - so the two paths do NOT both end here. The frame count staying in step with
|
||||
// the swap count rests entirely on c1 emitting exactly one present record per swap;
|
||||
// nothing here compares Presents() to a swap count. If that drifts, the frame fence and
|
||||
// nothing here compares the present count to a swap count. If that drifts, the frame fence and
|
||||
// TrimBufferPool's recycle watermark stop tracking frames - which is the reason the 1:1
|
||||
// was wanted, recorded here so a future swap-without-present is looked for rather than
|
||||
// assumed impossible. Presents() is exposed for a lane that wants to make the comparison.
|
||||
// assumed impossible.
|
||||
// The frame's damage rides to the native present for the length of this call.
|
||||
MG_Backend::SetCurrentPresentDamage(DamageOfRecord(present.DamageCount, present.Damage));
|
||||
table->Present();
|
||||
@@ -302,7 +303,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
"waits on it for its credit (CONTRACT-P5E §2.4); returning a credit for "
|
||||
"serial 0 would release a wait that is asking about frame N");
|
||||
}
|
||||
m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
|
||||
const Uint64 serial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
|
||||
// §2.4's other half, and the reason ServerSession::ReturnPresentCredit has had no
|
||||
// production caller since v1 wrote it: ONE CREDIT PER SWAP, returned after Present()
|
||||
// has returned rather than before it, because what the client is waiting for is the
|
||||
@@ -314,7 +315,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// or two ago at once, and a report posted only after this batch would race it. A
|
||||
// status query, no submit: the swap has just submitted.
|
||||
if (session->EventRingHasRoom()) ReportSignaledFences(/*flush=*/false);
|
||||
session->ReturnPresentCredit(m_lastPresentSerial);
|
||||
session->ReturnPresentCredit(serial);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -350,7 +351,8 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// MGPipeApplierReset passes because this IS the apply thread; the GL-thread direct
|
||||
// call it replaced is the Fatal arm.
|
||||
MG_Pipe::MGPipeApplierReset();
|
||||
++m_applierResets;
|
||||
if (MG_Util::PipeStats::Enabled())
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ApplierResets, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -608,7 +610,6 @@ namespace MobileGL::MG_Remote::Server {
|
||||
"release) is the only token that needs no entry",
|
||||
static_cast<unsigned long long>(bind.ClientContextToken));
|
||||
}
|
||||
++m_bindContexts;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md). THE NATIVE HALF OF THE SWITCH.
|
||||
//
|
||||
@@ -658,7 +659,7 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// to nothing.
|
||||
MG_Backend::DirectGLES::ReleaseTwinsForWireObjectDeath(death.Handle,
|
||||
static_cast<MG_Pipe::MGPipeKind>(death.Kind));
|
||||
++m_objectDeaths;
|
||||
if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ObjectDeaths, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -183,20 +183,6 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Bool OnSharedImage(const MG_Pipe::MGPSharedImageOp& op, Uint64 seq,
|
||||
MG_Pipe::MGPSharedImageReply& reply) override;
|
||||
|
||||
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
|
||||
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
|
||||
// checked, reset run); ObjectDeaths counts every record the sink dispatched.
|
||||
Uint64 ApplierResets() const { return m_applierResets; }
|
||||
Uint64 ObjectDeaths() const { return m_objectDeaths; }
|
||||
Uint64 BindContexts() const { return m_bindContexts; }
|
||||
// The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can
|
||||
// assert the sequence rather than only the count.
|
||||
Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; }
|
||||
|
||||
// The session's per-verb tallies; the record verbs' are RecordVerbSink's.
|
||||
Uint64 Presents() const { return m_presents; }
|
||||
Uint64 LastPresentSerial() const { return m_lastPresentSerial; }
|
||||
|
||||
private:
|
||||
// A backend change ends every fence and query this sink holds (they are the old
|
||||
// backend's natives).
|
||||
@@ -231,20 +217,10 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Bool QueryGpuComplete(QueryEntry& entry, Bool wait);
|
||||
UnorderedMap<Uint32, QueryEntry> m_queries;
|
||||
Uint64 m_presents = 0;
|
||||
Uint64 m_lastPresentSerial = 0;
|
||||
// P5c ct. m_applierResetSerial is BOTH the expected ContextSerial of the next record
|
||||
// and the count of accepted resets: the two are one number because the serial is the
|
||||
// session's own count of applier_reset records (CONTRACT-P5C.md §1: asserted, never
|
||||
// dispatched on). m_objectDeaths counts records dispatched, released twin or not -
|
||||
// the idempotent second path answering "nothing held it" is a legal record.
|
||||
// m_applierResetSerial is both the expected ContextSerial of the next record and the
|
||||
// count of accepted resets: the serial is the session's own count of applier_reset
|
||||
// records (CONTRACT-P5C.md §1: asserted, never dispatched on).
|
||||
Uint64 m_applierResetSerial = 0;
|
||||
Uint64 m_applierResets = 0;
|
||||
Uint64 m_objectDeaths = 0;
|
||||
// P14 S1. BindContexts counts the records the sink dispatched and bound on the session;
|
||||
// the token itself lives on the session (ServerSession::CurrentContextToken), which is
|
||||
// the object the control plane's CreateContext/DestroyContext also own - one table, one
|
||||
// owner, and the sink is only the ring's door into it.
|
||||
Uint64 m_bindContexts = 0;
|
||||
// The newest native fence (shared_image NativeFence) this session handed out, kept so a
|
||||
// later fence with nothing left to export still answers with a descriptor: by then this
|
||||
// one has signaled (the backend waited the work out), and a signaled sync_file stays so.
|
||||
|
||||
@@ -765,19 +765,6 @@ namespace {
|
||||
const auto* envelope = Protocol::GetCtrlEnvelope(buffer.data());
|
||||
if (const auto* flush = envelope->msg_as_LogFlush()) {
|
||||
if (flush->ack()) break;
|
||||
if (const char* inspect = std::getenv("MOBILEGL_TEST_SERVER_COUNTERS");
|
||||
inspect && std::strcmp(inspect, "1") == 0) {
|
||||
// SyncPeerLog has fenced the client's complete command prefix.
|
||||
// Acquire the applier's publication before reading its counters;
|
||||
// this serial client sends no more records until this ack.
|
||||
const auto applied = session.DataLink()->Progress()->appliedSeq.load(std::memory_order_acquire);
|
||||
const auto& verbs = session.Applier().Verbs();
|
||||
MGLOG_I("MGPipe server counters: applied=%llu resets=%llu reset-serial=%llu deaths=%llu",
|
||||
static_cast<unsigned long long>(applied),
|
||||
static_cast<unsigned long long>(verbs.ApplierResets()),
|
||||
static_cast<unsigned long long>(verbs.ExpectedApplierResetSerial()),
|
||||
static_cast<unsigned long long>(verbs.ObjectDeaths()));
|
||||
}
|
||||
FlushAck ack{control.get(), logForward.get(), flush->seq()};
|
||||
MobileGL::MG_Util::Debug::WithLogBarrier(&SendLogAck, &ack);
|
||||
continue;
|
||||
|
||||
@@ -306,6 +306,9 @@ namespace {
|
||||
EXPECT_NE(line.find(" wlivepk="), String::npos) << line;
|
||||
EXPECT_NE(line.find(" wdefpk="), String::npos) << line;
|
||||
EXPECT_NE(line.find(" wdefsync="), String::npos) << line;
|
||||
// The server's control-record counts, which CtWireScenario reads by these names.
|
||||
EXPECT_NE(line.find(" ar="), String::npos) << line;
|
||||
EXPECT_NE(line.find(" od="), String::npos) << line;
|
||||
}
|
||||
|
||||
// Per-frame fields carry two decimals for the same reason acc/draw does: they are small
|
||||
@@ -544,6 +547,8 @@ namespace {
|
||||
// MEASUREMENTS.md entry will quote.
|
||||
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageSegmentBytes), "stage-segment-bytes");
|
||||
EXPECT_STREQ(PS::NameOf(PS::CallClass::WireRecords), "wire-records");
|
||||
EXPECT_STREQ(PS::NameOf(PS::CallClass::ApplierResets), "applier-resets");
|
||||
EXPECT_STREQ(PS::NameOf(PS::CallClass::ObjectDeaths), "object-deaths");
|
||||
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
|
||||
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytTextureSyncList), "espryt-texture-sync-list");
|
||||
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytUnitBindingsEpoch), "espryt-unit-bindings-epoch");
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -3290,21 +3291,27 @@ TEST(ApplierResetWireRoundTrip, TheSerialSequenceIsAssertedAndTheServerResetRuns
|
||||
EXPECT_TRUE(applied);
|
||||
};
|
||||
|
||||
// The reset RUNS - asserted on the applier's own state, not on the tally (R-16: a probe
|
||||
// may not arm against a stub, and a tally a stub could also move is a stub's witness).
|
||||
// BoundVertexElements is one of the fields MGPipeApplierReset clears (PipeApply.cpp).
|
||||
// The reset runs - asserted on the applier's own state: BoundVertexElements is one of the
|
||||
// fields MGPipeApplierReset clears (PipeApply.cpp). The accepted count is the sink's
|
||||
// applier-resets PipeStats counter.
|
||||
namespace PS = MG_Util::PipeStats;
|
||||
PS::SetEnabledForTesting(true);
|
||||
PS::ResetForTesting();
|
||||
MGPipeApplier().BoundVertexElements = MakeHandle(77);
|
||||
send(0); // 0 = the first make-current (CONTRACT-P5C.md §1)
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
|
||||
EXPECT_EQ(sink.ApplierResets(), 1u);
|
||||
EXPECT_EQ(sink.ExpectedApplierResetSerial(), 1u);
|
||||
EXPECT_EQ(PS::TotalCalls(PS::CallClass::ApplierResets), 1u);
|
||||
|
||||
// The second edge carries the next serial and is accepted in order.
|
||||
// Each edge carries the next serial and is accepted in order; a serial the session does not
|
||||
// expect aborts (CtWireFatals below), so accepting 1 and then 2 shows the expected one moved.
|
||||
MGPipeApplier().BoundVertexElements = MakeHandle(78);
|
||||
send(1);
|
||||
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
|
||||
EXPECT_EQ(sink.ApplierResets(), 2u);
|
||||
EXPECT_EQ(sink.ExpectedApplierResetSerial(), 2u);
|
||||
EXPECT_EQ(PS::TotalCalls(PS::CallClass::ApplierResets), 2u);
|
||||
send(2);
|
||||
EXPECT_EQ(PS::TotalCalls(PS::CallClass::ApplierResets), 3u);
|
||||
PS::ResetForTesting();
|
||||
PS::SetEnabledForTesting(false);
|
||||
}
|
||||
|
||||
TEST(ObjectDeathWireRoundTrip, TheTextureAndFramebufferTwinsReleaseByHandleAndNeverByAStaleGeneration) {
|
||||
@@ -3323,6 +3330,12 @@ TEST(ObjectDeathWireRoundTrip, TheTextureAndFramebufferTwinsReleaseByHandleAndNe
|
||||
Uint64 bits;
|
||||
~RestorePush() { MG_Config::Features.PipePush = bits; }
|
||||
} restore{savedPush};
|
||||
// The deaths the sink dispatched are its object-deaths PipeStats counter.
|
||||
namespace PS = MG_Util::PipeStats;
|
||||
PS::SetEnabledForTesting(true);
|
||||
struct StatsOff {
|
||||
~StatsOff() { PS::SetEnabledForTesting(false); }
|
||||
} statsOff;
|
||||
|
||||
// Texture AND Framebuffer - the two kinds R-16 names for the death-recycle path. The
|
||||
// others share the one dispatch and the one table walk, so two kinds pin the shape: one
|
||||
@@ -3352,14 +3365,14 @@ TEST(ObjectDeathWireRoundTrip, TheTextureAndFramebufferTwinsReleaseByHandleAndNe
|
||||
ASSERT_EQ(liveGen(one.slot), 1u) << static_cast<Uint32>(one.kind);
|
||||
|
||||
// The death crosses and the twin retires: the slot's live generation goes back to 0.
|
||||
const Uint64 deathsBefore = sink.ObjectDeaths();
|
||||
const Uint64 deathsBefore = PS::TotalCalls(PS::CallClass::ObjectDeaths);
|
||||
const MGPHandleOnly death{MakeHandle(one.slot, 1), static_cast<Uint32>(one.kind), 0};
|
||||
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death)), kInvalidSeq);
|
||||
bool applied = false;
|
||||
EXPECT_TRUE(wire.PumpOne(&applied));
|
||||
EXPECT_TRUE(applied);
|
||||
EXPECT_EQ(liveGen(one.slot), 0u) << static_cast<Uint32>(one.kind);
|
||||
EXPECT_EQ(sink.ObjectDeaths(), deathsBefore + 1u);
|
||||
EXPECT_EQ(PS::TotalCalls(PS::CallClass::ObjectDeaths), deathsBefore + 1u);
|
||||
|
||||
// THE ABA HALF (LiveGenAt's semantics): the slot is recycled forward to a NEW object
|
||||
// at generation 2, and the dead object's handle - replayed, as a duplicated or
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
// therefore a LOWER BOUND on the per-draw accessor count, and it is
|
||||
// the bound over exactly the six gates section 2.3.1 tabulates.
|
||||
// texture-* Per (target, level) emission, both backends.
|
||||
// applier-resets ServerVerbSink::OnApplierReset, once per record accepted.
|
||||
// object-deaths ServerVerbSink::OnObjectDeath, once per record dispatched.
|
||||
//
|
||||
// Gates: the six of section 2.3.1, each counted exactly once per probe.
|
||||
//
|
||||
@@ -252,6 +254,8 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
"wire-host-wait-us",
|
||||
"wire-indirect-native-dispatches",
|
||||
"resource-readbacks",
|
||||
"applier-resets",
|
||||
"object-deaths",
|
||||
};
|
||||
const char* const kGateNames[kGateCount] = {
|
||||
"espryt-render-state", "espryt-texture-sync-list", "espryt-unit-bindings-epoch",
|
||||
@@ -658,6 +662,9 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
// never ran. It is deliberately NOT rsp: rsp reaches zero when the phase SUCCEEDS, so a
|
||||
// positive control built on it would fail on the day the debt is paid.
|
||||
line += " vbs=" + std::to_string(calls[static_cast<Uint32>(CallClass::ServerVerbBoundaries)]);
|
||||
// The control records the server applied in this window (see the CallClass enum).
|
||||
line += " ar=" + std::to_string(calls[static_cast<Uint32>(CallClass::ApplierResets)]);
|
||||
line += " od=" + std::to_string(calls[static_cast<Uint32>(CallClass::ObjectDeaths)]);
|
||||
// P6 GATE 8's SECOND NUMBER (CONTRACT-P6.md §9 item 8): records/frame AFTER chunking, i.e.
|
||||
// the count of records the wire encoder actually committed this window rather than the
|
||||
// number of times an emitter was asked for one. The gap between the two is the whole
|
||||
|
||||
@@ -292,6 +292,12 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
// GPU write - before even asking whether the draw had a client-memory array to snapshot.
|
||||
// Not on the summary line; read through TotalCalls by the scenario that pins it.
|
||||
ResourceReadbacks,
|
||||
// `ar` / `od` - the two control records, counted where the server applies them
|
||||
// (ServerVerbSink::OnApplierReset after its serial check, ServerVerbSink::OnObjectDeath per
|
||||
// record dispatched). A framebuffer has no other wire delete, so `od` is the only sign on
|
||||
// the server that its death crossed. Server-side counts: a separate client process prints 0.
|
||||
ApplierResets,
|
||||
ObjectDeaths,
|
||||
Count
|
||||
};
|
||||
|
||||
|
||||
@@ -82,4 +82,4 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_
|
||||
|
||||
## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h`)
|
||||
|
||||
关闭时每站点一次全局 load + 一条永不命中的分支。字节类(stage buffer / texture / UBO / client 数组、`pmap`、`resid`、`csob-blob`、`seg` = 写进 `SEG_STAGE` 的字节)、调用类(`draws`、`accessor-calls`、纹理上传 emit / box / rect / jobs、`csom` / `csob`、`mpr`、`trp`、`rsp`、`wrec`、`maxrec` / `ringwraps` / `ringpads` / `ringwaits`、门铃 `srv` / `srvpark` / `cli` / `clipark`)与六个 memo 门的 hit/miss。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行,按角色写日志。读法陷阱:`accessor-calls` 是静态下界,判性能只看 CPU 时间;spawn 下 client 不推进帧窗口(`frames=0`),窗口化字段在 spawn client 上无效,run total 仍有效([`docs/archive/notes/p6/README.md`](../../archive/notes/p6/README.md) §12.1)。站点清单(哪些路径**没有**接线)写在 `PipeStats.cpp` 头部,那份清单是契约。
|
||||
关闭时每站点一次全局 load + 一条永不命中的分支。字节类(stage buffer / texture / UBO / client 数组、`pmap`、`resid`、`csob-blob`、`seg` = 写进 `SEG_STAGE` 的字节)、调用类(`draws`、`accessor-calls`、纹理上传 emit / box / rect / jobs、`csom` / `csob`、`mpr`、`trp`、`rsp`、`wrec`、server 应用的控制记录 `ar`(applier_reset)/ `od`(object_death)、`maxrec` / `ringwraps` / `ringpads` / `ringwaits`、门铃 `srv` / `srvpark` / `cli` / `clipark`)与六个 memo 门的 hit/miss。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行,按角色写日志。读法陷阱:`accessor-calls` 是静态下界,判性能只看 CPU 时间;spawn 下 client 不推进帧窗口(`frames=0`),窗口化字段在 spawn client 上无效,run total 仍有效([`docs/archive/notes/p6/README.md`](../../archive/notes/p6/README.md) §12.1)。站点清单(哪些路径**没有**接线)写在 `PipeStats.cpp` 头部,那份清单是契约。
|
||||
|
||||
@@ -63,6 +63,7 @@ them wrong.
|
||||
| 2026-10-18 | `Negative control A2 (corrupted capability)` | Test / `integration-verify` | the same five-entry rerun with `MOBILEGL_PIPE_VERIFY_CORRUPT=IsCapabilityEnabled`, accepting any red; it proved that the comparator compares the capability field (P13 W3c moved the residual block's cross-check there) | unit `PipeInputsTest.EveryCorruptedFieldIsNamedByTheComparator` (`Unit tests on the verify runtime`): every field with storage, the capability field among them, corrupted in turn must be the field the comparator names; then, in a forked child with the knob set to `IsCapabilityEnabled`, each of Clear, DrawArrays and ReadPixels whose class fills the field must report it at entry, and one must |
|
||||
| 2026-10-18 | `Negative control A (corrupted snapshot)` and `Keep lane logs before controls` | Test / `integration-verify-split` | the first re-ran the ten `VerifySplit.ClearThenReadPixelsScenario` entries of both backends with the same knob and accepted any red; the second only kept the lane's logs from being overwritten by that rerun | the in-lane `VerifySplitCorrupted` and `VerifySplitReadCorrupted` entries of both backends, required by name (`Verify-split control entries`); that the ambient entries arm is the per-entry arm proof, and that they run fatal is `VerifySplitArming.PipeVerifyArmingScenario.Armed` (the ambient environment plus its marker and log), which requires `fatal=1` on the arming line |
|
||||
| 2026-10-18 | unit `PipeInputsTest.{PoisonOmitKnobArmsTheOmission, BadPoisonOmitKnobIsFatalNamingTheKnob}`, and the `MOBILEGL_PIPE_POISON_OMIT` knob: `Features.PipePoisonOmit`, its parser on every verb (`MGPipeValidateForVerb`'s `ParsePoisonOmissionKnob`, compiled into every build) and the server stamp's own parse of it | Test / unit (every poison lane); product code | the knob existed only for negative control B, and the two cases pinned its parser and its `Fatal{PipeVerifyBadKnob}` | the PoisonOmitted and VerifySplitPoisonOmitted entries set the harness marker `MGITEST_POISON_OMIT` to the same pair, and the re-executed child arms it through `MGPipeSetPoisonOmission`, compiled only with `-DMOBILEGL_PIPE_VERIFY=ON` (`Harness/PoisonOmissionArm.h`); `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale`, `ReadingAnOmittedFieldAbortsNamingTheVerb` and `TheMutationNoticeRefreshesTheValueButNotTheStamp` withhold the stamp test-side and still run in every poison lane; the setter's scope to its verb, not its class, is checked by `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale` in a forked child, on the verify runtime only |
|
||||
| 2026-10-18 | the `serial == resets` assertion of `CtWireScenario.ApplierResetCrossesAtThePrimedEdgeInSerialOrder`, and the `MOBILEGL_TEST_SERVER_COUNTERS` knob: `ServerMain`'s `MGPipe server counters:` line on every LogFlush and `ServerVerbSink`'s test-only tallies (`ApplierResets`, `ObjectDeaths`, `ExpectedApplierResetSerial`) | Test / `integration-split`, `integration-split-strict`, `integration-verify-split`, `integration (inproc)`; product code | the knob existed only so the spawn and tcp arms could read the sink's tallies; the serial assertion could not fail, since `OnApplierReset` moves the count and the serial together and any other serial latches `Fatal{ProtocolCorruption, "ApplierReset.ContextSerial"}` | `ar` / `od` on the server role's `MGPipe stats:` line, counted in `ServerVerbSink::OnApplierReset` / `OnObjectDeath`; every CtWire entry sets `MOBILEGL_PIPE_STATS=1` with period 1 and reads them on every arm, inproc included, over a window closed by a swap and a readback; the unit cases `ApplierResetWireRoundTrip.TheSerialSequenceIsAssertedAndTheServerResetRuns` and `ObjectDeathWireRoundTrip.TheTextureAndFramebufferTwinsReleaseByHandleAndNeverByAStaleGeneration` read the same counters, and the first shows the expected serial moving by having serial 2 accepted |
|
||||
|
||||
## Kept after investigation
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def start(args):
|
||||
if args.state.exists():
|
||||
raise RuntimeError(f'stale fixture state; run stop first: {args.state}')
|
||||
env = dict(os.environ, MOBILEGL_IPC_ROLE='server', MOBILEGL_IPC_DIAL='no',
|
||||
MOBILEGL_IPC_TOKEN='', MOBILEGL_IPC_LOG_FORWARD='1', MOBILEGL_TEST_SERVER_COUNTERS='1',
|
||||
MOBILEGL_IPC_TOKEN='', MOBILEGL_IPC_LOG_FORWARD='1',
|
||||
MOBILEGL_PIPE_STATS='1', MOBILEGL_PIPE_STATS_PERIOD='1')
|
||||
for key in ('MOBILEGL_TRANSPORT', 'MOBILEGL_IPC_SERVER_PATH', 'MOBILEGL_IPC_RING_MB', 'MOBILEGL_IPC_STAGE_MB',
|
||||
'MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_ENDPOINT', 'MOBILEGL_BACKEND_TYPE'):
|
||||
|
||||
@@ -64,7 +64,7 @@ def run(args, case, transport, repeat):
|
||||
LIBGL_ALWAYS_SOFTWARE='1', EGL_PLATFORM='surfaceless',
|
||||
MESA_GL_VERSION_OVERRIDE='3.3', MESA_GLSL_VERSION_OVERRIDE='330')
|
||||
for key in ('MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_DATA', 'MOBILEGL_IPC_TOKEN', 'MOBILEGL_IPC_DIAL',
|
||||
'MOBILEGL_IPC_REQUIRE_SAME_BUILD', 'MOBILEGL_TEST_SERVER_COUNTERS'):
|
||||
'MOBILEGL_IPC_REQUIRE_SAME_BUILD'):
|
||||
env.pop(key, None)
|
||||
if transport == 'tcp':
|
||||
env.update(MOBILEGL_IPC_CONTROL=args.endpoint, MOBILEGL_IPC_DATA='stream', MOBILEGL_IPC_TOKEN='',
|
||||
|
||||
@@ -122,7 +122,7 @@ def run(args, transport, case, repeat):
|
||||
LIBGL_ALWAYS_SOFTWARE='1', EGL_PLATFORM='surfaceless',
|
||||
MESA_GL_VERSION_OVERRIDE='3.3', MESA_GLSL_VERSION_OVERRIDE='330')
|
||||
for key in ('MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_DATA', 'MOBILEGL_IPC_TOKEN', 'MOBILEGL_IPC_DIAL',
|
||||
'MOBILEGL_IPC_REQUIRE_SAME_BUILD', 'MOBILEGL_TEST_SERVER_COUNTERS'):
|
||||
'MOBILEGL_IPC_REQUIRE_SAME_BUILD'):
|
||||
env.pop(key, None)
|
||||
if Path('/usr/share/glvnd/egl_vendor.d/50_mesa.json').exists():
|
||||
env['__EGL_VENDOR_LIBRARY_FILENAMES'] = '/usr/share/glvnd/egl_vendor.d/50_mesa.json'
|
||||
|
||||
@@ -140,7 +140,7 @@ def start_server(args, device):
|
||||
command = [sys.executable, str(script), 'start', '--serial', args.serial,
|
||||
'--package', args.package, '--listen', args.listen, '--token', args.token,
|
||||
'--allow-idle', '--state-file', str(args.server_idle_state), '--env',
|
||||
'MOBILEGL_TEST_SERVER_COUNTERS=1;MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1;'
|
||||
'MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1;'
|
||||
'MOBILEGL_LOG_FILE_PATH=/data/data/' + args.package + '/files/mgl.log']
|
||||
result = subprocess.run(command, check=True, capture_output=True, text=True, timeout=30)
|
||||
(args.out / 'server-start.txt').write_text(result.stdout + result.stderr, encoding='utf-8')
|
||||
|
||||
Reference in New Issue
Block a user