mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Preserve launch timing docs and exercise production TCP protocol
This commit is contained in:
@@ -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` | ✓ | ✓ | ✓ |
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user