diff --git a/MobileGL/MG_Test/Wire/ApplyThreadPolicyTest.cpp b/MobileGL/MG_Test/Wire/ApplyThreadPolicyTest.cpp index e0e72fff..a8a5d36b 100644 --- a/MobileGL/MG_Test/Wire/ApplyThreadPolicyTest.cpp +++ b/MobileGL/MG_Test/Wire/ApplyThreadPolicyTest.cpp @@ -7,30 +7,39 @@ // End of Source File Header // P11 B1 (CONTRACT-P11 B1): the apply thread's affinity and spin, selected per session from the -// peer's Hello dial mode and data plane. Every row of ApplyThreadPolicy.h's table, with the Redmi's -// topology (cpu0-5 performance, cpu6-7 prime: big 0xc0 of 0xff) and the shapes that must not be read -// as one. +// peer's Hello dial mode and data plane, and the core reserved for a dialled-in shared-segment peer +// on every topology shape that matters - the Redmi's (cpu0-5 performance, cpu6-7 prime: big 0xc0, +// prime 0xc0 of 0xff), a lone prime core beside other big cores, a lone prime core alone, symmetric +// machines, an unreadable topology. #include #include +#include +#include + using namespace MobileGL; using namespace MobileGL::MG_Remote::Server; namespace { constexpr Uint64 kRedmiBig = 0xc0; + constexpr Uint64 kRedmiPrime = 0xc0; constexpr Uint64 kRedmiOnline = 0xff; constexpr Uint32 kConfigured = 50; constexpr Bool kShm = true; constexpr Bool kStream = false; + + ApplyThreadPolicy Connected(const char* affinity, Uint64 big, Uint64 prime, Uint64 online) { + return SelectApplyThreadPolicy(affinity, false, kConfigured, ApplyPeer::Connected, kShm, big, prime, online); + } } // namespace TEST(ApplyThreadPolicy, AutoKeepsEveryBigCoreAndTheConfiguredSpinForAnInProcessOrForkedPeer) { // inproc and the same-app fork spawn: exactly the pre-B1 `auto` (A4's measured shapes). for (const ApplyPeer peer : {ApplyPeer::InProcess, ApplyPeer::Forked}) { - const auto policy = - SelectApplyThreadPolicy("auto", false, kConfigured, peer, kShm, kRedmiBig, kRedmiOnline); + const auto policy = SelectApplyThreadPolicy("auto", false, kConfigured, peer, kShm, kRedmiBig, kRedmiPrime, + kRedmiOnline); EXPECT_TRUE(policy.recognised); EXPECT_EQ(policy.requestedMask, kRedmiBig) << ApplyPeerName(peer); EXPECT_EQ(policy.spinUs, kConfigured) << ApplyPeerName(peer); @@ -39,80 +48,107 @@ TEST(ApplyThreadPolicy, AutoKeepsEveryBigCoreAndTheConfiguredSpinForAnInProcessO TEST(ApplyThreadPolicy, AutoKeepsEveryBigCoreForADialledInPeerOnTheStreamPlane) { // tcp: an io thread feeds the apply thread; nothing changes for a TCP client. - const auto policy = - SelectApplyThreadPolicy("auto", false, kConfigured, ApplyPeer::Connected, kStream, kRedmiBig, kRedmiOnline); + const auto policy = SelectApplyThreadPolicy("auto", false, kConfigured, ApplyPeer::Connected, kStream, kRedmiBig, + kRedmiPrime, kRedmiOnline); EXPECT_EQ(policy.requestedMask, kRedmiBig); EXPECT_EQ(policy.spinUs, kConfigured); } -TEST(ApplyThreadPolicy, AutoGivesADialledInSharedSegmentPeerTheLowestPrimeCoreOnly) { - const auto policy = - SelectApplyThreadPolicy("auto", false, kConfigured, ApplyPeer::Connected, kShm, kRedmiBig, kRedmiOnline); +TEST(ApplyThreadPolicy, SeveralPrimeCoresGiveTheApplyThreadTheLowestOfThem) { + const auto policy = Connected("auto", kRedmiBig, kRedmiPrime, kRedmiOnline); EXPECT_TRUE(policy.recognised); EXPECT_EQ(policy.requestedMask, 0x40u) - << "the LOWEST big core: the scheduler puts a foreground client's GL thread on the highest"; + << "the LOWEST prime core: the scheduler puts a foreground client's GL thread on the highest"; EXPECT_EQ(policy.spinUs, kConnectedPeerSpinUs); + EXPECT_NE(std::string(policy.rule).find("lowest of the prime cores"), std::string::npos) << policy.rule; // A non-contiguous prime set: still exactly one core, the lowest. - EXPECT_EQ(SelectApplyThreadPolicy("auto", false, kConfigured, ApplyPeer::Connected, kShm, 0x0a, 0xff).requestedMask, - 0x02u); - EXPECT_EQ(ReservedApplyCore(kRedmiBig, kRedmiOnline), 0x40u) << "the core the broker tells the helper to avoid"; + EXPECT_EQ(Connected("auto", 0x0a, 0x0a, 0xff).requestedMask, 0x02u); +} + +TEST(ApplyThreadPolicy, ALonePrimeCoreIsNeverTheApplyThreadsItTakesABigCoreBesideIt) { + // cpu0-3 little, cpu4 the only core at the peak (3.3 GHz), cpu5-7 big (3.0, within 15%): the + // lowest-numbered big core IS the prime here, and it stays the client's - the apply thread takes cpu5. + ReservedCoreReason reason = ReservedCoreReason::NoAsymmetry; + EXPECT_EQ(ReservedApplyCore(0xf0, 0x10, 0xff, &reason), 0x20u); + EXPECT_EQ(reason, ReservedCoreReason::BigCoreBesideTheOnlyPrime); + const auto policy = Connected("auto", 0xf0, 0x10, 0xff); + EXPECT_EQ(policy.requestedMask, 0x20u); + EXPECT_NE(std::string(policy.rule).find("beside the only prime core"), std::string::npos) << policy.rule; + // A Snapdragon 8 Gen 2 shape: cpu0-2 little, cpu3-6 big within 15%, cpu7 the lone prime. + EXPECT_EQ(ReservedApplyCore(0xf8, 0x80, 0xff), 0x08u); +} + +TEST(ApplyThreadPolicy, ALonePrimeCoreWithNoOtherBigCoreLeavesTheApplyThreadUnpinned) { + // A Tensor-G3-like shape read at 85%: only cpu8 is big, and it is the prime. Pinning the spinning + // apply thread there would take the one core the client wants; it stays unpinned, and says so. + ReservedCoreReason reason = ReservedCoreReason::NoAsymmetry; + EXPECT_EQ(ReservedApplyCore(0x100, 0x100, 0x1ff, &reason), 0u); + EXPECT_EQ(reason, ReservedCoreReason::OnlyPrimeCoreLeftAlone); + const auto policy = Connected("auto", 0x80, 0x80, 0xff); + EXPECT_EQ(policy.requestedMask, 0u); + EXPECT_NE(std::string(policy.rule).find("unpinned - the only prime core"), std::string::npos) << policy.rule; + EXPECT_EQ(policy.spinUs, kConnectedPeerSpinUs); +} + +TEST(ApplyThreadPolicy, NoAsymmetryLeavesTheApplyThreadUnpinned) { + // A symmetric machine (every cpu big and prime) and an unreadable topology (0): nothing to split. + for (const auto& shape : {std::pair{0xff, 0xff}, std::pair{0, 0}}) { + ReservedCoreReason reason = ReservedCoreReason::LowestOfSeveralPrimeCores; + EXPECT_EQ(ReservedApplyCore(shape.first, shape.second, 0xff, &reason), 0u) << shape.first; + EXPECT_EQ(reason, ReservedCoreReason::NoAsymmetry) << shape.first; + EXPECT_NE(std::string(Connected("auto", shape.first, shape.second, 0xff).rule).find("no asymmetric"), + std::string::npos); + } + // ...and the in-process / forked rows keep the whole (symmetric) big set as before. + EXPECT_EQ(SelectApplyThreadPolicy("auto", false, kConfigured, ApplyPeer::Forked, kShm, 0xff, 0xff, 0xff) + .requestedMask, + 0xffu); } TEST(ApplyThreadPolicy, AnEmptyOrMissingAffinityIsAuto) { for (const char* text : {static_cast(nullptr), ""}) { - EXPECT_EQ(SelectApplyThreadPolicy(text, false, kConfigured, ApplyPeer::Connected, kShm, kRedmiBig, + EXPECT_EQ(Connected(text, kRedmiBig, kRedmiPrime, kRedmiOnline).requestedMask, 0x40u); + EXPECT_EQ(SelectApplyThreadPolicy(text, false, kConfigured, ApplyPeer::Forked, kShm, kRedmiBig, kRedmiPrime, kRedmiOnline) .requestedMask, - 0x40u); - EXPECT_EQ(SelectApplyThreadPolicy(text, false, kConfigured, ApplyPeer::Forked, kShm, kRedmiBig, kRedmiOnline) - .requestedMask, kRedmiBig); + EXPECT_EQ(ReservedApplyCoreForConfig(text, kRedmiBig, kRedmiPrime, kRedmiOnline), 0x40u); } } -TEST(ApplyThreadPolicy, ADialledInPeerGetsNoAffinityWhereThereIsNoPrimePairToSplit) { - // A symmetric machine (every cpu is "big"), a single prime core, an unreadable topology: none - // has a second prime core to leave to the client, so the scheduler places both threads. - for (const Uint64 big : {Uint64{0xff}, Uint64{0x80}, Uint64{0}}) { - EXPECT_EQ(SelectApplyThreadPolicy("auto", false, kConfigured, ApplyPeer::Connected, kShm, big, 0xff) - .requestedMask, - 0u) - << big; - EXPECT_EQ(ReservedApplyCore(big, 0xff), 0u) << big; - } - // ...and the in-process / forked rows keep the whole (symmetric) big set as before. - EXPECT_EQ(SelectApplyThreadPolicy("auto", false, kConfigured, ApplyPeer::Forked, kShm, 0xff, 0xff).requestedMask, - 0xffu); -} - TEST(ApplyThreadPolicy, AnExplicitSpinWinsForADialledInPeer) { - const auto policy = - SelectApplyThreadPolicy("auto", true, 7, ApplyPeer::Connected, kShm, kRedmiBig, kRedmiOnline); + const auto policy = SelectApplyThreadPolicy("auto", true, 7, ApplyPeer::Connected, kShm, kRedmiBig, kRedmiPrime, + kRedmiOnline); EXPECT_EQ(policy.spinUs, 7u); EXPECT_EQ(policy.requestedMask, 0x40u); } -TEST(ApplyThreadPolicy, AnExplicitMaskOrOffWinsForEveryPeerAndPlane) { +TEST(ApplyThreadPolicy, AnExplicitMaskOrOffWinsForEveryPeerAndPlaneAndReservesNothing) { for (const ApplyPeer peer : {ApplyPeer::InProcess, ApplyPeer::Forked, ApplyPeer::Connected}) { for (const Bool plane : {kShm, kStream}) { - const auto off = SelectApplyThreadPolicy("off", false, kConfigured, peer, plane, kRedmiBig, kRedmiOnline); + const auto off = SelectApplyThreadPolicy("off", false, kConfigured, peer, plane, kRedmiBig, kRedmiPrime, + kRedmiOnline); EXPECT_TRUE(off.recognised); EXPECT_EQ(off.requestedMask, 0u) << ApplyPeerName(peer); EXPECT_EQ(off.spinUs, kConfigured) << "an operator's affinity keeps the configured spin"; - const auto mask = SelectApplyThreadPolicy("0x80", false, kConfigured, peer, plane, kRedmiBig, kRedmiOnline); + const auto mask = SelectApplyThreadPolicy("0x80", false, kConfigured, peer, plane, kRedmiBig, + kRedmiPrime, kRedmiOnline); EXPECT_TRUE(mask.recognised); EXPECT_EQ(mask.requestedMask, 0x80u) << ApplyPeerName(peer); - EXPECT_EQ(SelectApplyThreadPolicy("12", false, kConfigured, peer, plane, kRedmiBig, kRedmiOnline) + EXPECT_EQ(SelectApplyThreadPolicy("12", false, kConfigured, peer, plane, kRedmiBig, kRedmiPrime, + kRedmiOnline) .requestedMask, 12u); } } + // What the supervisor announces (and the helper avoids) under an operator's affinity: nothing. + EXPECT_EQ(ReservedApplyCoreForConfig("0x80", kRedmiBig, kRedmiPrime, kRedmiOnline), 0u); + EXPECT_EQ(ReservedApplyCoreForConfig("off", kRedmiBig, kRedmiPrime, kRedmiOnline), 0u); } TEST(ApplyThreadPolicy, UnrecognisedAffinityTextAppliesNothingAndSaysSo) { for (const char* text : {"big", "0xzz", "auto ", "3cores"}) { - const auto policy = - SelectApplyThreadPolicy(text, false, kConfigured, ApplyPeer::Connected, kShm, kRedmiBig, kRedmiOnline); + const auto policy = Connected(text, kRedmiBig, kRedmiPrime, kRedmiOnline); EXPECT_FALSE(policy.recognised) << text; EXPECT_EQ(policy.requestedMask, 0u) << text; } diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index 9e53f7f0..4d3518e5 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -90,6 +90,10 @@ foreach (test IN LISTS MOBILEGL_WIRE_TESTS) GTest::gtest_main ${LINK_LIBRARIES} ) + if (test STREQUAL "ServerSpawnTest") + # P11 B1: reads the broker's PairBind templates out of android-plugin/.../PairBindFrames.java. + target_compile_definitions(${test} PRIVATE MOBILEGL_SOURCE_ROOT="${MGL_ROOT}") + endif () if (test STREQUAL "ServerSpawnTest" OR test STREQUAL "PeerLatchTest" OR test STREQUAL "InProcessServerTest" OR test STREQUAL "ServerOwnedSurfaceTest" OR test STREQUAL "AdoptTierStreamTest") diff --git a/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp b/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp index 72e5894d..f2df6ffe 100644 --- a/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerSpawnTest.cpp @@ -65,6 +65,7 @@ #include #if !defined(_WIN32) +#include #include #include #include @@ -1388,8 +1389,135 @@ namespace { return "fd:" + std::to_string(control) + "," + std::to_string(aux); } + // ---- ID-P11-12: the broker's hand-off PairBinds, byte for byte --------------------------------- + // + // The server app's broker (Java) writes the PairBind pair itself, from two prefixes in + // PairBindFrames.java. These read them out of that source file, so the tests below send exactly + // what the broker sends - and the template test pins them to the native encoder. + std::string ReadJavaHexConstant(const std::string& source, const char* name) { + const auto at = source.find(std::string(name) + " ="); + if (at == std::string::npos) return {}; + const auto open = source.find('"', at); + const auto close = open == std::string::npos ? open : source.find('"', open + 1); + if (close == std::string::npos) return {}; + return source.substr(open + 1, close - open - 1); + } + + std::vector HexBytes(const std::string& hex) { + std::vector out; + for (std::size_t i = 0; i + 1 < hex.size(); i += 2) { + out.push_back(static_cast(std::stoul(hex.substr(i, 2), nullptr, 16))); + } + return out; + } + + struct BrokerTemplates { + std::vector control; + std::vector aux; + }; + + BrokerTemplates LoadBrokerTemplates() { + const std::string path = std::string(MOBILEGL_SOURCE_ROOT) + + "/android-plugin/app/src/main/java/top/mobilegl/plugin/PairBindFrames.java"; + std::ifstream file(path); + const std::string source((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + return {HexBytes(ReadJavaHexConstant(source, "CONTROL_PREFIX_HEX")), + HexBytes(ReadJavaHexConstant(source, "AUX_PREFIX_HEX"))}; + } + + // PairBindFrames.frame(nonce, aux), in C++: header (magic, length) + prefix + nonce. + std::vector BrokerFrame(const BrokerTemplates& templates, const std::uint8_t (&nonce)[16], bool aux) { + const auto& prefix = aux ? templates.aux : templates.control; + std::vector payload(prefix); + payload.insert(payload.end(), nonce, nonce + 16); + std::vector framed; + (void)Transport::AppendFrame(framed, payload.data(), payload.size()); + return framed; + } + + bool WriteAll(int fd, const std::vector& bytes) { + std::size_t done = 0; + while (done < bytes.size()) { + const ssize_t n = ::send(fd, bytes.data() + done, bytes.size() - done, MSG_NOSIGNAL); + if (n <= 0) return false; + done += static_cast(n); + } + return true; + } + } // namespace +TEST(ServerSpawnTest, TheBrokersPairBindTemplatesAreTheEncoders) { + // The broker cannot link the flatbuffers encoder; its bytes are the encoder's up to the nonce, and + // this is what keeps them so. A schema change that moves one byte of a PairBind turns this red. + const auto templates = LoadBrokerTemplates(); + ASSERT_FALSE(templates.control.empty()) << "CONTROL_PREFIX_HEX not found in PairBindFrames.java"; + ASSERT_FALSE(templates.aux.empty()) << "AUX_PREFIX_HEX not found in PairBindFrames.java"; + std::uint8_t nonce[Transport::kPairNonceBytes]; + for (int round = 0; round < 32; ++round) { + ASSERT_EQ(Transport::SocketTransport::MintNonce(nonce, sizeof(nonce)), MOBILEGL_OK); + for (const bool aux : {false, true}) { + const auto encoded = Transport::EncodePairBind(nonce, aux); + std::vector expected; + ASSERT_EQ(Transport::AppendFrame(expected, encoded.data(), encoded.size()), MOBILEGL_OK); + EXPECT_EQ(BrokerFrame(templates, nonce, aux), expected) << (aux ? "aux" : "control") << " round " << round; + } + } +} + +TEST(ServerSpawnTest, APairTheBrokerPresentedAtHandoffWaitsForAHelloThatComesSecondsLater) { + // ID-P11-12 end to end: the broker connects AND presents the PairBind pair at hand-off; the + // program it was for starts slowly (assets first) and says Hello five seconds later - past the + // 2000 ms pairing budget, which the broker's PairBinds already met. The paired session waits for that + // Hello (until EOF, ServerMain.cpp kPairedUnixHelloWaitMs) and comes up; the program, told by the + // helper's MOBILEGL_IPC_FD_PAIRED=1, does not present a second pair. + const auto templates = LoadBrokerTemplates(); + ASSERT_FALSE(templates.control.empty()); + const std::string endpoint = Endpoint("handoff"); + Session session; + ASSERT_EQ(Server::LaunchServer(ServerImage(), endpoint, &session.server), MOBILEGL_OK); + ScopedSessionCleanup cleanup{session}; + int control = -1, aux = -1; + ASSERT_TRUE(BrokerConnectsTwice(endpoint, &control, &aux)) << "the server never listened on " << endpoint; + std::uint8_t nonce[Transport::kPairNonceBytes]; + ASSERT_EQ(Transport::SocketTransport::MintNonce(nonce, sizeof(nonce)), MOBILEGL_OK); + ASSERT_TRUE(WriteAll(control, BrokerFrame(templates, nonce, false))); + ASSERT_TRUE(WriteAll(aux, BrokerFrame(templates, nonce, true))); + std::this_thread::sleep_for(std::chrono::seconds(5)); // the program loading its assets + const ScopedEnvironment paired("MOBILEGL_IPC_FD_PAIRED", "1"); + const ScopedIpcEndpoint configured(FdSpec(control, aux), "auto"); + ASSERT_EQ(Client::ClientSessionInstance().StartSpawned(), MOBILEGL_OK) + << "a pair presented at hand-off did not become a session when its Hello came 5 s later"; + EXPECT_TRUE(Client::ClientSession::Active() != nullptr); + Client::ClientSessionInstance().Stop(); + int exitCode = -1; + ASSERT_EQ(Server::ReapServer(session.server, 5000, &exitCode), MOBILEGL_OK); + EXPECT_EQ(exitCode, 0); + session.server.pid = -1; +} + +TEST(ServerSpawnTest, APairPresentedOnlyWhenTheProgramStartsIsRefusedByNameOnceThePairingBudgetIsGone) { + // Why the broker presents the pair: the flow before ID-P11-12's follow-up - the broker connects and + // writes nothing, the program presents the PairBinds only when it reaches its first EGL call - is + // refused by the server's PairAcceptor once 2000 ms have passed since the connect, and the program + // hears it by name rather than hanging. + const std::string endpoint = Endpoint("latepair"); + Session session; + ASSERT_EQ(Server::LaunchServer(ServerImage(), endpoint, &session.server), MOBILEGL_OK); + ScopedSessionCleanup cleanup{session}; + int control = -1, aux = -1; + ASSERT_TRUE(BrokerConnectsTwice(endpoint, &control, &aux)); + std::this_thread::sleep_for(std::chrono::milliseconds(3000)); + const ScopedIpcEndpoint configured(FdSpec(control, aux), "auto"); + const auto start = std::chrono::steady_clock::now(); + EXPECT_NE(Client::ClientSessionInstance().StartSpawned(), MOBILEGL_OK) + << "a pair presented after the pairing budget became a session"; + EXPECT_LT(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(), + 4000) + << "the refusal must be answered, not waited out"; + EXPECT_TRUE(Client::ClientSession::Active() == nullptr); +} + TEST(ServerSpawnTest, AnFdPairTheBrokerConnectedBringsASharedSegmentSessionUp) { // THE B1 CLIENT PATH END TO END against a real server process: the pair was connected by // somebody else, the client adopts it, presents its own PairBind pair (the server's