Always-on host CPU sampler ring (TCP host_profile)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-10-02 16:43:49 -07:00
parent 487224cd9c
commit 0901bb8b1e
5 changed files with 263 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
#ifndef PSX_HOST_SAMPLER_H
#define PSX_HOST_SAMPLER_H
/* Always-on host CPU sampler for the emulation thread (debug-tools builds).
* See host_sampler.c; TCP host_profile; tools/host_profile.py. */
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define HOST_SAMPLER_CAP 65536u /* ~65 s at 1 kHz */
typedef struct HostSample {
uint64_t rva; /* instruction pointer minus the executable's base */
uint32_t frame; /* guest frame (s_frame_count) */
uint32_t in_pass; /* 1 while a render pass ran */
} HostSample;
/* Start sampling the calling thread (the emulation thread). Idempotent. */
void host_sampler_start(void);
int host_sampler_supported(void);
/* Total samples recorded; the ring holds the newest HOST_SAMPLER_CAP. */
uint64_t host_sampler_seq(void);
uint64_t host_sampler_image_base(void);
/* Sample `seq`, if it is still in the ring. */
int host_sampler_get(uint64_t seq, HostSample *out);
#ifdef __cplusplus
}
#endif
#endif
+1
View File
@@ -369,6 +369,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/sio.c
${PSXRECOMP_ROOT}/runtime/src/memcard.c
${PSXRECOMP_ROOT}/runtime/src/debug_server.c
${PSXRECOMP_ROOT}/runtime/src/host_sampler.c
${PSXRECOMP_ROOT}/runtime/src/debug_trace_ranges.c
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c
+64
View File
@@ -17,6 +17,7 @@
#endif
#include <time.h>
#include "debug_server.h"
#include "host_sampler.h"
#include "psx_video_timing.h"
#include "psx_bss.h"
#include "nd_intro_ot.h"
@@ -5323,6 +5324,68 @@ static void handle_geom_correction(int id, const char *json)
(unsigned long long)ps.word_no_z);
}
/* host_profile — histogram of the always-on host CPU sampler (host_sampler.c)
* over a window of the ring: {"cmd":"host_profile","since":SEQ} or
* {"cmd":"host_profile","frames":N} (the last N guest frames), optional
* "pass":1 (only samples inside render passes) / "pass":0 (outside), "top":K.
* Addresses are executable-relative; tools/host_profile.py names them. Take
* "seq" from one reply as "since" of the next to profile what ran between. */
static void handle_host_profile(int id, const char *json)
{
enum { MAXB = 4096 };
static uint64_t rvas[MAXB];
static uint32_t counts[MAXB];
const uint64_t seq = host_sampler_seq();
const uint64_t oldest = seq > HOST_SAMPLER_CAP ? seq - HOST_SAMPLER_CAP : 0;
int since = json_get_int(json, "since", -1);
const int frames = json_get_int(json, "frames", -1);
const int pass = json_get_int(json, "pass", -1);
int top = json_get_int(json, "top", 60);
if (top < 1) top = 1;
if (top > 400) top = 400;
uint64_t from = since >= 0 ? (uint64_t)since : oldest;
if (from < oldest) from = oldest;
uint32_t frame_lo = 0;
if (frames > 0) frame_lo = (uint32_t)s_frame_count - (uint32_t)frames;
int nb = 0;
uint64_t total = 0, dropped = 0;
for (uint64_t i = from; i < seq; i++) {
HostSample smp;
if (!host_sampler_get(i, &smp)) continue;
if (frames > 0 && (int32_t)(smp.frame - frame_lo) < 0) continue;
if (pass >= 0 && (int)smp.in_pass != pass) continue;
total++;
int b = 0;
while (b < nb && rvas[b] != smp.rva) b++;
if (b == nb) {
if (nb == MAXB) { dropped++; continue; }
rvas[nb] = smp.rva; counts[nb] = 0; nb++;
}
counts[b]++;
}
size_t cap = 256 + (size_t)top * 48;
char *buf = (char *)malloc(cap);
if (!buf) { send_err(id, "oom"); return; }
size_t n = (size_t)snprintf(buf, cap,
"{\"id\":%d,\"ok\":true,\"supported\":%d,\"seq\":%llu,"
"\"image_base\":\"0x%llX\",\"samples\":%llu,\"unbinned\":%llu,\"top\":[",
id, host_sampler_supported(), (unsigned long long)seq,
(unsigned long long)host_sampler_image_base(),
(unsigned long long)total, (unsigned long long)dropped);
for (int k = 0; k < top; k++) {
int best = -1;
for (int b = 0; b < nb; b++)
if (counts[b] && (best < 0 || counts[b] > counts[best])) best = b;
if (best < 0) break;
n += (size_t)snprintf(buf + n, cap - n, "%s[\"0x%llX\",%u]", k ? "," : "",
(unsigned long long)rvas[best], counts[best]);
counts[best] = 0;
}
snprintf(buf + n, cap - n, "]}");
send_fmt("%s", buf);
free(buf);
}
/* pgxp_shadow — read PGXP shadow slots: {"cmd":"pgxp_shadow","addr":A,
* "count":N} walks N guest words from A (RAM / scratchpad); "space":"gpr" or
* "gte" with "index"/"count" reads register shadows instead. Each slot: the
@@ -14459,6 +14522,7 @@ static const CmdEntry s_commands[] = {
{ "geom_correction", handle_geom_correction },
{ "pgxp", handle_pgxp },
{ "pgxp_shadow", handle_pgxp_shadow },
{ "host_profile", handle_host_profile },
{ "pgxp_miss_ring", handle_pgxp_miss_ring },
{ "ws_aspect_cone_site", handle_ws_aspect_cone_site },
{ "ws_margin", handle_ws_margin },
+165
View File
@@ -0,0 +1,165 @@
/* host_sampler.c - always-on host CPU sampler for the emulation thread
* (debug-tools builds; TCP host_profile, tools/host_profile.py).
*
* A background thread samples the emulation thread's instruction pointer
* every millisecond into a ring, tagged with the guest frame and whether a
* render pass was running. The ring covers the last ~65 seconds, so a probe
* asks for a window that already happened instead of arming a profiler
* before a workload. Addresses are stored relative to the executable's load
* base; tools/host_profile.py maps them to functions with the image's own
* symbol table (nm), so no debug info is needed.
*
* 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
#if defined(__linux__) && !defined(_GNU_SOURCE)
#define _GNU_SOURCE 1 /* REG_RIP, dladdr */
#endif
#include "host_sampler.h"
#include <stdint.h>
#include <string.h>
extern int g_psx_render_pass_active;
extern uint64_t s_frame_count;
static HostSample s_ring[HOST_SAMPLER_CAP];
static volatile uint64_t s_seq;
static uintptr_t s_image_base;
static int s_supported;
static void record(uintptr_t pc) {
const uint64_t n = s_seq;
HostSample *e = &s_ring[n % HOST_SAMPLER_CAP];
e->rva = (pc >= s_image_base) ? (uint64_t)(pc - s_image_base) : 0;
e->frame = (uint32_t)s_frame_count;
e->in_pass = g_psx_render_pass_active ? 1u : 0u;
s_seq = n + 1;
}
#if defined(_WIN32)
#include <windows.h>
static HANDLE s_target;
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
#endif
static DWORD WINAPI sampler_main(LPVOID arg) {
(void)arg;
/* A 1 ms period needs a high-resolution timer; Sleep(1) would follow
* whatever timer resolution the process last asked for. */
HANDLE timer = CreateWaitableTimerExW(NULL, NULL,
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
TIMER_ALL_ACCESS);
if (timer) {
LARGE_INTEGER due;
due.QuadPart = -10000; /* 1 ms, relative */
if (!SetWaitableTimer(timer, &due, 1, NULL, NULL, FALSE)) {
CloseHandle(timer);
timer = NULL;
}
}
for (;;) {
if (timer) WaitForSingleObject(timer, INFINITE);
else Sleep(1);
if (SuspendThread(s_target) == (DWORD)-1) continue;
CONTEXT ctx;
memset(&ctx, 0, sizeof ctx);
ctx.ContextFlags = CONTEXT_CONTROL;
if (GetThreadContext(s_target, &ctx)) {
#if defined(_M_X64) || defined(__x86_64__)
record((uintptr_t)ctx.Rip);
#elif defined(_M_ARM64) || defined(__aarch64__)
record((uintptr_t)ctx.Pc);
#endif
}
ResumeThread(s_target);
}
return 0;
}
void host_sampler_start(void) {
if (s_supported) return;
s_image_base = (uintptr_t)GetModuleHandleW(NULL);
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
GetCurrentProcess(), &s_target,
THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT |
THREAD_QUERY_INFORMATION,
FALSE, 0))
return;
HANDLE t = CreateThread(NULL, 0, sampler_main, NULL, 0, NULL);
if (!t) return;
SetThreadPriority(t, THREAD_PRIORITY_TIME_CRITICAL);
CloseHandle(t);
s_supported = 1;
}
#elif defined(__linux__) || defined(__APPLE__)
#include <signal.h>
#include <sys/time.h>
#include <ucontext.h>
#include <pthread.h>
#include <dlfcn.h>
static pthread_t s_thread;
static void on_prof(int sig, siginfo_t *si, void *uc_) {
(void)sig; (void)si;
if (!pthread_equal(pthread_self(), s_thread)) return;
ucontext_t *uc = (ucontext_t *)uc_;
#if defined(__linux__) && defined(__x86_64__)
record((uintptr_t)uc->uc_mcontext.gregs[REG_RIP]);
#elif defined(__linux__) && defined(__aarch64__)
record((uintptr_t)uc->uc_mcontext.pc);
#elif defined(__APPLE__) && defined(__x86_64__)
record((uintptr_t)uc->uc_mcontext->__ss.__rip);
#elif defined(__APPLE__) && defined(__aarch64__)
record((uintptr_t)uc->uc_mcontext->__ss.__pc);
#else
(void)uc;
#endif
}
void host_sampler_start(void) {
if (s_supported) return;
Dl_info info;
if (dladdr((void *)&host_sampler_start, &info) && info.dli_fbase)
s_image_base = (uintptr_t)info.dli_fbase;
s_thread = pthread_self();
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_sigaction = on_prof;
sa.sa_flags = SA_SIGINFO | SA_RESTART;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGPROF, &sa, NULL) != 0) return;
struct itimerval it;
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 1000;
it.it_value = it.it_interval;
if (setitimer(ITIMER_PROF, &it, NULL) != 0) return;
s_supported = 1;
}
#else
void host_sampler_start(void) {}
#endif
int host_sampler_supported(void) { return s_supported; }
uint64_t host_sampler_seq(void) { return s_seq; }
uint64_t host_sampler_image_base(void) { return (uint64_t)s_image_base; }
int host_sampler_get(uint64_t seq, HostSample *out) {
const uint64_t now = s_seq;
if (seq >= now || now - seq > HOST_SAMPLER_CAP) return 0;
*out = s_ring[seq % HOST_SAMPLER_CAP];
return 1;
}
#endif /* !PSX_NO_DEBUG_TOOLS */
+2
View File
@@ -85,6 +85,7 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "recomp_audio_drc.h"
#include "memcard.h"
#include "debug_server.h"
#include "host_sampler.h"
#include "crash_trace.h"
#include "freeze_heartbeat.h"
#include "config_loader.h"
@@ -16210,6 +16211,7 @@ session_reboot:
std::atexit(game_options_save_now);
#ifndef PSX_NO_DEBUG_TOOLS
debug_server_init(debug_port);
host_sampler_start(); /* this is the emulation thread */
#else
(void)debug_port;
#endif