Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp
T
AlxcBTW b82d688ed8 fix(agc): flush the log queue, bounded, where the process ends without exit handlers
Lines queued behind the logger were lost when the process ended through
abort(), std::terminate, the Windows crash report or a guest _Exit,
because only driver failure, Driver::stop and a normal exit flushed the
queue.

libc keeps one end flush (LibcSetEndFlush_nid_no_patch) and runs it from
those paths. Its first registration chains a SIGABRT handler in front of
the previous one, so every abort() and std::terminate in the process is
covered, on Linux as well; with the default disposition the handler
restores it and raises again, so raise(SIGABRT) still ends the process.
Guest abort() and _Exit, and ReportCrash for the first fatal exception,
run the flush directly. AsyncLog registers a bounded flush (FlushFor, 2 s)
that polls two counters and takes no lock, so a dying process cannot hang
on a blocked stream or on the queue lock; after its first timeout it
returns at once. Flush counts queued and finished entries instead of
watching the queue and the writer flag, so it waits for the lines queued
before the call and not for a moment in which no thread is logging.

agc_async_log_fatal runs the test executable as a child that queues lines
behind a held stream and ends in abort(), std::terminate(), raise(SIGABRT)
(POSIX) or an access violation (Windows), and once more with the stream
never released. The lines of the first four must arrive in order, the last
child must end after the bound, and every child has a watchdog.
agc_async_log gains the bounded flush case.
2026-10-15 20:42:16 +02:00

338 lines
11 KiB
C++

