Files
NX2navi/tests/log_cap_check.py
chrg6 bbea3ce2a0 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>
2026-11-13 18:42:10 -05:00

128 lines
4.9 KiB
Python

"""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())