mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
303 lines
15 KiB
C++
303 lines
15 KiB
C++
// MobileGL - MobileGL/MG_Test/Util/PipeStatsTest.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
// The MGPipe boundary counters (plan B section 11 P0, corollary in section 2.3.1).
|
|
// No GL context and no driver: the module is arithmetic over a fixed set of counters,
|
|
// which is exactly what has to be pinned before anyone reads a number off a device.
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <Config.h>
|
|
#include <MG_Util/Metrics/PipeStats.h>
|
|
|
|
#include <cstdio>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
namespace {
|
|
namespace PS = MobileGL::MG_Util::PipeStats;
|
|
using MobileGL::String;
|
|
using MobileGL::Uint32;
|
|
using MobileGL::Uint64;
|
|
|
|
class PipeStatsTest : public ::testing::Test {
|
|
protected:
|
|
void SetUp() override {
|
|
PS::ResetForTesting();
|
|
PS::SetEnabledForTesting(true);
|
|
}
|
|
void TearDown() override {
|
|
PS::SetEnabledForTesting(false);
|
|
PS::ResetForTesting();
|
|
}
|
|
};
|
|
|
|
// The off latch is the whole cost argument: every counting site in the two backends is
|
|
// written as `if (Enabled()) ...`, so a false latch has to mean "nothing is counted".
|
|
TEST_F(PipeStatsTest, EnabledLatchIsTheOnlyGate) {
|
|
PS::SetEnabledForTesting(false);
|
|
EXPECT_FALSE(PS::Enabled());
|
|
PS::SetEnabledForTesting(true);
|
|
EXPECT_TRUE(PS::Enabled());
|
|
}
|
|
|
|
TEST_F(PipeStatsTest, ByteClassesAccumulateIndependently) {
|
|
PS::AddBytes(PS::ByteClass::StageBuffer, 100);
|
|
PS::AddBytes(PS::ByteClass::StageBuffer, 40);
|
|
PS::AddBytes(PS::ByteClass::StageTexture, 7);
|
|
|
|
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageBuffer), 140u);
|
|
EXPECT_EQ(PS::FrameBytes(PS::ByteClass::StageBuffer), 140u);
|
|
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageTexture), 7u);
|
|
// Every other class untouched, the residual-value-block placeholder included.
|
|
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageUboGlobal), 0u);
|
|
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageUboNamed), 0u);
|
|
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageIndirectCmd), 0u);
|
|
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::ResidualValueBlock), 0u);
|
|
}
|
|
|
|
// The frame accumulator is what feeds TracyPlot; the run total is what feeds the JSON
|
|
// dump. A present must clear the first and keep the second.
|
|
TEST_F(PipeStatsTest, PresentClearsTheFrameButKeepsTheTotal) {
|
|
PS::AddBytes(PS::ByteClass::StageTexture, 512);
|
|
PS::AddCalls(PS::CallClass::Draws, 3);
|
|
PS::CountGate(PS::Gate::EsprytRenderState, /*hit=*/true);
|
|
|
|
PS::OnPresent();
|
|
|
|
EXPECT_EQ(PS::FrameBytes(PS::ByteClass::StageTexture), 0u);
|
|
EXPECT_EQ(PS::FrameCalls(PS::CallClass::Draws), 0u);
|
|
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageTexture), 512u);
|
|
EXPECT_EQ(PS::TotalCalls(PS::CallClass::Draws), 3u);
|
|
EXPECT_EQ(PS::TotalGateHits(PS::Gate::EsprytRenderState), 1u);
|
|
EXPECT_EQ(PS::FrameCount(), 1u);
|
|
}
|
|
|
|
TEST_F(PipeStatsTest, InitLatchesTheSummaryPeriodFromTheConfigAndNeverKeepsZero) {
|
|
// The device retrace harness never reaches the teardown dump, so the summary
|
|
// cadence is the only way a short fixture yields numbers at all: it must follow
|
|
// MOBILEGL_PIPE_STATS_PERIOD, and a zero must fall back rather than divide.
|
|
const Uint32 saved = MobileGL::MG_Config::Features.PipeStatsPeriod;
|
|
MobileGL::MG_Config::Features.PipeStatsPeriod = 7;
|
|
PS::Init();
|
|
EXPECT_EQ(PS::SummaryFramePeriod(), 7u);
|
|
MobileGL::MG_Config::Features.PipeStatsPeriod = 0;
|
|
PS::Init();
|
|
EXPECT_EQ(PS::SummaryFramePeriod(), PS::kDefaultSummaryFramePeriod);
|
|
MobileGL::MG_Config::Features.PipeStatsPeriod = saved;
|
|
PS::Init();
|
|
}
|
|
|
|
TEST_F(PipeStatsTest, GateHitsAndMissesAreSeparateCounters) {
|
|
for (Uint32 i = 0; i < 5; ++i) {
|
|
PS::CountGate(PS::Gate::MagmaPipelineMemo, /*hit=*/true);
|
|
}
|
|
PS::CountGate(PS::Gate::MagmaPipelineMemo, /*hit=*/false);
|
|
PS::CountGate(PS::Gate::MagmaDrawFastPath, /*hit=*/false);
|
|
|
|
EXPECT_EQ(PS::TotalGateHits(PS::Gate::MagmaPipelineMemo), 5u);
|
|
EXPECT_EQ(PS::TotalGateMisses(PS::Gate::MagmaPipelineMemo), 1u);
|
|
EXPECT_EQ(PS::TotalGateHits(PS::Gate::MagmaDrawFastPath), 0u);
|
|
EXPECT_EQ(PS::TotalGateMisses(PS::Gate::MagmaDrawFastPath), 1u);
|
|
}
|
|
|
|
// Bucket 0 is "no payload"; bucket n>0 is [2^(n-1), 2^n). The placeholder histogram is
|
|
// the SEG_CMD sizing input (section 4.5.7), so its bucketing is pinned now rather than
|
|
// when a generator first calls it.
|
|
TEST_F(PipeStatsTest, PayloadHistogramBucketsByPowerOfTwo) {
|
|
PS::RecordDrawPayloadBytes(0);
|
|
PS::RecordDrawPayloadBytes(1); // [1, 2) -> bucket 1
|
|
PS::RecordDrawPayloadBytes(2); // [2, 4) -> bucket 2
|
|
PS::RecordDrawPayloadBytes(3); // [2, 4) -> bucket 2
|
|
PS::RecordDrawPayloadBytes(48); // [32, 64) -> bucket 6
|
|
PS::RecordDrawPayloadBytes(64); // [64, 128)-> bucket 7
|
|
|
|
EXPECT_EQ(PS::TotalPayloadBucket(0), 1u);
|
|
EXPECT_EQ(PS::TotalPayloadBucket(1), 1u);
|
|
EXPECT_EQ(PS::TotalPayloadBucket(2), 2u);
|
|
EXPECT_EQ(PS::TotalPayloadBucket(6), 1u);
|
|
EXPECT_EQ(PS::TotalPayloadBucket(7), 1u);
|
|
}
|
|
|
|
// A record far larger than the last bucket must land in the last bucket, not past the
|
|
// end of the array.
|
|
TEST_F(PipeStatsTest, PayloadHistogramSaturatesInsteadOfOverflowing) {
|
|
PS::RecordDrawPayloadBytes(~Uint64{0});
|
|
EXPECT_EQ(PS::TotalPayloadBucket(PS::kPayloadHistogramBuckets - 1), 1u);
|
|
EXPECT_EQ(PS::TotalPayloadBucket(PS::kPayloadHistogramBuckets), 0u);
|
|
}
|
|
|
|
// The summary line's shape is what an operator greps and what the smoke check in this
|
|
// package matches, so it is pinned here rather than left to the log reader's memory.
|
|
TEST_F(PipeStatsTest, SummaryLineCarriesEveryClassAndGate) {
|
|
PS::AddCalls(PS::CallClass::Draws, 4);
|
|
PS::AddCalls(PS::CallClass::AccessorCalls, 50);
|
|
PS::AddBytes(PS::ByteClass::StageBuffer, 4096);
|
|
PS::OnPresent();
|
|
|
|
const String line = PS::FormatWindowLine();
|
|
EXPECT_NE(line.find("MGPipe stats:"), String::npos) << line;
|
|
EXPECT_NE(line.find("draws=4"), String::npos) << line;
|
|
// 50 accessor calls over 4 draws, two decimals, no <iomanip>.
|
|
EXPECT_NE(line.find("acc/draw=12.50"), String::npos) << line;
|
|
EXPECT_NE(line.find("buf=4096.00"), String::npos) << line;
|
|
for (Uint32 i = 0; i < static_cast<Uint32>(PS::Gate::Count); ++i) {
|
|
EXPECT_NE(line.find("="), String::npos);
|
|
}
|
|
EXPECT_NE(line.find("gates["), String::npos) << line;
|
|
EXPECT_NE(line.find("tex[emit="), String::npos) << line;
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// P2's two render-state CSO counters ride the same line, short-named. Push-only:
|
|
// the pull build has no CSO to mint and must stay symbol-identical.
|
|
EXPECT_NE(line.find("cso[csom="), String::npos) << line;
|
|
EXPECT_NE(line.find("csob="), String::npos) << line;
|
|
// P3a's persistent-map acquisition attempts ride the same bracket. It is the counter
|
|
// the storage-regrow gate reads, so its short name is pinned where an operator's
|
|
// grep would break.
|
|
EXPECT_NE(line.find("mpr="), String::npos) << line;
|
|
#endif
|
|
}
|
|
|
|
// Per-frame fields carry two decimals for the same reason acc/draw does: they are small
|
|
// and load-bearing (bytes/f sizes SEG_STAGE), and integer division silently rounds a
|
|
// whole unit off each of them. 26 draws over 14 frames is 1.86, not 1.
|
|
TEST_F(PipeStatsTest, PerFrameFieldsKeepTwoDecimals) {
|
|
PS::AddCalls(PS::CallClass::Draws, 26);
|
|
PS::AddBytes(PS::ByteClass::StageBuffer, 1360);
|
|
for (Uint32 i = 0; i < 14; ++i) {
|
|
PS::OnPresent();
|
|
}
|
|
|
|
const String line = PS::FormatWindowLine();
|
|
EXPECT_NE(line.find("draws/f=1.86"), String::npos) << line;
|
|
EXPECT_NE(line.find("buf=97.14"), String::npos) << line;
|
|
}
|
|
|
|
// Successive summaries report WINDOWS, not run totals: a run total over a workload that
|
|
// changes shape (load, then steady state) averages away the very number section 2.3.1
|
|
// wants. Advancing the window is an explicit call, not a side effect of formatting.
|
|
TEST_F(PipeStatsTest, SummaryLinesReportDisjointWindows) {
|
|
PS::AddCalls(PS::CallClass::Draws, 10);
|
|
PS::OnPresent();
|
|
const String first = PS::FormatWindowLine();
|
|
EXPECT_NE(first.find("draws=10"), String::npos) << first;
|
|
PS::AdvanceSummaryWindow();
|
|
|
|
PS::AddCalls(PS::CallClass::Draws, 3);
|
|
PS::OnPresent();
|
|
const String second = PS::FormatWindowLine();
|
|
EXPECT_NE(second.find("draws=3"), String::npos) << second;
|
|
EXPECT_NE(second.find("frames=2"), String::npos) << second;
|
|
}
|
|
|
|
// FormatWindowLine is pure. It used to rewrite the window bases as a side effect of
|
|
// formatting, so any second reader - a probe, a test, a second reporting channel -
|
|
// silently zeroed the next window.
|
|
TEST_F(PipeStatsTest, FormattingTwiceDoesNotConsumeTheWindow) {
|
|
PS::AddCalls(PS::CallClass::Draws, 7);
|
|
PS::OnPresent();
|
|
|
|
const String first = PS::FormatWindowLine();
|
|
const String second = PS::FormatWindowLine();
|
|
EXPECT_EQ(first, second) << first << "\n" << second;
|
|
EXPECT_NE(second.find("draws=7"), String::npos) << second;
|
|
|
|
// ...and advancing explicitly does close it.
|
|
PS::AdvanceSummaryWindow();
|
|
const String third = PS::FormatWindowLine();
|
|
EXPECT_NE(third.find("draws=0"), String::npos) << third;
|
|
}
|
|
|
|
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroDraws) {
|
|
PS::AddCalls(PS::CallClass::AccessorCalls, 12);
|
|
PS::OnPresent();
|
|
const String line = PS::FormatWindowLine();
|
|
// No draw in the window means there is no per-draw number - and "0.00" beside a
|
|
// non-zero acc= would read as one.
|
|
EXPECT_NE(line.find("acc/draw=n/a"), String::npos) << line;
|
|
EXPECT_NE(line.find("acc=12"), String::npos) << line;
|
|
}
|
|
|
|
// A window with no Present in it has no per-frame reading at all. This used to divide by
|
|
// a faked 1 and print the window TOTALS under a "/f" label: a scenario slice that draws
|
|
// 47 times and never presents reported 1,404,550 staged bytes as a per-frame figure,
|
|
// which is a 47x overstatement of the SEG_STAGE sizing input this package exists to
|
|
// produce.
|
|
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroFrames) {
|
|
PS::AddCalls(PS::CallClass::Draws, 47);
|
|
PS::AddBytes(PS::ByteClass::StageBuffer, 1404550);
|
|
|
|
const String line = PS::FormatWindowLine();
|
|
EXPECT_EQ(PS::FrameCount(), 0u);
|
|
EXPECT_NE(line.find("window=0"), String::npos) << line;
|
|
EXPECT_NE(line.find("draws/f=n/a"), String::npos) << line;
|
|
// The bracket is relabelled rather than divided: totals, and marked as totals.
|
|
EXPECT_EQ(line.find("bytes/f["), String::npos) << line;
|
|
EXPECT_NE(line.find("bytes[buf=1404550"), String::npos) << line;
|
|
}
|
|
|
|
TEST_F(PipeStatsTest, JsonDumpNamesEveryCounter) {
|
|
PS::AddBytes(PS::ByteClass::StageUboNamed, 256);
|
|
PS::CountGate(PS::Gate::MagmaDynamicTail, /*hit=*/false);
|
|
PS::RecordDrawPayloadBytes(9);
|
|
PS::OnPresent();
|
|
|
|
const String json = PS::FormatJson();
|
|
for (Uint32 i = 0; i < static_cast<Uint32>(PS::ByteClass::Count); ++i) {
|
|
const String name = PS::NameOf(static_cast<PS::ByteClass>(i));
|
|
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
|
|
}
|
|
for (Uint32 i = 0; i < static_cast<Uint32>(PS::CallClass::Count); ++i) {
|
|
const String name = PS::NameOf(static_cast<PS::CallClass>(i));
|
|
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
|
|
}
|
|
for (Uint32 i = 0; i < static_cast<Uint32>(PS::Gate::Count); ++i) {
|
|
const String name = PS::NameOf(static_cast<PS::Gate>(i));
|
|
EXPECT_NE(json.find("\"" + name + "\""), String::npos) << name << " missing from " << json;
|
|
}
|
|
EXPECT_NE(json.find("\"stage-ubo-named\": 256"), String::npos) << json;
|
|
EXPECT_NE(json.find("\"frames\": 1"), String::npos) << json;
|
|
EXPECT_NE(json.find("cmd-bytes-per-draw-histogram"), String::npos) << json;
|
|
}
|
|
|
|
// The counter names are the TracyPlot series names and the JSON keys; a rename is a
|
|
// breaking change for every recorded baseline, so the whole set is pinned.
|
|
TEST_F(PipeStatsTest, CounterNamesAreStable) {
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageBuffer), "stage-buffer");
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageTexture), "stage-texture");
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageUboGlobal), "stage-ubo-global");
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageUboNamed), "stage-ubo-named");
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageVertexClient), "stage-vertex-client");
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndexClient), "stage-index-client");
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndirectCmd), "stage-indirect-cmd");
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::PersistentMapPush), "persistent-map-push");
|
|
EXPECT_STREQ(PS::NameOf(PS::ByteClass::ResidualValueBlock), "residual-value-block");
|
|
#if MOBILEGL_PIPE_PUSH
|
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoMints), "render-state-cso-mints");
|
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::RenderStateCsoBinds), "render-state-cso-binds");
|
|
EXPECT_STREQ(PS::NameOf(PS::CallClass::MapPersistentRoundtrips), "map-persistent-roundtrips");
|
|
#endif
|
|
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");
|
|
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaDrawFastPath), "magma-draw-fastpath");
|
|
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaPipelineMemo), "magma-pipeline-memo");
|
|
EXPECT_STREQ(PS::NameOf(PS::Gate::MagmaDynamicTail), "magma-dynamic-tail");
|
|
}
|
|
|
|
// A summary is emitted every kSummaryFramePeriod presents. The period is a constant the
|
|
// smoke check depends on, so a change to it has to break a test.
|
|
TEST_F(PipeStatsTest, SummaryPeriodIsOneHundredAndTwentyFrames) {
|
|
EXPECT_EQ(PS::SummaryFramePeriod(), 120u);
|
|
for (Uint64 i = 0; i < PS::SummaryFramePeriod(); ++i) {
|
|
PS::OnPresent();
|
|
}
|
|
EXPECT_EQ(PS::FrameCount(), PS::SummaryFramePeriod());
|
|
}
|
|
} // namespace
|