diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp index be5e3b2d..54365f0f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp @@ -268,6 +268,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::abort(); } ++m_wireStoreCount; + NoteWireStorePeaks(); + PublishWireReclaimGauges(); // Under transport, defined bytes follow as resource_subdata records. An // undefined store has no shadow to upload and no implicit zero snapshot. if (initialBytes != nullptr && desc.HasDefinedContent) { @@ -576,6 +578,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { DeferWireRelease(std::move(found->second.buffer), found->second.lastUseSerial); m_wireBuffers.erase(found); ++m_sliceEpochCounter; + PublishWireReclaimGauges(); } void VkBufferManager::DeferWireRelease(VkBufferObject&& buffer, Uint64 lastUseSerial) { @@ -599,6 +602,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { entry.buffer = std::move(buffer); m_deferredWireBytes += entry.bytes; m_deferredWireReleases.push_back(std::move(entry)); + NoteWireStorePeaks(); // EVERY PARK SWEEPS. The frame boundary is not a reclaim point this arm can lean on - a // pbuffer replay that snapshot-exits delivers ONE present for 1.3 M calls (ID-P7-32) - // so the only cadence that tracks the workload is the workload itself. @@ -619,9 +623,24 @@ namespace MobileGL::MG_Backend::DirectVulkan { !pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) { WireBufferSyncFatal("deferred-watermark"); } + ++m_wireDeferredSyncs; SweepDeferredWireReleases(); } + void VkBufferManager::NoteWireStorePeaks() { + m_wireStoreCountPeak = std::max(m_wireStoreCountPeak, m_wireStoreCount); + m_deferredWireBytesPeak = std::max(m_deferredWireBytesPeak, m_deferredWireBytes); + } + + void VkBufferManager::PublishWireReclaimGauges() { + if (!MG_Util::PipeStats::Enabled()) return; + using MG_Util::PipeStats::Gauge; + MG_Util::PipeStats::PublishGauge(Gauge::WireBuffers, static_cast(m_wireBuffers.size())); + MG_Util::PipeStats::PublishGauge(Gauge::WireStoresPeak, m_wireStoreCountPeak); + MG_Util::PipeStats::PublishGauge(Gauge::WireDeferredBytesPeak, m_deferredWireBytesPeak); + MG_Util::PipeStats::PublishGauge(Gauge::WireDeferredSyncs, m_wireDeferredSyncs); + } + void VkBufferManager::WireBufferSyncFatal(const char* site) { MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-write-sync\"} {site=%s}", site); std::abort(); @@ -715,6 +734,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { // records still hold, so nothing this arm minted outlives the count. m_wireBuffers.clear(); m_wireStoreCount = 0; + m_wireStoreCountPeak = 0; + m_deferredWireBytesPeak = 0; + m_wireDeferredSyncs = 0; #endif m_copyProvider = nullptr; m_initInfo = {}; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h index f8e9ebcc..4e5a5155 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h @@ -310,6 +310,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // for the census to count), shared by the host write that cannot wait for the GPU and // the watermark that cannot. `site` says which. [[noreturn]] static void WireBufferSyncFatal(const char* site); + // The wbuf[] gauges (PipeStats.h, Gauge::WireBuffers..WireDeferredSyncs). The peaks are + // taken at the two points the numbers can rise - a park and a mint - and published when + // the stats channel is on; MagmaWireReclaimScenario reads them off the server's line. + void NoteWireStorePeaks(); + void PublishWireReclaimGauges(); #endif Bool InitializeTransientArenas(); static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind); @@ -354,6 +359,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { // Live VkBuffers this arm owns: the stores held by m_wireBuffers plus the parked ones. // Maintained rather than counted, because the publish runs on every park. Uint64 m_wireStoreCount = 0; + // Run maxima of the two numbers above and the watermark's sync count, for the gauges. + Uint64 m_wireStoreCountPeak = 0; + Uint64 m_deferredWireBytesPeak = 0; + Uint64 m_wireDeferredSyncs = 0; #endif // Size m_liveResources had just after the last sweep; the next sweep waits for it to double. SizeT m_liveResourcesLastPruned = 0; diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 5db1d66a..00857180 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -3439,6 +3439,51 @@ if (MOBILEGL_BUILD_DISAGGREGATED) endforeach() endif() +# ===================================================================================== +# P7 WAVE 4 PACKAGE M2: THE WIRE ARM RECLAIMS ORPHANED BUFFER STORES INSIDE A FRAME +# ===================================================================================== +# +# ID-P7-27 / ID-P7-32: bsl-esc-menu-854 delivers one present per replay, and the split server +# held every orphaned VkBuffer until a frame boundary that never came. The two cases read the +# server's own wbuf[] gauges (PipeStats.h) off its `MGPipe stats:` line, so the flavour pins the +# stats channel at one line per swap, and MOBILEGL_IPC_WIRE_DEFERRED_MB=8 keeps the watermark +# case at 48 MiB of traffic instead of 400. +# +# INPROC AND SPAWN ONLY - the ID-P7-14 shape, named in scripts/ci/spawn_lane_parity.py +# (MAGMA_SERVER_ENV_KNOB_NO_TCP, one tail per case). Both knobs the lane depends on are SERVER-process environment: under inproc +# and spawn the server inherits this entry's ENVIRONMENT, under tcp the server is the +# TcpServer.Start supervisor's session child, started once for the whole lane with the fixture's +# environment - setting the stats channel and an 8 MiB watermark there would change every other +# tcp entry's server - and its log is not this entry's private server log to read. +if (MOBILEGL_BUILD_DISAGGREGATED) + target_sources(MobileGLIntegrationTest PRIVATE Scenarios/MagmaWireReclaimScenario.cpp) + set(MGL_ITEST_MAGMA_RECLAIM_FLAVOUR_ENV + "MOBILEGL_BACKEND_TYPE=DirectVulkan" + "MOBILEGL_IPC_ROLE_SPLIT_STATE=1" "MOBILEGL_IPC_STRICT_ERRORS=1" + "MOBILEGL_PIPE_STATS=1" "MOBILEGL_PIPE_STATS_PERIOD=1" + "MOBILEGL_IPC_WIRE_DEFERRED_MB=8" + "MGITEST_MAGMA_RECLAIM_LANE=1" + "MOBILEGL_ITEST_REQUIRE_GPU=1" "MGITEST_SPLIT_LANE=1" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV}) # the ICD pin, see the Fm lane above + foreach(mglItestArm IN ITEMS Split Spawn) + mgl_itest_magma_arm_shape(${mglItestArm} reclaim) + mgl_itest_join_environment(MGL_ITEST_MAGMA_RECLAIM_ENVIRONMENT + ${MGL_ITEST_MAGMA_RECLAIM_FLAVOUR_ENV} ${mglItestArmTransportEnv}) + foreach(reclaimCase + RespecifiesWithNoDrawBetweenKeepTheLiveStoreCountBounded + RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.Reclaim." + TEST_FILTER "MagmaWireReclaimScenario.${reclaimCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${MGL_ITEST_MAGMA_RECLAIM_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/magma-reclaim${mglItestArmLogTag}-${reclaimCase}.log") + endforeach() + endforeach() +endif() + # P5c ct (MG_Remote/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: diff --git a/MobileGL/MG_IntegrationTest/Scenarios/MagmaWireReclaimScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/MagmaWireReclaimScenario.cpp new file mode 100755 index 00000000..6256d0ce --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/MagmaWireReclaimScenario.cpp @@ -0,0 +1,312 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/MagmaWireReclaimScenario.cpp +// 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 +// +// P7 wave 4 package M2 (ID-P7-27 / ID-P7-32): THE MAGMA WIRE ARM MUST RECLAIM ORPHANED BUFFER +// STORES INSIDE A FRAME. +// +// THE DEFECT. Every glBufferData that crosses the wire reaches VkBufferManager:: +// RespecifyWireBuffer, which orphans the old VkBuffer (legitimately - a recorded draw may still +// name it) and mints a new one. Before this package the orphan went into a per-frame-slot bucket +// that only a FRAME BOUNDARY emptied, and minecraft-1.21.4-fabric-iris-bsl-esc-menu-854 has two +// eglSwapBuffers in 1,303,535 calls - the server sees one present per replay. On lavapipe the +// split server held 25,923 dead stores against 28 live wire buffers and 41k mappings; on the +// Redmi it died in scudo at 1.2 GiB while the monolith passed at 807 MiB. +// +// WHAT THE TWO CASES PIN, both inside ONE frame (no swap between the first and the last +// respecify), because that is the only shape in which the bug existed: +// * RespecifiesWithNoDrawBetween... - the stores no GPU command ever named. They are dead the +// moment they are orphaned, so the live store count must stay a small multiple of the wire +// buffers alive (k = 4 here; the mechanism gives 1). +// * RespecifyAndDrawEachStore... - every orphan was drawn from, so it is named by recorded, +// unsubmitted work and nothing retires it inside the frame except the +// MOBILEGL_IPC_WIRE_DEFERRED_MB watermark's forced sync point. The parked bytes must stay +// within the budget plus one store, and every draw must still land on its own strip - a +// store destroyed while its draw was pending shows up as a wrong strip (or a dead lavapipe). +// +// READ FROM THE SERVER, NOT THE PICTURE. The numbers are the wbuf[] gauges the server role's +// VkBufferManager publishes (PipeStats.h, Gauge::WireBuffers..WireDeferredSyncs): RUN MAXIMA, +// because the peak lives inside the frame and a swap-time sample would read after the frame +// boundary's own sweep. They reach this process through the server's `MGPipe stats:` line, so +// the lane pins MOBILEGL_PIPE_STATS=1 and a private log path, and the entries are registered on +// the inproc and spawn arms ONLY: the tcp arm's server is the pre-started fixture supervisor, +// whose environment the lane cannot set and whose log this process cannot read +// (scripts/ci/spawn_lane_parity.py names the exception). + +#include +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/PipeStatsWindow.h" +#include "../Harness/ScenarioFixture.h" +#include "../Harness/SplitRuntimePeek.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + // Set by the MagmaWireReclaim ctest entries and by nothing else; a harness marker, never + // read by the library. The whole-binary informational lanes run this file without it and + // without the stats channel, and skip. + constexpr const char* kLaneMarker = "MGITEST_MAGMA_RECLAIM_LANE"; + + constexpr int kWidth = 64; + constexpr int kHeight = 16; + constexpr std::uint64_t kMiB = 1024u * 1024u; + + // Case 1: many small respecifies of ONE buffer and no draw between them. + constexpr int kUndrawnRespecifies = 1024; + constexpr std::size_t kUndrawnStoreBytes = 4096; + constexpr long long kLivePerWireBuffer = 4; + + // Case 2: respecify-and-draw, one strip per store. 48 stores of 1 MiB is six times the + // 8 MiB budget the lane sets, so the watermark has to fire several times in the frame. + constexpr int kDrawnRespecifies = 48; + constexpr std::size_t kDrawnStoreBytes = static_cast(kMiB); + + constexpr const char* kVertex = R"(#version 330 core +layout(location = 0) in vec2 aPos; +void main() { gl_Position = vec4(aPos, 0.0, 1.0); })"; + + constexpr const char* kFragment = R"(#version 330 core +uniform vec4 uColor; +out vec4 fragColor; +void main() { fragColor = uColor; })"; + + // Two triangles covering pixel columns [x0, x1) over the whole height, in NDC. + void WriteStripQuad(float* out, int x0, int x1) { + const float l = -1.0f + 2.0f * static_cast(x0) / kWidth; + const float r = -1.0f + 2.0f * static_cast(x1) / kWidth; + const float quad[12] = {l, -1.0f, r, -1.0f, r, 1.0f, l, -1.0f, r, 1.0f, l, 1.0f}; + for (int i = 0; i < 12; ++i) out[i] = quad[i]; + } + + // A colour per strip that no neighbour shares and the clear (all zero) is not. + Rgba8 StripColor(int strip) { + Rgba8 c; + c.r = static_cast(40 + (strip * 37) % 200); + c.g = static_cast(40 + (strip * 91) % 200); + c.b = static_cast(255 - strip); + c.a = 255; + return c; + } + + bool Near(const Rgba8& a, const Rgba8& b) { + return std::abs(int(a.r) - int(b.r)) <= 1 && std::abs(int(a.g) - int(b.g)) <= 1 && + std::abs(int(a.b) - int(b.b)) <= 1 && std::abs(int(a.a) - int(b.a)) <= 1; + } + + std::uint64_t DeferredBudgetBytes() { + const char* value = std::getenv("MOBILEGL_IPC_WIRE_DEFERRED_MB"); + const std::uint64_t mb = (value != nullptr && *value != '\0') ? std::strtoull(value, nullptr, 10) : 64u; + return mb * kMiB; + } + + class MagmaWireReclaimScenario : public ScenarioTest { + protected: + GLuint m_program = 0, m_vao = 0, m_buffer = 0; + GLint m_colorLocation = -1; + ColorFbo m_target{}; + + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + if (Gl().BackendName() != "DirectVulkan") { + GTEST_SKIP() << "the reclaim under test is the Magma wire arm's VkBufferManager"; + } + if (std::getenv(kLaneMarker) == nullptr) { + GTEST_SKIP() << kLaneMarker << " is not set: this run has no private stats " + "channel to read the server's wbuf[] gauges from"; + } + const SplitRuntimeState runtime = PeekSplitRuntime(); + if (!runtime.transportResolved || !runtime.sessionActive) { + GTEST_SKIP() << "not a split run (" << runtime.transportName + << "): the monolith arm has no wire buffer stores"; + } + std::string error; + m_program = CompileProgram(kVertex, kFragment, &error); + ASSERT_NE(m_program, 0u) << error; + m_colorLocation = glGetUniformLocation(m_program, "uColor"); + ASSERT_GE(m_colorLocation, 0); + m_target = MakeColorFbo(kWidth, kHeight); + ASSERT_NE(m_target.fbo, 0u); + BindFbo(m_target); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + ClearTo(0.0f, 0.0f, 0.0f, 0.0f); + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + glGenBuffers(1, &m_buffer); + glBindBuffer(GL_ARRAY_BUFFER, m_buffer); + } + + void TearDown() override { + if (Ready()) { + if (m_buffer) glDeleteBuffers(1, &m_buffer); + if (m_vao) glDeleteVertexArrays(1, &m_vao); + if (m_program) glDeleteProgram(m_program); + if (m_target.fbo) DestroyColorFbo(m_target); + } + ScenarioTest::TearDown(); + } + + // glBufferData of `bytes` (a strip quad at offset 0, zeros after) and the attribute + // pointer re-armed on the new store. + void Respecify(std::vector& bytes, int x0, int x1) { + WriteStripQuad(reinterpret_cast(bytes.data()), x0, x1); + glBufferData(GL_ARRAY_BUFFER, static_cast(bytes.size()), bytes.data(), GL_STREAM_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr); + glEnableVertexAttribArray(0); + } + + void DrawStrip(int strip) { + const Rgba8 c = StripColor(strip); + glUseProgram(m_program); + glUniform4f(m_colorLocation, c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, 1.0f); + glDrawArrays(GL_TRIANGLES, 0, 6); + } + + // The swap that closes the counted frame, and the server's line for it. + PipeStatsWindow::Window CloseFrameAndReadServerWindow(const PipeStatsWindow::LogMark& mark) { + Gl().EndFrame(); + if (const SplitRuntimeState runtime = PeekSplitRuntime(); runtime.transportResolved) { + EXPECT_TRUE(WaitForSplitAppliedForTesting(runtime.emitSeq)); + } + PipeStatsWindow::Window window = PipeStatsWindow::LastFromServerLogSince(mark); + if (window.found) RecordProperty("stats_line_server", window.line.c_str()); + return window; + } + + // A setup frame that draws once from the buffer, so the first orphan of the counted + // frame is a store the GPU really named, then the mark the counted window starts at. + PipeStatsWindow::LogMark SetupFrame() { + std::vector bytes(kUndrawnStoreBytes, 0); + Respecify(bytes, 0, kWidth); + DrawStrip(0); + Gl().EndFrame(); + BindFbo(m_target); + if (const SplitRuntimeState runtime = PeekSplitRuntime(); runtime.transportResolved) { + EXPECT_TRUE(WaitForSplitAppliedForTesting(runtime.emitSeq)); + } + return PipeStatsWindow::MarkLaneLog(); + } + }; + + // -------------------------------------------------------------------------------------- + // CASE 1. 1024 glBufferData of one 4 KiB buffer and no draw between them: 1023 of the + // orphans were never named by a GPU command and are dead the moment they are orphaned. + // Before M2 every one of them waited for the frame boundary, so the peak was ~1025 live + // VkBuffers for one wire buffer. + TEST_F(MagmaWireReclaimScenario, RespecifiesWithNoDrawBetweenKeepTheLiveStoreCountBounded) { + if (!Ready() || IsSkipped()) return; + const PipeStatsWindow::LogMark mark = SetupFrame(); + + std::vector bytes(kUndrawnStoreBytes, 0); + for (int i = 0; i < kUndrawnRespecifies; ++i) { + // Only the LAST store's quad covers the target; the rest are never drawn. + const bool last = (i + 1 == kUndrawnRespecifies); + Respecify(bytes, 0, last ? kWidth : 1); + } + ClearTo(0.0f, 0.0f, 0.0f, 0.0f); + DrawStrip(7); + const Image image = ReadPixels(kWidth, kHeight); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + EXPECT_TRUE(Near(image.At(kWidth / 2, kHeight / 2), StripColor(7))) + << "read " << image.At(kWidth / 2, kHeight / 2) << ", wanted " << StripColor(7) + << ": the draw from the last of " << kUndrawnRespecifies << " respecified stores did " + "not land - the store it names was reclaimed, or never reached"; + + const PipeStatsWindow::Window window = CloseFrameAndReadServerWindow(mark); + ASSERT_TRUE(window.found) << "no 'MGPipe stats:' window in the server log " + << PipeStatsWindow::ServerLibraryLogPath(); + const long long wireBuffers = PipeStatsWindow::CounterOrAbsent(window, "wbufs"); + const long long livePeak = PipeStatsWindow::CounterOrAbsent(window, "wlivepk"); + ASSERT_GT(wireBuffers, 0) << "the server published no wbufs= gauge: " << window.line; + ASSERT_GE(livePeak, 0) << window.line; + RecordProperty("wire_buffers", static_cast(wireBuffers)); + RecordProperty("wire_stores_peak", static_cast(livePeak)); + EXPECT_LE(livePeak, kLivePerWireBuffer * wireBuffers) + << "the Magma server held " << livePeak << " VkBuffers at once for " << wireBuffers + << " wire buffer record(s) inside one frame of " << kUndrawnRespecifies + << " glBufferData calls. Orphans no GPU command named are dead at the park; a count " + "that tracks the respecifies is the bsl-esc-menu-854 leak (ID-P7-32). " << window.line; + } + + // -------------------------------------------------------------------------------------- + // CASE 2. 48 respecify-and-draw rounds of a 1 MiB store, one strip each, in one frame. + // Every orphan is named by recorded, unsubmitted work; only the watermark's forced sync + // point can retire it before the frame ends. Before M2: 48 MiB parked against the lane's + // 8 MiB budget. The strips are the soundness half: a store destroyed while its draw was + // still pending does not leave its own colour behind. + TEST_F(MagmaWireReclaimScenario, RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget) { + if (!Ready() || IsSkipped()) return; + const PipeStatsWindow::LogMark mark = SetupFrame(); + + ClearTo(0.0f, 0.0f, 0.0f, 0.0f); + std::vector bytes(kDrawnStoreBytes, 0); + for (int strip = 0; strip < kDrawnRespecifies; ++strip) { + Respecify(bytes, strip, strip + 1); + DrawStrip(strip); + } + const Image image = ReadPixels(kWidth, kHeight); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + int wrongStrips = 0; + for (int strip = 0; strip < kDrawnRespecifies; ++strip) { + const Rgba8 got = image.At(strip, kHeight / 2); + if (!Near(got, StripColor(strip))) { + ++wrongStrips; + ADD_FAILURE() << "strip " << strip << " reads " << got << ", wanted " << StripColor(strip) + << ": the draw from that strip's own store did not land"; + } + if (wrongStrips > 4) break; + } + + const PipeStatsWindow::Window window = CloseFrameAndReadServerWindow(mark); + ASSERT_TRUE(window.found) << "no 'MGPipe stats:' window in the server log " + << PipeStatsWindow::ServerLibraryLogPath(); + const long long wireBuffers = PipeStatsWindow::CounterOrAbsent(window, "wbufs"); + const long long livePeak = PipeStatsWindow::CounterOrAbsent(window, "wlivepk"); + const long long deferredPeak = PipeStatsWindow::CounterOrAbsent(window, "wdefpk"); + const long long syncs = PipeStatsWindow::CounterOrAbsent(window, "wdefsync"); + ASSERT_GT(wireBuffers, 0) << "the server published no wbufs= gauge: " << window.line; + ASSERT_GE(deferredPeak, 0) << window.line; + RecordProperty("wire_buffers", static_cast(wireBuffers)); + RecordProperty("wire_stores_peak", static_cast(livePeak)); + RecordProperty("wire_deferred_bytes_peak", std::to_string(deferredPeak).c_str()); + RecordProperty("wire_deferred_syncs", static_cast(syncs)); + + // MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the knob's negative control (no forced sync). Such + // a run is held to the lane's own 8 MiB, so that what goes red is the server's + // numbers and not a precondition of this case. + const std::uint64_t configured = DeferredBudgetBytes(); + const std::uint64_t budget = configured != 0 ? configured : 8u * kMiB; + // One store past the budget is the most a park can add before the watermark answers it. + const long long deferredBound = static_cast(budget + kDrawnStoreBytes); + EXPECT_LE(deferredPeak, deferredBound) + << "the Magma server parked " << deferredPeak << " bytes of orphaned stores inside one " + "frame against a MOBILEGL_IPC_WIRE_DEFERRED_MB budget of " << budget + << " bytes (+ one " << kDrawnStoreBytes << "-byte store). " << window.line; + const long long liveBound = + wireBuffers + static_cast(budget / kDrawnStoreBytes) + 2; + EXPECT_LE(livePeak, liveBound) + << "the Magma server held " << livePeak << " VkBuffers at once; the records (" + << wireBuffers << ") plus a budget's worth of parked stores allow " << liveBound + << ". " << window.line; + } + + } // namespace +} // namespace MGITest diff --git a/MobileGL/MG_Test/Util/PipeStatsTest.cpp b/MobileGL/MG_Test/Util/PipeStatsTest.cpp index 912b914f..90e0d41d 100644 --- a/MobileGL/MG_Test/Util/PipeStatsTest.cpp +++ b/MobileGL/MG_Test/Util/PipeStatsTest.cpp @@ -305,6 +305,12 @@ namespace { EXPECT_NE(line.find("srvpark="), String::npos) << line; EXPECT_NE(line.find("cli="), String::npos) << line; EXPECT_NE(line.find("clipark="), String::npos) << line; + // P7 wave 4 M2's wire-buffer bracket: MagmaWireReclaimScenario reads these four by name + // off the server's line, so a rename reds that lane as "gauge absent", not as a leak. + EXPECT_NE(line.find("wbuf[wbufs="), String::npos) << line; + EXPECT_NE(line.find(" wlivepk="), String::npos) << line; + EXPECT_NE(line.find(" wdefpk="), String::npos) << line; + EXPECT_NE(line.find(" wdefsync="), String::npos) << line; #endif } diff --git a/MobileGL/MG_Util/Metrics/PipeStats.cpp b/MobileGL/MG_Util/Metrics/PipeStats.cpp index a7b0da49..55bcf3d6 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.cpp +++ b/MobileGL/MG_Util/Metrics/PipeStats.cpp @@ -682,6 +682,13 @@ namespace MobileGL::MG_Util::PipeStats { line += " srvpark=" + std::to_string(Read(g_gauges[static_cast(Gauge::ServerParks)])); line += " cli=" + std::to_string(Read(g_gauges[static_cast(Gauge::ClientWaits)])); line += " clipark=" + std::to_string(Read(g_gauges[static_cast(Gauge::ClientParks)])); + // P7 wave 4 M2: the Magma wire arm's buffer stores (see the Gauge enum). RUN MAXIMA on + // a windowed line for the wait ledger's reason and one more: the number that matters is + // the peak INSIDE a frame, which no swap-time sample can see. + line += "] wbuf[wbufs=" + std::to_string(Read(g_gauges[static_cast(Gauge::WireBuffers)])); + line += " wlivepk=" + std::to_string(Read(g_gauges[static_cast(Gauge::WireStoresPeak)])); + line += " wdefpk=" + std::to_string(Read(g_gauges[static_cast(Gauge::WireDeferredBytesPeak)])); + line += " wdefsync=" + std::to_string(Read(g_gauges[static_cast(Gauge::WireDeferredSyncs)])); #endif line += "] gates["; for (Uint32 i = 0; i < kGateCount; ++i) { @@ -758,7 +765,16 @@ namespace MobileGL::MG_Util::PipeStats { json += " \"client-waits\": " + std::to_string(Read(g_gauges[static_cast(Gauge::ClientWaits)])) + ",\n"; json += " \"client-parks\": " + - std::to_string(Read(g_gauges[static_cast(Gauge::ClientParks)])) + "\n"; + std::to_string(Read(g_gauges[static_cast(Gauge::ClientParks)])) + ",\n"; + // P7 wave 4 M2's four, run maxima / a run count as on the summary line's wbuf[]. + json += " \"wire-buffers\": " + + std::to_string(Read(g_gauges[static_cast(Gauge::WireBuffers)])) + ",\n"; + json += " \"wire-stores-peak\": " + + std::to_string(Read(g_gauges[static_cast(Gauge::WireStoresPeak)])) + ",\n"; + json += " \"wire-deferred-bytes-peak\": " + + std::to_string(Read(g_gauges[static_cast(Gauge::WireDeferredBytesPeak)])) + ",\n"; + json += " \"wire-deferred-syncs\": " + + std::to_string(Read(g_gauges[static_cast(Gauge::WireDeferredSyncs)])) + "\n"; #endif json += " },\n \"cmd-bytes-per-draw-histogram\": ["; for (Uint32 i = 0; i < kPayloadHistogramBuckets; ++i) { diff --git a/MobileGL/MG_Util/Metrics/PipeStats.h b/MobileGL/MG_Util/Metrics/PipeStats.h index 0c587bbe..28409eff 100644 --- a/MobileGL/MG_Util/Metrics/PipeStats.h +++ b/MobileGL/MG_Util/Metrics/PipeStats.h @@ -345,6 +345,20 @@ namespace MobileGL::MG_Util::PipeStats { ServerParks, ClientWaits, ClientParks, + // P7 wave 4 M2 (ID-P7-32): THE MAGMA WIRE ARM'S BUFFER STORES, published by the SERVER + // role's VkBufferManager whenever they change (so, like `srv`, a two-process client + // prints 0 for all four). `WireBuffers` is the wire buffer records alive now; + // `WireStoresPeak` the most VkBuffers the arm ever held at once - the stores those + // records own plus orphaned ones still parked - and `WireDeferredBytesPeak` the most + // bytes ever parked. They are MAXIMA on purpose: the defect they exist for lived INSIDE + // one frame (bsl-esc-menu-854 held 25,923 dead stores against 28 live under a single + // present), and a value sampled at the swap that closes the window would read after + // the frame boundary's own sweep and prove nothing. `WireDeferredSyncs` counts the + // MOBILEGL_IPC_WIRE_DEFERRED_MB watermark's forced sync points. + WireBuffers, + WireStoresPeak, + WireDeferredBytesPeak, + WireDeferredSyncs, Count }; diff --git a/scripts/ci/spawn_lane_parity.py b/scripts/ci/spawn_lane_parity.py index 690fd994..1137a60a 100644 --- a/scripts/ci/spawn_lane_parity.py +++ b/scripts/ci/spawn_lane_parity.py @@ -78,8 +78,15 @@ MAGMA_INPROC_ONLY = ("MagmaRunAheadScenario.", "MagmaWireCacheScenario.") # two arms and the same tcp absence. One list, one mechanism, for every server-side knob. # `.MsFlip1.` (B2 review round 2) is the shader-resolve knob twin of the knob-free `.MsFlip.`, # which keeps its tcp entry. +# P7 wave 4 M2: MagmaWireReclaimScenario reads the SERVER's wbuf[] PipeStats gauges off the server's +# private log with the stats channel and MOBILEGL_IPC_WIRE_DEFERRED_MB=8 in the entry's environment - +# server-process environment again, so the same two arms and the same tcp absence. Its three cases +# are three tails (one key each) under the `.Reclaim.` prefix. MAGMA_SERVER_ENV_KNOB_NO_TCP = (".ShaderMip1.", ".ShaderMip2.", ".DepthMip.", - ".DefaultBlitShape1.", ".MsResolve1.", ".StaleSerial.", ".MsFlip1.") + ".DefaultBlitShape1.", ".MsResolve1.", ".StaleSerial.", ".MsFlip1.", + ".Reclaim.MagmaWireReclaimScenario.RespecifiesWithNoDrawBetweenKeepTheLiveStoreCountBounded", + ".Reclaim.MagmaWireReclaimScenario.RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget", + ".Reclaim.MagmaWireReclaimScenario.ManySmallRespecifyAndDrawRoundsStayUnderTheStoreCountCeiling") def lane_names(build_dir, label):