Preserve launch timing docs and exercise production TCP protocol

This commit is contained in:
Matthew Stanley
2026-10-06 22:50:51 -07:00
parent 8ba237ccf5
commit dfdf2a6e5e
3 changed files with 125 additions and 4 deletions
+7 -4
View File
@@ -430,8 +430,6 @@ If an inspection need isn't covered by the existing commands, **do not fall back
The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` is absolute: **no `fprintf(stderr, …)` in source code, ever, for any reason**.
<!-- BEGIN AUTOGENERATED COMMAND INDEX -- edit tools/gen_tcp_commands.py, not this block -->
## `host_launch_timings` — retained host startup and mod-commit costs
`{"cmd":"host_launch_timings","count":256}` reads the newest completed host
@@ -461,11 +459,13 @@ including Resident Loading when enabled. No work is moved off-thread and no
verification or cache policy changes. These scopes do not cover every action
between Play and TCP availability.
<!-- BEGIN AUTOGENERATED COMMAND INDEX -- edit tools/gen_tcp_commands.py, not this block -->
## Complete command index (generated)
**349 commands registered** — 336 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
**352 commands registered** — 339 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
68 of 349 have prose above; **281 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
69 of 352 have prose above; **283 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
@@ -615,6 +615,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `guest_tty_dump` | ✓ | | |
| `history` | ✓ | ✓ | ✓ |
| `hle_dump` | ✓ | | ✓ |
| `host_launch_timings` | ✓ | | ✓ |
| `idle_skip` | ✓ | | |
| `imask_trace` | ✓ | | |
| `input_route_append` | ✓ | | |
@@ -683,6 +684,8 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `pgxp` | ✓ | | ✓ |
| `pgxp_miss_ring` | ✓ | | |
| `pgxp_shadow` | ✓ | | |
| `pgxp_store_ring` | ✓ | | |
| `pgxp_tri_ring` | ✓ | | |
| `phase_hot` | ✓ | | |
| `phase_profile` | ✓ | | ✓ |
| `ping` | ✓ | ✓ | ✓ |
+6
View File
@@ -135,6 +135,12 @@ if(BUILD_TESTING)
set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME host_launch_timings_protocol_test COMMAND ${CMAKE_COMMAND} -E env
"CC=${CMAKE_C_COMPILER}" "CXX=${CMAKE_CXX_COMPILER}" "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_host_launch_timings_protocol.py")
set_tests_properties(host_launch_timings_protocol_test PROPERTIES SKIP_RETURN_CODE 77)
add_test(NAME tcp_command_index_check COMMAND "${Python3_EXECUTABLE}"
"${PSXRECOMP_ROOT}/tools/gen_tcp_commands.py" --check)
add_test(NAME wtrace_dump_test COMMAND ${CMAKE_COMMAND} -E env
"CC=${CMAKE_C_COMPILER}" "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_wtrace_dump.py")
@@ -0,0 +1,112 @@
"""Execute the production TCP handler/helpers and real ring through the C ABI.
No server, game, SDL, or persistent log. Missing compilers are an explicit skip.
"""
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
from test_wtrace_dump import matching_brace
ROOT = Path(__file__).resolve().parents[2]
def function(source, name):
match = re.search(r'^(?:static |extern "C" )[^;{}]+\b' + name +
r'\([^;{}]*\)\s*\{', source, re.M)
if not match:
raise AssertionError(f"production function missing: {name}")
return source[match.start():matching_brace(source, match.end() - 1) + 1]
class HostLaunchProtocol(unittest.TestCase):
@classmethod
def setUpClass(cls):
cc, cxx = os.environ.get("CC", "cc"), os.environ.get("CXX", "c++")
if not shutil.which(cc) or not shutil.which(cxx):
raise unittest.SkipTest("set CC and CXX to the native compilers")
cls.temp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.temp.cleanup)
temp = Path(cls.temp.name)
debug = (ROOT / "runtime/src/debug_server.c").read_text(encoding="utf-8")
runtime = (ROOT / "runtime/src/mod_runtime.cpp").read_text(encoding="utf-8")
harness = '''#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "host_launch_timing.h"
void seed_host_launch_timing_fixture(int count);
static void debug_server_send_line(const char *line) { puts(line); }
static void send_err(int id, const char *error) { (void)id; (void)error; abort(); }
'''
for name in ("json_get_int", "json_escape_string", "handle_host_launch_timings"):
harness += function(debug, name) + "\n"
harness += '''int main(int argc, char **argv) {
if (argc != 3) return 2;
seed_host_launch_timing_fixture(atoi(argv[2]));
handle_host_launch_timings(42, argv[1]);
return 0;
}
'''
bridge = '#include "host_launch_timing.h"\n'
for name in ("host_launch_timing_snapshot", "host_launch_timing_stage_name"):
bridge += function(runtime, name) + "\n"
bridge += '''extern "C" void seed_host_launch_timing_fixture(int count) {
char label[64];
for (unsigned i = 0; i < 63; ++i) label[i] = i % 3 == 0 ? '"' : i % 3 == 1 ? '\\\\' : '\\1';
label[63] = 0;
for (int i = 0; i < count; ++i) {
PSXRecompV4::HostLaunchTimingScope scope(HOST_LAUNCH_MEDIA_PROVIDER, label, label);
if (i % 2 == 0) scope.success();
}
}
'''
(temp / "handler.c").write_text(harness, encoding="utf-8")
(temp / "bridge.cpp").write_text(bridge, encoding="utf-8")
include = str(ROOT / "runtime/include")
cls.exe = temp / ("protocol.exe" if os.name == "nt" else "protocol")
for args in ([cc, "-std=c99", "-Wall", "-Wextra", "-Werror", "-I", include,
"-c", str(temp / "handler.c"), "-o", str(temp / "handler.o")],
[cxx, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-pthread",
"-I", include, str(temp / "bridge.cpp"), str(temp / "handler.o"),
"-o", str(cls.exe)]):
subprocess.run(args, check=True, capture_output=True, text=True)
def query(self, count=None, seeded=300):
request = {"cmd": "host_launch_timings"}
if count is not None:
request["count"] = count
output = subprocess.check_output([str(self.exe), json.dumps(request), str(seeded)], text=True)
return json.loads(output)
def test_wrapped_maximum_labels_and_default_count(self):
result = self.query()
self.assertEqual((result["capacity"], result["total"], result["overwritten"]), (256, 300, 44))
self.assertTrue(result["nested_durations_overlap"])
self.assertEqual([e["seq"] for e in result["entries"]], list(range(45, 301)))
for e in result["entries"]:
self.assertEqual(e["stage"], "media_provider")
self.assertEqual(e["package"], '"\\\x01' * 21)
self.assertEqual(e["feature"], e["package"])
self.assertEqual(e["labels_truncated"], 0)
self.assertEqual(e["ok"], int(e["seq"] % 2 == 1))
def test_count_clamping(self):
for count, expected in ((0, 1), (-10, 1), (1, 1), (3, 3), (256, 256), (1000, 256)):
result = self.query(count)
self.assertEqual(len(result["entries"]), expected)
self.assertEqual(result["entries"][-1]["seq"], 300)
def test_empty_ring(self):
result = self.query(seeded=0)
self.assertEqual((result["total"], result["overwritten"], result["entries"]), (0, 0, []))
if __name__ == "__main__":
suite = unittest.defaultTestLoader.loadTestsFromTestCase(HostLaunchProtocol)
result = unittest.TextTestRunner().run(suite)
raise SystemExit(77 if result.skipped else 0 if result.wasSuccessful() else 1)