mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Host sampler: accessors in every build; tools/host_profile.py names hot functions
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a51504fd3b)
This commit is contained in:
@@ -12,9 +12,8 @@
|
||||
* Windows: SuspendThread / GetThreadContext / ResumeThread from a sampler
|
||||
* thread. POSIX: the process CPU-time timer (SIGPROF) delivered to the
|
||||
* emulation thread, which reads its own interrupted PC from the ucontext.
|
||||
* Other hosts report the sampler as unsupported. */
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
|
||||
* Other hosts, and builds without debug tools, report the sampler as
|
||||
* unsupported. */
|
||||
#if defined(__linux__) && !defined(_GNU_SOURCE)
|
||||
#define _GNU_SOURCE 1 /* REG_RIP, dladdr */
|
||||
#endif
|
||||
@@ -31,6 +30,7 @@ static volatile uint64_t s_seq;
|
||||
static uintptr_t s_image_base;
|
||||
static int s_supported;
|
||||
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
static void record(uintptr_t pc) {
|
||||
const uint64_t n = s_seq;
|
||||
HostSample *e = &s_ring[n % HOST_SAMPLER_CAP];
|
||||
@@ -39,8 +39,13 @@ static void record(uintptr_t pc) {
|
||||
e->in_pass = g_psx_render_pass_active ? 1u : 0u;
|
||||
s_seq = n + 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#if defined(PSX_NO_DEBUG_TOOLS)
|
||||
/* Production builds carry no sampler: it reports unsupported. */
|
||||
void host_sampler_start(void) {}
|
||||
|
||||
#elif defined(_WIN32)
|
||||
#include <windows.h>
|
||||
|
||||
static HANDLE s_target;
|
||||
@@ -161,5 +166,3 @@ int host_sampler_get(uint64_t seq, HostSample *out) {
|
||||
*out = s_ring[seq % HOST_SAMPLER_CAP];
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif /* !PSX_NO_DEBUG_TOOLS */
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Name the hot host functions of a running psxrecomp game.
|
||||
|
||||
Reads the always-on host CPU sampler (TCP host_profile, runtime/src/
|
||||
host_sampler.c) over a window of the ring and maps each executable-relative
|
||||
address to the function containing it, using the image's own symbol table
|
||||
(nm), so a Release build without debug info profiles too.
|
||||
|
||||
host_profile.py --port 4531 --exe game.exe --frames 300 [--pass 1] [--top 40]
|
||||
|
||||
--frames N: the last N guest frames. --pass 1 / 0: only samples inside /
|
||||
outside render passes. Prints per-function sample counts and shares.
|
||||
"""
|
||||
import argparse
|
||||
import bisect
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def query(port, **kw):
|
||||
with socket.create_connection(("127.0.0.1", port), 5) as s:
|
||||
s.settimeout(30)
|
||||
s.sendall((json.dumps(dict(id=1, cmd="host_profile", **kw)) + "\n").encode())
|
||||
with s.makefile("rb") as f:
|
||||
return json.loads(f.readline())
|
||||
|
||||
|
||||
def find_nm(explicit):
|
||||
if explicit:
|
||||
return explicit
|
||||
for name in ("llvm-nm", "nm"):
|
||||
p = shutil.which(name)
|
||||
if p:
|
||||
return p
|
||||
sys.exit("no nm on PATH; pass --nm")
|
||||
|
||||
|
||||
def preferred_base(exe, out):
|
||||
"""The address nm's symbol values are relative to: a PE image's
|
||||
ImageBase (optional header), a Mach-O executable's __mh_execute_header,
|
||||
0 for a position-independent ELF."""
|
||||
with open(exe, "rb") as f:
|
||||
head = f.read(4096)
|
||||
if head[:2] == b"MZ":
|
||||
pe = int.from_bytes(head[0x3C:0x40], "little")
|
||||
magic = int.from_bytes(head[pe + 24:pe + 26], "little")
|
||||
if magic == 0x20B: # PE32+
|
||||
return int.from_bytes(head[pe + 24 + 24:pe + 24 + 32], "little")
|
||||
return int.from_bytes(head[pe + 24 + 28:pe + 24 + 32], "little")
|
||||
for line in out.splitlines():
|
||||
parts = line.split(None, 2)
|
||||
if len(parts) == 3 and parts[2] in ("__mh_execute_header", "_mh_execute_header"):
|
||||
return int(parts[0], 16)
|
||||
return 0
|
||||
|
||||
|
||||
def image_size(exe):
|
||||
"""SizeOfImage of a PE executable, or None."""
|
||||
with open(exe, "rb") as f:
|
||||
head = f.read(4096)
|
||||
if head[:2] != b"MZ":
|
||||
return None
|
||||
pe = int.from_bytes(head[0x3C:0x40], "little")
|
||||
return int.from_bytes(head[pe + 24 + 56:pe + 24 + 60], "little")
|
||||
|
||||
|
||||
def symbols(nm, exe):
|
||||
"""(rva, name) sorted, from the image's text symbols."""
|
||||
out = subprocess.run([nm, "--defined-only", exe], capture_output=True,
|
||||
text=True, check=True).stdout
|
||||
base = preferred_base(exe, out)
|
||||
table = []
|
||||
for line in out.splitlines():
|
||||
parts = line.split(None, 2)
|
||||
if len(parts) == 3 and parts[1] in ("T", "t"):
|
||||
table.append((int(parts[0], 16) - base, parts[2]))
|
||||
table.sort()
|
||||
return table
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--port", type=int, required=True)
|
||||
ap.add_argument("--exe", required=True)
|
||||
ap.add_argument("--frames", type=int, default=0)
|
||||
ap.add_argument("--since", type=int, default=-1)
|
||||
ap.add_argument("--pass", dest="in_pass", type=int, default=-1)
|
||||
ap.add_argument("--top", type=int, default=40)
|
||||
ap.add_argument("--nm")
|
||||
a = ap.parse_args()
|
||||
kw = {"top": 400}
|
||||
if a.frames > 0:
|
||||
kw["frames"] = a.frames
|
||||
if a.since >= 0:
|
||||
kw["since"] = a.since
|
||||
if a.in_pass >= 0:
|
||||
kw["pass"] = a.in_pass
|
||||
r = query(a.port, **kw)
|
||||
if not r.get("ok") or not r.get("supported"):
|
||||
sys.exit("sampler unavailable: %s" % r)
|
||||
table = symbols(find_nm(a.nm), a.exe)
|
||||
keys = [t[0] for t in table]
|
||||
size = image_size(a.exe)
|
||||
per = {}
|
||||
for rva_s, n in r["top"]:
|
||||
rva = int(rva_s, 16)
|
||||
i = bisect.bisect_right(keys, rva) - 1
|
||||
if (size is not None and rva >= size) or i < 0:
|
||||
name = "(outside the executable: system code, waits)"
|
||||
else:
|
||||
name = table[i][1]
|
||||
per[name] = per.get(name, 0) + n
|
||||
total = r["samples"] or 1
|
||||
print("samples %d (binned top 400 addresses; %d not binned) seq %d" %
|
||||
(r["samples"], r["unbinned"], r["seq"]))
|
||||
for name, n in sorted(per.items(), key=lambda kv: -kv[1])[:a.top]:
|
||||
print("%6d %5.1f%% %s" % (n, 100.0 * n / total, name))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user