mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
226 lines
11 KiB
C++
226 lines
11 KiB
C++
// MobileGL - MobileGL/MG_IntegrationTest/Harness/SplitRuntimePeek.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
|
|
|
|
#include "SplitRuntimePeek.h"
|
|
|
|
// MGITEST_SPLIT_RUNTIME_PEEK is defined by MG_IntegrationTest/CMakeLists.txt under
|
|
// MOBILEGL_BUILD_DISAGGREGATED and nowhere else. NO SOURCE PROBE decides it: the three symbols
|
|
// below are c0's, they exist in every disaggregated build from the contract commit onward, and
|
|
// their VALUES are what answer the question. That is the whole of the fix for review findings
|
|
// M-1, M-2 and M-3 - there is no longer a string for anyone to rename, comment out, or land
|
|
// outside a probed directory.
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK) && !defined(__ANDROID__)
|
|
#include <Config.h>
|
|
|
|
#include <MG_Remote/Client/CapsMirror.h>
|
|
#include <MG_Remote/Client/ClientSession.h>
|
|
#include <MG_Remote/Client/EmitTables.h>
|
|
#include <MG_Remote/Client/GpuWritePending.h>
|
|
#include <MG_Remote/Server/ServerLoop.h>
|
|
#include <chrono>
|
|
#include <atomic>
|
|
#include <thread>
|
|
#define MGITEST_SPLIT_RUNTIME_PEEK_LIVE 1
|
|
#endif
|
|
// P13 W5: THE RECORD-ARM HALF OF THE PEEK, which needs no transport. A library built with the
|
|
// record arm and without MG_Remote (the FCL shape) still answers dataArmIsRecord, so the scenarios
|
|
// that branch on it assert the arm the process really runs rather than the frontend arm a missing
|
|
// peek would imply.
|
|
#if defined(MGITEST_RECORD_ARM_PEEK) && !defined(__ANDROID__)
|
|
#include <Config.h>
|
|
#define MGITEST_RECORD_ARM_PEEK_LIVE 1
|
|
#endif
|
|
|
|
namespace MGITest {
|
|
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
namespace {
|
|
std::atomic<bool> g_holdArmed{false}, g_holdActive{false}, g_holdRelease{true};
|
|
void HoldOneAppliedBatch() {
|
|
if (!g_holdArmed.exchange(false, std::memory_order_acq_rel)) return;
|
|
g_holdActive.store(true, std::memory_order_release);
|
|
const auto until = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
|
while (!g_holdRelease.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < until)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
g_holdActive.store(false, std::memory_order_release);
|
|
}
|
|
}
|
|
#endif
|
|
bool ArmSplitApplyHoldForTesting() {
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
g_holdRelease.store(false, std::memory_order_release);
|
|
g_holdArmed.store(true, std::memory_order_release);
|
|
MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(&HoldOneAppliedBatch);
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
bool WaitForSplitApplyHoldForTesting(unsigned int timeoutMs) {
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
|
while (std::chrono::steady_clock::now() < until) {
|
|
if (g_holdActive.load(std::memory_order_acquire)) return true;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
#else
|
|
(void)timeoutMs;
|
|
#endif
|
|
return false;
|
|
}
|
|
bool SplitApplyHoldIsActiveForTesting() {
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
return g_holdActive.load(std::memory_order_acquire);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
void ReleaseSplitApplyHoldForTesting() {
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
g_holdRelease.store(true, std::memory_order_release);
|
|
g_holdArmed.store(false, std::memory_order_release);
|
|
MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(nullptr);
|
|
while (g_holdActive.load(std::memory_order_acquire))
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
#endif
|
|
}
|
|
bool SplitProducerIsParkedForTesting() {
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
// This is the only reader intended for the observer thread. Do not call
|
|
// PeekSplitRuntime there: its encoder and client-ledger fields are not atomic.
|
|
auto* session = MobileGL::MG_Remote::Client::ClientSession::Active();
|
|
return session && session->Control() && session->Control()->producerParked.load(std::memory_order_acquire);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
bool WaitForSplitAppliedForTesting(unsigned long long seq, unsigned int timeoutMs) {
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
auto* session = MobileGL::MG_Remote::Client::ClientSession::Active();
|
|
auto* control = session ? session->Control() : nullptr;
|
|
if (!control) return false;
|
|
const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
|
do {
|
|
if (control->Progress.appliedSeq.load(std::memory_order_acquire) >= seq) return true;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
} while (std::chrono::steady_clock::now() < until);
|
|
return control->Progress.appliedSeq.load(std::memory_order_acquire) >= seq;
|
|
#else
|
|
(void)seq; (void)timeoutMs;
|
|
return false;
|
|
#endif
|
|
}
|
|
void DelaySplitRetirementForTesting(bool enabled) {
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
MobileGL::MG_Remote::Server::ServerLoopInstance().SetBeforeRetireHookForTesting(
|
|
enabled ? +[] { std::this_thread::sleep_for(std::chrono::milliseconds(30)); } : nullptr);
|
|
#else
|
|
(void)enabled;
|
|
#endif
|
|
}
|
|
|
|
SplitRuntimeState PeekSplitRuntime() {
|
|
SplitRuntimeState state;
|
|
#if defined(MGITEST_RECORD_ARM_PEEK_LIVE)
|
|
state.dataArmIsRecord = MobileGL::MG_Config::DataArmIsRecord();
|
|
#endif
|
|
#if defined(MGITEST_SPLIT_RUNTIME_PEEK_LIVE)
|
|
using MobileGL::MG_Config::TransportMode;
|
|
state.peekAvailable = true;
|
|
state.totalVerbSlots = MobileGL::MG_Remote::Client::kRemoteEmitSlotCount;
|
|
switch (MobileGL::MG_Config::Transport) {
|
|
case TransportMode::Monolith: state.transportName = "monolith"; break;
|
|
case TransportMode::InProcess: state.transportName = "inproc"; break;
|
|
default: state.transportName = "non-monolith"; break;
|
|
}
|
|
state.transportResolved = MobileGL::MG_Config::Transport != TransportMode::Monolith;
|
|
state.dataArmIsRecord = MobileGL::MG_Config::DataArmIsRecord();
|
|
|
|
// Active() is c0's one deliberately non-aborting accessor: "does a session exist" has a
|
|
// legitimate no. Everything below it is only reached through a live session, so nothing
|
|
// here can trip one of c0's Fatal stubs.
|
|
MobileGL::MG_Remote::Client::ClientSession* session =
|
|
MobileGL::MG_Remote::Client::ClientSession::Active();
|
|
state.sessionActive = session != nullptr;
|
|
state.implementedVerbs = MobileGL::MG_Remote::Client::ImplementedVerbCount();
|
|
if (session != nullptr) {
|
|
// Encoder() returns the member; EmitSeq() returns m_emitSeq. Neither is a stub, and
|
|
// neither emits anything - this is a read.
|
|
const MobileGL::MG_Remote::Wire::PipeWireEncoder& encoder = session->Encoder();
|
|
state.emitSeq = encoder.EmitSeq();
|
|
state.runAheadArmed = session->RunAheadArmed();
|
|
// P7 wave 2 package C, OQ-10: read through the SAME accessor the frontend's own
|
|
// MGPipeResourceOpsHaveSubDataResident reads (MG_Impl/Pipe/PipeFill.cpp), so a
|
|
// green case here and a resident emission there cannot disagree about the bit.
|
|
state.residentSubDataCap =
|
|
MobileGL::MG_Remote::Client::CapsMirrorInstance().HasCap(
|
|
MobileGL::MG_Pipe::kCapResidentSubData);
|
|
state.presentCredit = MobileGL::MG_Config::Ipc.PresentCredit;
|
|
state.presentCreditWaits = session->PresentCreditWaits();
|
|
if (const auto* control = session->Control()) {
|
|
state.appliedSeq = control->Progress.appliedSeq.load(std::memory_order_acquire);
|
|
state.retiredSeq = control->Progress.retiredSeq.load(std::memory_order_acquire);
|
|
state.presentAckSerial = control->Progress.presentAckSerial.load(std::memory_order_acquire);
|
|
}
|
|
|
|
// The producer's ledger. Every one of these is a plain member read on the encoder
|
|
// or on the RingProducer it holds; none of them emits, publishes or waits, so a
|
|
// case may read them between two GL calls without changing what the next record is.
|
|
state.maxRecordBytes = encoder.MaxRecordBytesSeen();
|
|
state.maxRecordBytesCap = encoder.MaxRecordBytesCap();
|
|
state.cmdWraps = encoder.CmdWraps();
|
|
state.cmdWrapPads = encoder.CmdWrapPads();
|
|
state.cmdBytesWritten = encoder.CmdBytesWritten();
|
|
state.stageReclaimWaits = encoder.StageReclaimWaits();
|
|
state.replyPostings = session->ReplyPostings();
|
|
state.packBufferReadbackMarks = MobileGL::MG_Record::ProducerMarkCount(
|
|
MobileGL::MG_Record::GpuWriteProducer::ReadPixelsPackBuffer);
|
|
const auto fences = MobileGL::MG_Remote::Client::ReadFencePollCounters();
|
|
state.fenceLocalAnswers = fences.LocalAnswers;
|
|
state.fenceEscalations = fences.Escalations;
|
|
state.fenceRoundTrips = fences.RoundTrips;
|
|
state.fenceServerReports = fences.ServerReports;
|
|
}
|
|
#endif
|
|
return state;
|
|
}
|
|
|
|
std::string SplitRuntimeSkipReason() {
|
|
const SplitRuntimeState state = PeekSplitRuntime();
|
|
if (!state.peekAvailable) {
|
|
return "this build did not compile MG_Remote (no -DMOBILEGL_BUILD_DISAGGREGATED=ON), or "
|
|
"this is the Android binary, which links the shipping libMobileGL.so built "
|
|
"-fvisibility=hidden and can reach no internal symbol. MOBILEGL_TRANSPORT is "
|
|
"ACCEPTED AND SILENTLY IGNORED in such a build (CONTRACT-P5 5), so a green here "
|
|
"would be a monolith run under a name that says split";
|
|
}
|
|
if (!state.transportResolved) {
|
|
return "MG_Config::Transport resolved to '" + state.transportName +
|
|
"', not to a split transport. The variable is read from the process, not from a "
|
|
"log line - a DEBUG-level pull build prints the same KEY=VALUE string out of "
|
|
"ConfigLoader's env dump (review M-5). Check MOBILEGL_TRANSPORT reached this "
|
|
"process";
|
|
}
|
|
if (!state.sessionActive) {
|
|
return "MG_Remote::Client::ClientSession::Active() is null: no client session exists in "
|
|
"this process. c0 shipped Start() as a Fatal stub and Active() as a deliberate "
|
|
"null, so this is what 'packages s1 (construction and handshake) and c1 have not "
|
|
"landed' looks like from inside a running test. The entry stays registered (gate "
|
|
"G14) and skips rather than passing against the monolith path";
|
|
}
|
|
if (state.implementedVerbs == 0) {
|
|
return "MG_Remote::Client::ImplementedVerbCount() is 0 of " +
|
|
std::to_string(state.totalVerbSlots) +
|
|
": the emit table has no real emitter, so every verb this scenario issues would "
|
|
"take the Fatal{UnmigratedVerb} arm or fall through. Package c1 owns it";
|
|
}
|
|
return {};
|
|
}
|
|
|
|
} // namespace MGITest
|