Files
NX2navi/tests/log_cap_check.py
chrg6 313ef09081 Open the log's end reserve only when the process is ending, not at a bug capture's log sync in play.
The review of the log tail change found that main.c log_sink opened the 4 MiB reserve past the cap on g_log_final as
well as on g_log_ending. bugcap.c write_log_tail calls rt_log_sync in play, which sets g_log_final, so after a bug
capture in a trace = perf session past the cap, ordinary play lines filled the reserve (about 6 minutes at 12 KB/s),
the end's lines were dropped again, and the "end of the run follows" line came mid-run.

The reserve is now keyed on g_log_ending alone, as E-10's design had it. The ends that relied on rt_log_sync now call
rt_log_ending first: dispatch.c fatal_end and main.c crash_handler right after their guard, vk_device.c
lost_wait_exit's watchdog before its sync, and plat_exit through the new main.c log_sync_ending (plat_exit_hook in a
game run), so every path that ends through plat_exit marks the end too. rt_exit_begin and perf_summary_flush call it
as before.

Not reproduced in the game before the change. tests/log_cap_check.py gains a capture case (rt_log_sync in play, 1000
more play lines, then the end): on the code before this change 58 play lines took the reserve, the end line came at
the sync and no end line reached the file; now no play line passes the cap and the end's lines fill the reserve. A
fatal case runs rt_log_ending and then rt_log_sync, and static checks hold crash_handler, log_sync_ending, fatal_end
and lost_wait_exit to calling rt_log_ending before rt_log_sync. log_cap_check, fatal_exit_check (with a stand-in for
rt_log_ending) and the compile checks of main.c, dispatch.c and vk_device.c pass, with no new -Wall warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:23 -05:00

173 lines
7.1 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, which a fatal error and the crash report call before
rt_log_sync) 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. rt_log_sync alone (a bug capture in play, runtime/bugcap.c write_log_tail) does not
open the reserve. Builds log_sink with a small cap and reserve against stand-ins for the file and stderr, and checks
statically that every end that syncs the log marks it as ending first. 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) {
// ending: rt_log_ending (an orderly end); fatal: rt_log_ending, then rt_log_sync (a fatal error, the crash report);
// capture: rt_log_sync in play (a bug capture), more play, then rt_log_ending
const char *how = argc > 1 ? argv[1] : "ending";
int fails = 0, 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
int play = count("play "), cut = count("reached 64 MiB");
if (!strcmp(how, "capture")) {
rt_log_sync();
for (int i = 0; i < 1000; i++) line("more", i);
int more = count("more "), early = count("the end of the run follows");
printf("capture: after rt_log_sync in play %d of 1000 play lines in the file, the end's line %d time(s)\n", more,
early);
if (more || early) printf("FAIL: rt_log_sync alone opened the end's reserve\n"), fails++;
}
size_t before_end = file_n;
if (strcmp(how, "fatal")) {
rt_log_ending();
} else {
rt_log_ending();
rt_log_sync();
}
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); %d end lines, the line before them %d time(s); the "
"file %zu bytes (cap %llu, reserve %llu)\n", how, play, cut, 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 static_checks():
"""Every end that syncs the log marks the process as ending first, so its lines reach the file past the cap."""
bad = 0
rt = ROOT / 'runtime'
ends = [('main.c', 'static void crash_handler(const PlatCrash *x)'),
('main.c', 'static void log_sync_ending(void)'),
('dispatch.c', 'static void fatal_end(int code, const char *fmt, va_list ap) {'),
('vk_device.c', 'static void lost_wait_exit(void *arg)')]
for file, sig in ends:
text = (rt / file).read_text(encoding='utf-8')
if sig not in text:
print(f'static: {file}: {sig} is missing: FAIL', flush=True)
bad += 1
continue
start = text.index(sig)
brace = text.index('{', start)
depth, end = 1, brace + 1
while depth:
depth += (text[end] == '{') - (text[end] == '}')
end += 1
body = re.sub(r'//[^\n]*', '', text[start:end])
e, s = body.find('rt_log_ending()'), body.find('rt_log_sync()')
ok = 0 <= e < s
print(f'static: {file} {sig.split("(")[0].split()[-1].lstrip("*")} calls rt_log_ending before rt_log_sync: '
f'{"ok" if ok else "FAIL"}', flush=True)
bad += not ok
ok = 'plat_exit_hook = log_sync_ending;' in source
print(f'static: main.c plat_exit_hook is log_sync_ending: {"ok" if ok else "FAIL"}', flush=True)
return bad + (not ok)
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 = static_checks()
for how in ('ending', 'fatal', 'capture'):
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())