// Sampling profiler for PS4 test builds. // // PS4 has no perf/Razor access for homebrew, so the app samples itself: a helper thread sends // SIGPROF to each registered thread every millisecond, and the handler records the interrupted // instruction pointer. Every 10 seconds the samples are aggregated and appended to // /data/OpenCE/samples.log: per thread, the busiest eboot addresses (ELF virtual addresses, // symbolize with `llvm-symbolizer --obj= 0x...`), system module addresses ("sys") and JIT // code addresses ("jit", 16-byte buckets). Every 60 s the busiest JIT code (64 KiB chunks) is // copied to /data/OpenCE/jitcode.bin and the emulated MEM1 to mem1.bin, so the generated x86 // and the guest code behind it can be disassembled offline (blocks: the jit-perf map written by // Common/JitRegister). // // Started by ps4_sampler_start() (DolphinNoGUI, ps4.ini profile=on); threads register // themselves with ps4_sampler_register_thread(). #include #include #include #include #include #include #include #include #include #include extern "C" char __text_start[]; // defined by the OpenOrbis link.x at the start of .text extern "C" int pthread_getthreadid_np(void); // libkernel (FreeBSD libthr) extern "C" void ps4_boot_trace(const char* stage); namespace { constexpr int kSigProf = 27; // FreeBSD value constexpr int kSaSiginfo = 0x40, kSaRestart = 0x0002; constexpr int kMcontextOffset = 0x40; // measured on hardware (see ps4_crashlog.cpp) constexpr int kRipIndex = 20, kRspIndex = 23; // Samples in system libraries (libkernel/libc at 0x800000000+) are attributed to the eboot // function that called into them: the first eboot code address on the stack, tagged. constexpr uint64_t kSystemLibs = 0x800000000ULL, kCallerTag = 1ULL << 62; constexpr uintptr_t kTextWindow = 0x40000000; constexpr int kMaxThreads = 4; constexpr int kMaxSamples = 16384; // per thread and window (10 s at 1 kHz fits) struct ThreadSamples { std::atomic thread{}; const char* name = nullptr; uint64_t samples[kMaxSamples]; std::atomic count{0}; }; ThreadSamples g_threads[kMaxThreads]; std::atomic g_thread_count{0}; void profHandler(int, siginfo_t*, void* context) { const pthread_t self = pthread_self(); for (int i = 0; i < g_thread_count.load(std::memory_order_acquire); i++) { ThreadSamples& t = g_threads[i]; if (t.thread.load(std::memory_order_relaxed) != self) continue; const auto* regs = reinterpret_cast(static_cast(context) + kMcontextOffset); const int n = t.count.load(std::memory_order_relaxed); if (n < kMaxSamples) { uint64_t sample = regs[kRipIndex]; // Inside a system module: keep the exact address (tagged). The module list at the // top of samples.log maps it to a module offset, which the decrypted modules' symbol // tables name. (Blaming "the first eboot address on the stack" picked up stale // values and named the wrong callers.) if (sample >= kSystemLibs && sample < 2 * kSystemLibs) sample |= kCallerTag; t.samples[n] = sample; t.count.store(n + 1, std::memory_order_release); } return; } } void writeLine(int fd, const char* text) { size_t length = strlen(text); while (length > 0) { const ssize_t written = write(fd, text, length); if (written <= 0) return; text += written; length -= static_cast(written); } } void noteHotJit(uint64_t address, int count); // Aggregates one thread's window: sorts the samples in place and prints the top entries. void report(int fd, ThreadSamples& t, double window_start) { // Copy first: the handler keeps writing into t.samples once count is reset. static uint64_t s[kMaxSamples]; const int n = std::min(t.count.load(std::memory_order_acquire), kMaxSamples); memcpy(s, t.samples, sizeof(uint64_t) * n); t.count.store(0, std::memory_order_release); if (n == 0) return; const uintptr_t text = reinterpret_cast(__text_start); // Eboot and JIT code: bucket by 16 bytes (eboot addresses relative to .text; JIT addresses // tagged with bit 63). for (int i = 0; i < n; i++) { if (s[i] & kCallerTag) { s[i] &= ~0xFULL; // system module address (see the module list) continue; } const bool eboot = s[i] >= text && s[i] < text + kTextWindow; s[i] = eboot ? ((s[i] - text) & ~0xFULL) : (s[i] | (1ULL << 63)) & ~0xFULL; } std::sort(s, s + n); struct Entry { uint64_t key; int count; }; static Entry entries[kMaxSamples]; int unique = 0, eboot_samples = 0; for (int i = 0; i < n;) { int j = i; while (j < n && s[j] == s[i]) j++; entries[unique++] = {s[i], j - i}; if (!(s[i] >> 62)) eboot_samples += j - i; i = j; } std::sort(entries, entries + unique, [](const Entry& a, const Entry& b) { return a.count > b.count; }); char line[256]; snprintf(line, sizeof(line), "== %.0f s, %s: %d samples, %d in eboot code (%.0f%%)\n", window_start, t.name, n, eboot_samples, 100.0 * eboot_samples / n); writeLine(fd, line); // Every bucket seen at least twice (up to 1500): a function spread over many 16-byte buckets // fell below a top-60 cut, hiding about half of a busy thread. Aggregated per function // offline (symbolizer, JIT block map). for (int i = 0; i < unique && i < 1500 && entries[i].count >= 2; i++) { if (entries[i].key >> 63) { const uint64_t address = entries[i].key & ~(1ULL << 63); snprintf(line, sizeof(line), " %5.1f%% jit 0x%llx\n", 100.0 * entries[i].count / n, static_cast(address)); noteHotJit(address, entries[i].count); } else if (entries[i].key & kCallerTag) snprintf(line, sizeof(line), " %5.1f%% sys 0x%llx\n", 100.0 * entries[i].count / n, static_cast(entries[i].key & ~kCallerTag)); else snprintf(line, sizeof(line), " %5.1f%% elf 0x%llx\n", 100.0 * entries[i].count / n, static_cast(entries[i].key)); writeLine(fd, line); } } // 64 KiB chunks of JIT code sampled since the last dump (dumpJitCode). constexpr int kMaxHotChunks = 96; uint64_t g_hot_chunks[kMaxHotChunks]; int g_hot_chunk_count = 0; void noteHotJit(uint64_t address, int count) { if (count < 3) return; const uint64_t chunk = address & ~0xFFFFULL; for (int i = 0; i < g_hot_chunk_count; i++) { if (g_hot_chunks[i] == chunk) return; } if (g_hot_chunk_count < kMaxHotChunks) g_hot_chunks[g_hot_chunk_count++] = chunk; } double now() { timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return static_cast(ts.tv_sec) + static_cast(ts.tv_nsec) / 1e9; } // Loaded modules and their segments, so "sys" samples can be mapped to module offsets. struct ModuleSegment { void* address; uint32_t size; int32_t prot; }; struct ModuleInfo { size_t size; char name[256]; ModuleSegment segments[4]; uint32_t segment_count; uint8_t fingerprint[20]; }; extern "C" int32_t sceKernelGetModuleList(int32_t* handles, size_t count, size_t* available); extern "C" int32_t sceKernelGetModuleInfo(int32_t handle, ModuleInfo* info); void writeModuleList(int fd) { int32_t handles[256]; size_t count = 0; static size_t s_listed = 0; // relisted when modules get loaded (e.g. RADV's GnmDriver) if (sceKernelGetModuleList(handles, 256, &count) != 0 || count == s_listed) return; s_listed = count; char line[512]; for (size_t m = 0; m < count && m < 256; m++) { ModuleInfo info; memset(&info, 0, sizeof(info)); info.size = sizeof(info); if (sceKernelGetModuleInfo(handles[m], &info) != 0) continue; int len = snprintf(line, sizeof(line), "module %s", info.name); for (uint32_t g = 0; g < info.segment_count && g < 4; g++) len += snprintf(line + len, sizeof(line) - len, " seg %p+0x%x/%d", info.segments[g].address, info.segments[g].size, info.segments[g].prot); snprintf(line + len, sizeof(line) - len, "\n"); writeLine(fd, line); } } } // namespace extern "C" int32_t sceKernelQueryMemoryProtection(void* address, void** start, void** end, uint32_t* protection); // Emulated MEM1 (set by Core/HW/Memmap.cpp), dumped with the JIT code. extern "C" void* ps4_profile_guest_ram; extern "C" uint32_t ps4_profile_guest_ram_size; extern "C" { void* ps4_profile_guest_ram = nullptr; uint32_t ps4_profile_guest_ram_size = 0; } namespace { bool isReadable(uint64_t address, uint64_t size) { void* start = nullptr; void* end = nullptr; uint32_t protection = 0; return sceKernelQueryMemoryProtection(reinterpret_cast(address), &start, &end, &protection) == 0 && (protection & 1) && address + size <= reinterpret_cast(end); } // jitcode.bin: records of {u64 address, u64 size, bytes}. Copied through a buffer first so the // kernel never reads JIT memory for the file write. void dumpJitCode() { const int fd = open("/data/OpenCE/jitcode.bin", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd >= 0) { static uint8_t buffer[0x10000]; for (int i = 0; i < g_hot_chunk_count; i++) { if (!isReadable(g_hot_chunks[i], sizeof(buffer))) continue; memcpy(buffer, reinterpret_cast(g_hot_chunks[i]), sizeof(buffer)); const uint64_t header[2] = {g_hot_chunks[i], sizeof(buffer)}; write(fd, header, sizeof(header)); write(fd, buffer, sizeof(buffer)); } close(fd); } g_hot_chunk_count = 0; if (ps4_profile_guest_ram && ps4_profile_guest_ram_size) { const int ram = open("/data/OpenCE/mem1.bin", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (ram >= 0) { static uint8_t copy[0x100000]; for (uint32_t offset = 0; offset < ps4_profile_guest_ram_size; offset += sizeof(copy)) { const uint32_t size = std::min(sizeof(copy), ps4_profile_guest_ram_size - offset); memcpy(copy, static_cast(ps4_profile_guest_ram) + offset, size); write(ram, copy, size); } close(ram); } } } std::atomic g_sampler_on{false}; void* samplerThread(void*) { const int fd = open("/data/OpenCE/samples.log", O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fd < 0) return nullptr; writeModuleList(fd); const double start = now(); double window_start = start, last_dump = start; const timespec period = {0, 1000000}, paused = {0, 100000000}; for (;;) { // Profiler switched off in the menus: no signals, no new windows until it is back on. if (!g_sampler_on.load(std::memory_order_relaxed)) { nanosleep(&paused, nullptr); window_start = now(); continue; } nanosleep(&period, nullptr); const int threads = g_thread_count.load(std::memory_order_acquire); for (int i = 0; i < threads; i++) pthread_kill(g_threads[i].thread.load(std::memory_order_relaxed), kSigProf); if (now() - window_start >= 10.0) { writeModuleList(fd); for (int i = 0; i < threads; i++) report(fd, g_threads[i], window_start - start); window_start = now(); if (window_start - last_dump >= 60.0 && ps4_profile_guest_ram) { // Dolphin's JIT and MEM1 (not SMW) dumpJitCode(); last_dump = window_start = now(); writeLine(fd, "(jitcode.bin and mem1.bin updated)\n"); } } } return nullptr; } } // namespace // --------------------------------------------------------------------------------------------- // Hang dumps: every thread that matters registers here (ps4_watch_thread, also done by // ps4_sampler_register_thread). ps4_dump_thread_stacks() signals each one; the handler records // its instruction pointer and every eboot code address on its stack (return addresses, newest // first). Used by DolphinNoGUI's watchdog when emulation stops advancing. namespace { constexpr int kSigDump = 30; // SIGUSR1 (FreeBSD) constexpr int kMaxWatched = 96, kMaxFrames = 48; // every thread of the app (pthread_create wrap) // A thread away from its registered stack (Cemu's CPU threads run the emulated threads on fiber // stacks): this much above rsp is scanned, within its mapping. constexpr uintptr_t kForeignStackScan = 16 * 1024; struct WatchedThread { std::atomic thread{}; char name[32] = {}; // copied: some callers pass temporaries // Highest stack address the dump may read: the thread's stack position when it registered // (plus its caller's frame). Reading a fixed amount above rsp ran off the top of small thread // stacks (the Wiimote scanner's) and crashed the app in the middle of a hang dump. uintptr_t stack_limit = 0; int tid = 0; // kernel thread id (pthread_getthreadid_np): mutex objects record their owner's uint64_t rip = 0; uint64_t frames[kMaxFrames]; int frame_count = 0; // Blocked in _umtx_op (the kernel's wait for mutexes, condition variables, semaphores): its // arguments - object, operation, value, second object (condition variable waits: the mutex) - // and the first 16 bytes of both objects (a FreeBSD mutex starts with its owner's thread id). bool in_umtx = false; uint64_t umtx_obj = 0, umtx_op = 0, umtx_val = 0, umtx_obj2 = 0; uint32_t umtx_words[4] = {}, umtx_words2[4] = {}; std::atomic done{0}; }; WatchedThread g_watched[kMaxWatched]; std::atomic g_watched_count{0}; void dumpHandler(int, siginfo_t*, void* context) { const pthread_t self = pthread_self(); const uintptr_t text = reinterpret_cast(__text_start); for (int i = 0; i < g_watched_count.load(std::memory_order_acquire); i++) { WatchedThread& t = g_watched[i]; if (t.thread.load(std::memory_order_relaxed) != self) continue; const auto* regs = reinterpret_cast(static_cast(context) + kMcontextOffset); t.rip = regs[kRipIndex]; // libkernel's _umtx_op stub: "mov $0x1c6, %rax; mov %rcx, %r10; syscall" (12 bytes), the // interrupted thread sits right after the syscall. FreeBSD mcontext: rdi 1, rsi 2, rdx 3, // r10 10. Only then are the argument registers known to be the wait's. t.in_umtx = false; if (t.rip >= kSystemLibs && t.rip < 2 * kSystemLibs) { const auto* code = reinterpret_cast(t.rip - 12); static const uint8_t kUmtxStub[12] = {0x48, 0xc7, 0xc0, 0xc6, 0x01, 0x00, 0x00, 0x49, 0x89, 0xca, 0x0f, 0x05}; if (memcmp(code, kUmtxStub, sizeof(kUmtxStub)) == 0) { // Registers only: the objects are read by the dumping thread, after checking that // they are mapped (a register that wasn't a pointer crashed v02.37 here). t.in_umtx = true; t.umtx_obj = regs[1]; t.umtx_op = regs[2]; t.umtx_val = regs[3]; t.umtx_obj2 = regs[10]; } } // Only scan a stack that is plausibly this registration's: a thread that registered and // exited can leave its pthread handle to a new thread, whose stack is elsewhere (a v02.34 // hang dump crashed scanning from one thread's rsp to another's stack limit). const uint64_t rsp = regs[kRspIndex]; const bool same_thread = pthread_getthreadid_np() == t.tid; const bool sane = t.stack_limit > rsp && t.stack_limit - rsp <= (1u << 20); const auto* stack = reinterpret_cast(rsp); uintptr_t limit_address = rsp; if (same_thread && sane) { limit_address = t.stack_limit; } else if (same_thread) { void* start = nullptr; void* end = nullptr; uint32_t protection = 0; if (sceKernelQueryMemoryProtection(reinterpret_cast(rsp), &start, &end, &protection) == 0 && (protection & 1)) limit_address = std::min(rsp + kForeignStackScan, reinterpret_cast(end)); } const auto* limit = reinterpret_cast(limit_address); int n = 0; for (const uint64_t* p = stack; p < limit && n < kMaxFrames; p++) { if (*p >= text && *p < text + kTextWindow) t.frames[n++] = *p - text; } t.frame_count = n; t.done.store(1, std::memory_order_release); return; } } void installDumpHandler() { static std::atomic installed{false}; if (installed.exchange(true)) return; struct sigaction action; memset(&action, 0, sizeof(action)); void (*handler)(int, siginfo_t*, void*) = dumpHandler; memcpy(&action, &handler, sizeof(handler)); action.sa_flags = kSaSiginfo | kSaRestart; sigemptyset(&action.sa_mask); sigaction(kSigDump, &action, nullptr); } } // namespace extern "C" void ps4_heap_private_arena(const char* name); // ps4_runtime.cpp extern "C" void ps4_watch_thread(const char* name) { installDumpHandler(); ps4_heap_private_arena(name); const pthread_t self = pthread_self(); for (int i = 0; i < std::min(g_watched_count.load(), kMaxWatched); i++) { if (g_watched[i].thread.load() == self) { // already watched (every thread is, from its start): the name it gives itself if (name) snprintf(g_watched[i].name, sizeof(g_watched[i].name), "%s", name); return; } } const int index = g_watched_count.fetch_add(1); if (index >= kMaxWatched) return; snprintf(g_watched[index].name, sizeof(g_watched[index].name), "%s", name ? name : "?"); // Up to and including this function's own return address (frame + 16): threads that register // first thing (e.g. the analytics reporter) sit within 256 bytes of their stack's top, and a // larger margin read past it. Where a thread is stuck lies deeper than this anyway. g_watched[index].stack_limit = reinterpret_cast(__builtin_frame_address(0)) + 16; g_watched[index].tid = pthread_getthreadid_np(); g_watched[index].thread.store(self); } extern "C" int32_t sceKernelQueryMemoryProtection(void* address, void** start, void** end, uint32_t* protection); // Copies 16 bytes at `address` if they lie in readable memory (else leaves zeros). static void readIfMapped(uint64_t address, uint32_t (&words)[4]) { memset(words, 0, sizeof(words)); void* start = nullptr; void* end = nullptr; uint32_t protection = 0; if (address < 0x10000 || sceKernelQueryMemoryProtection(reinterpret_cast(address), &start, &end, &protection) != 0 || !(protection & 1) || address + 16 > reinterpret_cast(end)) return; memcpy(words, reinterpret_cast(address), sizeof(words)); } // Writes every watched thread's stack to `path` (appending), headed by `reason`. extern "C" void ps4_dump_thread_stacks(const char* path, const char* reason) { const int fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0666); if (fd < 0) return; char line[256]; snprintf(line, sizeof(line), "==== %s (eboot addresses relative to .text, symbolize with " "llvm-symbolizer --obj=)\n", reason); writeLine(fd, line); const uintptr_t text = reinterpret_cast(__text_start); const int count = std::min(g_watched_count.load(std::memory_order_acquire), kMaxWatched); for (int i = 0; i < count; i++) { WatchedThread& t = g_watched[i]; t.done.store(0); if (pthread_kill(t.thread.load(), kSigDump) != 0) { snprintf(line, sizeof(line), "-- %s: signal failed (thread gone?)\n", t.name); writeLine(fd, line); continue; } const timespec pause = {0, 1000000}; for (int w = 0; w < 200 && !t.done.load(std::memory_order_acquire); w++) nanosleep(&pause, nullptr); if (!t.done.load(std::memory_order_acquire)) { snprintf(line, sizeof(line), "-- %s: no answer (signals blocked?)\n", t.name); writeLine(fd, line); continue; } const bool in_eboot = t.rip >= text && t.rip < text + kTextWindow; snprintf(line, sizeof(line), "-- %s (thread id %d): rip %s0x%llx\n", t.name, t.tid, in_eboot ? "elf " : "(system/JIT) ", static_cast(in_eboot ? t.rip - text : t.rip)); writeLine(fd, line); if (t.in_umtx) { readIfMapped(t.umtx_obj, t.umtx_words); readIfMapped(t.umtx_obj2, t.umtx_words2); snprintf(line, sizeof(line), " blocked in _umtx_op(obj %#llx, op %llu, val %#llx, obj2 %#llx); obj: %08x " "%08x %08x %08x; obj2: %08x %08x %08x %08x\n", static_cast(t.umtx_obj), static_cast(t.umtx_op), static_cast(t.umtx_val), static_cast(t.umtx_obj2), t.umtx_words[0], t.umtx_words[1], t.umtx_words[2], t.umtx_words[3], t.umtx_words2[0], t.umtx_words2[1], t.umtx_words2[2], t.umtx_words2[3]); writeLine(fd, line); } writeLine(fd, " stack:"); for (int f = 0; f < t.frame_count; f++) { snprintf(line, sizeof(line), " 0x%llx", static_cast(t.frames[f])); writeLine(fd, line); } writeLine(fd, "\n"); } close(fd); } // Per-thread CPU time for the monitor: busy threads (CPU thread 0, video thread 1) read their own // CPU clock now and then; the monitor divides the growth by wall time. Shows how close a thread // is to its limit and whether something else shares its core (the sampler can't see that). namespace { constexpr clockid_t kThreadCpuClock = 14; // FreeBSD CLOCK_THREAD_CPUTIME_ID std::atomic g_thread_cpu_ns[2]; } // namespace extern "C" void ps4_thread_cpu_sample(int slot) { timespec ts; if (slot < 0 || slot > 1 || clock_gettime(kThreadCpuClock, &ts) != 0) return; g_thread_cpu_ns[slot].store(static_cast(ts.tv_sec) * 1000000000ull + static_cast(ts.tv_nsec), std::memory_order_relaxed); } // Wait-time counters for the monitor (ns): 0 CPU thread waiting for the GPU thread (FlushGpu), // 1 GPU loop sleeping for lack of work, 2 video thread waiting for the Vulkan recording thread, // 3 vkWaitForFences. namespace { std::atomic g_wait_ns[6]; // 4: video-thread pipeline compile ns, 5: their count } // namespace extern "C" void ps4_wait_add(int slot, long long ns) { if (slot >= 0 && slot < 6 && ns > 0) g_wait_ns[slot].fetch_add(ns, std::memory_order_relaxed); } extern "C" long long ps4_wait_total(int slot) { return slot >= 0 && slot < 6 ? g_wait_ns[slot].load(std::memory_order_relaxed) : 0; } extern "C" double ps4_thread_cpu_seconds(int slot) { if (slot < 0 || slot > 1) return 0; return g_thread_cpu_ns[slot].load(std::memory_order_relaxed) / 1e9; } // The PS4 doesn't let a thread read another thread's CPU clock (pthread_getcpuclockid fails), so // busy threads report their own: ps4_thread_cpu_tick() in their loops stores the thread's CPU // time in its watched slot, at most every ~10 ms (rdtsc check; the clock read is a system call). namespace { std::atomic g_watched_cpu_ns[kMaxWatched]; } // namespace extern "C" void ps4_thread_cpu_tick() { static thread_local int t_slot = -2; static thread_local uint64_t t_last = 0; const uint64_t now = __builtin_ia32_rdtsc(); if (now - t_last < 16000000) // ~10 ms at 1.6 GHz return; t_last = now; if (t_slot == -2) { t_slot = -1; const pthread_t self = pthread_self(); for (int i = 0; i < std::min(g_watched_count.load(), kMaxWatched); i++) { if (g_watched[i].thread.load() == self) t_slot = i; } } timespec ts; if (t_slot >= 0 && clock_gettime(kThreadCpuClock, &ts) == 0) { g_watched_cpu_ns[t_slot].store(static_cast(ts.tv_sec) * 1000000000ull + ts.tv_nsec, std::memory_order_relaxed); } } // How busy each watched thread was since the previous call: its self-reported CPU time // (ps4_thread_cpu_tick), else its CPU clock read from here (pthread_getcpuclockid, where the // system allows it). "name N%" for those at or above min_percent, busiest first, into `out`. // One caller (the monitor); no allocations. extern "C" void ps4_thread_busy_report(char* out, size_t size, int min_percent) { static uint64_t s_prev_cpu[kMaxWatched]; static uint64_t s_prev_wall = 0; timespec now; clock_gettime(CLOCK_MONOTONIC, &now); const uint64_t wall = static_cast(now.tv_sec) * 1000000000ull + now.tv_nsec; const uint64_t elapsed = s_prev_wall ? wall - s_prev_wall : 0; s_prev_wall = wall; int percents[kMaxWatched]; const int count = std::min(g_watched_count.load(std::memory_order_acquire), kMaxWatched); for (int i = 0; i < count; i++) { percents[i] = -1; uint64_t ns = g_watched_cpu_ns[i].load(std::memory_order_relaxed); if (ns == 0) { clockid_t clock; timespec cpu; if (pthread_getcpuclockid(g_watched[i].thread.load(), &clock) != 0 || clock_gettime(clock, &cpu) != 0) continue; ns = static_cast(cpu.tv_sec) * 1000000000ull + cpu.tv_nsec; } if (elapsed && s_prev_cpu[i] && ns >= s_prev_cpu[i]) percents[i] = static_cast((ns - s_prev_cpu[i]) * 100 / elapsed); s_prev_cpu[i] = ns; } size_t length = 0; out[0] = 0; for (;;) { int best = -1; for (int i = 0; i < count; i++) if (percents[i] >= min_percent && (best < 0 || percents[i] > percents[best])) best = i; if (best < 0 || length + 40 >= size) break; length += snprintf(out + length, size - length, "%s%s %d%%", length ? ", " : "", g_watched[best].name, percents[best]); percents[best] = -1; } } extern "C" void ps4_sampler_register_thread(const char* name) { ps4_watch_thread(name); // a thread that is started again (SMW (from M33): the emulation thread at every game start) takes // over its predecessor's slot instead of a new one for (int i = 0; i < g_thread_count.load(); i++) { if (g_threads[i].name && name && strcmp(g_threads[i].name, name) == 0) { g_threads[i].thread.store(pthread_self()); return; } } const int index = g_thread_count.load(); if (index >= kMaxThreads) return; g_threads[index].name = name; g_threads[index].thread.store(pthread_self()); g_thread_count.store(index + 1, std::memory_order_release); } extern "C" void ps4_sampler_start() { static std::atomic s_started{false}; g_sampler_on.store(true); if (s_started.exchange(true)) return; struct sigaction action; memset(&action, 0, sizeof(action)); void (*handler)(int, siginfo_t*, void*) = profHandler; memcpy(&action, &handler, sizeof(handler)); // the handler union's member names differ action.sa_flags = kSaSiginfo | kSaRestart; sigemptyset(&action.sa_mask); sigaction(kSigProf, &action, nullptr); pthread_t thread; if (pthread_create(&thread, nullptr, samplerThread, nullptr) == 0) ps4_boot_trace("sampler: started, writing /data/OpenCE/samples.log every 10 s"); } // The menus' Profiler switch: starts the sampler the first time, then pauses / resumes it. extern "C" void ps4_sampler_set_enabled(int on) { if (on) ps4_sampler_start(); else if (g_sampler_on.exchange(false)) ps4_boot_trace("sampler: paused"); }