mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-16 06:23:23 +09:00
219 lines
11 KiB
C++
219 lines
11 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h
|
|
// Copyright (c) 2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
//
|
|
// Reading ONE PipeStats summary window out of the library's own log, for the scenarios whose
|
|
// claim is about a counter rather than about pixels.
|
|
//
|
|
// WHY THROUGH A LOG FILE AT ALL. MG_Util::PipeStats is internal to the library and this module
|
|
// cannot link against it (ScenarioFixture.h has the long version: on Android this binary links
|
|
// the SHIPPING libMobileGL.so, built -fvisibility=hidden). The library's `MGPipe stats:` line is
|
|
// the only channel, so a lane that wants to read a counter sets MOBILEGL_PIPE_STATS=1,
|
|
// MOBILEGL_PIPE_STATS_PERIOD=1 - one line per eglSwapBuffers - and a MOBILEGL_LOG_FILE_PATH of
|
|
// its OWN.
|
|
//
|
|
// THE LOG PATH HAS TO BE PRIVATE TO ONE CTEST ENTRY, and that is not a style rule: the library
|
|
// opens it fopen(path, "w"), so every process launched in a lane TRUNCATES it. Two entries of one
|
|
// lane reading the same path race under `ctest -j`, and the shape of the failure is an empty read
|
|
// that looks exactly like "the counter was never emitted". So a case that reads a window gets a
|
|
// ctest entry whose TEST_FILTER selects that case alone, with a log path nothing else writes -
|
|
// the rule PipeVerifyArmingScenario and CsoContentAddressingScenario already follow.
|
|
//
|
|
// THE WINDOW IS "SINCE THE PREVIOUS LINE" (PipeStats::FormatWindowLine), so the caller closes the
|
|
// setup window with a swap, runs the workload, swaps again, and reads the LAST line - which then
|
|
// covers the workload and nothing else.
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <string>
|
|
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
extern "C" void MGPipeSyncPeerLog();
|
|
#endif
|
|
|
|
namespace MGITest::PipeStatsWindow {
|
|
|
|
// P6: MOBILEGL_LOG_FILE_PATH IS A BASE NAME, NOT A FILE. The library writes one log per
|
|
// ROLE - `<stem>.client<ext>` and `<stem>.server<ext>` - because under inproc both roles are
|
|
// threads of one process and a single file made every per-side assertion a search.
|
|
//
|
|
// THE RULE IS COPIED HERE AND IT HAS TO BE, which is worth stating because the obvious fix is
|
|
// to call the library's own MG_Util::Debug::RoleLogPath and delete this. That does not link:
|
|
// this module links the SHIPPING libMobileGL.so, built -fvisibility=hidden, and the role-path
|
|
// helpers are `t` (local) in it - only MG_Test, which links the static archive, may call them.
|
|
//
|
|
// SO THE COPY IS FLAVOUR-AWARE INSTEAD. That is the part the first copy got wrong: it derived
|
|
// `.client` in BOTH flavours, while the pull build has one role and writes
|
|
// MOBILEGL_LOG_FILE_PATH unchanged. In the verify lane it therefore opened a file that never
|
|
// existed and every reader reported "the library never logged" - a product failure that had
|
|
// not happened. Keep this `#if` and Log.cpp's RoleLogPath in step.
|
|
inline std::string RoleLogPath(const char* roleSuffix) {
|
|
const char* base = std::getenv("MOBILEGL_LOG_FILE_PATH");
|
|
if (base == nullptr || *base == '\0') return {};
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
std::string path(base);
|
|
const std::string::size_type slash = path.find_last_of("/\\");
|
|
const std::string::size_type dot = path.find_last_of('.');
|
|
if (dot == std::string::npos || (slash != std::string::npos && dot < slash)) {
|
|
return path + "." + roleSuffix;
|
|
}
|
|
return path.substr(0, dot) + "." + roleSuffix + path.substr(dot);
|
|
#else
|
|
(void)roleSuffix;
|
|
return std::string(base);
|
|
#endif
|
|
}
|
|
|
|
// The lane's private CLIENT log path, or empty when the lane configured none. In the pull
|
|
// build this IS the whole log.
|
|
inline std::string LibraryLogPath() { return RoleLogPath("client"); }
|
|
|
|
// The server role's half, or EMPTY when this build has no separate server half. Empty rather
|
|
// than "the same path again": every caller below concatenates the two, and a pull build that
|
|
// named one file twice would show each line twice - which reads as a doubled counter or a
|
|
// repeated diagnostic rather than as a path mistake.
|
|
inline std::string ServerLibraryLogPath() {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
return RoleLogPath("server");
|
|
#else
|
|
return {};
|
|
#endif
|
|
}
|
|
|
|
inline std::string ReadWholeFile(const std::string& path) {
|
|
if (path.empty()) return {};
|
|
std::ifstream file(path, std::ios::binary);
|
|
if (!file.good()) return {};
|
|
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
inline std::string ReadFileSince(const std::string& path, std::uintmax_t offset) {
|
|
if (path.empty()) return {};
|
|
std::ifstream file(path, std::ios::binary);
|
|
if (!file.good()) return {};
|
|
file.seekg(static_cast<std::streamoff>(offset));
|
|
return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
|
|
}
|
|
|
|
// A "the log is this long right now" snapshot, ONE OFFSET PER ROLE.
|
|
//
|
|
// The scenarios that assert on arming take a mark before the workload and read what was
|
|
// appended after it, so that only bytes this case caused can satisfy - or refute - the claim.
|
|
// The roles are separate files, so this cannot be one scalar: a single offset applied to the
|
|
// concatenation would slide by however much the OTHER role happened to write, and the read
|
|
// would start mid-line in the wrong file.
|
|
struct LogMark {
|
|
std::uintmax_t client = 0;
|
|
std::uintmax_t server = 0;
|
|
};
|
|
|
|
inline std::uintmax_t FileSizeOrZero(const std::string& path) {
|
|
if (path.empty()) return 0;
|
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
|
if (!file.good()) return 0;
|
|
const std::streamoff size = file.tellg();
|
|
return size < 0 ? 0 : static_cast<std::uintmax_t>(size);
|
|
}
|
|
|
|
inline LogMark MarkLaneLog() {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
MGPipeSyncPeerLog();
|
|
#endif
|
|
return LogMark{FileSizeOrZero(LibraryLogPath()), FileSizeOrZero(ServerLibraryLogPath())};
|
|
}
|
|
|
|
// BOTH ROLES, and for the arming diagnostics the SERVER's is the one that matters. Those lines
|
|
// are emitted by the BACKEND - "demoted to an ordinary varying", the reroute and emulation
|
|
// banners - and under inproc the backend runs on the apply thread, which is the server role.
|
|
// Reading only the client's half finds nothing and reports "the emulation is not armed",
|
|
// which is the most expensive possible way to be wrong: it accuses the product of a defect
|
|
// that the reader itself invented.
|
|
inline std::string ReadLaneLogSince(const LogMark& mark) {
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
MGPipeSyncPeerLog();
|
|
#endif
|
|
return ReadFileSince(LibraryLogPath(), mark.client)
|
|
+ ReadFileSince(ServerLibraryLogPath(), mark.server);
|
|
}
|
|
|
|
inline std::string ReadLaneLog() { return ReadLaneLogSince(LogMark{}); }
|
|
|
|
// The last summary line in the log, verbatim. `found` is false when the library never emitted
|
|
// one, which is a different failure from "the counter read zero" and has to be reported as
|
|
// one: it means the stats channel never reached the process, not that the workload did
|
|
// nothing.
|
|
struct Window {
|
|
bool found = false;
|
|
std::string line;
|
|
};
|
|
|
|
inline Window Last(const std::string& log) {
|
|
Window window;
|
|
const std::string marker = "MGPipe stats:";
|
|
const std::size_t at = log.rfind(marker);
|
|
if (at == std::string::npos) return window;
|
|
const std::size_t end = log.find('\n', at);
|
|
window.line = log.substr(at, end == std::string::npos ? std::string::npos : end - at);
|
|
window.found = true;
|
|
return window;
|
|
}
|
|
|
|
// BOTH ROLES, and the server's is where the numbers are. The `draws` / `vbs` counters
|
|
// increment in the backend's PrepareForDraw, which runs on the APPLY THREAD - the server
|
|
// role - so under inproc split the summary line lands in the server's log. Reading only the
|
|
// client's would find the "counters ON" banner (client-side) but no window, which reads as
|
|
// "the stats channel never reached the process". Concatenated, Last() takes whichever role
|
|
// emitted the final summary.
|
|
inline Window LastFromLaneLog() {
|
|
return Last(ReadWholeFile(LibraryLogPath()) + ReadWholeFile(ServerLibraryLogPath()));
|
|
}
|
|
|
|
// One counter out of that line, by its short name ("mpr", "draws", "csom"), or -1 when the
|
|
// line does not carry it. The search includes the SEPARATOR before the name and the `=` after
|
|
// it, so "draws" cannot match "draws/f=" and "mpr" cannot match a longer name ending in it -
|
|
// a substring match here would read a neighbouring counter's value and report it as this
|
|
// one's, which is the one way a counter assertion can be wrong without ever failing.
|
|
inline long long CounterOrAbsent(const Window& window, const char* shortName) {
|
|
if (!window.found) return -1;
|
|
// A counter is preceded either by a space (` mpr=`, ` draws=`) or by its bracket's
|
|
// opening (`cso[csom=`, `bytes/f[stage-buffer=`); nothing in the line is preceded by
|
|
// anything else.
|
|
for (const char* prefix : {" ", "["}) {
|
|
const std::string key = std::string(prefix) + shortName + "=";
|
|
const std::size_t at = window.line.find(key);
|
|
if (at == std::string::npos) continue;
|
|
return std::strtoll(window.line.c_str() + at + key.size(), nullptr, 10);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// The same lookup for a counter that is printed as a FIXED-POINT PER-FRAME FIGURE rather
|
|
// than as an integer, which is every member of the bytes/f[...] bracket: FormatWindowLine
|
|
// divides each byte class by the window's frame count and prints two decimals whenever the
|
|
// window contains a Present. `pmap` is one of those, so CounterOrAbsent's strtoll reads
|
|
// "0.37" as 0 and an assertion that a push HAPPENED silently becomes an assertion that it
|
|
// pushed at least one whole byte per frame - the one way this counter can be wrong without
|
|
// ever failing. Returns -1.0 when the line does not carry the name; every real value of a
|
|
// byte class is >= 0, so the sentinel cannot collide with one.
|
|
inline double CounterAsDoubleOrAbsent(const Window& window, const char* shortName) {
|
|
if (!window.found) return -1.0;
|
|
for (const char* prefix : {" ", "["}) {
|
|
const std::string key = std::string(prefix) + shortName + "=";
|
|
const std::size_t at = window.line.find(key);
|
|
if (at == std::string::npos) continue;
|
|
return std::strtod(window.line.c_str() + at + key.size(), nullptr);
|
|
}
|
|
return -1.0;
|
|
}
|
|
|
|
} // namespace MGITest::PipeStatsWindow
|