Write the end of the run to the log file past its 64 MiB cap, up to a 4 MiB reserve counted on its own.

main.c log_sink wrote nothing to the file once g_log_bytes passed LOG_CAP: every later line, the exit summary,
"frametime: exit", the stutter map's exit lines and FATAL and crash lines included, went to stderr only, which a
game started from the launcher or Explorer does not keep. A trace = perf session reaches the cap in about 90
minutes (the fresh 0.10.0 log grew 12 KB/s), and the play-test asks for an hour or more of exactly that. A reserve
compared against g_log_bytes would not help: that counter adds every line, dropped ones too, so 4 MiB of dropped
lines (under 6 minutes at that rate) would use it up before the end came.

The new rt_log_ending marks the process as ending; rt_exit_begin (the orderly ends) and perf_summary_flush (the
frame-time summary, also on the cheats rebuild exit) call it, and rt_log_sync (a fatal error, the crash report)
counts as ending too. The exit summary and the stutter map are written before rt_log_sync, so keying on
g_log_final alone would have missed them. While ending, a line past the cap goes to the file through
g_log_tail, a counter of the bytes written past the cap, up to LOG_END_RESERVE (4 MiB), after one line saying the
end of the run follows. The cut line says the end will still come. The file can end 4 MiB past the cap.

perf_summary_flush now calls rt_log_ending before ft_stop, so tests/exit_order_check.py's static check of it wants
ft_stop before ft_report rather than first. The check also prints one closing line for the static checks and the
program together: the program's own "ok" line had hidden a failing static check.

Not reproduced before the change (no log here reached the cap). The new tests/log_cap_check.py builds log_sink
with a 4 KiB cap and a 2 KiB reserve: past the cap ordinary lines are dropped with one cut line, and after
rt_log_ending or rt_log_sync the end's lines fill the reserve after one "end follows" line, however much was
dropped before. exit_order_check (rt_exit_begin marks the end once; exit code 0 at this commit), logq_check and the
compile checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 17:25:40 -05:00
parent 8c1ec42406
commit bbea3ce2a0
6 changed files with 168 additions and 9 deletions

View File

