Files
NX2navi/tests/child_timeout_check.py
chrg6 72047d12af Stop sending a request again to a new translator child after the first one gave no answer for the read timeout.
plat_child_read returned -1 both for a broken pipe and for a child that sent nothing for PLAT_CHILD_READ_MS (120 s),
and svc_request retried any failure once on a fresh child. The translator is deterministic, so a program it hangs on
hangs the second child too: a compute shader, which the replay thread looks up synchronously (vk_exec_draw.c
shader_for), held the game for about 4 minutes instead of 2. plat_child_read now returns -2 on the timeout (win32.c and
linux.c), child_exchange_raw returns 1, 0 for a broken pipe or a child that could not start, and -1 for the timeout,
and svc_request stops the child and gives -1 without asking again; a dead child is still restarted once.
translate_one fails such a request with its own reason, keeps its job and code for diagnosis and deletes whatever the
stopped child wrote. The carry pass's child runs at idle priority, so a busy machine can starve it that long:
carry_exchange does not resend after a timeout either, and carry_translate counts it as a failed input, not as an
unavailable translator, so one slow program does not count towards the three that stop the pass. platform.h lets a
build set PLAT_CHILD_READ_MS.

Not reproduced before the change (no translator hang appears in the available logs). tests/child_timeout_check.py
(new) builds the real child and request functions with a 300 ms timeout: a silent child gives -2 and costs one
timeout with one child started, an exiting child is restarted once, an answering child gives its line. Every runtime
file compiles, with no new warnings in gfx_shader.c and win32.c.

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

168 lines
6.1 KiB
Python

"""Check how a translator child that stops answering is handled (runtime/platform/win32.c plat_child_read,
runtime/gfx_shader.c child_exchange_raw and svc_request). The check cuts the child functions out of win32.c and the
request functions out of gfx_shader.c, builds them with PLAT_CHILD_READ_MS at 300 ms (platform.h lets a build set it)
and runs three children through them:
- a child that keeps its pipe open and never writes: plat_child_read gives -2 after the timeout, and svc_request
gives -1 having started one child, so a stuck translator holds its caller one timeout, not two;
- a child that exits at once: the read fails with a broken pipe (0 or -1) at once, and svc_request restarts it once
and gives 0;
- a child that answers one line: svc_request gives 1 and the line without its line end.
Windows only (the child functions are win32.c's). No GPU, no game files, any Python.
Usage: child_timeout_check.py
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
def compiler():
"""$CC, else MSYS2 UCRT64's clang, else clang on PATH."""
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 extract(src, head, tail=''):
"""The text from head through its brace-balanced body (plus tail)."""
i = src.index(head)
j = src.index('{', i)
depth = 0
for k in range(j, len(src)):
if src[k] == '{':
depth += 1
elif src[k] == '}':
depth -= 1
if depth == 0:
return src[i:k + 1] + tail + '\n'
raise SystemExit(f'unbalanced: {head}')
HARNESS = r'''
#include <windows.h>
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#define PLAT_CHILD_READ_MS 300
#include "platform/platform.h"
#define WPATH 2048
#define LINE_MAX_LEN 4096
static int widen(const char *s, wchar_t *w, int cap) { return MultiByteToWideChar(CP_UTF8, 0, s, -1, w, cap) > 0; }
static volatile int64_t n_requests;
static int starts;
static const char *child_arg;
'''
STUBS = r'''
static Svc one;
static int child_start(Svc *v) {
uint32_t err = 0;
starts++;
v->child = plat_child_start("C:\\Windows\\System32\\cmd.exe", child_arg, &err);
v->in_len = 0;
return v->child != NULL;
}
static Svc *svc_get(void) { return &one; }
static void svc_put(Svc *v) { (void)v; }
static int child_exchange(Svc *v, const char *line, char *reply, size_t reply_size) {
return child_exchange_raw(v, line, reply, reply_size);
}
'''
MAIN = r'''
static const char *SILENT = "/c ping -n 30 127.0.0.1 >NUL", *GONE = "/c exit 0", *ANSWER = "/c echo ok 7";
static int request(const char *arg, char *reply, size_t n, ULONGLONG *ms) {
child_arg = arg, starts = 0;
ULONGLONG t0 = GetTickCount64();
int r = svc_request("job", reply, n);
*ms = GetTickCount64() - t0;
if (one.child) child_stop(&one);
return r;
}
int main(void) {
char buf[64], reply[256];
ULONGLONG ms;
/* plat_child_read alone */
PlatChild *c = plat_child_start("C:\\Windows\\System32\\cmd.exe", SILENT, NULL);
assert(c);
ULONGLONG t0 = GetTickCount64();
long got = plat_child_read(c, buf, sizeof buf);
ms = GetTickCount64() - t0;
printf("silent child: plat_child_read %ld after %llu ms\n", got, (unsigned long long)ms);
assert(got == -2 && ms >= PLAT_CHILD_READ_MS && ms < 1000);
plat_child_stop(c);
c = plat_child_start("C:\\Windows\\System32\\cmd.exe", GONE, NULL);
assert(c);
got = plat_child_read(c, buf, sizeof buf);
printf("exited child: plat_child_read %ld\n", got);
assert(got == 0 || got == -1);
plat_child_stop(c);
/* svc_request */
int r = request(SILENT, reply, sizeof reply, &ms);
printf("silent child: svc_request %d, %d children, %llu ms\n", r, starts, (unsigned long long)ms);
assert(r == -1 && starts == 1 && ms < 2 * PLAT_CHILD_READ_MS);
r = request(GONE, reply, sizeof reply, &ms);
printf("exited child: svc_request %d, %d children, %llu ms\n", r, starts, (unsigned long long)ms);
assert(r == 0 && starts == 2);
r = request(ANSWER, reply, sizeof reply, &ms);
printf("answering child: svc_request %d, %d children, reply '%s'\n", r, starts, reply);
assert(r == 1 && starts == 1 && !strcmp(reply, "ok 7"));
printf("child timeout check: a silent child costs one timeout and is not asked again; a dead one is restarted once\n");
return 0;
}
'''
def main():
if os.name != 'nt':
print('child timeout check: Windows only (win32.c); skipped')
return 0
win = (ROOT / 'runtime' / 'platform' / 'win32.c').read_text(encoding='utf-8')
gfx = (ROOT / 'runtime' / 'gfx_shader.c').read_text(encoding='utf-8')
parts = [
HARNESS,
extract(win, 'struct PlatChild {', ';'),
extract(win, 'PlatChild *plat_child_start('),
extract(win, 'void plat_child_stop('),
extract(win, 'int plat_child_write('),
extract(win, 'long plat_child_read('),
extract(gfx, 'typedef struct { // one translator child', ' Svc;'),
'static int child_start(Svc *v);\n',
extract(gfx, 'static void child_stop('),
extract(gfx, 'static int child_exchange_raw('),
STUBS,
extract(gfx, 'static int svc_request('),
MAIN,
]
work = ROOT / 'build' / 'test' / 'child_timeout'
work.mkdir(parents=True, exist_ok=True)
c = work / 'check.c'
c.write_text(''.join(parts), encoding='utf-8')
exe = work / 'check.exe'
env = dict(os.environ)
if MSYS_BIN.exists():
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
subprocess.check_call([compiler(), '-O1', '-g', '-Wall', '-Wno-unused-function', '-march=x86-64-v3',
'-I', str(ROOT / 'runtime'), str(c), '-o', str(exe)], env=env)
return subprocess.run([str(exe)], env=env, timeout=60).returncode
if __name__ == '__main__':
sys.exit(main())