Skip to content

perf(agc): format and write steady-state log lines off the queue worker - #6371

Merged
adriadam10 merged 5 commits into
boykopovar:mainfrom
AlxcBTW:perf/worker-logging
Oct 16, 2026
Merged

adriadam10 merged 5 commits into
boykopovar:mainfrom
AlxcBTW:perf/worker-logging

Conversation

@AlxcBTW

@AlxcBTW AlxcBTW commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

Texture creation logs one stdout line per texture and mip (Texture address=...) through WriteLogLine, which formats with the MinGW printf and flushes every line on the calling thread, here the AGC queue worker. In Sword of the Sea (PPSA25344) cave gameplay, queue worker 0 spent 10-17 % of its samples in __mingw_pformat / WriteLogLine, and stdout held 575,778 Texture address= lines (96 MB) per 1500 s row.

The benefit is less CPU time on the queue worker and 96 MB less output. No frame-rate change is claimed.

  • AsyncLog keeps a bounded queue and a logger thread. AGC_LOG_DEFERRED captures numeric arguments on the caller and formats them on the logger thread.
  • Sink. WriteLogLine hands finished lines to a per-module LogSink in libc/LogMacros.hpp that AgcDriver installs. Lines that go through it keep their call order across stdout and stderr and are written with one flush per batch.
  • No drops. A full queue blocks the caller. Flush waits, without a limit, until every line queued before the call is written: on driver failure, in Driver::stop and at exit. APS5_LOG_SYNC=1 restores the synchronous path.
  • Process end. Where the process ends without those flushes, a bounded flush (2 s) writes out the queue first: abort() and std::terminate (libc chains a SIGABRT handler in front of the previous one when AsyncLog registers, so every abort site is covered, on Linux too; raise(SIGABRT) still ends the process), guest abort(), the Windows crash report of a fatal exception, and guest _Exit. The flush polls two counters and takes no lock, so a stuck stream or a thread that died holding the queue lock cannot hang a dying process, and after its first timeout later calls return at once. AsyncLog registers it with LibcSetEndFlush_nid_no_patch. A title that installs its own SIGABRT disposition through libkernel replaces the handler, as it replaces the crash report's.
  • Not covered by the sink. libSceAgcDriver has about 220 direct fprintf(stderr, ...) calls and one fputs (115 APS5_LOG_* sites go through the sink). They are written at once on the calling thread, so for those lines stdout and stderr no longer interleave in call order: a direct stderr line can appear before older queued stdout lines. APS5_LOG_SYNC=1 keeps one order for everything. Stated in docs/dev/ARCHITECTURE.md.
  • Repeated notes. Identical deferred write-back failed notes print three times per 10 s, then are counted into one repeated N more times line.
  • Counter. A [log] 10 s line reports lines, bytes, caller time and logger time (APS5_LOG_STATS=0 hides it).
  • Docs. docs/dev/ARCHITECTURE.md, section "Log output": the logger thread, the bounded queue, the LogSink hook, APS5_LOG_SYNC and APS5_LOG_STATS, the flush points and the stream order.

Tested

