From 11e650981d271d8d9d22202d908f9166225bd926 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:57:15 +0100 Subject: [PATCH 1/2] fix(libSceNet): pace hot zero-timeout epoll polls and record epoll events in the observation log Fixes hub T54 --- core/libs/prx/libSceNet/CMakeLists.txt | 15 ++ core/libs/prx/libSceNet/Export.cpp | 39 ++++- core/libs/tests/NetEpollPerCall.cpp | 207 +++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/NetEpollPerCall.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index e7d76485ef0..d08ff192337 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -230,4 +230,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(net_epoll_per_call_tests ${LIBS_INCLUDE_DIR}/tests/NetEpollPerCall.cpp) + target_include_directories(net_epoll_per_call_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_epoll_per_call_tests PRIVATE libkernel libc libSceNetCtl) + configure_windows_unwind(net_epoll_per_call_tests) + if(WIN32) + target_link_libraries(net_epoll_per_call_tests PRIVATE ws2_32) + endif() + foreach(scenario IN ITEMS udp tcp refused closed spin observe) + add_test(NAME net_epoll_per_call_${scenario} COMMAND net_epoll_per_call_tests ${scenario}) + set_tests_properties(net_epoll_per_call_${scenario} PROPERTIES TIMEOUT 40) + if(WIN32) + set_tests_properties(net_epoll_per_call_${scenario} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 64afa933e7a..0a3487db410 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libSceNetCtl/include/NetBridge.hpp" @@ -2390,6 +2391,24 @@ int APS5_VABI sceNetGetSockInfo6(std::int32_t s, NetSockInfo6* info, std::uint32 return netCall.Ret(written); } +// A guest that polls with a zero timeout in a loop that never sleeps (a throwaway epoll per +// Socket.Poll call in a managed runtime does this) would otherwise burn every core and starve the +// threads that make progress. Hot repeated empty polls from one thread are first yielded and then +// paced by about a millisecond; a ready event or a pause in the loop starts the count again. +static void pace_empty_poll(bool ready) { + constexpr auto HotGap = std::chrono::microseconds(500); + constexpr unsigned YieldAfter = 32; + constexpr unsigned SleepAfter = 256; + thread_local std::chrono::steady_clock::time_point last{}; + thread_local unsigned streak = 0; + const auto now = std::chrono::steady_clock::now(); + if (ready || now - last >= HotGap) streak = 0; + else if (streak < SleepAfter) ++streak; + if (streak >= SleepAfter) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + else if (streak >= YieldAfter) std::this_thread::yield(); + last = streak == 0 ? now : std::chrono::steady_clock::now(); +} + int APS5_VABI sceNetEpollCreate(const char* name, int flags) { NetBridgeCall netCall("libSceNet", __func__, [&](NetBridgeJson& netIn) { netIn.Int("flags", flags); }); if (!name || flags != 0) return netCall.Ret(fail(NET_EINVAL)); @@ -2459,7 +2478,10 @@ int APS5_VABI sceNetEpollAbort(int eid, int flags) { } int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* event) { - NetBridgeCall netCall("libSceNet", __func__, [&](NetBridgeJson& netIn) { netIn.Int("eid", eid); netIn.Int("op", op); netIn.Int("id", id); }); + NetBridgeCall netCall("libSceNet", __func__, [&](NetBridgeJson& netIn) { + netIn.Int("eid", eid); netIn.Int("op", op); netIn.Int("id", id); + if (event != nullptr) netIn.Uint("events", event->events); + }); bool kernel = false; { std::lock_guard lk(g_mutex); @@ -2491,6 +2513,15 @@ int APS5_VABI sceNetEpollControl(int eid, int op, int id, const NetEpollEvent* e int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int timeout) { NetBridgeCall netCall("libSceNet", __func__, [&](NetBridgeJson& netIn) { netIn.Int("eid", eid); netIn.Int("maxevents", maxevents); netIn.Int("timeout", timeout); }); if (!events || maxevents <= 0) return netCall.Ret(fail(NET_EINVAL)); + const auto readyResult = [&](int count) { + if (netCall.On() && count > 0) { + netCall.Out().Begin("ready"); + for (int index = 0; index < count; ++index) netCall.Out().Uint(std::to_string(events[index].ident), events[index].events); + netCall.Out().End(); + } + if (timeout == 0) pace_empty_poll(count > 0); + return netCall.Ret(count); + }; struct Watch { int descriptor; std::shared_ptr registration; @@ -2539,7 +2570,7 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int } } if (watches.empty()) { - if (timeout == 0) return netCall.Ret(0); + if (timeout == 0) return readyResult(0); std::unique_lock lk(g_mutex); const auto now = std::chrono::steady_clock::now(); if (timeout > 0 && now >= deadline) return netCall.Ret(0); @@ -2609,10 +2640,10 @@ int APS5_VABI sceNetEpollWait(int eid, NetEpollEvent* events, int maxevents, int events[count].events = guest_events; ++count; } - if (count > 0) return netCall.Ret(count); + if (count > 0) return readyResult(count); } } - if (timeout == 0) return netCall.Ret(0); + if (timeout == 0) return readyResult(0); } } diff --git a/core/libs/tests/NetEpollPerCall.cpp b/core/libs/tests/NetEpollPerCall.cpp new file mode 100644 index 00000000000..efb5ef4d1c6 --- /dev/null +++ b/core/libs/tests/NetEpollPerCall.cpp @@ -0,0 +1,207 @@ +// A managed runtime (Mono behind Socket.Poll) answers every poll with a throwaway epoll: +// create, add, wait with a zero timeout, delete, destroy. These checks drive that pattern. +#include "../prx/libSceNet/Export.cpp" +#include +#include +#include +#include + +static void Require(bool value, const std::source_location location = std::source_location::current()) { + if (value) return; + std::fprintf(stderr, "requirement failed at %s:%u\n", location.file_name(), location.line()); + std::abort(); +} + +static constexpr std::uint32_t In = 1, Out = 2, Err = 8, Hup = 16; +static constexpr int NonBlockingOption = 0x1200; + +// One Socket.Poll(0, mode): returns the events the wait reported, or -1 for none. +static std::int64_t PollOnce(int descriptor, std::uint32_t mask) { + const int epoll = sceNetEpollCreate("per-call", 0); + Require(epoll >= 0); + NetEpollEvent request{}; + request.events = mask; + request.data.u64 = 0x1234; + Require(sceNetEpollControl(epoll, 1, descriptor, &request) == 0); + NetEpollEvent ready{}; + const int count = sceNetEpollWait(epoll, &ready, 16, 0); + Require(count == 0 || count == 1); + if (count == 1) Require(ready.ident == static_cast(descriptor) && ready.data.u64 == 0x1234); + Require(sceNetEpollControl(epoll, 3, descriptor, nullptr) == 0); + Require(sceNetEpollDestroy(epoll) == 0); + return count == 0 ? -1 : static_cast(ready.events); +} + +static std::int64_t PollUntil(int descriptor, std::uint32_t mask, std::chrono::milliseconds limit) { + const auto deadline = std::chrono::steady_clock::now() + limit; + for (;;) { + const auto seen = PollOnce(descriptor, mask); + if (seen != -1 || std::chrono::steady_clock::now() >= deadline) return seen; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + } +} + +static std::array LoopbackAddress(int descriptor) { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(descriptor, address.data(), &size) == 0 && size == address.size()); + return address; +} + +static void SetNonBlocking(int descriptor) { + const int on = 1; + Require(sceNetSetsockopt(descriptor, 0xffff, NonBlockingOption, &on, sizeof(on)) == 0); +} + +static void CheckUdp() { + const int sender = sceNetSocket("per-call-udp", 2, 2, 17); + const int receiver = sceNetSocket("per-call-udp", 2, 2, 17); + Require(sender >= 0 && receiver >= 0); + SetNonBlocking(sender); + const auto address = LoopbackAddress(receiver); + // A fresh epoll must report a writable datagram socket at once, every time, and nothing readable. + for (int round = 0; round != 2000; ++round) { + Require(PollOnce(sender, Out) == Out); + Require(PollOnce(sender, In | Out) == Out); + } + for (int round = 0; round != 300; ++round) Require(PollOnce(receiver, In) == -1); + const char payload[] = "per-call"; + Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); + Require(PollUntil(receiver, In, std::chrono::seconds(5)) == In); + Require(PollOnce(receiver, In) == In); // level triggered: still readable until read + Require(PollOnce(receiver, Out) == Out); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); +} + +static void CheckTcpConnect() { + const int listener = sceNetSocket("per-call-tcp", 2, 1, 6); + Require(listener >= 0); + const auto address = LoopbackAddress(listener); + Require(sceNetListen(listener, 4) == 0); + const int client = sceNetSocket("per-call-tcp", 2, 1, 6); + Require(client >= 0); + SetNonBlocking(client); + const int result = sceNetConnect(client, address.data(), address.size()); + Require(result == 0 || (result == static_cast(0x80410124) && *sceNetErrnoLoc() == 36)); + // Mid-connect (or just connected): writable is the connect-complete signal and must arrive. + const auto writable = PollUntil(client, Out, std::chrono::seconds(5)); + Require(writable > 0 && (writable & Out) && !(writable & Err)); + int error = -1; + std::uint32_t size = sizeof(error); + Require(sceNetGetsockopt(client, 0xffff, 0x1007, &error, &size) == 0 && error == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + Require(PollOnce(client, In) == -1); + const char byte = 'x'; + Require(sceNetSend(accepted, &byte, 1, 0) == 1); + Require(PollUntil(client, In, std::chrono::seconds(5)) == In); + // The peer going away is reported as readable or hang-up, never as silence. + char received = 0; + Require(sceNetRecv(client, &received, 1, 0) == 1); + Require(sceNetSocketClose(accepted) == 0); + const auto closed = PollUntil(client, In, std::chrono::seconds(5)); + Require(closed > 0 && (closed & (In | Hup))); + for (const int descriptor : {client, listener}) Require(sceNetSocketClose(descriptor) == 0); +} + +static void CheckTcpRefused() { + const int probe = sceNetSocket("per-call-closed", 2, 1, 6); + Require(probe >= 0); + const auto address = LoopbackAddress(probe); + Require(sceNetSocketClose(probe) == 0); // nothing listens on this port any more + const int client = sceNetSocket("per-call-closed", 2, 1, 6); + Require(client >= 0); + SetNonBlocking(client); + const int result = sceNetConnect(client, address.data(), address.size()); + if (result != 0) { + // A failed connect must wake a poll that asks for writable or for errors. + const auto seen = PollUntil(client, Out | Err, std::chrono::seconds(10)); + Require(seen > 0 && (seen & (Err | Hup | Out))); + } + Require(sceNetSocketClose(client) == 0); +} + +static void CheckClosedDescriptor() { + const int socket = sceNetSocket("per-call-close", 2, 2, 17); + Require(socket >= 0); + const int epoll = sceNetEpollCreate("per-call-close", 0); + NetEpollEvent request{}; + request.events = Out; + Require(sceNetEpollControl(epoll, 1, socket, &request) == 0); + Require(sceNetSocketClose(socket) == 0); + NetEpollEvent ready{}; + // The closed descriptor leaves the epoll: no event, and no later control succeeds on it. + Require(sceNetEpollWait(epoll, &ready, 1, 0) == 0); + for (const int operation : {1, 2, 3}) { + Require(sceNetEpollControl(epoll, operation, socket, operation == 3 ? nullptr : &request) == static_cast(0x80410109)); + Require(*sceNetErrnoLoc() == 9); + } + Require(sceNetEpollDestroy(epoll) == 0); + Require(sceNetEpollDestroy(epoll) == static_cast(0x80410109)); +} + +// Seven threads that never sleep must all finish, and they are paced instead of spinning at +// full speed (the pacing starts after a few hundred hot empty polls from one thread). +static void CheckSpinningThreads() { + std::array sockets{}; + for (int& socket : sockets) { + socket = sceNetSocket("per-call-spin", 2, 2, 17); + Require(socket >= 0); + } + const auto start = std::chrono::steady_clock::now(); + std::vector> workers; + for (const int socket : sockets) + workers.push_back(std::async(std::launch::async, [socket] { + int polls = 0; + while (polls < 1500) { + Require(PollOnce(socket, In) == -1); + ++polls; + } + return polls; + })); + for (auto& worker : workers) Require(worker.get() == 1500); + Require(std::chrono::steady_clock::now() - start > std::chrono::milliseconds(200)); + for (const int socket : sockets) Require(sceNetSocketClose(socket) == 0); +} + +// The observation log must show what the game asks an epoll for and what the wait reported. +static void CheckObservation(const std::filesystem::path& path) { + const int socket = sceNetSocket("per-call-observe", 2, 2, 17); + Require(socket >= 0); + Require(PollOnce(socket, In | Out) == Out); + Require(sceNetSocketClose(socket) == 0); + std::string text; + for (int attempt = 0; attempt != 40 && text.find("sceNetEpollWait") == std::string::npos; ++attempt) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::ifstream input(path, std::ios::binary); + text.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + } + Require(text.find("\"fn\":\"sceNetEpollControl\",\"in\":{\"eid\":") != std::string::npos); + Require(text.find("\"events\":3}") != std::string::npos); + Require(text.find("\"ready\":{\"" + std::to_string(socket) + "\":2}") != std::string::npos); +} + +int main(int argc, char** argv) { + Require(argc == 2); + const std::string mode = argv[1]; + const auto observePath = std::filesystem::temp_directory_path() / "anyps5-net-epoll-per-call.jsonl"; + if (mode == "observe") { + std::error_code ignored; + std::filesystem::remove(observePath, ignored); +#ifdef _WIN32 + Require(_putenv_s("ANYPS5_NET_OBSERVE", observePath.string().c_str()) == 0); +#else + Require(setenv("ANYPS5_NET_OBSERVE", observePath.c_str(), 1) == 0); +#endif + } + Require(sceNetInit_nid_postfix() == 0); + if (mode == "udp") CheckUdp(); + else if (mode == "tcp") CheckTcpConnect(); + else if (mode == "refused") CheckTcpRefused(); + else if (mode == "closed") CheckClosedDescriptor(); + else if (mode == "spin") CheckSpinningThreads(); + else if (mode == "observe") CheckObservation(observePath); + else Require(false); + std::printf("epoll per call %s passed\n", mode.c_str()); +} From 98bd1f257fe71195c834bfb5a590e3b789908bd9 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:07:59 +0100 Subject: [PATCH 2/2] style(libSceNet): drop code comments per the conventions --- core/libs/prx/libSceNet/Export.cpp | 4 ---- core/libs/tests/NetEpollPerCall.cpp | 15 ++------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 0a3487db410..fd16519f3d5 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2391,10 +2391,6 @@ int APS5_VABI sceNetGetSockInfo6(std::int32_t s, NetSockInfo6* info, std::uint32 return netCall.Ret(written); } -// A guest that polls with a zero timeout in a loop that never sleeps (a throwaway epoll per -// Socket.Poll call in a managed runtime does this) would otherwise burn every core and starve the -// threads that make progress. Hot repeated empty polls from one thread are first yielded and then -// paced by about a millisecond; a ready event or a pause in the loop starts the count again. static void pace_empty_poll(bool ready) { constexpr auto HotGap = std::chrono::microseconds(500); constexpr unsigned YieldAfter = 32; diff --git a/core/libs/tests/NetEpollPerCall.cpp b/core/libs/tests/NetEpollPerCall.cpp index efb5ef4d1c6..70c61e7a329 100644 --- a/core/libs/tests/NetEpollPerCall.cpp +++ b/core/libs/tests/NetEpollPerCall.cpp @@ -1,5 +1,3 @@ -// A managed runtime (Mono behind Socket.Poll) answers every poll with a throwaway epoll: -// create, add, wait with a zero timeout, delete, destroy. These checks drive that pattern. #include "../prx/libSceNet/Export.cpp" #include #include @@ -15,7 +13,6 @@ static void Require(bool value, const std::source_location location = std::sourc static constexpr std::uint32_t In = 1, Out = 2, Err = 8, Hup = 16; static constexpr int NonBlockingOption = 0x1200; -// One Socket.Poll(0, mode): returns the events the wait reported, or -1 for none. static std::int64_t PollOnce(int descriptor, std::uint32_t mask) { const int epoll = sceNetEpollCreate("per-call", 0); Require(epoll >= 0); @@ -60,7 +57,6 @@ static void CheckUdp() { Require(sender >= 0 && receiver >= 0); SetNonBlocking(sender); const auto address = LoopbackAddress(receiver); - // A fresh epoll must report a writable datagram socket at once, every time, and nothing readable. for (int round = 0; round != 2000; ++round) { Require(PollOnce(sender, Out) == Out); Require(PollOnce(sender, In | Out) == Out); @@ -69,7 +65,7 @@ static void CheckUdp() { const char payload[] = "per-call"; Require(sceNetSendto(sender, payload, sizeof(payload), 0, address.data(), address.size()) == sizeof(payload)); Require(PollUntil(receiver, In, std::chrono::seconds(5)) == In); - Require(PollOnce(receiver, In) == In); // level triggered: still readable until read + Require(PollOnce(receiver, In) == In); Require(PollOnce(receiver, Out) == Out); Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(receiver) == 0); } @@ -84,7 +80,6 @@ static void CheckTcpConnect() { SetNonBlocking(client); const int result = sceNetConnect(client, address.data(), address.size()); Require(result == 0 || (result == static_cast(0x80410124) && *sceNetErrnoLoc() == 36)); - // Mid-connect (or just connected): writable is the connect-complete signal and must arrive. const auto writable = PollUntil(client, Out, std::chrono::seconds(5)); Require(writable > 0 && (writable & Out) && !(writable & Err)); int error = -1; @@ -96,7 +91,6 @@ static void CheckTcpConnect() { const char byte = 'x'; Require(sceNetSend(accepted, &byte, 1, 0) == 1); Require(PollUntil(client, In, std::chrono::seconds(5)) == In); - // The peer going away is reported as readable or hang-up, never as silence. char received = 0; Require(sceNetRecv(client, &received, 1, 0) == 1); Require(sceNetSocketClose(accepted) == 0); @@ -109,13 +103,12 @@ static void CheckTcpRefused() { const int probe = sceNetSocket("per-call-closed", 2, 1, 6); Require(probe >= 0); const auto address = LoopbackAddress(probe); - Require(sceNetSocketClose(probe) == 0); // nothing listens on this port any more + Require(sceNetSocketClose(probe) == 0); const int client = sceNetSocket("per-call-closed", 2, 1, 6); Require(client >= 0); SetNonBlocking(client); const int result = sceNetConnect(client, address.data(), address.size()); if (result != 0) { - // A failed connect must wake a poll that asks for writable or for errors. const auto seen = PollUntil(client, Out | Err, std::chrono::seconds(10)); Require(seen > 0 && (seen & (Err | Hup | Out))); } @@ -131,7 +124,6 @@ static void CheckClosedDescriptor() { Require(sceNetEpollControl(epoll, 1, socket, &request) == 0); Require(sceNetSocketClose(socket) == 0); NetEpollEvent ready{}; - // The closed descriptor leaves the epoll: no event, and no later control succeeds on it. Require(sceNetEpollWait(epoll, &ready, 1, 0) == 0); for (const int operation : {1, 2, 3}) { Require(sceNetEpollControl(epoll, operation, socket, operation == 3 ? nullptr : &request) == static_cast(0x80410109)); @@ -141,8 +133,6 @@ static void CheckClosedDescriptor() { Require(sceNetEpollDestroy(epoll) == static_cast(0x80410109)); } -// Seven threads that never sleep must all finish, and they are paced instead of spinning at -// full speed (the pacing starts after a few hundred hot empty polls from one thread). static void CheckSpinningThreads() { std::array sockets{}; for (int& socket : sockets) { @@ -165,7 +155,6 @@ static void CheckSpinningThreads() { for (const int socket : sockets) Require(sceNetSocketClose(socket) == 0); } -// The observation log must show what the game asks an epoll for and what the wait reported. static void CheckObservation(const std::filesystem::path& path) { const int socket = sceNetSocket("per-call-observe", 2, 2, 17); Require(socket >= 0);