#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp"
#include <atomic>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <system_error>
#include <thread>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <sys/resource.h>
#endif
namespace {
using namespace std::chrono_literals;
using Clock = AgcDriver::AsyncLog::Clock;
void Check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
std::string ReadAll(std::FILE* file) {
std::fflush(file);
std::rewind(file);
std::string text;
char buffer[4096];
for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read);
return text;
}
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream stream(path, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
}
void ClearLogEnvironment() {
#ifdef _WIN32
_putenv("APS5_LOG_SYNC=");
_putenv("APS5_LOG_TIMESTAMPS=");
#else
unsetenv("APS5_LOG_SYNC");
unsetenv("APS5_LOG_TIMESTAMPS");
#endif
}
void LockStream(std::FILE* file) {
#ifdef _WIN32
_lock_file(file);
#else
flockfile(file);
#endif
}
void UnlockStream(std::FILE* file) {
#ifdef _WIN32
_unlock_file(file);
#else
funlockfile(file);
#endif
}
class HeldStream {
public:
explicit HeldStream(std::FILE* file) : file(file) {
holder = std::thread([this] {
LockStream(this->file);
held.store(true);
while (!releaseRequested.load()) std::this_thread::sleep_for(1ms);
released.store(true);
UnlockStream(this->file);
});
while (!held.load()) std::this_thread::sleep_for(1ms);
}
HeldStream(const HeldStream&) = delete;
HeldStream& operator=(const HeldStream&) = delete;
~HeldStream() {
Release();
holder.join();
}
void Release() {
releaseRequested.store(true);
}
bool Released() const {
return released.load();
}
private:
std::FILE* file;
std::atomic<bool> held{false};
std::atomic<bool> releaseRequested{false};
std::atomic<bool> released{false};
std::thread holder;
};
std::string LargeLine(std::size_t index, std::size_t bytes) {
std::string line = "large " + std::to_string(index) + " ";
line.resize(bytes - 1, 'x');
line += '\n';
return line;
}
void OrderPerThread() {
std::FILE* file = std::tmpfile();
Check(file != nullptr, "temporary file");
auto& log = AgcDriver::AsyncLog::Get();
std::string expected;
for (int i = 0; i < 2000; ++i) {
log.Defer(file, "deferred %d %s %llu\n", i, AgcDriver::LogLiteral{"literal"}, static_cast<unsigned long long>(i) * 3ull);
expected += "deferred " + std::to_string(i) + " literal " + std::to_string(static_cast<unsigned long long>(i) * 3ull) + "\n";
const std::string plain = "plain " + std::to_string(i) + "\n";
log.Write(file, plain.data(), plain.size());
expected += plain;
}
log.Flush();
Check(ReadAll(file) == expected, "deferred and written lines changed order or content");
std::fclose(file);
}
void LongLine() {
std::FILE* file = std::tmpfile();
Check(file != nullptr, "temporary file");
const std::string longText(3000, 'x');
AgcDriver::AsyncLog::Get().Defer(file, "%s|%d\n", AgcDriver::LogLiteral{longText.c_str()}, 7);
AgcDriver::AsyncLog::Get().Flush();
Check(ReadAll(file) == longText + "|7\n", "a line longer than the stack buffer changed");
std::fclose(file);
}
void Coalescing() {
auto& log = AgcDriver::AsyncLog::Get();
const std::string line = "[gpu] coalescing test line\n";
int printed = 0;
for (int i = 0; i < 50; ++i) printed += log.Admit(line) ? 1 : 0;
Check(printed == 3, "identical lines in one window were not limited to the first three");
Check(log.Admit("[gpu] another line\n"), "a different line was suppressed");
}
void FlushWaitsForHeldStream() {
std::FILE* file = std::tmpfile();
Check(file != nullptr, "temporary file");
auto& log = AgcDriver::AsyncLog::Get();
{
HeldStream held(file);
log.Defer(file, "held %d %s\n", 7, AgcDriver::LogLiteral{"line"});
std::thread releaser([&] {
std::this_thread::sleep_for(2400ms);
held.Release();
});
log.Flush();
const bool waited = held.Released();
releaser.join();
Check(waited, "Flush returned while the logger still waited for the stream");
}
Check(ReadAll(file) == "held 7 line\n", "the line queued behind a held stream changed");
std::fclose(file);
}
void BoundedFlushGivesUp() {
std::FILE* file = std::tmpfile();
Check(file != nullptr, "temporary file");
auto& log = AgcDriver::AsyncLog::Get();
Check(log.FlushFor(0ms), "FlushFor waited with nothing queued");
{
HeldStream held(file);
log.Defer(file, "bounded %d\n", 1);
Check(!log.FlushFor(100ms), "FlushFor reported a line written while its stream was held");
held.Release();
Check(log.FlushFor(10s), "FlushFor missed the line written after the stream was released");
}
Check(ReadAll(file) == "bounded 1\n", "the line queued behind a held stream changed");
std::fclose(file);
}
void FullQueueBlocksCaller() {
std::FILE* file = std::tmpfile();
Check(file != nullptr, "temporary file");
auto& log = AgcDriver::AsyncLog::Get();
constexpr std::size_t lineBytes = 1024 * 1024;
constexpr std::size_t lines = 2 * AgcDriver::AsyncLog::Capacity / lineBytes + 4;
constexpr std::size_t filled = AgcDriver::AsyncLog::Capacity / lineBytes / 2;
std::atomic<std::size_t> queued{0};
bool blocked = false;
{
HeldStream held(file);
std::thread producer([&] {
for (std::size_t i = 0; i < lines; ++i) {
const std::string line = LargeLine(i, lineBytes);
log.Write(file, line.data(), line.size());
queued.store(i + 1);
}
});
auto last = queued.load();
auto changedAt = Clock::now();
const auto deadline = changedAt + 10s;
while (last < lines && Clock::now() < deadline && (last < filled || Clock::now() - changedAt < 500ms)) {
std::this_thread::sleep_for(5ms);
const auto now = queued.load();
if (now != last) {
last = now;
changedAt = Clock::now();
}
}
blocked = last >= filled && last < lines && Clock::now() - changedAt >= 500ms;
held.Release();
producer.join();
}
Check(blocked, "the caller was not blocked by a full queue");
log.Flush();
std::fflush(file);
std::rewind(file);
std::string line;
for (std::size_t i = 0; i < lines; ++i) {
const std::string expected = LargeLine(i, lineBytes);
line.resize(expected.size());
Check(std::fread(line.data(), 1, line.size(), file) == line.size() && line == expected, "a line written through a full queue changed or went missing");
}
Check(std::fgetc(file) == EOF, "a full queue wrote extra output");
std::fclose(file);
}
constexpr int ChildLines = 50;
[[noreturn]] void EndChild(const std::string& mode, const char* output) {
std::FILE* file = std::fopen(output, "wb");
Check(file != nullptr, "child output file");
#ifdef _WIN32
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
#else
const rlimit noCore{0, 0};
setrlimit(RLIMIT_CORE, &noCore);
#endif
std::thread([] {
std::this_thread::sleep_for(8s);
std::_Exit(99);
}).detach();
AgcDriver::InstallAsyncLog_nid_no_patch();
auto& log = AgcDriver::AsyncLog::Get();
HeldStream held(file);
for (int i = 0; i < ChildLines; ++i) {
log.Defer(file, "deferred %d\n", i);
WriteLogLine(file, "plain %d\n", i);
}
std::thread releaser([&] {
std::this_thread::sleep_for(100ms);
if (mode != "blocked") held.Release();
});
if (mode == "terminate") std::terminate();
#ifdef _WIN32
if (mode == "fault") RaiseException(EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, nullptr);
#else
if (mode == "raise") {
std::raise(SIGABRT);
std::_Exit(0);
}
#endif
std::abort();
}
std::string Quote(const std::string& text) {
return "\"" + text + "\"";
}
void LinesSurviveEnd(const std::string& self, const std::string& mode) {
const auto output = std::filesystem::temp_directory_path() / ("aps5-async-log-" + mode + "-" + std::to_string(Clock::now().time_since_epoch().count()) + ".txt");
std::string command = Quote(self) + " child " + mode + " " + Quote(output.string());
#ifdef _WIN32
command = Quote(command);
#endif
const auto start = Clock::now();
const int status = std::system(command.c_str());
const auto lived = Clock::now() - start;
std::error_code error;
const bool started = std::filesystem::exists(output, error);
const std::string text = started ? ReadFile(output) : std::string();
std::filesystem::remove(output, error);
Check(started, "the child did not start");
Check(status != 0, "the child did not end abnormally");
Check(lived < 6s, "the child took too long to end");
if (mode == "blocked") {
Check(lived >= 1900ms, "the child ended before the flush gave up");
Check(text.empty(), "the child wrote through a held stream");
return;
}
std::string expected;
for (int i = 0; i < ChildLines; ++i) expected += "deferred " + std::to_string(i) + "\nplain " + std::to_string(i) + "\n";
Check(text == expected, "lines queued before the process ended were lost or changed");
}
}
int main(int argc, char** argv) {
try {
ClearLogEnvironment();
if (argc == 4 && std::strcmp(argv[1], "child") == 0) EndChild(argv[2], argv[3]);
if (argc == 2 && std::strcmp(argv[1], "fatal") == 0) {
LinesSurviveEnd(argv[0], "abort");
LinesSurviveEnd(argv[0], "terminate");
#ifdef _WIN32
LinesSurviveEnd(argv[0], "fault");
#else
LinesSurviveEnd(argv[0], "raise");
#endif
LinesSurviveEnd(argv[0], "blocked");
return 0;
}
OrderPerThread();
LongLine();
Coalescing();
FlushWaitsForHeldStream();
BoundedFlushGivesUp();
FullQueueBlocksCaller();
} catch (const std::exception& error) {
std::cerr << error.what() << std::endl;
return 1;
}
return 0;
}