Windows 11, MinGW-w64 GCC 15.2, NVIDIA RTX 3060 / 5070 lab machines for the title row. Rebased onto origin/main 44df21632.

  • Tests, Linux (ubuntu-latest, lavapipe), fork CI: agc_async_log (3.05 s) and agc_async_log_fatal (2.88 s) pass, with the guest exit tests (guest_exit_order, guest_system_exit, guest_quick_exit, guest_immediate_exit, guest_return_from_main, guest_system_relaunch, guest_nested_exit) as a regression check on the _Exit change: https://github.com/AlxcBTW/AnyPS5/actions/runs/38787857401. Each of the two tests also passes 45 times (three ctest loops of 15 repeats running at once).
  • Tests, Windows (windows-latest, WinLibs GCC 15.2 UCRT, the built libc.dll), fork CI: agc_async_log (3.19 s) and agc_async_log_fatal (2.86 s) pass, including the child that ends in an access violation through the crash report: https://github.com/AlxcBTW/AnyPS5/actions/runs/38787867338. Each also passes 30 times (three loops of 10 repeats at once).
  • Build: the Windows libs and relinker targets compile on the fork CI: https://github.com/AlxcBTW/AnyPS5/actions/runs/38787881151. tools/check_conventions.py --base origin/main passes.
  • What the tests check: agc_async_log: order of deferred and written lines, long lines, the repeat limit, Flush waiting for a stream another thread holds (2.4 s, longer than the 2 s timeout of the earlier head), a bounded flush that gives up and then succeeds, a full queue blocking its caller. agc_async_log_fatal: a child queues 100 lines behind a held stream and ends in abort(), in std::terminate(), in raise(SIGABRT) (Linux) or in an access violation (Windows); every line must arrive in order. A last child never releases the stream and must end after the 2 s bound with an empty file.
  • Each check fails when its fix is removed: the 2 s Flush timeout of the earlier head, a queue that does not block, no flush in the abort handler, no flush in ReportCrash, no SIGABRT handler, a 0 ms limit, an unbounded wait (the blocked child then hangs). These were run in a standalone build of the test on Windows against the real CrashReport.cpp. On Linux (fork CI), removing the re-raise fails the raise child (https://github.com/AlxcBTW/AnyPS5/actions/runs/38789705108) and removing the flush from the abort handler fails the abort child (https://github.com/AlxcBTW/AnyPS5/actions/runs/38790140072).
  • Sword of the Sea, cave gameplay row (10 s windows, same machine and scene, before and after): before, queue worker 0 spent 10-17 % of its samples in __mingw_pformat/WriteLogLine, and a 1500 s row wrote 575,778 Texture address= lines (96 MB). After, 131 lines per 10 s and 0.006 % of worker time ([log] counter). Logs: hdcave-s10-20261015-*/stderr.log, attached as a comment. This row ran on the first head of this PR. The commits added since then are the end flush, two counters in place of the writer flag, tests and docs; the row was not repeated.
  • Not run: the rest of ctest on this head, other titles, gameplay on Linux, macOS.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a (no GPU test ran; see Tested)
  • AI-assisted: yes

@AlxcBTW

AlxcBTW commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Evidence from the two cave rows (Sword of the Sea PPSA25344, HD output, console settings, RTX 5070 Laptop, Windows 11), as promised in the description.

Before (main 8856f18 + lab commits, row hdcave-s8-notex-20261015-140413): stdout.log 14305 lines, 1.7M, of them 10850 Texture address= lines in 1500 s.

After (same base + this change, row hdcave-s10-20261015-150703): stdout.log 14747 lines, 1.8M. The [log] counter in steady gameplay:

[log] 10.0 s: 194 lines (194 stdout, 0 stderr), 22.2 KiB written, 0 repeats coalesced; callers spent 0.66 ms queueing and formatting (0.007% of one thread), logger thread 37.69 ms, queue peak 0.5 KiB
[log] 10.3 s: 55 lines (55 stdout, 0 stderr), 6.5 KiB written, 0 repeats coalesced; callers spent 0.23 ms queueing and formatting (0.002% of one thread), logger thread 13.53 ms, queue peak 0.1 KiB
[log] 10.0 s: 131 lines (131 stdout, 0 stderr), 14.4 KiB written, 0 repeats coalesced; callers spent 0.62 ms queueing and formatting (0.006% of one thread), logger thread 41.03 ms, queue peak 0.3 KiB

Worker-0 sampling profile before the change (symbolized, inclusive): __mingw_fprintf 11.5 %, __mingw_pformat 11.2 %, WriteLogLine 5.7 % of the steady gameplay window. Frame rate in that scene did not change (0.8-1.6 FPS either way; the time moved into WAIT_REG_MEM memory waits), so this PR is about CPU time on the queue worker, not a measured FPS win on this title.

This was referenced Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 5875 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp L1
PR 5962 core/libs/prx/libc/src/specifics/windows/CrashReport.cpp L5
PR 6160 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp L126
PR 6473 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp L750, L1090

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1341 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 1896 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 3891 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4156 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4396 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4397 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4422 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4423 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4424 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 4520 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4521 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4522 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4523 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4566 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4599 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4650 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 4718 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4740 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4749 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4760 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4813 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 4891 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5426 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5431 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 5499 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5656 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5761 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5867 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
docs/dev/ARCHITECTURE.md
PR 6044 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ARCHITECTURE.md
PR 6208 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6222 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp

@simonbronk

Copy link
Copy Markdown
Contributor

Independent Windows validation of head 610c7e4 (AI-assisted). I compiled the actual AsyncLog.hpp and LogMacros.hpp and ran your existing tests/AsyncLog.cpp standalone with WinLibs GCC 15.2 r7 UCRT, -std=c++20 -O2 -DNDEBUG -static -pthread -Wall -Wextra. The author test passes both asynchronously and with APS5_LOG_SYNC=1, with timestamps unset.

I also reproduced an incomplete Flush: idle.wait_for returns after two seconds even when writing is still true, and the void API gives its caller no indication that the logger is still using the stream. That means Flush at exit does not guarantee queued output has finished; a client that closes its FILE after Flush may also race the logger.

The attached native regression holds a real UCRT FILE lock on another thread for 2400 ms, enqueues one deferred line and calls Flush. On two runs the original returns before the stream is released (2011/2021 ms). It then releases the lock and drains before closing, so the probe does not intentionally access a closed FILE.

A one-line candidate replacing idle.wait_for(lock, 2s, predicate) with idle.wait(lock, predicate) passes the same regression (2416/2399 ms, after release, correct line content). Your existing test also passes with that candidate in both async and sync modes. The patch passes git apply --check against the recorded header.

There is a shutdown-policy tradeoff: this candidate waits for completion, so permanently blocked output can hold shutdown indefinitely. If a two-second shutdown budget is intentional, an explicit timeout result plus managed worker/stream lifetime would be needed instead. I am sharing this as a review candidate, not claiming that unbounded shutdown is the desired policy.

Bundle: source, original/fixed headers, one-line patch, reproduction commands and raw results. No full emulator build, registered CTest run, GPU/title/FPS/performance measurement or Linux/macOS validation; no push to your branch.

async-log-flush-pr6371.zip

@AlxcBTW

AlxcBTW commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the independent validation and the regression. Head 103337a takes your candidate: Flush now waits until the queue is empty and the writer is idle, without a timeout. On the policy: the synchronous path on main already blocks the caller on the same stream lock for as long as the stream is held, so a permanently blocked stream holds shutdown there too; the asynchronous logger should not be the one that silently returns while output is still pending. A bounded wait that reports a timeout would only move the problem to the caller, who has nothing better to do with it than wait or close the stream under the logger.

@adriadam10 adriadam10 self-assigned this Oct 15, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the head merged onto current main: it builds, agc_async_log passes, and the only other ctest failures are agc_driver_tessellation (SEGFAULT on main too) and agc_driver_store_over_untracked (passes on rerun, load flake). Points to address:

  • Lines queued before a crash are lost. Only PrintFailure and Driver::stop call FlushAsyncLog_nid_no_patch (Lifecycle.cpp:42, :124). The fatal paths never drain the queue: CrashReport.cpp (abort/fast-fail/hang dump, :510-548), std::terminate in VulkanDevice.cpp:187, std::abort in GuestBufferMemory.cpp:2820. Those are the lines a crash report needs most. Drain the queue (bounded, since the process is dying) from those paths, or write them through the synchronous path, and add a test that exits through abort()/terminate with lines queued.
  • Ordering across streams. Only WriteLogLine-based stdout lines go through the sink; direct fprintf(stderr, ...)/printf calls in the driver (~500) are written synchronously, so stdout and stderr no longer interleave in call order. State this in the PR or make the sink cover them.
  • docs/dev/ARCHITECTURE.md needs a short section for the new logger thread, the bounded queue (blocks the caller when full), the LogSink hook and APS5_LOG_SYNC/APS5_LOG_STATS, under the diagram it changes.
  • Tests: tests/AsyncLog.cpp does not cover Flush waiting for a held stream (the regression simonbronk posted), nor a full queue blocking the caller. Both are cheap to add.
  • Not verified here: Windows/MinGW behaviour (the windows job is skipped on PR CI), Linux, any title. No FPS change is claimed, so the benefit is lower CPU and 96 MB less output; please keep the PR description to that.

Happy to re-review once these are in.

Texture creation logged one stdout line per texture and per mip through
WriteLogLine, which formatted with the mingw printf and flushed every
line on the queue worker (about 786k lines in a 25 minute cave run).

AsyncLog keeps a bounded queue and a logger thread. AGC_LOG_DEFERRED
captures numeric arguments on the caller and formats them on the logger
thread; WriteLogLine hands finished lines to a per-module sink in
libc/LogMacros.hpp that AgcDriver installs, so all AgcDriver stdout
lines keep their order and are written one flush per batch. A full queue
blocks the caller, so no line is dropped. Flush runs on driver failure,
shutdown and at exit. APS5_LOG_SYNC=1 restores the synchronous path.

Identical "deferred write-back failed" notes are printed three times per
10 s and then counted into one "repeated N more times" line. A
"[log] 10 s" line reports lines, bytes, caller time and logger time
(APS5_LOG_STATS=0 hides it).
…caller

Flush must not return while the logger still waits for a stream that
another thread holds (the regression reported on the pull request: it gave
up after two seconds), and a full queue must block its caller instead of
dropping a line. AsyncLog::Capacity becomes public so the test sizes its
load from it.
…t 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.
…n ARCHITECTURE.md

The logger thread, the bounded queue that blocks its caller, the LogSink
hook, APS5_LOG_SYNC and APS5_LOG_STATS, where the queue is flushed and
what stays outside it: direct fprintf(stderr) and printf calls are written
at once, so they can appear before older queued lines.
@AlxcBTW
AlxcBTW force-pushed the perf/worker-logging branch from 103337a to e44ab15 Compare October 15, 2026 19:08
@AlxcBTW

AlxcBTW commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@adriadam10 Rebased onto main 44df21632 (no conflicts) and added three commits on top of the previous two: tests, the end flush, docs. Point by point:

1. Lines queued before a crash

One bounded flush, run from the paths that end the process without the exit handlers.

  • AsyncLog::FlushFor(limit) waits until the entries queued before the call are written. It polls two atomic counters (queued, finished) and takes no lock, so a thread that died holding the queue lock, or a stream held by another thread, cannot hang it. Flush keeps its wait without a limit (driver failure, Driver::stop, exit). It now counts the same two values instead of pending.empty() && !writing, so it waits for the lines queued before the call and not for a moment in which no thread is logging.
  • AsyncLog registers FlushAtEnd (FlushFor(2 s); after its first timeout later calls return at once) with a new libc seam, LibcSetEndFlush_nid_no_patch (Shutdown.hpp, Process.cpp). libc runs it
    • from a SIGABRT handler that the first registration chains in front of the previous one. std::abort() and std::terminate() end there, so VulkanDevice.cpp:187, GuestBufferMemory.cpp:2820, the hang dump's std::abort() in CrashReport.cpp and the other abort/terminate sites need no edit of their own, and Linux is covered, where CrashReport.cpp is not built. With the default disposition the handler restores it and raises again, so raise(SIGABRT) still ends the process;
    • in ReportCrash (CrashReport.cpp) once the first fatal exception is claimed;
    • in guest abort() and in LibcTerminate_nid_no_patch (guest _Exit, quick_exit).
      The UCRT invalid-parameter handler does not run it: that handler returns and the process continues, so it is not a dying path.
  • The bound is 2 s. A line still queued after that is lost.

Test: agc_async_log_fatal runs the test executable as a child. The child queues 100 lines behind a stream that another thread holds and releases 100 ms later, then ends in abort(), in std::terminate(), in raise(SIGABRT) (Linux) or in an access violation through the crash report (Windows). A process that does not wait loses every line. A last child never releases the stream; it must end after the 2 s bound, with an empty file. Every child has a watchdog. Removing the flush from the abort handler, removing it from ReportCrash, removing the handler, a 0 ms limit and an unbounded wait each fail the test (checked in a standalone build of the test on Windows with the real CrashReport.cpp; on Linux, removing the re-raise fails the raise child, https://github.com/AlxcBTW/AnyPS5/actions/runs/38789705108, and removing the flush from the handler fails the abort child, https://github.com/AlxcBTW/AnyPS5/actions/runs/38790140072).

Known limits:

  • A title that installs its own SIGABRT disposition through libkernel's signal or sigaction replaces the host handler (Signals.cpp, Install), as it already replaces the crash report's abort handler. Guest abort() runs the flush itself, but an abort() in host code does not on that path.
  • The guest SIGALRM default termination (Signals.cpp:296) and fatal host signals on Linux, where no crash report exists, do not run it.

2. Order across streams

Not made to cover them. libSceAgcDriver has about 220 direct fprintf(stderr, ...) calls and one fputs outside tests/ and tools/ (counted with grep; 115 APS5_LOG_* sites go through the sink). Their messages have their own formats, so moving them to APS5_LOG_ERR would change them. Stated in the description and in docs/dev/ARCHITECTURE.md: lines through the sink keep call order across stdout and stderr, a direct fprintf(stderr, ...) is written at once and can appear before older queued lines, APS5_LOG_SYNC=1 keeps one order for everything. The FATAL: line that GuestBufferMemory.cpp:2820 prints before its abort() is such a line: it comes before the queued lines that the abort handler then writes.

3. docs/dev/ARCHITECTURE.md

New section "Log output" after the Graphics section: a four-node diagram, the logger thread and the 16 MiB queue that blocks its caller, LogSink and APS5_LOG_SYNC, the order rule above, the flush points (unbounded and bounded), the [log] line and APS5_LOG_STATS, and which test covers what.

4. Tests (tests/AsyncLog.cpp)

  • FlushWaitsForHeldStream holds the stream's lock on another thread (flockfile, _lock_file on Windows) for 2.4 s, queues a line and requires Flush to return only after the release. It reimplements the regression simonbronk posted, and it fails on the earlier head with the 2 s timeout.
  • FullQueueBlocksCaller queues more than twice AsyncLog::Capacity (now public) from a producer thread behind the held stream. The producer must stall below that and stop making progress, and after the release every line must arrive in order, none dropped. It fails when the wait for free space is removed.
  • BoundedFlushGivesUp: FlushFor returns false while the stream is held and true after the release.
  • agc_async_log_fatal: see point 1.

5. Description

Edited to the CPU time of the queue worker and the 96 MB of output, with no frame-rate claim. It also states point 2 and the new tests.

Run where

Not run: the rest of ctest (your run of the previous head names agc_driver_tessellation and a load flake in agc_driver_store_over_untracked), any title with this head, macOS. The Sword of the Sea row in the description ran on the first head and was not repeated.

@github-actions github-actions Bot added the docs label Oct 15, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked the new head merged onto current main: it builds, agc_async_log and agc_async_log_fatal pass. All points from the last review are addressed: the bounded end flush on abort/terminate/crash report/_Exit with a test that ends through each path, the stream-order note, the "Log output" section in docs/dev/ARCHITECTURE.md, and tests for Flush on a held stream and a full queue. Windows/MinGW behaviour and the title measurement rest on your fork runs; merging once the full Build is green.

@adriadam10
adriadam10 merged commit 332b0b1 into boykopovar:main Oct 16, 2026
14 of 15 checks passed
adriadam10 added a commit that referenced this pull request Oct 16, 2026
On Windows the atexit handlers of a DLL run after the process has ended
its other threads, so the logger thread can be gone with lines still
queued and the unbounded Flush waits forever (guest_shader_link timed
out on 4 of the last 7 completed main builds since #6371). Run the bounded
FlushAtEnd there instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants