Repository navigation
perf(host): write the log in batches, a few writes a second instead of one per line - #1675
Merged
Merged
Conversation
…f one per line stderr is unbuffered (hos_main.c), so each line the log writer took was a write of its own: 30-90 a second in play, every one extending hookshot_log.txt in the game folder. With the game on a slow disk (a SATA SSD, 90% full) those writes kept the disk busy: its 4 KB reads took 10-500 ms where they take 0.4 ms, and what the game read from it meanwhile (pages of its own code, the game's files) waited as long. The writer now joins the lines it takes into one write, and after the line it is woken for it waits up to HOS_LOG_BATCH_MS (default 250, at most 2000; 0: as before) so the lines of that time go together. A queue half full goes out at once, before a burst can fill it (a full queue has the thread that logs write the line itself). Nothing waiting is lost: every way out writes the queue first (log_flush at exit and in hos_fatal, log_crash_filter), and the writer holds no lock while it waits. tests/test_log_batch.py: the writer extracted from hos_thread.c with the CRT stream calls as spies (the lines of that time in one write and in order, 0 keeps the old timing, a burst goes out without the wait, log_flush writes what waits once). Windows only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wait Windows CI builds the fixture with GCC, whose C front end warns on log_main: a static function with no return statement, though its for (;;) never ends (clang, which builds the game, does not). The warning is off for the extracted block only. The batch case checked nothing written 120 ms into a 250 ms wait; it now runs on a 1 s wait, and a default case checks that a line with no setting waits its 250 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DUDUYG
added a commit
to DUDUYG/Hookshot
that referenced
this pull request
Nov 10, 2026
…1675 merged, NotRexed#1765) NotRexed#1660 is in main now (its own commit): the conflicts were this branch's later changes of the same lines, pcache_init split around pcache_file_read and the save test's play_cache_save checks, kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
stderris unbuffered (hos_main.c), so every line the log writer takes is a write of its own. In play that is 30–90 writes a second, each one extendinghookshot_log.txtin the game folder.With the game on a slow disk (a SATA SSD, 90% full, on my PC) those small appends keep the disk busy:
hookshot.exe, the game's files) waits just as long.The fix
log_writejoins the lines it is given into onefwrite. On the text-mode stream that is oneWriteFile, or a few for a large batch.log_mainwaits up toHOS_LOG_BATCH_MSso the lines of that time go out together:0behaves as before.log_queuewakes it at each line.log_flushat exit, inhos_fataland at the window's close, andlog_crash_filter), and the writer holds no lock while it waits.TerminateProcess) could lose up to that much. Before, it could lose whatever the writer had not reached yet.Measured
Test setup:
HOS_LOG_BATCH_MS=0)* That 367 ms frame overlapped a page-file read on the other disk (memory pressure), not the game disk.
The effect is intermittent: one of the three unbatched runs was clean. In the second run, a cluster of 40–111 ms frames lined up with game-disk reads of 20–53 ms and with 800–2,000 pages read in by hard faults in each of those frames (
hookshot.exeis on that disk). The average stays at 60 fps (capped).Tests
tests/test_log_batch.pyextracts the Windows log block ofhost/hos_thread.cunmodified, withfputs,fwriteandfflushas spies. It runs on real threads, SRW locks and condition variables, and checks five cases:0writes a line at once.log_flushwrites what waits, and the writer's wait then ends with nothing written twice.It is Windows only;
run_all.pyskips it on Linux. GCC's C front end warns thatlog_main(afor (;;)thread) has no return statement, so the fixture turns-Wreturn-typeoff around the extracted block only; clang, which builds the game, does not warn.Results:
main's writer failsbatch,defaultandflush.Sleepin place of the condition-variable wait failsburst.Also checked:
host/hos_thread.ccompiles with-Wall -Wextrawith the same single warning asmain(an unused parameter at the file's end).tests/run_all.py: 225 checks, 33 skipped.test_log_batch.pypasses. Two checks fail for reasons of this PC, as onmain:test_precompile_compute_guard.pyneedswindows.hfor an MSVC target.test_nup_paths.pyneeds the symlink privilege.🤖 Generated with Claude Code