mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Backend, MG_Util, MG_Test, MG_IntegrationTest, scripts] (P7): integration-magma-split and -spawn carry MagmaWireReclaimScenario, which reads the server's wbuf[] gauges and reds on a wire arm that keeps orphaned buffer stores until the frame ends
Rule J's red-once for M2. The server role's VkBufferManager publishes four PipeStats gauges (push builds only, so the pull image is untouched): wire buffer records alive, and the run maxima of live wire VkBuffers and of parked bytes, plus the watermark's forced-sync count - maxima because the defect lived inside one frame and a swap-time sample reads after the frame boundary's own sweep. Two cases, each inside a single frame: 1024 undrawn respecifies of one 4 KiB buffer (live peak <= 4 x wire buffers), and 48 respecify-and-draw rounds of a 1 MiB store under the lane's MOBILEGL_IPC_WIRE_DEFERRED_MB=8 (parked peak <= budget + one store, every strip its own colour). Red at base (ec46a550's defer path, counted): 1025 vs 4 live, 49,287,168 vs 9,437,184 bytes and 49 vs 11 live, on both arms; green after: 1 live, 9,437,184 bytes / 9 live with 5 forced syncs. The knob's negative control (=0) reds the second case with the same 49,287,168 bytes. Inproc and spawn only: the tcp server is the lane-wide fixture supervisor, whose environment and log the entry cannot reach - named in spawn_lane_parity.py as MAGMA_NOT_ON_TCP (the ID-P7-14 shape), and PipeStatsTest pins the four short names.
This commit is contained in:
@@ -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<Uint64>(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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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 <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#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<std::size_t>(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<float>(x0) / kWidth;
|
||||
const float r = -1.0f + 2.0f * static_cast<float>(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<std::uint8_t>(40 + (strip * 37) % 200);
|
||||
c.g = static_cast<std::uint8_t>(40 + (strip * 91) % 200);
|
||||
c.b = static_cast<std::uint8_t>(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<std::uint8_t>& bytes, int x0, int x1) {
|
||||
WriteStripQuad(reinterpret_cast<float*>(bytes.data()), x0, x1);
|
||||
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(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<std::uint8_t> 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<std::uint8_t> 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<int>(wireBuffers));
|
||||
RecordProperty("wire_stores_peak", static_cast<int>(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<std::uint8_t> 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<int>(wireBuffers));
|
||||
RecordProperty("wire_stores_peak", static_cast<int>(livePeak));
|
||||
RecordProperty("wire_deferred_bytes_peak", std::to_string(deferredPeak).c_str());
|
||||
RecordProperty("wire_deferred_syncs", static_cast<int>(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<long long>(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<long long>(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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -682,6 +682,13 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
line += " srvpark=" + std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::ServerParks)]));
|
||||
line += " cli=" + std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::ClientWaits)]));
|
||||
line += " clipark=" + std::to_string(Read(g_gauges[static_cast<Uint32>(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<Uint32>(Gauge::WireBuffers)]));
|
||||
line += " wlivepk=" + std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::WireStoresPeak)]));
|
||||
line += " wdefpk=" + std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::WireDeferredBytesPeak)]));
|
||||
line += " wdefsync=" + std::to_string(Read(g_gauges[static_cast<Uint32>(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<Uint32>(Gauge::ClientWaits)])) + ",\n";
|
||||
json += " \"client-parks\": " +
|
||||
std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::ClientParks)])) + "\n";
|
||||
std::to_string(Read(g_gauges[static_cast<Uint32>(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<Uint32>(Gauge::WireBuffers)])) + ",\n";
|
||||
json += " \"wire-stores-peak\": " +
|
||||
std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::WireStoresPeak)])) + ",\n";
|
||||
json += " \"wire-deferred-bytes-peak\": " +
|
||||
std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::WireDeferredBytesPeak)])) + ",\n";
|
||||
json += " \"wire-deferred-syncs\": " +
|
||||
std::to_string(Read(g_gauges[static_cast<Uint32>(Gauge::WireDeferredSyncs)])) + "\n";
|
||||
#endif
|
||||
json += " },\n \"cmd-bytes-per-draw-histogram\": [";
|
||||
for (Uint32 i = 0; i < kPayloadHistogramBuckets; ++i) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user