@@ -118,7 +118,10 @@ static volatile uint32_t exit_tid; // the thread that runs the orderly end, 0
int rt_exit_begin(void) {
uint32_t me = plat_thread_id(), was = 0;
if (__atomic_compare_exchange_n(&exit_tid, &was, me, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) return 1;
if (__atomic_compare_exchange_n(&exit_tid, &was, me, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
rt_log_ending(); // the end's lines reach the log file past its cap (main.c log_sink)
return 1;
}
if (was == me) return 0;
for (;;) plat_sleep_ms(1000);
}

View File

@@ -24,6 +24,7 @@ void rt_vlogf(const char *fmt, va_list ap);
void rt_log_raw(const char *p, size_t n);
const char *rt_log_path(void); // the log file, NULL when this run writes none
void rt_log_sync(void); // writes the queued log lines now; later lines are written as they are made
void rt_log_ending(void); // the process is ending: its lines reach the log file past its 64 MiB cap
enum { RT_TRACE_SVC = 1, RT_TRACE_HOSTCALL = 2, RT_TRACE_IPC = 4, RT_TRACE_FS = 8, RT_TRACE_GFX = 16, RT_TRACE_PERF = 32,
RT_TRACE_SCHED = 64, RT_TRACE_AUDIO = 128, RT_TRACE_PRESENT = 256 };
extern unsigned rt_trace; // from NX2NAVI_TRACE=svc,host,ipc,fs,gfx,perf,sched,audio,present (or all)

View File

@@ -62,8 +62,15 @@ unsigned rt_trace;
// `2> file` keep their own file. Each start moves the earlier logs to nx2navi.1.log .. nx2navi.4.log. A file stops at 64 MiB
// (NX2NAVI_TRACE can write that much). Lines from before log_open wait in a static buffer: mem_init runs first so that
// nothing else lands in the guest address space, and an earlier open would put the CRT's heap there.
// The end of the run still reaches a file past its cap: once the process is ending (rt_log_ending at the start of an
// orderly end, gfx_present.c rt_exit_begin, and of the frame-time summary, perf.c perf_summary_flush; rt_log_sync at a
// fatal error and in the crash report), up to LOG_END_RESERVE more bytes are written, counted on their own. A
// trace = perf session reaches the cap in about 90 minutes, and every line after it, the exit summary, "frametime:
// exit", the stutter map and FATAL lines included, went to stderr only, which a game started from the launcher or
// Explorer does not keep.
#define LOG_KEEP 5
#define LOG_CAP (64ull << 20)
#define LOG_END_RESERVE (4ull << 20)
enum { LOG_EARLY, LOG_FILE, LOG_OFF };
static int g_log_state;
static PlatAppend *g_log_file;
@@ -76,6 +83,10 @@ static uint64_t g_log_bytes;
// crash report.
static PlatMutex g_err_lock = PLAT_MUTEX_INIT;
static volatile int32_t g_log_final;
static volatile int32_t g_log_ending; // rt_log_ending ran: the process is ending
static uint64_t g_log_tail; // bytes of the end the file was given past LOG_CAP
void rt_log_ending(void) { __atomic_store_n(&g_log_ending, 1, __ATOMIC_RELEASE); }
// The log's destinations: the file first, then stderr. logq.c calls it: from the log thread in a game run, else from
// the thread that logs. A write to stderr can block for as long as its reader likes: a console window holds every write
@@ -89,9 +100,21 @@ static void log_sink(const char *p, size_t n) {
uint64_t at = __atomic_fetch_add(&g_log_bytes, n, __ATOMIC_RELAXED);
if (at + n <= LOG_CAP) {
plat_append_write(g_log_file, p, n);
} else if (at <= LOG_CAP) { // the first write past the cap says so, once
static const char cut[] = "[host] log: the file reached 64 MiB, the rest of this run goes to stderr only\n";
plat_append_write(g_log_file, cut, sizeof cut - 1);
} else {
if (at <= LOG_CAP) { // the first write past the cap says so, once
static const char cut[] = "[host] log: the file reached 64 MiB, the rest of this run goes to stderr only, "
"but for the lines of its end\n";
plat_append_write(g_log_file, cut, sizeof cut - 1);
}
if (__atomic_load_n(&g_log_ending, __ATOMIC_ACQUIRE) || __atomic_load_n(&g_log_final, __ATOMIC_ACQUIRE)) {
uint64_t t = __atomic_fetch_add(&g_log_tail, n, __ATOMIC_RELAXED);
if (!t) {
static const char end[] = "[host] log: the end of the run follows; the lines between the cap and it "
"were not written to this file\n";
plat_append_write(g_log_file, end, sizeof end - 1);
}
if (t + n <= LOG_END_RESERVE) plat_append_write(g_log_file, p, n);
}
}
}
// without the file stderr is the log (run_game.py, `2> file`), and it is waited for

View File

@@ -764,6 +764,7 @@ static void session_report(uint64_t now) {
__attribute__((weak)) void vk_texstat_frame(uint64_t frame, uint64_t d) { (void)frame, (void)d; }
void perf_summary_flush(void) { // at exit (prof_write)
rt_log_ending(); // the summary and the stutter map reach the log file past its cap (main.c log_sink)
ft_stop();
if (rt_trace & RT_TRACE_PERF) seg_close(__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), kern_now_ns(), 1);
if (summary_t0) {

View File

@@ -65,6 +65,8 @@ static char seen[1024];
STEP(fs_save_wait) STEP(rtss_limit_restore) STEP(input_rumble_hd_stop) STEP(vk_pipeline_prewarm_marker_clear)
STEP(prof_write) STEP(vk_mem_exit_log) STEP(logq_flush) STEP(vk_pipeline_save_exit) STEP(fs_save_flush)
static int cfg_save(void) { strcat(seen, "cfg_save "); return 1; }
static int ending;
static void rt_log_ending(void) { ending++; }
'''
MAIN = r'''
@@ -95,9 +97,9 @@ int main(int argc, char **argv) {
int first = rt_exit_begin(), again = rt_exit_begin();
HANDLE t = CreateThread(NULL, 0, second, NULL, 0, NULL);
int parked = WaitForSingleObject(t, 300) == WAIT_TIMEOUT && second_back == -1;
printf("once: first caller %d, its own thread again %d, another thread %s\n", first, again,
parked ? "stays in rt_exit_begin" : "came back");
if (first != 1 || again != 0 || !parked) printf("FAIL: the end must run once\n"), fails++;
printf("once: first caller %d, its own thread again %d, another thread %s; the log told of the end %d time(s)\n",
first, again, parked ? "stays in rt_exit_begin" : "came back", ending);
if (first != 1 || again != 0 || !parked || ending != 1) printf("FAIL: the end must run once\n"), fails++;
// an update under way: ft_stop waits for it; after ft_stop no update starts
HANDLE p = CreateThread(NULL, 0, present, (void *)(intptr_t)20, 0, NULL);
@@ -156,8 +158,9 @@ def static_checks():
flush = strip_comments(function(perf, 'void perf_summary_flush(void)'))
present = strip_comments(function(perf, 'void perf_present(uint64_t frame)'))
e, f, s, l = (present.find(x) for x in ('ft_enter()', 'ft_frame(', 'session_report(', 'ft_leave()'))
ok = flush[flush.index('{') + 1:].strip().startswith('ft_stop();') and 0 <= e < f < s < l
print(f'perf.c: perf_summary_flush starts with ft_stop, perf_present updates inside ft_enter/ft_leave: '
stop, report = flush.find('ft_stop();'), flush.find('ft_report(')
ok = 0 <= stop < report and 0 <= e < f < s < l
print(f'perf.c: perf_summary_flush calls ft_stop before ft_report, perf_present updates inside ft_enter/ft_leave: '
f'{"ok" if ok else "FAIL"}')
bad += not ok
return bad
@@ -188,6 +191,7 @@ def main():
want_all = ' '.join(STEPS) + ' '
want_seconds = ' '.join(s for s in STEPS if s != 'cfg_save') + ' '
rc = subprocess.run([str(exe), want_all, want_seconds], env=env, timeout=60).returncode
print(f'exit_order_check, the static checks and the program: {"FAILED" if bad or rc else "ok"}')
return 1 if bad or rc else 0

127
tests/log_cap_check.py Normal file
View File

@@ -0,0 +1,127 @@
"""The log file's cap and the end of the run (runtime/main.c log_sink): past the cap ordinary lines are dropped, with
one line that says so; once the process is ending (rt_log_ending, or rt_log_sync at a fatal error and in the crash
report) its lines are still written, after one line that says the end follows, up to a reserve of their own, however
much was dropped before. Builds log_sink with a small cap and reserve against stand-ins for the file and stderr. No
game files.
Usage: python tests/log_cap_check.py [path to clang]"""
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
source = (ROOT / 'runtime' / 'main.c').read_text(encoding='utf-8')
def compiler():
if len(sys.argv) > 1:
return sys.argv[1]
if os.environ.get('CC'):
return os.environ['CC']
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
return str(MSYS_BIN / 'clang.exe')
return shutil.which('clang') or 'clang'
def function(signature):
start = source.index(signature)
brace = source.index('{', start)
depth, end = 1, brace + 1
while depth:
depth += (source[end] == '{') - (source[end] == '}')
end += 1
return source[start:end]
HARNESS = r'''
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef int PlatMutex;
#define PLAT_MUTEX_INIT 0
typedef struct PlatAppend PlatAppend;
static void plat_mutex_lock(PlatMutex *m) { (void)m; }
static int plat_mutex_trylock(PlatMutex *m) { (void)m; return 1; }
static void plat_mutex_unlock(PlatMutex *m) { (void)m; }
static char file[1 << 16];
static size_t file_n;
static int plat_append_write(PlatAppend *f, const void *p, size_t n) {
(void)f;
if (file_n + n <= sizeof file) memcpy(file + file_n, p, n);
file_n += n;
return 1;
}
static int plat_stderr_write(const void *p, size_t n) { (void)p, (void)n; return 1; }
static void logq_sync(void) {}
#define LOG_CAP 4096ull
#define LOG_END_RESERVE 2048ull
enum { LOG_EARLY, LOG_FILE, LOG_OFF };
static int g_log_state = LOG_FILE;
static PlatAppend *g_log_file;
static uint64_t g_log_bytes;
'''
MAIN = r'''
static int count(const char *what) {
int n = 0;
for (const char *p = file; (p = strstr(p, what)) != NULL && p < file + file_n; p++) n++;
return n;
}
static int line(const char *tag, int i) { // returns the line's length
char b[64];
int n = snprintf(b, sizeof b, "%s %04d ........................\n", tag, i);
log_sink(b, (size_t)n);
return n;
}
int main(int argc, char **argv) {
int fails = 0, how = argc > 1 && !strcmp(argv[1], "sync");
int play_n = 0, end_n = 0;
for (int i = 0; i < 1000; i++) play_n = line("play", i); // 35,000 bytes, many times the cap
size_t before_end = file_n;
int play = count("play "), cut = count("reached 64 MiB");
if (how) rt_log_sync();
else rt_log_ending();
for (int i = 0; i < 100; i++) end_n = line("last", i); // 3,400 bytes, more than the reserve
int end = count("last "), follows = count("the end of the run follows");
printf("%s: %d play lines in the file, the cut line %d time(s); after %s %d end lines, the line before them %d "
"time(s); the file %zu bytes (cap %llu, reserve %llu)\n", how ? "rt_log_sync" : "rt_log_ending", play, cut,
how ? "rt_log_sync" : "rt_log_ending", end, follows, file_n, LOG_CAP, LOG_END_RESERVE);
if (play != (int)(LOG_CAP / play_n) || cut != 1) printf("FAIL: lines past the cap\n"), fails++;
if (follows != 1 || end != (int)(LOG_END_RESERVE / end_n)) printf("FAIL: the end's lines\n"), fails++;
if (file_n - before_end > LOG_END_RESERVE + 200) printf("FAIL: past the reserve\n"), fails++;
return fails ? 1 : 0;
}
'''
def main():
parts = [HARNESS]
parts.append(re.search(r'static PlatMutex g_err_lock = PLAT_MUTEX_INIT;\n(.*?\n)\nvoid rt_log_ending', source, re.S)
.group(0).rsplit('\n', 1)[0])
parts.append(function('void rt_log_ending(void)'))
parts.append(function('static void log_sink(const char *p, size_t n)'))
parts.append(function('void rt_log_sync(void)'))
parts.append(MAIN)
work = ROOT / 'build' / 'test' / 'log_cap'
work.mkdir(parents=True, exist_ok=True)
src = work / 'log_cap_check.c'
src.write_text('\n'.join(parts), encoding='utf-8')
exe = work / ('log_cap_check.exe' if os.name == 'nt' else 'log_cap_check')
env = dict(os.environ)
if os.name == 'nt' and MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
subprocess.check_call([compiler(), '-O1', '-g', '-Wall', '-Wno-unused-function', str(src), '-o', str(exe)], env=env)
bad = 0
for how in ('ending', 'sync'):
bad += subprocess.run([str(exe), how], env=env, timeout=60).returncode != 0
print(f'log_cap_check: {"FAILED" if bad else "ok"}')
return 1 if bad else 0
if __name__ == '__main__':
sys.exit(main())