mirror of
https://github.com/niakw/Prospero.Eden-Encore.git
synced 2026-10-17 14:01:09 +02:00
2034 lines
107 KiB
C++
2034 lines
107 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
#include "performance.h"
|
|
#include "ps5_cpu_topology.h"
|
|
#include "direct_pool_accounting.h"
|
|
#include "direct_pool_probe_policy.h"
|
|
#include "direct_pool_region_scan.h"
|
|
#include "hle_counters.h"
|
|
#include "gpu_fault_rate_limit.h"
|
|
#include "crash_report.h"
|
|
#include "stall_watchdog.h"
|
|
#include "../src/fastmem.h"
|
|
#include "common/cpu_features.h"
|
|
#include "common/sparse_large_vector.h"
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <set>
|
|
#include <cerrno>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cpuid.h>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <pthread.h>
|
|
#include <sched.h>
|
|
#include <semaphore>
|
|
#include <stdexcept>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <latch>
|
|
#include <time.h>
|
|
#if defined(EDEN_DEV_PROFILE) || defined(PS5_NATIVE)
|
|
#include <signal.h>
|
|
#endif
|
|
#ifdef PS5_NATIVE
|
|
#include <sys/param.h>
|
|
#include <sys/cpuset.h>
|
|
#include <sys/sysctl.h>
|
|
#endif
|
|
|
|
// dynarmic A64 dispatch-path counters (headless/dynarmic/jit_group_support.inc).
|
|
extern "C" void eden_jit_path_counters(unsigned core, unsigned long long* out) __attribute__((weak));
|
|
namespace Common {
|
|
// Address space Eden's sparse tables span and the memory they hold (src/memory_pages.cpp).
|
|
void SparseUsage(std::size_t* reserved, std::size_t* committed) noexcept;
|
|
#ifdef PS5_NATIVE
|
|
void SparseJitUsage(std::size_t* reserved, std::size_t* committed) noexcept;
|
|
void SparseJitUsageFast(std::size_t* reserved, std::size_t* committed) noexcept;
|
|
std::size_t DenseJitDirectBytes() noexcept;
|
|
#endif
|
|
} // namespace Common
|
|
|
|
namespace Eden::Performance {
|
|
namespace {
|
|
constexpr std::array names{"CPUCore_0", "CPUCore_1", "CPUCore_2", "CPUCore_3",
|
|
"GPU", "HostTiming", "VSyncThread"};
|
|
struct Worker {
|
|
pthread_t thread{};
|
|
clockid_t clock{};
|
|
int clock_error = ENOENT, affinity_error = ENOENT, allowed = 0;
|
|
unsigned long long mask = 0;
|
|
long long wall_ns = 0, mono_ns = 0;
|
|
bool registered = false;
|
|
};
|
|
std::mutex workers_mutex;
|
|
std::array<Worker, names.size()> workers;
|
|
// Periodic native GPU and optional host-thread capture must not concurrently
|
|
// consume the same PC sample cursors. Keep this lock DISTINCT from the
|
|
// owner-written worker mutex so costly diagnostic maps/hex never stop cores
|
|
// from publishing CPU samples or frontends from polling development PCs.
|
|
std::mutex snapshot_mutex;
|
|
// A GPU/guest worker is normally created again for the next title inside
|
|
// the same process. Registration is NOT a permanent OS thread identity:
|
|
// clear it on owner-thread exit before a later profiler tries to signal a
|
|
// recycled pthread_t. Thread-local declaration is touched before starting
|
|
// the optional fast sampler, so the fast sampler's jthread is joined FIRST.
|
|
struct WorkerRegistration {
|
|
unsigned index = unsigned(names.size());
|
|
pthread_t owner{};
|
|
~WorkerRegistration() {
|
|
if (index >= names.size()) return;
|
|
const std::lock_guard lock(workers_mutex);
|
|
auto& entry = workers[index];
|
|
if (entry.registered && pthread_equal(entry.thread, owner))
|
|
entry.registered = false;
|
|
}
|
|
};
|
|
thread_local WorkerRegistration owned_worker;
|
|
#ifdef EDEN_DEV_PROFILE
|
|
static_assert(std::atomic<uintptr_t>::is_always_lock_free);
|
|
// Lock-free signal-handler samples: use ring slots so the 500 Hz fast
|
|
// sampler does not silently stop after 8192 (~16 seconds) GPU samples.
|
|
std::array<std::atomic<uintptr_t>, 8192> sampled_pcs{};
|
|
std::atomic<unsigned> pc_count{};
|
|
// Guest core 0 host PCs (JIT code, HLE, memory callbacks), sampled with the GPU thread.
|
|
std::array<std::atomic<uintptr_t>, 65536> sampled_core_pcs{};
|
|
std::atomic<unsigned> core_pc_count{};
|
|
unsigned core_pc_reported{};
|
|
pthread_t core_sample_thread{};
|
|
std::atomic<bool> core_sample_ready{};
|
|
unsigned pc_reported{};
|
|
// Unlike a detached 500 Hz sampler, this thread-local worker registration
|
|
// clears readiness at guest-core exit. Do not signal a stale pthread_t after
|
|
// return to the library, nor leave detached samplers across title launches.
|
|
struct PcCoreRegistration {
|
|
~PcCoreRegistration() { if (active) core_sample_ready.store(false, std::memory_order_release); }
|
|
bool active = false;
|
|
};
|
|
thread_local PcCoreRegistration pc_core_registration;
|
|
bool pc_sampling{};
|
|
#if defined(EDEN_DEV_WAIT_CALLERS) && defined(PS5_NATIVE)
|
|
// Same firmware6.02 register offsets qualified by the C46 caller sampler.
|
|
constexpr size_t runtime_rsp_offset = 248, runtime_rbp_offset = 136;
|
|
using CallerChain = std::array<uintptr_t, 8>;
|
|
std::array<CallerChain, 8192> sampled_callers{};
|
|
|
|
CallerChain CaptureCallerChain(uintptr_t rsp, uintptr_t frame, uintptr_t handler_stack) {
|
|
CallerChain callers{};
|
|
// Read only aligned frames in the interrupted stack-top page. No allocation,
|
|
// symbolization or logging in the signal handler. Reject unknown/alternate stacks.
|
|
if (rsp <= 0x10000 || rsp < handler_stack || rsp - handler_stack >= 65536 ||
|
|
(rsp & 4095) > 4096 - 8 * sizeof(uintptr_t)) return callers;
|
|
for (unsigned i = 0; i < callers.size(); ++i) {
|
|
if (frame < rsp || (frame >> 12) != (rsp >> 12) ||
|
|
(frame & (alignof(uintptr_t) - 1)) ||
|
|
(frame & 4095) > 4096 - 2 * sizeof(uintptr_t)) break;
|
|
const auto* chain = reinterpret_cast<const uintptr_t*>(frame);
|
|
callers[i] = chain[1];
|
|
if (chain[0] <= frame || chain[0] - frame > 4096) break;
|
|
frame = chain[0];
|
|
}
|
|
return callers;
|
|
}
|
|
#endif
|
|
// Core 0's A32 blocks in emission order (P0): dynarmic's code cache grows linearly until a full
|
|
// clear, so entries stay sorted and a sampled host PC finds its guest block by binary search.
|
|
struct JitBlock {
|
|
uintptr_t entry;
|
|
unsigned long long size, location;
|
|
};
|
|
constexpr std::size_t jit_block_capacity = std::size_t{1} << 20;
|
|
JitBlock* core0_blocks = nullptr;
|
|
std::atomic<std::size_t> core0_block_count{0};
|
|
|
|
void PcSignal(int, siginfo_t*, void* context) {
|
|
if (core_sample_ready.load(std::memory_order_acquire) &&
|
|
pthread_equal(pthread_self(), core_sample_thread)) {
|
|
const unsigned core_index = core_pc_count.load(std::memory_order_relaxed);
|
|
sampled_core_pcs[core_index % sampled_core_pcs.size()].store(
|
|
static_cast<const uintptr_t*>(context)[224 / sizeof(uintptr_t)],
|
|
std::memory_order_relaxed);
|
|
core_pc_count.store(core_index + 1, std::memory_order_release);
|
|
return;
|
|
}
|
|
const unsigned index = pc_count.load(std::memory_order_relaxed);
|
|
#if defined(EDEN_DEV_WAIT_CALLERS) && defined(PS5_NATIVE)
|
|
// Caller-chain slots contain eight non-atomic words. Keep this special
|
|
// firmware oracle bounded until its lifetime and concurrency are proven:
|
|
// only the normal GPU/core PC-only samplers are cyclic.
|
|
if (index >= sampled_pcs.size()) return;
|
|
#endif
|
|
const unsigned slot = index % sampled_pcs.size();
|
|
// Native firmware 6.02 ABI, qualified by the standalone PC-sampling oracle.
|
|
// SDK FreeBSD ucontext.mc_rip points at the wrong field on this firmware.
|
|
sampled_pcs[slot].store(static_cast<const uintptr_t*>(context)[224 / sizeof(uintptr_t)],
|
|
std::memory_order_relaxed);
|
|
#if defined(EDEN_DEV_WAIT_CALLERS) && defined(PS5_NATIVE)
|
|
const auto* words = static_cast<const uintptr_t*>(context);
|
|
char handler_stack_marker;
|
|
sampled_callers[slot] = CaptureCallerChain(words[runtime_rsp_offset / sizeof(uintptr_t)],
|
|
words[runtime_rbp_offset / sizeof(uintptr_t)], reinterpret_cast<uintptr_t>(&handler_stack_marker));
|
|
#endif
|
|
pc_count.store(index + 1, std::memory_order_release);
|
|
}
|
|
#endif
|
|
#ifdef PS5_NATIVE
|
|
std::array<unsigned, 5> worker_cpus{};
|
|
bool worker_topology_ready = false;
|
|
// Published as ONE atomic snapshot only after a physically verified probe.
|
|
// Shader-pool setup must never read worker_cpus while the next title is
|
|
// rebuilding the mutable topology, nor mistake logical-only placement
|
|
// (physical_verified=0) for measured cores.
|
|
std::atomic<std::uint64_t> verified_physical_worker_mask{0};
|
|
// Only published after SetSecondaryPlacement(true) confirms exact OS
|
|
// affinity. An explicit logical-only A/B split can use its own nonoverlap
|
|
// evidence without pretending that CPUID physical topology was verified.
|
|
std::atomic<std::uint64_t> verified_secondary_placement_mask{0};
|
|
// Physical core (x2APIC above the SMT shift) of each allowed CPU, -1 if unknown.
|
|
std::array<int, 64> cpu_core = [] { std::array<int, 64> c{}; c.fill(-1); return c; }();
|
|
// CPUs outside guest cores 0-2 and their SMT siblings, and not the GPU thread CPU.
|
|
unsigned long long secondary_cpus = 0;
|
|
// The process CPU set seen by the topology probe (threads may inherit narrower sets).
|
|
cpuset_t topology_allowed{};
|
|
bool topology_allowed_valid = false;
|
|
std::atomic<bool> placement_secondary{false};
|
|
std::atomic<unsigned> secondary_reports{};
|
|
void CheckWorkerTopology() {
|
|
// A later game may start from a launcher thread with a narrower inherited CPU set.
|
|
// Preserve the process-wide physically verified layout, rather than probing that subset.
|
|
if (worker_topology_ready) {
|
|
std::printf("EDEN_WORKER_TOPOLOGY ready=1 cached=1 cpus=%u,%u,%u,%u,%u\n",
|
|
worker_cpus[0], worker_cpus[1], worker_cpus[2], worker_cpus[3], worker_cpus[4]);
|
|
return;
|
|
}
|
|
// A new title/firmware check may fail before CPUID probing begins.
|
|
// Never reuse a previous game's affinity decision in that case.
|
|
verified_physical_worker_mask.store(0, std::memory_order_release);
|
|
verified_secondary_placement_mask.store(0, std::memory_order_release);
|
|
worker_topology_ready = false;
|
|
secondary_cpus = 0;
|
|
worker_cpus.fill(0);
|
|
cpu_core.fill(-1);
|
|
topology_allowed = {};
|
|
topology_allowed_valid = false;
|
|
cpuset_t original{};
|
|
if (cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &original)) {
|
|
std::printf("EDEN_WORKER_TOPOLOGY ready=0 reason=affinity_unavailable errno=%d\n", errno);
|
|
return;
|
|
}
|
|
// This is the probe thread's permitted set, not the number of hardware
|
|
// threads on the PS5. Shader workers can inherit a narrower affinity.
|
|
std::uint64_t allowed_mask = 0;
|
|
unsigned allowed_count = 0;
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu) {
|
|
if (CPU_ISSET(cpu, &original)) {
|
|
allowed_mask |= std::uint64_t{1} << cpu;
|
|
++allowed_count;
|
|
}
|
|
}
|
|
std::printf("EDEN_PS5_CPU_ACCESS hardware_reported=%u affinity_allowed=%u allowed_mask=0x%llx scope=probe_thread\n",
|
|
std::thread::hardware_concurrency(), allowed_count,
|
|
static_cast<unsigned long long>(allowed_mask));
|
|
// AMD family 17h+ can expose physical topology via the extended leaf
|
|
// even if x2APIC leaf 0xB is missing/unusable on this firmware.
|
|
unsigned va{}, vb{}, vc{}, vd{};
|
|
__cpuid(0, va, vb, vc, vd);
|
|
const bool amd_vendor = vb == 0x68747541u &&
|
|
vd == 0x69746e65u && vc == 0x444d4163u;
|
|
const bool has_x2apic = __get_cpuid_max(0, nullptr) >= 0xbu;
|
|
bool has_amd_topology = false;
|
|
if (amd_vendor && __get_cpuid_max(0x80000000u, nullptr) >= 0x8000001eu) {
|
|
__cpuid(1, va, vb, vc, vd);
|
|
const unsigned base_family = (va >> 8) & 15u;
|
|
const unsigned family = base_family == 15u ?
|
|
base_family + ((va >> 20) & 255u) : base_family;
|
|
__cpuid(0x80000001u, va, vb, vc, vd);
|
|
has_amd_topology = family >= 0x17u && (vc & (1u << 22)) != 0;
|
|
}
|
|
// Even when CPUID is unusable retain a valid OS affinity mask for
|
|
// the explicit *logical-only* trial; that trial is not physical proof.
|
|
topology_allowed = original;
|
|
topology_allowed_valid = true;
|
|
if (!has_x2apic && !has_amd_topology) {
|
|
// This is not yet a failure: the kernel may still expose authoritative
|
|
// SMT groups even when neither architectural CPUID leaf is usable.
|
|
std::printf("EDEN_PS5_CPU_CPUID unavailable=1 trying_kernel_topology=1\n");
|
|
}
|
|
// The PS5's FreeBSD scheduler may expose genuine SMT sibling groups
|
|
// even when CPUID per-affinity yields bogus 13-core/one-core readings.
|
|
// Probe is read-only and bounded; failure preserves the existing fallback.
|
|
std::array<int, 64> os_cpu_core{};
|
|
os_cpu_core.fill(-1);
|
|
bool os_topology_valid = false;
|
|
// kern.sched.topology_spec is available on older FreeBSD. ULE exposed
|
|
// kern.sched.ule.topology_spec while reworking scheduler sysctls in
|
|
// January 2026; the original alias was later reintroduced upstream.
|
|
// Sony's fork may expose neither. Probe both READ-ONLY without creating
|
|
// an artificial SMT map if the scheduler hides sibling relationships.
|
|
size_t topology_length = 0;
|
|
int os_topology_errno = 0;
|
|
// A single errno for both probes hid which sysctl the Sony kernel
|
|
// actually exports. Keep the historical fields and add per-OID errors.
|
|
std::array<int, 2> oid_errno{};
|
|
const char* os_topology_source = "none";
|
|
constexpr std::array oids{"kern.sched.topology_spec",
|
|
"kern.sched.ule.topology_spec"};
|
|
for (size_t index = 0; index < oids.size(); ++index) {
|
|
const char* oid = oids[index];
|
|
size_t length = 0;
|
|
if (sysctlbyname(oid, nullptr, &length, nullptr, 0) != 0) {
|
|
oid_errno[index] = os_topology_errno = errno;
|
|
continue;
|
|
}
|
|
if (length == 0 || length > 65536) {
|
|
oid_errno[index] = os_topology_errno = EOVERFLOW;
|
|
continue;
|
|
}
|
|
std::string topology_xml(length, '\0');
|
|
if (sysctlbyname(oid, topology_xml.data(), &length, nullptr, 0) != 0 ||
|
|
length > topology_xml.size()) {
|
|
oid_errno[index] = os_topology_errno = errno ? errno : EOVERFLOW;
|
|
continue;
|
|
}
|
|
topology_length = length;
|
|
if (::Eden::CpuTopology::ParseSmtGroups(
|
|
std::string_view{topology_xml.data(), length}, allowed_mask, os_cpu_core)) {
|
|
os_topology_valid = true;
|
|
os_topology_source = oid;
|
|
os_topology_errno = 0;
|
|
break;
|
|
}
|
|
// A present but invalid scheduler map cannot justify SMT pinning.
|
|
// A second, independent ULE OID may still provide a valid map.
|
|
oid_errno[index] = os_topology_errno = EINVAL;
|
|
}
|
|
std::printf("EDEN_PS5_CPU_OS_TOPOLOGY available=%u bytes=%zu errno=%d "
|
|
"source=%s legacy_errno=%d ule_errno=%d\n",
|
|
unsigned(os_topology_valid), topology_length, os_topology_errno,
|
|
os_topology_source, oid_errno[0], oid_errno[1]);
|
|
std::array<int, 64> amd_cpu_core{}, x2_cpu_core{};
|
|
amd_cpu_core.fill(-1);
|
|
x2_cpu_core.fill(-1);
|
|
bool changed = false;
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu) {
|
|
if (!CPU_ISSET(cpu, &original)) continue;
|
|
cpuset_t one{};
|
|
CPU_SET(cpu, &one);
|
|
if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &one)) break;
|
|
changed = true;
|
|
// Affinity changes may take effect only after a scheduling point.
|
|
sched_yield();
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
cpuset_t verified{};
|
|
if (cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &verified) ||
|
|
std::memcmp(&one, &verified, 8)) break;
|
|
unsigned a{}, b{}, c{}, d{};
|
|
// Probe both CPUID representations on the pinned logical CPU.
|
|
// PS5 13.60 reports AMD CoreIds distinct for all 13 logical CPUs
|
|
// despite advertising SMT: reject those impossible physical groups.
|
|
if (has_amd_topology) {
|
|
__cpuid_count(0x8000001eu, 0, a, b, c, d);
|
|
const unsigned threads_per_core = ((b >> 8) & 255u) + 1u;
|
|
if (threads_per_core >= 1u && threads_per_core <= 8u)
|
|
amd_cpu_core[cpu] = static_cast<int>(((c & 255u) << 8) | (b & 255u));
|
|
}
|
|
if (has_x2apic) {
|
|
__cpuid_count(0xb, 0, a, b, c, d);
|
|
if (b && ((c >> 8) & 255u) == 1u && (a & 31u) < 16u)
|
|
x2_cpu_core[cpu] = static_cast<int>(d >> (a & 31u));
|
|
}
|
|
}
|
|
if (changed) {
|
|
cpuset_t restored{};
|
|
if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &original) ||
|
|
cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &restored) ||
|
|
std::memcmp(&original, &restored, 8))
|
|
throw std::runtime_error("Cannot restore topology-probe thread affinity");
|
|
}
|
|
// A valid physical grouping must classify EVERY allowed CPU and identify
|
|
// between five and eight distinct real cores on this Zen 2 PS5. Never
|
|
// choose whichever CPUID leaf looks fastest or assume logical IDs are
|
|
// physical cores. Only use the x2APIC fallback when AMD is implausible.
|
|
const auto summarize = [&](const std::array<int, 64>& ids) {
|
|
std::array<int, 64> distinct{};
|
|
unsigned classified = 0, count = 0;
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu) {
|
|
if (!CPU_ISSET(cpu, &original) || ids[cpu] < 0) continue;
|
|
++classified;
|
|
if (std::find(distinct.begin(), distinct.begin() + count, ids[cpu]) ==
|
|
distinct.begin() + count)
|
|
distinct[count++] = ids[cpu];
|
|
}
|
|
return std::pair{classified, count};
|
|
};
|
|
const auto [amd_classified, amd_distinct] = summarize(amd_cpu_core);
|
|
const auto [x2_classified, x2_distinct] = summarize(x2_cpu_core);
|
|
const bool amd_plausible = has_amd_topology && amd_classified == allowed_count &&
|
|
amd_distinct >= 5 && amd_distinct <= 8;
|
|
const bool x2_plausible = has_x2apic && x2_classified == allowed_count &&
|
|
x2_distinct >= 5 && x2_distinct <= 8;
|
|
const std::array<int, 64>* physical_source = nullptr;
|
|
const char* source_name = "none";
|
|
if (os_topology_valid) {
|
|
physical_source = &os_cpu_core;
|
|
source_name = "freebsd_sched";
|
|
} else if (amd_plausible) {
|
|
physical_source = &amd_cpu_core;
|
|
source_name = "amd_ext";
|
|
} else if (x2_plausible) {
|
|
physical_source = &x2_cpu_core;
|
|
source_name = has_amd_topology ? "x2apic_fallback" : "x2apic";
|
|
}
|
|
std::printf("EDEN_PS5_CPU_TOPOLOGY_SOURCES amd=%u/%u x2apic=%u/%u selected=%s\n",
|
|
amd_classified, amd_distinct, x2_classified, x2_distinct, source_name);
|
|
if (!physical_source) {
|
|
const unsigned observed = has_amd_topology ? amd_distinct : x2_distinct;
|
|
const unsigned classified = has_amd_topology ? amd_classified : x2_classified;
|
|
if (observed > 8)
|
|
std::printf("EDEN_PS5_CPU_TOPOLOGY_REJECT reason=more_than_8_physical_ids observed=%u\n",
|
|
observed);
|
|
std::printf("EDEN_PS5_CPU_PHYSICAL_SUMMARY classified=%u of=%u distinct_physical=%u valid=0\n",
|
|
classified, allowed_count, observed);
|
|
std::printf("EDEN_WORKER_TOPOLOGY ready=0 distinct_cores=0 cpus=0,0,0,0,0 amd_ext=%u x2apic=%u\n",
|
|
unsigned(has_amd_topology), unsigned(has_x2apic));
|
|
return;
|
|
}
|
|
std::array<unsigned, 5> cores{};
|
|
unsigned count = 0;
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu) {
|
|
if (!CPU_ISSET(cpu, &original)) continue;
|
|
const int core = (*physical_source)[cpu];
|
|
if (core < 0) continue;
|
|
cpu_core[cpu] = core;
|
|
if (count < cores.size() &&
|
|
std::find(cores.begin(), cores.begin() + count,
|
|
static_cast<unsigned>(core)) == cores.begin() + count) {
|
|
cores[count] = static_cast<unsigned>(core);
|
|
worker_cpus[count++] = cpu;
|
|
}
|
|
}
|
|
std::array<int, 64> physical_ids{};
|
|
std::array<std::uint64_t, 64> physical_masks{};
|
|
unsigned physical_count = 0, classified = 0;
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu) {
|
|
if (!CPU_ISSET(cpu, &original) || cpu_core[cpu] < 0) continue;
|
|
++classified;
|
|
unsigned group = 0;
|
|
while (group < physical_count && physical_ids[group] != cpu_core[cpu]) ++group;
|
|
if (group == physical_count) physical_ids[physical_count++] = cpu_core[cpu];
|
|
physical_masks[group] |= std::uint64_t{1} << cpu;
|
|
}
|
|
const bool physical_ids_plausible = physical_count >= 5 && physical_count <= 8 &&
|
|
classified == allowed_count;
|
|
worker_topology_ready = count == cores.size() && physical_ids_plausible;
|
|
std::printf("EDEN_PS5_CPU_PHYSICAL_SUMMARY classified=%u of=%u distinct_physical=%u valid=%u\n",
|
|
classified, allowed_count, physical_count, unsigned(physical_ids_plausible));
|
|
if (worker_topology_ready) {
|
|
for (unsigned group = 0; group < physical_count; ++group)
|
|
std::printf("EDEN_PS5_CPU_PHYSICAL core_id=%d logical_mask=0x%llx\n",
|
|
physical_ids[group], static_cast<unsigned long long>(physical_masks[group]));
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu) {
|
|
if (!CPU_ISSET(cpu, &original) || cpu_core[cpu] < 0) continue;
|
|
// Four guest CPU owners AND the GPU owner each need an entire
|
|
// physical core, not merely one SMT thread. Previous code excluded
|
|
// only the first three guest cores, admitting siblings of the
|
|
// fourth guest core and GPU to secondary shader/IO workers.
|
|
const bool primary_core =
|
|
std::find(cores.begin(), cores.end(), static_cast<unsigned>(cpu_core[cpu])) != cores.end();
|
|
if (!primary_core) secondary_cpus |= 1ULL << cpu;
|
|
}
|
|
std::printf("EDEN_WORKER_SECONDARY mask=%llx\n", secondary_cpus);
|
|
// Publish only after all CPU IDs and affinity/topology checks complete.
|
|
// Logical-only experimental placement deliberately never publishes
|
|
// this snapshot: a logical CPU ID is not physical-core evidence.
|
|
std::uint64_t verified_mask = 0;
|
|
for (unsigned cpu : worker_cpus)
|
|
if (cpu < 64) verified_mask |= std::uint64_t{1} << cpu;
|
|
verified_physical_worker_mask.store(verified_mask, std::memory_order_release);
|
|
// CPU slots withheld from the shader pool are NOT necessarily OS
|
|
// reserved: protect hot guest JIT cores from SMT sibling contention.
|
|
const std::uint64_t excluded = allowed_mask & ~(verified_mask | secondary_cpus);
|
|
std::uint64_t guest_smt_protected = 0;
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu) {
|
|
if (!(excluded & (std::uint64_t{1} << cpu))) continue;
|
|
// Preserve the legacy log key but protect SMT siblings of all
|
|
// five hot owners, including CPUCore_3 and the GPU worker.
|
|
const bool primary_sibling =
|
|
std::find(cores.begin(), cores.end(), static_cast<unsigned>(cpu_core[cpu])) != cores.end();
|
|
if (primary_sibling) guest_smt_protected |= std::uint64_t{1} << cpu;
|
|
}
|
|
std::printf("EDEN_PS5_CPU_SPLIT allowed=0x%llx primary=0x%llx secondary=0x%llx "
|
|
"guest_smt_protected=0x%llx other_allowed=0x%llx\n",
|
|
static_cast<unsigned long long>(allowed_mask),
|
|
static_cast<unsigned long long>(verified_mask),
|
|
secondary_cpus,
|
|
static_cast<unsigned long long>(guest_smt_protected),
|
|
static_cast<unsigned long long>(excluded & ~guest_smt_protected));
|
|
}
|
|
std::printf("EDEN_WORKER_TOPOLOGY ready=%d distinct_cores=%u cpus=%u,%u,%u,%u,%u amd_ext=%u x2apic=%u\n",
|
|
worker_topology_ready, count, worker_cpus[0], worker_cpus[1], worker_cpus[2],
|
|
worker_cpus[3], worker_cpus[4], unsigned(has_amd_topology), unsigned(has_x2apic));
|
|
}
|
|
// Explicit logical-CPU A/B trial when x2APIC topology is unverifiable.
|
|
// Different OS logical CPU IDs do NOT establish distinct physical cores.
|
|
void EnableExperimentalLogicalPlacementImpl() {
|
|
// No OS scheduler SMT groups and no trustworthy physical topology:
|
|
// leave all 13 permitted logical processors available to the kernel
|
|
// scheduler. Isolating guests on guessed SMT pairs previously restricted
|
|
// all renderer, audio, filesystem and service workers to only three CPUs.
|
|
// A successful cpuset pin is not evidence of an independent physical core.
|
|
// Do not pin anything until physical siblings can be proven.
|
|
std::uint64_t allowed_mask = 0;
|
|
if (topology_allowed_valid) {
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu)
|
|
if (CPU_ISSET(cpu, &topology_allowed))
|
|
allowed_mask |= std::uint64_t{1} << cpu;
|
|
}
|
|
std::printf("EDEN_EXPERIMENT_CPU result=skipped reason=%s allowed_mask=0x%llx physical_verified=0\n",
|
|
worker_topology_ready ? "physical_topology_already_available" :
|
|
topology_allowed_valid ? "physical_topology_unverified" : "affinity_unavailable",
|
|
static_cast<unsigned long long>(allowed_mask));
|
|
}
|
|
|
|
void PlaceSecondary(const char* name) {
|
|
if (!placement_secondary.load(std::memory_order_relaxed) || !secondary_cpus) return;
|
|
cpuset_t mask{};
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu)
|
|
if (secondary_cpus & (1ULL << cpu)) CPU_SET(cpu, &mask);
|
|
const int result = cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &mask);
|
|
if (secondary_reports.fetch_add(1, std::memory_order_relaxed) < 64)
|
|
std::printf("EDEN_WORKER_PLACED name=%s mask=%llx error=%d\n", name, secondary_cpus, result ? errno : 0);
|
|
}
|
|
void PinWorker(unsigned index, cpuset_t& affinity) {
|
|
if (!worker_topology_ready || index >= worker_cpus.size() ||
|
|
!CPU_ISSET(worker_cpus[index], topology_allowed_valid ? &topology_allowed : &affinity)) return;
|
|
cpuset_t one{}, verified{};
|
|
CPU_SET(worker_cpus[index], &one);
|
|
if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &one)) {
|
|
std::printf("EDEN_WORKER_PIN name=%s error=%d\n", names[index], errno);
|
|
return;
|
|
}
|
|
if (cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &verified) ||
|
|
std::memcmp(&one, &verified, 8)) {
|
|
if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &affinity))
|
|
throw std::runtime_error("Cannot restore worker affinity");
|
|
return;
|
|
}
|
|
affinity = verified;
|
|
std::printf("EDEN_WORKER_PIN name=%s cpu=%u verified=1\n", names[index], worker_cpus[index]);
|
|
}
|
|
#endif
|
|
bool cpu_clocks_valid = false;
|
|
bool owner_cpu_clock_valid = false;
|
|
std::atomic<unsigned> sample_epoch{};
|
|
struct CpuSample {
|
|
unsigned epoch = ~0u;
|
|
long long mono_ns = 0, cpu_ns = -ENOTSUP;
|
|
unsigned long long thread = 0, pc = 0;
|
|
unsigned svc = ~0u, fpcr = 0;
|
|
unsigned long long compilations = 0, compile_ns = 0;
|
|
};
|
|
std::array<CpuSample, 4> cpu_samples;
|
|
// Read/written only by the worker owning this core; samples use workers_mutex.
|
|
std::array<unsigned, 4> sampled_epoch{~0u, ~0u, ~0u, ~0u};
|
|
clockid_t process_clock{};
|
|
int CurrentCpuClock(clockid_t* clock, bool process = false) {
|
|
#ifdef PS5_NATIVE
|
|
// Native titles must use the imported kernel APIs. Firmware rejects direct
|
|
// syscalls from the title. Qualification below rejects unusable CPU clocks.
|
|
if (process) { *clock = CLOCK_PROCESS_CPUTIME_ID; return 0; }
|
|
return pthread_getcpuclockid(pthread_self(), clock);
|
|
#else
|
|
return process ? clock_getcpuclockid(0, clock) : pthread_getcpuclockid(pthread_self(), clock);
|
|
#endif
|
|
}
|
|
long long ClockNs(clockid_t clock) {
|
|
timespec time{};
|
|
if (clock_gettime(clock, &time) != 0) return -errno;
|
|
return static_cast<long long>(time.tv_sec) * 1'000'000'000 + time.tv_nsec;
|
|
}
|
|
void CheckCpuClocks() {
|
|
clockid_t worker_clock{};
|
|
int worker_error = ENOENT;
|
|
const int process_error = CurrentCpuClock(&process_clock, true);
|
|
long long owner_begin = -1, owner_busy = -1, owner_idle = -1;
|
|
std::binary_semaphore ready{0}, proceed{0};
|
|
std::jthread worker([&] {
|
|
worker_error = CurrentCpuClock(&worker_clock);
|
|
ready.release();
|
|
proceed.acquire();
|
|
owner_begin = ClockNs(CLOCK_THREAD_CPUTIME_ID);
|
|
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(20);
|
|
while (std::chrono::steady_clock::now() < end) std::atomic_signal_fence(std::memory_order_seq_cst);
|
|
owner_busy = ClockNs(CLOCK_THREAD_CPUTIME_ID);
|
|
ready.release();
|
|
proceed.acquire();
|
|
owner_idle = ClockNs(CLOCK_THREAD_CPUTIME_ID);
|
|
});
|
|
ready.acquire();
|
|
const auto cpu_begin = worker_error ? -1 : ClockNs(worker_clock);
|
|
proceed.release();
|
|
ready.acquire();
|
|
const auto cpu_busy = worker_error ? -1 : ClockNs(worker_clock);
|
|
const auto process_idle = process_error ? -1 : ClockNs(process_clock);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
const auto cpu_idle = worker_error ? -1 : ClockNs(worker_clock);
|
|
const auto process_end = process_error ? -1 : ClockNs(process_clock);
|
|
proceed.release();
|
|
worker.join();
|
|
const auto own_busy = owner_busy - owner_begin;
|
|
const auto own_idle = owner_idle - owner_busy;
|
|
owner_cpu_clock_valid = owner_begin >= 0 && own_busy >= 5'000'000 &&
|
|
own_busy < 30'000'000 && own_idle >= 0 && own_idle < own_busy / 4 + 1'000'000;
|
|
std::printf("EDEN_PERF_OWNER_CLOCK_CHECK valid=%d busy_ns=%lld idle_ns=%lld\n",
|
|
owner_cpu_clock_valid, own_busy, own_idle);
|
|
const auto busy = cpu_busy - cpu_begin;
|
|
const auto idle = cpu_idle - cpu_busy;
|
|
const auto process_sleep = process_end - process_idle;
|
|
cpu_clocks_valid = !worker_error && !process_error && cpu_begin >= 0 && process_idle >= 0 &&
|
|
busy >= 1'000'000 && idle >= 0 && idle < busy / 4 + 1'000'000 &&
|
|
process_sleep >= 0 && process_sleep < 6'000'000;
|
|
std::printf("EDEN_PERF_CPU_CLOCK_CHECK valid=%d worker_error=%d process_error=%d "
|
|
"worker_clock=%d process_clock=%d busy_ns=%lld idle_ns=%lld process_sleep_ns=%lld\n",
|
|
cpu_clocks_valid, worker_error, process_error, int(worker_clock), int(process_clock),
|
|
busy, idle, process_sleep);
|
|
}
|
|
}
|
|
|
|
void EnableExperimentalLogicalPlacement() {
|
|
#ifdef PS5_NATIVE
|
|
EnableExperimentalLogicalPlacementImpl();
|
|
#endif
|
|
}
|
|
|
|
std::uint64_t PinnedWorkerMask() noexcept {
|
|
#ifdef PS5_NATIVE
|
|
// Atomic generation snapshot: PS5 pipeline workers are constructed on
|
|
// different threads, sometimes as the previous title is shutting down.
|
|
// Unknown / logical-only topology returns zero and keeps admission
|
|
// conservative, rather than double-counting unverified host cores.
|
|
return verified_physical_worker_mask.load(std::memory_order_acquire);
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
std::uint64_t VerifiedSecondaryPlacementMask() noexcept {
|
|
#ifdef PS5_NATIVE
|
|
return verified_secondary_placement_mask.load(std::memory_order_acquire);
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
void ProbeSchedulerOnce(const char* role) noexcept {
|
|
#ifdef PS5_NATIVE
|
|
// Console evidence shows equal-priority FIFO workers in the release;
|
|
// one always-runnable native worker can starve same-priority peers.
|
|
// Request RR for both native release and diagnostic builds, keeping
|
|
// priority 700, affinity and a fail-open FIFO fallback on refusal.
|
|
// One attempt per OS thread; never call scheduler syscalls per frame.
|
|
static thread_local bool reported = false;
|
|
if (reported) return;
|
|
reported = true;
|
|
int policy = -1;
|
|
sched_param priority{};
|
|
const int sched_error = pthread_getschedparam(pthread_self(), &policy, &priority);
|
|
const int original_policy = policy;
|
|
int rr_attempted = 0;
|
|
int rr_error = 0;
|
|
int rr_verified = 0;
|
|
if (!sched_error && policy == SCHED_FIFO && priority.sched_priority == 700) {
|
|
rr_attempted = 1;
|
|
// Keep the native priority and all affinity masks intact.
|
|
rr_error = pthread_setschedparam(pthread_self(), SCHED_RR, &priority);
|
|
if (!rr_error) {
|
|
sched_param observed_priority{};
|
|
int observed_policy = -1;
|
|
const int readback_error = pthread_getschedparam(
|
|
pthread_self(), &observed_policy, &observed_priority);
|
|
rr_verified = readback_error == 0 && observed_policy == SCHED_RR &&
|
|
observed_priority.sched_priority == priority.sched_priority;
|
|
rr_error = readback_error;
|
|
if (!readback_error) {
|
|
policy = observed_policy;
|
|
priority = observed_priority;
|
|
}
|
|
}
|
|
}
|
|
cpuset_t allowed{};
|
|
const int affinity_rc = cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1,
|
|
sizeof(unsigned long long), &allowed);
|
|
const int affinity_error = affinity_rc ? errno : 0;
|
|
unsigned long long mask = 0;
|
|
if (!affinity_rc) {
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu)
|
|
if (CPU_ISSET(cpu, &allowed)) mask |= 1ULL << cpu;
|
|
}
|
|
const char* const policy_name =
|
|
sched_error ? "unknown" : policy == SCHED_FIFO ? "FIFO" :
|
|
policy == SCHED_RR ? "RR" : "OTHER";
|
|
std::printf("EDEN_PS5_THREAD_SCHED role=%s sched_error=%d policy=%s(%d) priority=%d "
|
|
"affinity_error=%d allowed_mask=0x%llx original_policy=%d "
|
|
"rr_attempted=%d rr_error=%d rr_verified=%d\n",
|
|
role, sched_error, policy_name, policy, priority.sched_priority,
|
|
affinity_error, mask, original_policy, rr_attempted, rr_error, rr_verified);
|
|
#else
|
|
(void)role;
|
|
#endif
|
|
}
|
|
|
|
void SetSecondaryPlacement(bool enabled) {
|
|
#ifdef PS5_NATIVE
|
|
// Clear previous title/placement evidence BEFORE trying to repin. A
|
|
// success return from setaffinity alone is not a verified OS mask.
|
|
verified_secondary_placement_mask.store(0, std::memory_order_release);
|
|
placement_secondary.store(enabled, std::memory_order_relaxed);
|
|
if (!secondary_cpus || !topology_allowed_valid) return;
|
|
cpuset_t mask{};
|
|
if (enabled) {
|
|
for (unsigned cpu = 0; cpu < 64; ++cpu)
|
|
if (secondary_cpus & (1ULL << cpu)) CPU_SET(cpu, &mask);
|
|
} else {
|
|
mask = topology_allowed;
|
|
}
|
|
const int result = cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &mask);
|
|
cpuset_t actual{};
|
|
const bool verified = enabled && result == 0 &&
|
|
cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, 8, &actual) == 0 &&
|
|
std::memcmp(&mask, &actual, 8) == 0;
|
|
if (verified)
|
|
verified_secondary_placement_mask.store(secondary_cpus, std::memory_order_release);
|
|
std::printf("EDEN_WORKER_INHERIT enabled=%d mask=%llx error=%d verified=%u\n", enabled,
|
|
enabled ? secondary_cpus : 0ULL, result ? errno : 0, unsigned(verified));
|
|
#else
|
|
(void)enabled;
|
|
#endif
|
|
}
|
|
|
|
void SampleGpuFrame(unsigned frame) {
|
|
std::printf("EDEN_PERF_GPU_FRAME frame=%u mono_ns=%lld cpu_ns=%lld\n", frame,
|
|
ClockNs(CLOCK_MONOTONIC),
|
|
owner_cpu_clock_valid ? ClockNs(CLOCK_THREAD_CPUTIME_ID) : -static_cast<long long>(ENOTSUP));
|
|
}
|
|
|
|
extern "C" void ps5_opengl_heap_snapshot(const char* phase, unsigned iteration);
|
|
extern "C" unsigned eden_heap_arenas_created(void) __attribute__((weak));
|
|
extern "C" std::size_t eden_heap_committed(void) __attribute__((weak));
|
|
extern "C" std::size_t eden_heap_tcache_held(void) __attribute__((weak));
|
|
extern "C" std::size_t eden_heap_large_held(unsigned* blocks) __attribute__((weak));
|
|
extern "C" std::size_t eden_heap_large_physical_held(void) __attribute__((weak));
|
|
#ifdef PS5_NATIVE
|
|
extern "C" std::int64_t sceKernelGetDirectMemorySize();
|
|
extern "C" std::int32_t sceKernelAvailableDirectMemorySize(std::int64_t, std::int64_t, std::size_t, std::int64_t*,
|
|
std::size_t*);
|
|
// The allocated region at or after an offset (flag 1: find the next one).
|
|
struct DirectMemoryRegion { std::int64_t start; std::int64_t end; std::int32_t type; };
|
|
extern "C" std::int32_t sceKernelDirectMemoryQuery(std::int64_t, int, DirectMemoryRegion*, std::size_t);
|
|
#endif
|
|
|
|
// Query the actual contiguous direct-memory headroom available at guest
|
|
// launch. Fail closed (no expanded JIT) when firmware cannot report it.
|
|
bool QueryLargestDirectMemoryBlock(std::size_t* largest) noexcept {
|
|
if (!largest) return false;
|
|
*largest = 0;
|
|
#ifdef PS5_NATIVE
|
|
const std::int64_t total = sceKernelGetDirectMemorySize();
|
|
if (total <= 0) return false;
|
|
std::int64_t start = -1;
|
|
std::size_t observed = 0;
|
|
if (sceKernelAvailableDirectMemorySize(0, total, 0x4000,
|
|
&start, &observed) != 0 ||
|
|
!::Eden::DirectPool::IsKernelFreeSpanValid(total, start, observed))
|
|
return false;
|
|
*largest = observed;
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#ifdef PS5_NATIVE
|
|
// Native process-global observations must be reset between games: the next
|
|
// title can reserve a completely different amount of physical GPU memory.
|
|
static std::atomic<unsigned long long> largest_free_block{0};
|
|
static std::atomic<long long> checked_ns{0};
|
|
static std::atomic<bool> query_in_flight{false};
|
|
static std::atomic<unsigned> consecutive_failures{0};
|
|
static std::atomic<bool> has_valid_sample{false};
|
|
#endif
|
|
|
|
void ResetDirectMemoryProbeForTitle() noexcept {
|
|
#ifdef PS5_NATIVE
|
|
// The caller must have joined all GPU/renderer workers. Refuse to erase
|
|
// a still-running kernel probe, which could publish stale data afterward.
|
|
if (query_in_flight.load(std::memory_order_acquire)) std::abort();
|
|
has_valid_sample.store(false, std::memory_order_release);
|
|
consecutive_failures.store(0, std::memory_order_relaxed);
|
|
largest_free_block.store(0, std::memory_order_relaxed);
|
|
graphics_memory_short.store(true, std::memory_order_relaxed);
|
|
// Zero timestamp forces the new title's first probe immediately, rather
|
|
// than reusing the prior title's result for up to 100 ms.
|
|
checked_ns.store(0, std::memory_order_release);
|
|
#endif
|
|
}
|
|
|
|
#ifdef PS5_NATIVE
|
|
// The largest free block of direct memory is what the next graphics allocation needs.
|
|
// 100 ms minimum (250 ms only with >=4 GiB confirmed free);
|
|
// called on the GPU thread by the texture collector.
|
|
static void RefreshFreeMemory() {
|
|
const long long now = NowNs();
|
|
long long last = checked_ns.load(std::memory_order_relaxed);
|
|
if (last != 0 && (now <= last || now - last < 100'000'000))
|
|
return;
|
|
// Do not add any new atomics to the common per-frame fast-return path.
|
|
// At most once per 100 ms, a confirmed >=4-GiB contiguous free block
|
|
// permits 250-ms polling instead of blocking the GPU owner every 100 ms.
|
|
// Low memory, first probe, or an error retains the original fast cadence.
|
|
if (last != 0 &&
|
|
now - last < ::Eden::DirectPool::ProbeIntervalNs(
|
|
has_valid_sample.load(std::memory_order_acquire),
|
|
consecutive_failures.load(std::memory_order_relaxed),
|
|
largest_free_block.load(std::memory_order_relaxed)))
|
|
return;
|
|
// Two GPU/cache clients can hit this on the same refresh boundary.
|
|
// The old separate load/store let BOTH perform a synchronous kernel
|
|
// largest-free-range query, and counted two concurrent errors as two
|
|
// consecutive misses. Elect exactly one refresh owner every >=100ms.
|
|
if (!checked_ns.compare_exchange_strong(last, now, std::memory_order_acq_rel,
|
|
std::memory_order_relaxed))
|
|
return;
|
|
// If a kernel query itself takes >100 ms, a later timestamp claimant
|
|
// must not overtake it and publish a newer measurement before the older
|
|
// query eventually returns. One in-flight query at a time, without
|
|
// blocking any renderer caller on the query owner's kernel syscall.
|
|
if (query_in_flight.exchange(true, std::memory_order_acq_rel))
|
|
return;
|
|
#if defined(EDEN_DEV_PROFILE)
|
|
const long long query_start_ns = ClockNs(CLOCK_MONOTONIC);
|
|
#endif
|
|
std::int64_t start = 0;
|
|
std::size_t largest = 0;
|
|
// The physical direct-memory address-space bound is constant for this
|
|
// PS5 process. Re-reading it at each 100 ms GPU pressure check wastes
|
|
// an independent kernel call. Only cache a successful positive result:
|
|
// startup errors still retry and remain fail-closed as before.
|
|
// This variable is accessed exclusively by the elected single-flight
|
|
// query owner above; all other renderer callers return before it.
|
|
static std::int64_t cached_direct_total = 0;
|
|
if (cached_direct_total <= 0) {
|
|
const std::int64_t observed_total = sceKernelGetDirectMemorySize();
|
|
if (observed_total > 0) cached_direct_total = observed_total;
|
|
}
|
|
const bool known = cached_direct_total > 0 &&
|
|
sceKernelAvailableDirectMemorySize(0, cached_direct_total, 0x4000, &start, &largest) == 0 &&
|
|
::Eden::DirectPool::IsKernelFreeSpanValid(cached_direct_total, start, largest);
|
|
// Unknown is not the same as an observed low-memory condition. A single
|
|
// transient failed query previously flipped memory_short to true and
|
|
// immediately enabled expensive dirty-texture download/eviction. Keep
|
|
// the last kernel-confirmed headroom for ONE missed 100ms refresh only.
|
|
// First-ever failure and consecutive failures fail safely to "short".
|
|
if (known) {
|
|
largest_free_block.store(largest, std::memory_order_relaxed);
|
|
graphics_memory_short.store(largest < kShortMemory, std::memory_order_relaxed);
|
|
consecutive_failures.store(0, std::memory_order_relaxed);
|
|
has_valid_sample.store(true, std::memory_order_release);
|
|
} else {
|
|
const unsigned failed = consecutive_failures.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
if (!has_valid_sample.load(std::memory_order_acquire) || failed >= 2) {
|
|
largest_free_block.store(0, std::memory_order_relaxed);
|
|
graphics_memory_short.store(true, std::memory_order_relaxed);
|
|
}
|
|
// Only two lines per failure streak, no per-frame or allocator logging.
|
|
static std::atomic<unsigned> reported{0};
|
|
if ((failed == 1 || failed == 2) &&
|
|
reported.fetch_add(1, std::memory_order_relaxed) < 16)
|
|
std::printf("EDEN_PS5_DMEM_PROBE_FAILED consecutive=%u fallback=%s\n", failed,
|
|
failed == 1 && has_valid_sample.load(std::memory_order_relaxed)
|
|
? "last_confirmed" : "conservative");
|
|
}
|
|
#if defined(EDEN_DEV_PROFILE)
|
|
const long long query_end_ns = ClockNs(CLOCK_MONOTONIC);
|
|
if (query_start_ns > 0 && query_end_ns > query_start_ns &&
|
|
query_end_ns - query_start_ns >= 2'000'000) {
|
|
static std::atomic<unsigned> slow_queries_logged{0};
|
|
if (slow_queries_logged.fetch_add(1, std::memory_order_relaxed) < 16)
|
|
std::printf("EDEN_PS5_DMEM_PROBE_SLOW latency_ns=%lld known=%u\n",
|
|
query_end_ns - query_start_ns, unsigned(known));
|
|
}
|
|
#endif
|
|
query_in_flight.store(false, std::memory_order_release);
|
|
}
|
|
static bool GraphicsMemoryShort() {
|
|
RefreshFreeMemory();
|
|
return graphics_memory_short.load(std::memory_order_relaxed);
|
|
}
|
|
// What the caches' "memory in use" is measured against (performance.h, graphics_memory_free).
|
|
static unsigned long long GraphicsMemoryFree() {
|
|
RefreshFreeMemory();
|
|
return largest_free_block.load(std::memory_order_relaxed);
|
|
}
|
|
[[maybe_unused]] static const bool graphics_memory_probe_installed =
|
|
(graphics_memory_probe.store(&GraphicsMemoryShort, std::memory_order_relaxed),
|
|
graphics_memory_free.store(&GraphicsMemoryFree, std::memory_order_relaxed), true);
|
|
#endif
|
|
|
|
#ifdef EDEN_DEV_PROFILE
|
|
namespace {
|
|
// Fixed-size, owned-name counters: no per-request allocator/mutex contention
|
|
// between the four guest CPUs during the development all-on profiling pass.
|
|
// Shipping neither constructs nor retains this ~400 KiB profiling table.
|
|
HleCounters hle_calls;
|
|
} // namespace
|
|
#endif
|
|
|
|
void RecordHle(const char* service, unsigned command, long long ns) {
|
|
#ifdef EDEN_DEV_PROFILE
|
|
hle_calls.Record(service, command, ns);
|
|
#else
|
|
(void)service;
|
|
(void)command;
|
|
(void)ns;
|
|
#endif
|
|
}
|
|
|
|
// Report true JIT ownership once at meaningful lifecycle stages; this
|
|
// does not enumerate all kernel direct-memory ranges or run on a frame.
|
|
void ReportJitCodeState(const char* phase) {
|
|
#ifdef PS5_NATIVE
|
|
std::size_t reserved = 0, committed = 0;
|
|
::Common::SparseJitUsage(&reserved, &committed);
|
|
std::printf("EDEN_JIT_MEMORY phase=%s sparse_reserved=%zu sparse_committed=%zu dense_direct=%zu\n",
|
|
phase, reserved, committed, ::Common::DenseJitDirectBytes());
|
|
#else
|
|
(void)phase;
|
|
#endif
|
|
}
|
|
|
|
// A Sony region enumeration may cost up to 8192 synchronous kernel calls.
|
|
// Do not make every menu -> game -> menu cycle pay for developer diagnostics.
|
|
// Explicit opt-in is checked once at title launch; never from a frame callback.
|
|
static std::atomic<bool> direct_region_scan_enabled{false};
|
|
void SetDirectMemoryRegionScanEnabled(bool enabled) noexcept {
|
|
direct_region_scan_enabled.store(enabled, std::memory_order_release);
|
|
}
|
|
// GPU frame reports never enumerate kernel direct-memory regions.
|
|
void ReportDirectMemoryState(const char* phase) {
|
|
#ifdef PS5_NATIVE
|
|
const std::int64_t total = sceKernelGetDirectMemorySize();
|
|
if (total <= 0) {
|
|
std::printf("EDEN_MEMORY_LAYOUT phase=%s status=unavailable\n", phase);
|
|
return;
|
|
}
|
|
std::int64_t largest_start = -1;
|
|
std::size_t largest = 0;
|
|
const int largest_rc = sceKernelAvailableDirectMemorySize(
|
|
0, total, 0x4000, &largest_start, &largest);
|
|
const bool largest_valid = largest_rc == 0 &&
|
|
::Eden::DirectPool::IsKernelFreeSpanValid(total, largest_start, largest);
|
|
|
|
// An early kernel stop can mean EOF or error; never equate structurally
|
|
// valid records with a complete scan. Even a valid free_upper is only
|
|
// an upper bound, NOT allocatable RAM or a GPU/JIT admission value.
|
|
::Eden::DirectPool::RegionScan scan{total};
|
|
int query_rc = 0;
|
|
const bool scan_requested = direct_region_scan_enabled.load(std::memory_order_acquire);
|
|
// 4=disabled by default; owner ledger and largest free are still reported.
|
|
unsigned scan_stop = scan_requested ? 0u : 4u;
|
|
if (scan_requested) {
|
|
// 0=end-of-extent, 1=query-stop, 2=invalid, 3=record-cap.
|
|
while (scan.cursor < total && scan.regions < 8192) {
|
|
DirectMemoryRegion region{};
|
|
query_rc = sceKernelDirectMemoryQuery(scan.cursor, 1, ®ion, sizeof(region));
|
|
if (query_rc != 0) {
|
|
scan_stop = 1;
|
|
break;
|
|
}
|
|
if (!scan.Include(region.start, region.end)) {
|
|
scan_stop = 2;
|
|
break;
|
|
}
|
|
}
|
|
if (!scan_stop && !scan.ReachedExtent()) scan_stop = 3;
|
|
}
|
|
const long long free_upper = scan_requested
|
|
? static_cast<long long>(scan.FreeUpperBound()) : -1LL;
|
|
std::size_t jit_reserved = 0, jit_committed = 0;
|
|
::Common::SparseJitUsage(&jit_reserved, &jit_committed);
|
|
// Native GPU/guest/CPU use the SAME kernel-exposed direct pool (12 GiB
|
|
// observed on FW 13.60, not the full installed 16 GiB GDDR6). Report
|
|
// physical owners, not uncommitted virtual VA reservations. The Sony
|
|
// system's own reservation must never be counted as an Encore heap.
|
|
unsigned large_blocks = 0;
|
|
const std::size_t heap_large = eden_heap_large_held ? eden_heap_large_held(&large_blocks) : 0;
|
|
const bool heap_large_physical_known = eden_heap_large_physical_held != nullptr;
|
|
const std::size_t heap_large_physical =
|
|
heap_large_physical_known ? eden_heap_large_physical_held() : 0;
|
|
std::printf("EDEN_HEAP_LIFETIME phase=%s pieces=%zu large=%zu large_blocks=%u tcache=%zu large_physical=%zu physical_known=%u\n",
|
|
phase, eden_heap_committed ? eden_heap_committed() : std::size_t{0},
|
|
heap_large, large_blocks, eden_heap_tcache_held ? eden_heap_tcache_held() : std::size_t{0},
|
|
heap_large_physical, unsigned(heap_large_physical_known));
|
|
std::printf("EDEN_JIT_SPARSE_MEMORY phase=%s reserved=%zu committed=%zu\n",
|
|
phase, jit_reserved, jit_committed);
|
|
const std::size_t jit_dense = ::Common::DenseJitDirectBytes();
|
|
std::printf("EDEN_JIT_DENSE_MEMORY phase=%s physically_owned=%zu\n",
|
|
phase, jit_dense);
|
|
std::size_t sparse_virtual = 0, sparse_physical = 0;
|
|
::Common::SparseUsage(&sparse_virtual, &sparse_physical);
|
|
const std::size_t heap_roots = eden_heap_committed ? eden_heap_committed() : std::size_t{0};
|
|
const auto account = ::Eden::DirectPool::Summarize(total, {
|
|
.heap_roots = heap_roots,
|
|
.heap_large = heap_large_physical,
|
|
.sparse_tables = sparse_physical,
|
|
.jit_sparse = jit_committed,
|
|
.jit_dense = jit_dense,
|
|
});
|
|
// These five tracked owner sets are distinct direct-memory allocations.
|
|
// Untracked is NOT free: the renderer, guest and firmware may own it.
|
|
// The largest contiguous free block comes exclusively from a kernel query.
|
|
// Lifecycle checkpoints only: no per-frame region enumeration or logging.
|
|
std::printf("EDEN_DIRECT_POOL_OWNERS phase=%s extent=%llu heap_roots=%zu "
|
|
"heap_large=%zu heap_large_requested=%zu large_owner_known=%u sparse_tables=%zu jit_sparse=%zu jit_dense=%zu "
|
|
"tracked=%llu unclassified=%llu tracked_within_extent=%u "
|
|
"largest_free_known=%u largest_free=%zu\n",
|
|
phase, static_cast<unsigned long long>(account.extent_bytes),
|
|
heap_roots, heap_large_physical, heap_large, unsigned(heap_large_physical_known),
|
|
sparse_physical, jit_committed, jit_dense,
|
|
static_cast<unsigned long long>(account.tracked_bytes),
|
|
static_cast<unsigned long long>(account.not_tracked_bytes),
|
|
unsigned(account.within_extent), unsigned(largest_valid),
|
|
largest_valid ? largest : std::size_t{0});
|
|
std::printf("EDEN_MEMORY_LAYOUT phase=%s largest_rc=%d total=%lld largest=%zu "
|
|
"largest_start=%lld largest_valid=%u free_upper=%lld scanned_regions=%u scan_valid=%d "
|
|
"scan_reached_extent=%u scan_stop=%u query_rc=%d short=%d\n",
|
|
phase, largest_rc, static_cast<long long>(total),
|
|
largest_valid ? largest : size_t{0},
|
|
static_cast<long long>(largest_start), unsigned(largest_valid), free_upper,
|
|
scan.regions, int(scan_requested && scan.valid),
|
|
unsigned(scan_requested && scan.ReachedExtent()),
|
|
scan_stop, query_rc, int(graphics_memory_short.load(std::memory_order_relaxed)));
|
|
#else
|
|
(void)phase;
|
|
#endif
|
|
}
|
|
|
|
// Live gameplay progress is published ONLY by the GPU owner and read by the
|
|
// independent pad heartbeat. A stalled GPU owner can no longer silently stop
|
|
// its own diagnostic report while the controller worker stays alive.
|
|
std::atomic<unsigned long long> latest_gpu_report_ns{0};
|
|
std::atomic<unsigned> latest_gpu_report_frame{0};
|
|
// This heartbeat is needed in shipping builds too: the GPU may stop
|
|
// reporting while the native controller/pad thread remains responsive.
|
|
std::atomic<unsigned long long> last_gpu_stall_warn_ns{0};
|
|
void ResetGpuReportForTitle() noexcept {
|
|
latest_gpu_report_ns.store(0, std::memory_order_release);
|
|
latest_gpu_report_frame.store(0, std::memory_order_relaxed);
|
|
last_gpu_stall_warn_ns.store(0, std::memory_order_relaxed);
|
|
}
|
|
// Read the native guest-worker CPU clocks only after a confirmed stale
|
|
// rendering report. Never sample CPU clocks in the frame or pad poll hot paths.
|
|
// A running host clock is NOT proof that emulated guest instructions advance.
|
|
void ReportGpuStallWorkerClocks(unsigned long long age_ms, long long epoch_ms, long long mono_ns) {
|
|
std::array<Worker, names.size()> snapshot{};
|
|
{
|
|
// A wedged worker cannot make the input heartbeat block on this lock.
|
|
std::unique_lock<std::mutex> lock(workers_mutex, std::try_to_lock);
|
|
if (!lock.owns_lock()) {
|
|
std::fprintf(stderr,
|
|
"EDEN_GPU_STALL_WORKERS age_ms=%llu state=lock_busy cpu_progress=not_measured "
|
|
"epoch_ms=%lld mono_ns=%lld\n",
|
|
age_ms, epoch_ms, mono_ns);
|
|
return;
|
|
}
|
|
snapshot = workers;
|
|
}
|
|
for (unsigned i = 0; i < snapshot.size(); ++i) {
|
|
const auto& worker = snapshot[i];
|
|
if (!worker.registered) continue;
|
|
timespec usage{};
|
|
int cpu_clock_error = worker.clock_error;
|
|
if (!cpu_clock_error && clock_gettime(worker.clock, &usage) != 0)
|
|
cpu_clock_error = errno;
|
|
const long long cpu_ns = cpu_clock_error ?
|
|
-1 : static_cast<long long>(usage.tv_sec) * 1000000000LL + usage.tv_nsec;
|
|
std::fprintf(stderr,
|
|
"EDEN_GPU_STALL_WORKER role=%s age_ms=%llu cpu_ns=%lld clock_error=%d "
|
|
"cpu_progress=not_measured epoch_ms=%lld mono_ns=%lld\n",
|
|
names[i], age_ms, cpu_ns, cpu_clock_error, epoch_ms, mono_ns);
|
|
}
|
|
}
|
|
bool GpuReportStale(unsigned long long now_ns, unsigned long long progress_ns) noexcept {
|
|
constexpr unsigned long long kStallNs = 20'000'000'000ULL;
|
|
return progress_ns != 0 && now_ns > progress_ns &&
|
|
now_ns - progress_ns >= kStallNs;
|
|
}
|
|
void ReportInputThreadGpuLiveness() {
|
|
// Invoked once per 1250 pad polls, including in the release. No clock
|
|
// query or logging at all until the first GPU report is published.
|
|
const auto progress_ns = latest_gpu_report_ns.load(std::memory_order_acquire);
|
|
if (progress_ns == 0) return;
|
|
const auto now = static_cast<unsigned long long>(Common::g_wall_clock.GetTimeNS().count());
|
|
if (!GpuReportStale(now, progress_ns)) return;
|
|
// Only the independent input worker invokes this, once per heartbeat
|
|
// (~5 seconds); throttle warnings to at most once per 30 seconds.
|
|
const auto last_warn_ns = last_gpu_stall_warn_ns.load(std::memory_order_relaxed);
|
|
if (last_warn_ns != 0 && now >= last_warn_ns &&
|
|
now - last_warn_ns < 30'000'000'000ULL) return;
|
|
last_gpu_stall_warn_ns.store(now, std::memory_order_relaxed);
|
|
const auto age_ms = (now - progress_ns) / 1'000'000ULL;
|
|
// Correlate the GPU stall, controller and worker clocks on the same
|
|
// absolute and monotonic anchor, also with a detailed profile enabled.
|
|
const long long epoch_ms = ClockNs(CLOCK_REALTIME) / 1000000LL;
|
|
const long long mono_ns = ClockNs(CLOCK_MONOTONIC);
|
|
// Release counters below are disabled by design. Never misrepresent
|
|
// their zeros as evidence that CPU/GPU work or IPC stopped.
|
|
if (!detailed_gpu_profile.load(std::memory_order_relaxed)) {
|
|
std::fprintf(stderr,
|
|
"EDEN_GPU_REPORT_STALLED age_ms=%llu frame=%u detail=not_measured "
|
|
"epoch_ms=%lld mono_ns=%lld\n",
|
|
age_ms, latest_gpu_report_frame.load(std::memory_order_relaxed),
|
|
epoch_ms, mono_ns);
|
|
ReportGpuStallWorkerClocks(age_ms, epoch_ms, mono_ns);
|
|
return;
|
|
}
|
|
std::fprintf(stderr,
|
|
"EDEN_GPU_REPORT_STALLED age_ms=%llu frame=%u gpu_ticks=%llu "
|
|
"draws=%llu guest_writes=%llu guest_reads=%llu "
|
|
"guest_dequeues=%llu guest_ipc=%llu cache_collisions=%llu "
|
|
"dma_active_ms=%llu puller_active_ms=%llu epoch_ms=%lld mono_ns=%lld\n",
|
|
age_ms,
|
|
latest_gpu_report_frame.load(std::memory_order_relaxed),
|
|
gpu_command_tick.calls.load(std::memory_order_relaxed),
|
|
rasterizer_draw.calls.load(std::memory_order_relaxed),
|
|
guest_cpu_write.calls.load(std::memory_order_relaxed),
|
|
guest_cpu_read.calls.load(std::memory_order_relaxed),
|
|
guest_dequeue_wait.calls.load(std::memory_order_relaxed),
|
|
guest_ipc_wait.calls.load(std::memory_order_relaxed),
|
|
cache_lock_contended.load(std::memory_order_relaxed),
|
|
ActiveDiagnosticWaitNs(gpu_dma_sync_active_since_ns) / 1'000'000ULL,
|
|
ActiveDiagnosticWaitNs(gpu_puller_semaphore_active_since_ns) / 1'000'000ULL,
|
|
epoch_ms, mono_ns);
|
|
ReportGpuStallWorkerClocks(age_ms, epoch_ms, mono_ns);
|
|
}
|
|
void ReportGpuThread(unsigned frame) {
|
|
// Exactly one atomic GPU-owner heartbeat and clock read per report window.
|
|
// Do not gate it behind the diagnostic profile: releases must also reveal
|
|
// a frozen renderer while the independent input thread remains alive.
|
|
latest_gpu_report_frame.store(frame, std::memory_order_relaxed);
|
|
latest_gpu_report_ns.store(
|
|
static_cast<unsigned long long>(Common::g_wall_clock.GetTimeNS().count()),
|
|
std::memory_order_release);
|
|
if (detailed_gpu_profile.load(std::memory_order_relaxed)) {
|
|
// Fresh UTC/monotonic correlation every sampled five-second frame
|
|
// window, even when the session-start segment has already rotated.
|
|
// No per-draw timing, and only two clock queries per report.
|
|
std::printf("EDEN_TIME_ANCHOR epoch_ms=%lld mono_ns=%lld phase=frame_report frame=%u\n",
|
|
ClockNs(CLOCK_REALTIME) / 1000000LL,
|
|
ClockNs(CLOCK_MONOTONIC), frame);
|
|
}
|
|
// Heap growth and arena use over the run (allocation failures abort the title).
|
|
ps5_opengl_heap_snapshot("vulkan_report", frame);
|
|
{
|
|
// What the heap and Eden's large tables hold of the memory the CPU shares with the GPU:
|
|
// the heap's pieces (they only grow), its large blocks alive now, and the tables' own
|
|
// slots out of the address range they span.
|
|
unsigned large_blocks = 0;
|
|
const std::size_t large = eden_heap_large_held ? eden_heap_large_held(&large_blocks) : 0;
|
|
std::size_t table_span = 0, table_held = 0;
|
|
::Common::SparseUsage(&table_span, &table_held);
|
|
std::printf("EDEN_PERF_HEAP arenas=%u pieces=%zu large=%zu large_blocks=%u tables=%zu table_span=%zu tcache_bytes=%zu\n",
|
|
eden_heap_arenas_created ? eden_heap_arenas_created() : 1u,
|
|
eden_heap_committed ? eden_heap_committed() : std::size_t{0}, large, large_blocks, table_held,
|
|
table_span, eden_heap_tcache_held ? eden_heap_tcache_held() : std::size_t{0});
|
|
}
|
|
#ifdef EDEN_DEV_PROFILE
|
|
// A game can use thousands of distinct service/command pairs.
|
|
// Printing one line per pair ON THE GPU THREAD every five seconds
|
|
// creates another avoidable frame hitch even without an HLE mutex.
|
|
// Scan the atomic counters without locks or allocations, but report
|
|
// only the 32 heaviest cumulative service commands plus totals.
|
|
struct HleReportRow {
|
|
const char* name{};
|
|
unsigned command{};
|
|
std::uint64_t calls{};
|
|
std::uint64_t ns{};
|
|
std::uint64_t ge20ms{}, ge100ms{}, max_ns{};
|
|
};
|
|
std::array<HleReportRow, 32> heaviest{};
|
|
std::size_t heavy_count = 0;
|
|
std::size_t active_keys = 0;
|
|
std::uint64_t total_hle_calls = 0, total_hle_ns = 0;
|
|
hle_calls.ForEachLatency([&](const char* name, unsigned command, std::uint64_t calls,
|
|
std::uint64_t ns, std::uint64_t ge20ms,
|
|
std::uint64_t ge100ms, std::uint64_t max_ns) {
|
|
++active_keys;
|
|
total_hle_calls += calls;
|
|
total_hle_ns += ns;
|
|
if (ns < 1'000'000) return;
|
|
const HleReportRow candidate{name, command, calls, ns, ge20ms, ge100ms, max_ns};
|
|
if (heavy_count < heaviest.size()) {
|
|
heaviest[heavy_count++] = candidate;
|
|
return;
|
|
}
|
|
std::size_t least = 0;
|
|
for (std::size_t i = 1; i < heaviest.size(); ++i)
|
|
if (heaviest[i].ns < heaviest[least].ns) least = i;
|
|
if (candidate.ns > heaviest[least].ns)
|
|
heaviest[least] = candidate;
|
|
});
|
|
std::sort(heaviest.begin(), heaviest.begin() + heavy_count,
|
|
[](const HleReportRow& a, const HleReportRow& b) {
|
|
return a.ns > b.ns;
|
|
});
|
|
for (std::size_t i = 0; i < heavy_count; ++i) {
|
|
const auto& row = heaviest[i];
|
|
std::printf("EDEN_DEV_HLE service=%s cmd=%u calls=%llu ns=%llu\n",
|
|
row.name, row.command,
|
|
static_cast<unsigned long long>(row.calls),
|
|
static_cast<unsigned long long>(row.ns));
|
|
}
|
|
// Distinguish slow individual HLE calls from large cumulative totals.
|
|
// Snapshot only eight rows on the five-second GPU-owner report path.
|
|
unsigned tail_reported = 0;
|
|
for (std::size_t i = 0; i < heavy_count && tail_reported < 8; ++i) {
|
|
const auto& row = heaviest[i];
|
|
if (!row.ge20ms) continue;
|
|
std::printf("EDEN_DEV_HLE_TAIL service=%s cmd=%u ge20ms=%llu ge100ms=%llu max_ns=%llu\n",
|
|
row.name, row.command,
|
|
static_cast<unsigned long long>(row.ge20ms),
|
|
static_cast<unsigned long long>(row.ge100ms),
|
|
static_cast<unsigned long long>(row.max_ns));
|
|
++tail_reported;
|
|
}
|
|
std::printf("EDEN_DEV_HLE_SUMMARY keys=%zu reported=%zu calls=%llu ns=%llu\n",
|
|
active_keys, heavy_count,
|
|
static_cast<unsigned long long>(total_hle_calls),
|
|
static_cast<unsigned long long>(total_hle_ns));
|
|
if (const auto overflow = hle_calls.OverflowCalls(); overflow)
|
|
std::printf("EDEN_DEV_HLE_OVERFLOW calls=%llu ns=%llu\n",
|
|
static_cast<unsigned long long>(overflow),
|
|
static_cast<unsigned long long>(hle_calls.OverflowNs()));
|
|
#endif
|
|
#ifdef PS5_NATIVE
|
|
// Called on the native GPU thread inside its periodic frame-report branch.
|
|
// A complete sceKernelDirectMemoryQuery region walk (up to 8192 syscalls)
|
|
// here stalls actual rendering and can imitate the FC27 hitch under study.
|
|
// The 100ms collector already publishes the confirmed largest free block;
|
|
// reading its atomics requires ZERO additional firmware syscalls.
|
|
std::printf("EDEN_MEMORY_LIVE frame=%u largest_last_confirmed=%llu short=%u\n",
|
|
frame, largest_free_block.load(std::memory_order_relaxed),
|
|
unsigned(graphics_memory_short.load(std::memory_order_relaxed)));
|
|
// Preserve the existing five-second parser's sparse JIT time series
|
|
// without reintroducing a contended mutex or 8192 kernel queries.
|
|
std::size_t jit_reserved_live = 0, jit_committed_live = 0;
|
|
::Common::SparseJitUsageFast(&jit_reserved_live, &jit_committed_live);
|
|
std::printf("EDEN_JIT_SPARSE_MEMORY phase=dev-profile reserved=%zu committed=%zu\n",
|
|
jit_reserved_live, jit_committed_live);
|
|
#endif
|
|
const auto load = [](const Totals& totals, bool calls) {
|
|
return (calls ? totals.calls : totals.nanoseconds).load(std::memory_order_relaxed);
|
|
};
|
|
// Cumulative totals; the analyzer takes deltas between consecutive reports.
|
|
std::printf("EDEN_DEV_GPU frame=%u mono_ns=%lld cpu_ns=%lld idle_ns=%llu dispatch_calls=%llu "
|
|
"dispatch_ns=%llu drain_calls=%llu drain_ns=%llu present_calls=%llu present_ns=%llu "
|
|
"full_calls=%llu full_ns=%llu draws=%llu\n", frame, ClockNs(CLOCK_MONOTONIC),
|
|
owner_cpu_clock_valid ? ClockNs(CLOCK_THREAD_CPUTIME_ID) : -static_cast<long long>(ENOTSUP),
|
|
load(gpu_queue_wait, false), load(gpu_dispatch, true), load(gpu_dispatch, false),
|
|
load(gpu_fence_drain, true), load(gpu_fence_drain, false),
|
|
load(gpu_present_wait, true), load(gpu_present_wait, false),
|
|
load(gpu_queue_full, true), load(gpu_queue_full, false), load(rasterizer_draw, true));
|
|
// R424: distinguish FIFO backpressure from GPU fence completion.
|
|
// The existing EDEN_DEV_GPU.present_* cumulative counter is unchanged.
|
|
std::printf("EDEN_PRESENT_WAIT_PHASE frame=%u free_calls=%llu free_ns=%llu "
|
|
"fence_calls=%llu fence_ns=%llu\n", frame,
|
|
load(gpu_present_free_wait, true), load(gpu_present_free_wait, false),
|
|
load(gpu_present_fence_wait, true), load(gpu_present_fence_wait, false));
|
|
// Breakdown of GPU-owner command types. In the R293 quiet path no clocks
|
|
// or counters are touched; this runs only inside ReportGpuThread's opt-in
|
|
// five-second diagnostic callback. Values are cumulative, not FPS.
|
|
std::printf("EDEN_GPU_COMMANDS frame=%u tick_calls=%llu tick_ns=%llu "
|
|
"flush_calls=%llu flush_ns=%llu invalidate_calls=%llu invalidate_ns=%llu\n",
|
|
frame, load(gpu_command_tick, true), load(gpu_command_tick, false),
|
|
load(gpu_command_flush, true), load(gpu_command_flush, false),
|
|
load(gpu_command_invalidate, true), load(gpu_command_invalidate, false));
|
|
// R405: completed fence waits are a subset of scheduler.Push time, not
|
|
// the GPU's busy time. This is reported only in the detailed five-second path.
|
|
std::printf("EDEN_GPU_DMA_SYNC frame=%u calls=%llu ns=%llu active_ns=%llu\n",
|
|
frame, load(gpu_dma_sync_wait, true), load(gpu_dma_sync_wait, false),
|
|
ActiveDiagnosticWaitNs(gpu_dma_sync_active_since_ns));
|
|
// R406: GPU puller semaphore equality loop is a different blocking path.
|
|
// Snapshot in-flight waits, not only completed calls, to retain evidence
|
|
// even when the game never reaches the RAII destructor.
|
|
std::printf("EDEN_GPU_PULLER_SEMAPHORE frame=%u calls=%llu ns=%llu active_ns=%llu\n",
|
|
frame, load(gpu_puller_semaphore_wait, true),
|
|
load(gpu_puller_semaphore_wait, false),
|
|
ActiveDiagnosticWaitNs(gpu_puller_semaphore_active_since_ns));
|
|
// R384: confirm that R382's prelock FIFO dispatch optimization actually
|
|
// fired on the PS5. A zero counter is meaningful: do not attribute an
|
|
// FPS gain to a preflight that never ran.
|
|
std::printf("EDEN_SCHEDULER_PREFLIGHT frame=%u dispatched=%llu\n",
|
|
frame, scheduler_preflight_dispatches.load(std::memory_order_relaxed));
|
|
std::printf("EDEN_DEV_GUEST cpu_write_calls=%llu cpu_write_ns=%llu cpu_read_calls=%llu cpu_read_ns=%llu "
|
|
"sync_calls=%llu sync_ns=%llu dequeue_calls=%llu dequeue_ns=%llu",
|
|
load(guest_cpu_write, true), load(guest_cpu_write, false),
|
|
load(guest_cpu_read, true), load(guest_cpu_read, false),
|
|
load(guest_sync_wait, true), load(guest_sync_wait, false),
|
|
load(guest_dequeue_wait, true), load(guest_dequeue_wait, false));
|
|
std::printf(" ipc_calls=%llu ipc_ns=%llu", load(guest_ipc_wait, true), load(guest_ipc_wait, false));
|
|
std::printf(" cache_lock_contended=%llu cache_lock_blocked=%llu cache_lock_wait_ns=%llu",
|
|
cache_lock_contended.load(std::memory_order_relaxed),
|
|
cache_lock_blocked.load(std::memory_order_relaxed),
|
|
cache_lock_wait_ns.load(std::memory_order_relaxed));
|
|
std::printf(" fs_file_calls=%llu fs_file_ns=%llu fs_file_bytes=%llu fs_storage_calls=%llu fs_storage_ns=%llu "
|
|
"fs_storage_bytes=%llu",
|
|
load(guest_fs_file, true), load(guest_fs_file, false),
|
|
guest_fs_file_bytes.load(std::memory_order_relaxed),
|
|
load(guest_fs_storage, true), load(guest_fs_storage, false),
|
|
guest_fs_storage_bytes.load(std::memory_order_relaxed));
|
|
for (unsigned core = 0; core < jit_callbacks.size(); ++core)
|
|
std::printf(" r%u=%llu w%u=%llu x%u=%llu", core, jit_callbacks[core].reads.load(std::memory_order_relaxed),
|
|
core, jit_callbacks[core].writes.load(std::memory_order_relaxed),
|
|
core, jit_callbacks[core].exclusive_writes.load(std::memory_order_relaxed));
|
|
for (unsigned core = 0; core < 3; ++core)
|
|
std::printf(" idle%u=%llu/%llu/%llu", core, guest_idle[core].calls.load(std::memory_order_relaxed),
|
|
guest_idle[core].nanoseconds.load(std::memory_order_relaxed) / 1000000,
|
|
guest_idle[core].sleeps.load(std::memory_order_relaxed));
|
|
std::printf(" cond=");
|
|
for (unsigned slot = 0; slot < render_conditions.size(); ++slot)
|
|
std::printf("%s%llu", slot ? "," : "", render_conditions[slot].load(std::memory_order_relaxed));
|
|
std::printf("\n");
|
|
#ifdef EDEN_DEV_PROFILE
|
|
if (detailed_gpu_profile.load(std::memory_order_relaxed)) {
|
|
// R393: only report the 12 largest cumulative IPC waits at 5s intervals.
|
|
// This is not a call stack and does not say that a thread is currently stuck.
|
|
struct IpcRow {
|
|
unsigned long long tid{}, calls{}, ns{}, max_ns{};
|
|
};
|
|
std::array<IpcRow, 12> ipc_heaviest{};
|
|
unsigned ipc_used = 0;
|
|
for (const auto& source : guest_ipc_threads) {
|
|
const auto calls = source.calls.load(std::memory_order_relaxed);
|
|
if (calls == 0) continue;
|
|
const IpcRow item{source.tid.load(std::memory_order_relaxed), calls,
|
|
source.ns.load(std::memory_order_relaxed),
|
|
source.max_ns.load(std::memory_order_relaxed)};
|
|
if (ipc_used < ipc_heaviest.size()) {
|
|
ipc_heaviest[ipc_used++] = item;
|
|
continue;
|
|
}
|
|
unsigned least = 0;
|
|
for (unsigned i = 1; i < ipc_heaviest.size(); ++i)
|
|
if (ipc_heaviest[i].ns < ipc_heaviest[least].ns) least = i;
|
|
if (item.ns > ipc_heaviest[least].ns)
|
|
ipc_heaviest[least] = item;
|
|
}
|
|
std::sort(ipc_heaviest.begin(), ipc_heaviest.begin() + ipc_used,
|
|
[](const IpcRow& a, const IpcRow& b) { return a.ns > b.ns; });
|
|
for (unsigned i = 0; i < ipc_used; ++i) {
|
|
const auto& row = ipc_heaviest[i];
|
|
std::printf("EDEN_DEV_IPC_THREAD tid=%llu calls=%llu ns=%llu max_ns=%llu\n",
|
|
row.tid, row.calls, row.ns, row.max_ns);
|
|
}
|
|
// Completed calls cannot reveal an IPC waiting indefinitely. Read the
|
|
// lock-free per-thread pending timestamp without touching guest IPC.
|
|
// Top eight active calls over 500 ms; a long call is a suspect, not
|
|
// proof of a deadlock (normal services can legitimately wait).
|
|
struct ActiveIpcRow {
|
|
unsigned long long tid{};
|
|
long long started{}, age_ns{};
|
|
};
|
|
std::array<ActiveIpcRow, 8> pending{};
|
|
unsigned pending_used = 0;
|
|
const auto observed_ns = NowNs();
|
|
for (const auto& source : guest_ipc_threads) {
|
|
const auto started = source.active_since_ns.load(std::memory_order_acquire);
|
|
if (started <= 0 || observed_ns <= started ||
|
|
observed_ns - started < 500'000'000) continue;
|
|
const auto tid = source.tid.load(std::memory_order_relaxed);
|
|
if (!tid) continue;
|
|
const ActiveIpcRow item{tid, started, observed_ns - started};
|
|
if (pending_used < pending.size()) {
|
|
pending[pending_used++] = item;
|
|
continue;
|
|
}
|
|
unsigned shortest = 0;
|
|
for (unsigned i = 1; i < pending.size(); ++i)
|
|
if (pending[i].age_ns < pending[shortest].age_ns) shortest = i;
|
|
if (item.age_ns > pending[shortest].age_ns)
|
|
pending[shortest] = item;
|
|
}
|
|
std::sort(pending.begin(), pending.begin() + pending_used,
|
|
[](const ActiveIpcRow& a, const ActiveIpcRow& b) {
|
|
return a.age_ns > b.age_ns;
|
|
});
|
|
for (unsigned i = 0; i < pending_used; ++i)
|
|
std::printf("EDEN_DEV_IPC_ACTIVE tid=%llu age_ms=%.3f\n",
|
|
pending[i].tid, pending[i].age_ns / 1'000'000.0);
|
|
std::printf("EDEN_DEV_IPCHIST lt50us=%llu lt500us=%llu lt5ms=%llu ge5ms=%llu "
|
|
"overflow_calls=%llu\n",
|
|
guest_ipc_hist[0].load(std::memory_order_relaxed),
|
|
guest_ipc_hist[1].load(std::memory_order_relaxed),
|
|
guest_ipc_hist[2].load(std::memory_order_relaxed),
|
|
guest_ipc_hist[3].load(std::memory_order_relaxed),
|
|
guest_ipc_overflow_calls.load(std::memory_order_relaxed));
|
|
}
|
|
#endif
|
|
// These are 1-in-16 GPU draw and 1-in-64 guest-write SAMPLES, not
|
|
// full totals. Each phase reports its own sample count and window cost.
|
|
// Never issue a kernel query or print per-draw logging while the locks live.
|
|
if (detailed_gpu_profile.load(std::memory_order_relaxed)) {
|
|
constexpr const char* cache_phase_names[] = {
|
|
"pair_wait", "pair_held", "pipeline_configure", "descriptor_sync",
|
|
"fill_views", "graphics_buffers", "descriptor_acquire",
|
|
"dynamic_states", "query_draw",
|
|
"texture_cpu_refresh", "texture_alias_scan", "texture_alias_copy",
|
|
"stage_texture_handles", "bind_texture_buffers", "bind_stage_descriptors",
|
|
"refresh_render_targets", "check_feedback_loop", "commit_graphics_draw",
|
|
"descriptor_ring_finish", "descriptor_ring_preflight",
|
|
"guest_write_buffer_wait", "guest_write_buffer_work",
|
|
"guest_write_texture_wait", "guest_write_texture_work",
|
|
"guest_write_shader_invalidate"
|
|
};
|
|
static_assert(std::size(cache_phase_names) == cache_phase_totals.size());
|
|
constexpr const char* drop_names[] = {
|
|
"no_pipeline", "configure_rejected", "built_pipeline_failed", "descriptor_buffer_full",
|
|
"worker_pipeline_failed"
|
|
};
|
|
static_assert(std::size(drop_names) == graphics_draw_drop_totals.size());
|
|
static std::array<unsigned long long, graphics_draw_drop_totals.size()> last_drops{};
|
|
for (unsigned type = 0; type < graphics_draw_drop_totals.size(); ++type) {
|
|
const auto total = graphics_draw_drop_totals[type].load(std::memory_order_relaxed);
|
|
// Title reset is also supported: counters can decrease between games.
|
|
const auto window = total >= last_drops[type] ? total - last_drops[type] : total;
|
|
last_drops[type] = total;
|
|
std::printf("EDEN_GPU_DRAW_DROP frame=%u reason=%s total=%llu window=%llu\n",
|
|
frame, drop_names[type], total, window);
|
|
}
|
|
// Keep the established lifetime totals for existing parsers, and also
|
|
// publish exact per-snapshot deltas. Summing cumulative values across
|
|
// snapshots multiplies the apparent cache-lock and pipeline costs.
|
|
// Only the GPU owner records these samples; no per-draw work is added.
|
|
static std::array<unsigned long long, cache_phase_totals.size()> last_samples{};
|
|
static std::array<unsigned long long, cache_phase_totals.size()> last_total_ns{};
|
|
for (unsigned phase = 0; phase < cache_phase_totals.size(); ++phase) {
|
|
auto& entry = cache_phase_totals[phase];
|
|
const auto samples = entry.samples.load(std::memory_order_relaxed);
|
|
const auto total_ns = entry.total_ns.load(std::memory_order_relaxed);
|
|
// ResetCachePhases() runs between titles in the same PS5 process.
|
|
const auto window_samples =
|
|
samples >= last_samples[phase] ? samples - last_samples[phase] : samples;
|
|
const auto window_ns =
|
|
total_ns >= last_total_ns[phase] ? total_ns - last_total_ns[phase] : total_ns;
|
|
last_samples[phase] = samples;
|
|
last_total_ns[phase] = total_ns;
|
|
std::printf("EDEN_GPU_CACHE_PHASE frame=%u phase=%s samples=%llu total_ns=%llu window_samples=%llu window_ns=%llu max_ns=%llu\n",
|
|
frame, cache_phase_names[phase],
|
|
samples, total_ns, window_samples, window_ns,
|
|
entry.max_ns.exchange(0, std::memory_order_relaxed));
|
|
}
|
|
// Guest CPU callback contention, split by actual cache ownership.
|
|
// Only 2 x 3 atomic loads/5s; no snapshot locking or output on a draw.
|
|
constexpr const char* guest_cache_domains[] = {"buffer", "texture"};
|
|
static std::array<std::array<unsigned long long, 3>, 2> previous_cache_domains{};
|
|
for (unsigned d = 0; d < guest_cache_domain_totals.size(); ++d) {
|
|
auto& stat = guest_cache_domain_totals[d];
|
|
const unsigned long long totals[] = {
|
|
stat.contended.load(std::memory_order_relaxed),
|
|
stat.blocked.load(std::memory_order_relaxed),
|
|
stat.wait_ns.load(std::memory_order_relaxed)
|
|
};
|
|
unsigned long long changes[3]{};
|
|
for (unsigned type = 0; type < 3; ++type) {
|
|
const auto last = previous_cache_domains[d][type];
|
|
changes[type] = totals[type] >= last ? totals[type] - last : totals[type];
|
|
previous_cache_domains[d][type] = totals[type];
|
|
}
|
|
std::printf("EDEN_GUEST_CACHE_DOMAIN frame=%u domain=%s "
|
|
"contended=%llu blocked=%llu wait_ns=%llu "
|
|
"window_contended=%llu window_blocked=%llu window_wait_ns=%llu\n",
|
|
frame, guest_cache_domains[d],
|
|
totals[0], totals[1], totals[2],
|
|
changes[0], changes[1], changes[2]);
|
|
// High-threshold tails distinguish many brief cache waits from
|
|
// a single frame-scale lock hold. Never infer frame causality.
|
|
static std::array<std::array<unsigned long long, 2>, 2> previous_slow_waits{};
|
|
const unsigned long long slow_totals[] = {
|
|
stat.waited_ge1ms.load(std::memory_order_relaxed),
|
|
stat.waited_ge10ms.load(std::memory_order_relaxed)
|
|
};
|
|
unsigned long long slow_window[2]{};
|
|
for (unsigned t = 0; t < 2; ++t) {
|
|
const auto prev = previous_slow_waits[d][t];
|
|
slow_window[t] = slow_totals[t] >= prev ? slow_totals[t] - prev : slow_totals[t];
|
|
previous_slow_waits[d][t] = slow_totals[t];
|
|
}
|
|
std::printf("EDEN_GUEST_CACHE_LOCK_TAIL frame=%u domain=%s "
|
|
"wait_ge1ms=%llu wait_ge10ms=%llu max_wait_ns=%llu\n",
|
|
frame, guest_cache_domains[d], slow_window[0], slow_window[1],
|
|
stat.max_wait_ns.exchange(0, std::memory_order_relaxed));
|
|
}
|
|
}
|
|
const auto window = Eden::Fastmem::WindowStats();
|
|
std::printf("EDEN_FASTMEM window=%llx pages=%llu chunks=%llu direct_reads=%llu direct_writes=%llu "
|
|
"faults=%llu maps=%llu unmaps=%llu protects=%llu kernel_calls=%llu kernel_ns=%llu failures=%llu\n",
|
|
static_cast<unsigned long long>(window.window), static_cast<unsigned long long>(window.mapped_pages),
|
|
static_cast<unsigned long long>(window.aliased_chunks),
|
|
static_cast<unsigned long long>(window.direct_reads),
|
|
static_cast<unsigned long long>(window.direct_writes),
|
|
static_cast<unsigned long long>(Eden::Fastmem::Faults()),
|
|
static_cast<unsigned long long>(window.map_calls), static_cast<unsigned long long>(window.unmap_calls),
|
|
static_cast<unsigned long long>(window.protect_calls),
|
|
static_cast<unsigned long long>(window.kernel_calls), static_cast<unsigned long long>(window.kernel_ns),
|
|
static_cast<unsigned long long>(window.failures));
|
|
// Guest cores record their owner CPU clocks at their next JIT exit.
|
|
Snapshot();
|
|
}
|
|
|
|
void RegisterWorker(const char* name) {
|
|
Eden::Crash::NameThread(name); // a crash report names the thread
|
|
#ifdef PS5_NATIVE
|
|
// CPU and GPU owners compete with service and pipeline threads.
|
|
// Diagnose and qualify fair scheduling before guest work starts.
|
|
ProbeSchedulerOnce(name);
|
|
#endif
|
|
#if defined(EDEN_DEV_PROFILE) && defined(PS5_NATIVE)
|
|
Eden::Stall::NoteThread(name);
|
|
#endif
|
|
for (unsigned i = 0; i < names.size(); ++i) {
|
|
if (std::strcmp(name, names[i])) continue;
|
|
Worker worker;
|
|
worker.thread = pthread_self();
|
|
// Construct this thread's exit hook BEFORE pc_core_registration and
|
|
// the optional jthread sampler. TLS destruction will therefore stop
|
|
// the producer, clear core readiness, and finally unregister worker.
|
|
owned_worker.owner = worker.thread;
|
|
#ifdef EDEN_DEV_PROFILE
|
|
if (pc_sampling && (i == 4 || i == pc_sample_core.load())) {
|
|
sigset_t mask;
|
|
sigemptyset(&mask);
|
|
sigaddset(&mask, SIGUSR2);
|
|
if (pthread_sigmask(SIG_UNBLOCK, &mask, nullptr))
|
|
throw std::runtime_error("Cannot enable development PC sampler");
|
|
if (i == pc_sample_core.load()) {
|
|
core_sample_thread = pthread_self();
|
|
pc_core_registration.active = true;
|
|
core_sample_ready.store(true, std::memory_order_release);
|
|
// dev-settings pc_fast=on: 500 Hz from 45s after registration.
|
|
// Unlike detached threads, a thread-local jthread requests
|
|
// stop and joins when its owning guest CPU worker exits. Bound
|
|
// the boot delay in 200ms stop-aware steps, so game teardown
|
|
// never waits another 45 seconds for an abandoned sampler.
|
|
if (pc_fast.load()) {
|
|
static thread_local std::jthread fast_sampler;
|
|
const pthread_t target = pthread_self();
|
|
fast_sampler = std::jthread([target](std::stop_token stop) {
|
|
#ifdef PS5_NATIVE
|
|
PlaceSecondary("pc_sampler");
|
|
#endif
|
|
for (unsigned tick = 0; tick < 225 && !stop.stop_requested(); ++tick)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
while (!stop.stop_requested() && pthread_kill(target, SIGUSR2) == 0)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
worker.clock_error = CurrentCpuClock(&worker.clock);
|
|
worker.wall_ns = Common::g_wall_clock.GetTimeNS().count();
|
|
worker.mono_ns = ClockNs(CLOCK_MONOTONIC);
|
|
#ifdef PS5_NATIVE
|
|
cpuset_t affinity{};
|
|
const int result = cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1,
|
|
sizeof(unsigned long long), &affinity);
|
|
#else
|
|
cpu_set_t affinity{};
|
|
const int result = sched_getaffinity(0, sizeof(affinity), &affinity);
|
|
#endif
|
|
#ifdef PS5_NATIVE
|
|
if (result == 0) PinWorker(i, affinity);
|
|
#endif
|
|
worker.affinity_error = result == 0 ? 0 : errno;
|
|
if (!worker.affinity_error) {
|
|
for (unsigned cpu = 0; cpu < CPU_SETSIZE; ++cpu) {
|
|
if (!CPU_ISSET(cpu, &affinity)) continue;
|
|
++worker.allowed;
|
|
if (cpu < 64) worker.mask |= 1ULL << cpu;
|
|
}
|
|
}
|
|
worker.registered = true;
|
|
{
|
|
const std::lock_guard lock(workers_mutex);
|
|
workers[i] = worker;
|
|
}
|
|
owned_worker.index = i;
|
|
return;
|
|
}
|
|
#ifdef PS5_NATIVE
|
|
PlaceSecondary(name);
|
|
#endif
|
|
}
|
|
|
|
bool CpuSampleDue(unsigned core) noexcept {
|
|
return sampled_epoch[core] != sample_epoch.load(std::memory_order_relaxed);
|
|
}
|
|
void SampleCpu(unsigned core, unsigned long long thread, unsigned long long pc, unsigned svc, unsigned fpcr) {
|
|
const auto epoch = sample_epoch.load(std::memory_order_relaxed);
|
|
if (sampled_epoch.at(core) == epoch) return;
|
|
sampled_epoch[core] = epoch;
|
|
const CpuSample sample{epoch, ClockNs(CLOCK_MONOTONIC),
|
|
owner_cpu_clock_valid ? ClockNs(CLOCK_THREAD_CPUTIME_ID) : -ENOTSUP, thread, pc, svc, fpcr,
|
|
compilation[core].calls.load(std::memory_order_relaxed),
|
|
compilation[core].nanoseconds.load(std::memory_order_relaxed)};
|
|
std::lock_guard lock(workers_mutex);
|
|
cpu_samples[core] = sample;
|
|
}
|
|
|
|
void Snapshot() {
|
|
// PC sample cursors and one-shot block dumps require serialized consumers,
|
|
// but MUST NOT hold workers_mutex while formatting a large snapshot.
|
|
const std::lock_guard snapshot_guard(snapshot_mutex);
|
|
// Request a fresh owner-written sample; report the last completed sample with
|
|
// its own timestamp. An idle core may remain stale; never infer zero CPU use.
|
|
sample_epoch.fetch_add(1, std::memory_order_relaxed);
|
|
const auto mono = ClockNs(CLOCK_MONOTONIC);
|
|
std::printf("EDEN_PERF_SAMPLE mono_ns=%lld process_cpu_ns=%lld wall_ns=%lld\n",
|
|
mono, cpu_clocks_valid ? ClockNs(process_clock) : -static_cast<long long>(ENOTSUP),
|
|
static_cast<long long>(Common::g_wall_clock.GetTimeNS().count()));
|
|
#ifdef EDEN_DEV_PROFILE
|
|
std::map<uintptr_t, unsigned> counts;
|
|
const unsigned end = pc_count.load(std::memory_order_acquire);
|
|
if (end < pc_reported) pc_reported = end; // wrap/reset safety
|
|
if (end - pc_reported > sampled_pcs.size()) {
|
|
const unsigned lost = end - pc_reported - static_cast<unsigned>(sampled_pcs.size());
|
|
pc_reported = end - sampled_pcs.size();
|
|
std::printf("EDEN_PERF_PC_SAMPLES_LOST source=gpu count=%u\n", lost);
|
|
}
|
|
#if defined(EDEN_DEV_WAIT_CALLERS) && defined(PS5_NATIVE)
|
|
std::map<std::array<uintptr_t, 9>, unsigned> caller_counts;
|
|
#endif
|
|
while (pc_reported < end) {
|
|
const unsigned index = pc_reported++;
|
|
++counts[sampled_pcs[index % sampled_pcs.size()].load(std::memory_order_relaxed)];
|
|
#if defined(EDEN_DEV_WAIT_CALLERS) && defined(PS5_NATIVE)
|
|
std::array<uintptr_t, 9> key{
|
|
sampled_pcs[index % sampled_pcs.size()].load(std::memory_order_relaxed)};
|
|
std::copy(sampled_callers[index % sampled_callers.size()].begin(),
|
|
sampled_callers[index % sampled_callers.size()].end(), key.begin() + 1);
|
|
++caller_counts[key];
|
|
#endif
|
|
}
|
|
#if defined(EDEN_DEV_WAIT_CALLERS) && defined(PS5_NATIVE)
|
|
for (const auto& [chain, count] : caller_counts) {
|
|
std::printf("EDEN_PERF_NATIVE_CALLERS mono_ns=%lld pc=%llx count=%u callers=", mono,
|
|
static_cast<unsigned long long>(chain[0]), count);
|
|
for (unsigned i = 1; i < chain.size(); ++i)
|
|
std::printf("%s%llx", i == 1 ? "" : ",", static_cast<unsigned long long>(chain[i]));
|
|
std::printf("\n");
|
|
}
|
|
#endif
|
|
for (const auto& [pc, count] : counts)
|
|
std::printf("EDEN_PERF_NATIVE_PC mono_ns=%lld pc=%llx count=%u\n", mono,
|
|
static_cast<unsigned long long>(pc), count);
|
|
std::map<uintptr_t, unsigned> core_counts;
|
|
const unsigned core_end = core_pc_count.load(std::memory_order_acquire);
|
|
if (core_end < core_pc_reported) core_pc_reported = core_end;
|
|
if (core_end - core_pc_reported > sampled_core_pcs.size()) {
|
|
const unsigned lost = core_end - core_pc_reported - static_cast<unsigned>(sampled_core_pcs.size());
|
|
core_pc_reported = core_end - sampled_core_pcs.size();
|
|
std::printf("EDEN_PERF_PC_SAMPLES_LOST source=guest count=%u\n", lost);
|
|
}
|
|
while (core_pc_reported < core_end) {
|
|
const auto index = core_pc_reported++;
|
|
++core_counts[sampled_core_pcs[index % sampled_core_pcs.size()].load(std::memory_order_relaxed)];
|
|
}
|
|
for (const auto& [pc, count] : core_counts)
|
|
std::printf("EDEN_PERF_CORE_PC mono_ns=%lld pc=%llx count=%u\n", mono,
|
|
static_cast<unsigned long long>(pc), count);
|
|
// The same samples by guest block: location (A32 PC in the low word), block offset, count.
|
|
if (const std::size_t blocks = core0_blocks ? core0_block_count.load(std::memory_order_acquire) : 0) {
|
|
std::map<std::pair<unsigned long long, unsigned long long>, unsigned> guest_counts;
|
|
for (const auto& [pc, count] : core_counts) {
|
|
const JitBlock* begin = core0_blocks;
|
|
const JitBlock* it = std::upper_bound(begin, begin + blocks, pc,
|
|
[](uintptr_t value, const JitBlock& block) { return value < block.entry; });
|
|
if (it == begin) continue;
|
|
--it;
|
|
if (pc - it->entry < it->size) guest_counts[{it->location, pc - it->entry}] += count;
|
|
}
|
|
for (const auto& [key, count] : guest_counts)
|
|
std::printf("EDEN_PERF_GUEST_PC mono_ns=%lld location=%llx host_offset=%llx count=%u\n", mono,
|
|
key.first, key.second, count);
|
|
// P1: guest and host code of this window's six hottest blocks not dumped before.
|
|
std::map<unsigned long long, unsigned> block_counts;
|
|
for (const auto& [key, count] : guest_counts) block_counts[key.first] += count;
|
|
std::vector<std::pair<unsigned, unsigned long long>> ranked;
|
|
for (const auto& [location, count] : block_counts) ranked.emplace_back(count, location);
|
|
std::sort(ranked.rbegin(), ranked.rend());
|
|
static std::set<unsigned long long> dumped;
|
|
unsigned printed = 0;
|
|
for (const auto& [count, location] : ranked) {
|
|
if (printed == 6) break;
|
|
if (!dumped.insert(location).second) continue;
|
|
++printed;
|
|
const JitBlock* block = nullptr;
|
|
for (std::size_t i = blocks; i-- > 0;)
|
|
if (core0_blocks[i].location == location) { block = &core0_blocks[i]; break; }
|
|
// A report runs on the native GPU render thread. Formatting
|
|
// each 4 KiB code block with 4096 separate snprintf calls and
|
|
// growing std::strings could visibly stall FC27 every five
|
|
// seconds. Use bounded stack buffers and direct hex digits:
|
|
// byte-for-byte identical guest/host diagnostic log format.
|
|
constexpr char hex[] = "0123456789abcdef";
|
|
std::array<char, 64 * 8 + 1> guest{};
|
|
std::size_t guest_written = 0;
|
|
const unsigned long long pc = location & 0xffffffffull;
|
|
for (unsigned long long at = pc; at < pc + 256 && guest_read32; at += 4) {
|
|
unsigned word = 0;
|
|
if (!guest_read32(at, word)) break;
|
|
for (int nibble = 7; nibble >= 0; --nibble)
|
|
guest[guest_written++] = hex[(word >> (nibble * 4)) & 0xfu];
|
|
}
|
|
guest[guest_written] = '\0';
|
|
std::printf("EDEN_PERF_BLOCK_GUEST location=%llx count=%u words=%s\n",
|
|
location, count, guest.data());
|
|
if (block) {
|
|
std::array<char, 4096 * 2 + 1> host{};
|
|
const auto* bytes = reinterpret_cast<const unsigned char*>(block->entry);
|
|
const auto byte_count = std::min<unsigned long long>(block->size, 4096);
|
|
for (unsigned long long i = 0; i < byte_count; ++i) {
|
|
const unsigned char value = bytes[i];
|
|
host[2 * i] = hex[value >> 4];
|
|
host[2 * i + 1] = hex[value & 0xfu];
|
|
}
|
|
host[2 * byte_count] = '\0';
|
|
std::printf("EDEN_PERF_BLOCK_HOST location=%llx entry=%llx size=%llu bytes=%s\n",
|
|
location, static_cast<unsigned long long>(block->entry),
|
|
block->size, host.data());
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
std::printf("EDEN_GPU_UNMAPPED_COUNTERS read=%llu write=%llu guest_map_zero=%llu guest_null_mapped=%llu guest_alias_mapped=%llu guest_alias_access=%llu remap_replaced=%llu remap_mismatch=%llu bad_map_range=%llu bad_unmap_range=%llu bad_physical=%llu remap_cache_evictions=%llu\n",
|
|
static_cast<unsigned long long>(::Eden::GpuFault::reads.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::writes.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::guest_map_zero.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::guest_null_mapped.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::guest_alias_mapped.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::guest_alias_access.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::remap_replaced.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::remap_mismatch.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::bad_map_range.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::bad_unmap_range.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::bad_physical.load(std::memory_order_relaxed)),
|
|
static_cast<unsigned long long>(::Eden::GpuFault::remap_cache_evictions.load(std::memory_order_relaxed)));
|
|
// Take an immutable owner-written copy; priority/CPU-clock syscalls,
|
|
// formatting, and stdout flushes must NOT hold workers_mutex. Otherwise
|
|
// all guest CPU workers can block in SampleCpu() on this GPU report.
|
|
std::array<Worker, names.size()> worker_snapshot{};
|
|
std::array<CpuSample, 4> cpu_snapshot{};
|
|
{
|
|
const std::lock_guard worker_guard(workers_mutex);
|
|
worker_snapshot = workers;
|
|
cpu_snapshot = cpu_samples;
|
|
}
|
|
for (unsigned i = 0; i < worker_snapshot.size(); ++i) {
|
|
const auto& worker = worker_snapshot[i];
|
|
sched_param priority{};
|
|
int policy = -1;
|
|
const int priority_error = worker.registered ?
|
|
pthread_getschedparam(worker.thread, &policy, &priority) : ENOENT;
|
|
const auto cpu = !cpu_clocks_valid ? -static_cast<long long>(ENOTSUP) :
|
|
worker.clock_error ? -static_cast<long long>(worker.clock_error) :
|
|
ClockNs(worker.clock);
|
|
std::printf("EDEN_PERF_WORKER mono_ns=%lld name=%s cpu_ns=%lld clock_id=%d affinity_error=%d "
|
|
"allowed=%d mask_low64=%llx priority_error=%d policy=%d priority=%d "
|
|
"registered_wall_ns=%lld registered_mono_ns=%lld\n",
|
|
mono, names[i], cpu, int(worker.clock), worker.affinity_error, worker.allowed, worker.mask,
|
|
priority_error, policy, priority.sched_priority, worker.wall_ns, worker.mono_ns);
|
|
if (i < compilation.size()) {
|
|
const auto& sample = cpu_snapshot[i];
|
|
std::printf("EDEN_PERF_CPU_POINT core=%u phase=%u epoch=%u mono_ns=%lld cpu_ns=%lld thread=%llu pc=%llx svc=%x fpcr=%u compilations=%llu compile_ns=%llu\n",
|
|
i, unsigned(cpu_state[i].phase.load(std::memory_order_relaxed)), sample.epoch,
|
|
sample.mono_ns, sample.cpu_ns, sample.thread, sample.pc, sample.svc, sample.fpcr,
|
|
sample.compilations, sample.compile_ns);
|
|
#ifdef EDEN_DEV_PROFILE
|
|
const auto& dups = jit_duplicates[i];
|
|
std::printf("EDEN_PERF_DUPLICATES mono_ns=%lld core=%u first=%llu again=%llu lead_1ms=%llu lead_10ms=%llu "
|
|
"lead_100ms=%llu lead_1s=%llu lead_10s=%llu lead_more=%llu\n", mono, i,
|
|
dups[0].load(std::memory_order_relaxed), dups[1].load(std::memory_order_relaxed),
|
|
dups[2].load(std::memory_order_relaxed), dups[3].load(std::memory_order_relaxed),
|
|
dups[4].load(std::memory_order_relaxed), dups[5].load(std::memory_order_relaxed),
|
|
dups[6].load(std::memory_order_relaxed), dups[7].load(std::memory_order_relaxed));
|
|
if (eden_jit_path_counters) {
|
|
unsigned long long path[8]{};
|
|
eden_jit_path_counters(i, path);
|
|
std::printf("EDEN_PERF_JITPATH mono_ns=%lld core=%u runs=%llu thunks=%llu lookups=%llu hits=%llu "
|
|
"contended=%llu flush_all=%llu flush_locations=%llu far_links=%llu\n", mono, i,
|
|
path[0], path[1], path[2], path[3], path[4], path[5], path[6], path[7]);
|
|
}
|
|
std::printf("EDEN_PERF_PROGRESS mono_ns=%lld core=%u compilations=%llu compile_ns=%llu evacuations=%llu "
|
|
"translate_ns=%llu optimize_ns=%llu emit_ns=%llu ranges_ns=%llu\n",
|
|
mono, i, compilation[i].calls.load(std::memory_order_relaxed),
|
|
compilation[i].nanoseconds.load(std::memory_order_relaxed),
|
|
evacuations[i].load(std::memory_order_relaxed),
|
|
jit_phase_ns[i][0].load(std::memory_order_relaxed), jit_phase_ns[i][1].load(std::memory_order_relaxed),
|
|
jit_phase_ns[i][2].load(std::memory_order_relaxed), jit_phase_ns[i][3].load(std::memory_order_relaxed));
|
|
#else
|
|
std::printf("EDEN_PERF_PROGRESS mono_ns=%lld core=%u compilations=%llu compile_ns=%llu evacuations=%llu\n",
|
|
mono, i, compilation[i].calls.load(std::memory_order_relaxed),
|
|
compilation[i].nanoseconds.load(std::memory_order_relaxed),
|
|
evacuations[i].load(std::memory_order_relaxed));
|
|
#endif
|
|
}
|
|
}
|
|
std::fflush(stdout);
|
|
#ifdef EDEN_DEV_PROFILE
|
|
// A late diagnostic cost measurement is not included in the reported
|
|
// game's five-second frame interval; make this observer effect explicit.
|
|
const long long snapshot_end = ClockNs(CLOCK_MONOTONIC);
|
|
if (mono > 0 && snapshot_end >= mono)
|
|
std::printf("EDEN_DEV_SNAPSHOT_COST mono_ns=%lld elapsed_ns=%lld\n",
|
|
mono, snapshot_end - mono);
|
|
#endif
|
|
}
|
|
|
|
#ifndef EDEN_DEV_PROFILE
|
|
// Release builds: the JIT's block hook (derived a32_interface.cpp) resolves here and does nothing,
|
|
// as does the shared JIT's compile-phase hook (headless/dynarmic/jit_impl.inc). Native packages
|
|
// cannot import an undefined weak symbol.
|
|
extern "C" void eden_jit_block(unsigned, unsigned long long, const void*, unsigned long long) {}
|
|
extern "C" void eden_jit_phases(unsigned, unsigned long long, unsigned long long, unsigned long long,
|
|
unsigned long long) {}
|
|
#endif
|
|
|
|
#ifdef EDEN_DEV_PROFILE
|
|
extern "C" void eden_jit_block(unsigned core, unsigned long long location, const void* entry,
|
|
unsigned long long size) {
|
|
if (core != 0) return;
|
|
if (!core0_blocks) core0_blocks = new JitBlock[jit_block_capacity];
|
|
std::size_t count = core0_block_count.load(std::memory_order_relaxed);
|
|
const auto address = reinterpret_cast<uintptr_t>(entry);
|
|
if (count && address < core0_blocks[count - 1].entry) count = 0; // the cache was cleared
|
|
if (count >= jit_block_capacity) return;
|
|
core0_blocks[count] = JitBlock{address, size, location};
|
|
core0_block_count.store(count + 1, std::memory_order_release);
|
|
}
|
|
|
|
void BeginPcSampling() {
|
|
struct sigaction previous{}, action{};
|
|
if (sigaction(SIGUSR2, nullptr, &previous) || previous.sa_handler != SIG_DFL)
|
|
throw std::runtime_error("Development sampler signal already in use");
|
|
action.sa_sigaction = PcSignal;
|
|
action.sa_flags = SA_SIGINFO | SA_RESTART;
|
|
sigemptyset(&action.sa_mask);
|
|
if (sigaction(SIGUSR2, &action, nullptr))
|
|
throw std::runtime_error("Cannot install development PC sampler");
|
|
// Development process lifetime; polling stops before guest-worker shutdown.
|
|
pc_sampling = true;
|
|
#if defined(EDEN_DEV_WAIT_CALLERS) && defined(PS5_NATIVE)
|
|
std::printf("EDEN_PERF_CALLER_MODE enabled=1 firmware=6.02 depth=8 page=4096\n");
|
|
#endif
|
|
std::printf("EDEN_PERF_PC_ANCHOR address=%llx capacity=%zu\n",
|
|
static_cast<unsigned long long>(reinterpret_cast<uintptr_t>(&PcSignal)), sampled_pcs.size());
|
|
std::printf("EDEN_PERF_LIBC memcpy=%p memset=%p memmove=%p memcmp=%p\n",
|
|
reinterpret_cast<void*>(&std::memcpy), reinterpret_cast<void*>(&std::memset),
|
|
reinterpret_cast<void*>(&std::memmove), reinterpret_cast<void*>(&std::memcmp));
|
|
}
|
|
|
|
void PollGpuPc() {
|
|
if (!pc_sampling) return;
|
|
#if defined(EDEN_DEV_WAIT_CALLERS) && defined(PS5_NATIVE)
|
|
const bool gpu_capacity = pc_count.load(std::memory_order_acquire) < sampled_pcs.size();
|
|
#else
|
|
const bool gpu_capacity = true; // bounded ring; retain recent samples
|
|
#endif
|
|
// SampleCpu() needs workers_mutex to publish guest progress. Do not hold
|
|
// it across a possibly blocking pthread_kill kernel call at 20 Hz.
|
|
// The already-existing worker registration is process-lifetime; the
|
|
// snapshot lock only protects copying its pthread_t, not OS liveness.
|
|
pthread_t gpu_target{};
|
|
bool gpu_ready = false;
|
|
if (gpu_capacity) {
|
|
const std::lock_guard lock(workers_mutex);
|
|
if (workers[4].registered) {
|
|
gpu_target = workers[4].thread;
|
|
gpu_ready = true;
|
|
}
|
|
}
|
|
if (gpu_ready && pthread_kill(gpu_target, SIGUSR2))
|
|
throw std::runtime_error("Cannot sample development render thread");
|
|
if (core_sample_ready.load(std::memory_order_acquire))
|
|
pthread_kill(core_sample_thread, SIGUSR2);
|
|
}
|
|
#endif
|
|
|
|
extern "C" unsigned eden_heap_arenas_created(void) __attribute__((weak));
|
|
|
|
void AllocatorCheck() {
|
|
std::printf("EDEN_PERF_HEAP arenas=%u\n", eden_heap_arenas_created ? eden_heap_arenas_created() : 1u);
|
|
constexpr unsigned ops = 50000, live_slots = 1024;
|
|
const auto run = [](unsigned seed) {
|
|
std::vector<void*> live(live_slots, nullptr);
|
|
unsigned x = seed;
|
|
const auto begin = std::chrono::steady_clock::now();
|
|
for (unsigned i = 0; i < ops; ++i) {
|
|
x = x * 1664525u + 1013904223u;
|
|
void*& slot = live[x % live_slots];
|
|
std::free(slot);
|
|
slot = std::malloc(16 + ((x >> 16) & 511));
|
|
}
|
|
const auto end = std::chrono::steady_clock::now();
|
|
for (void* pointer : live) std::free(pointer);
|
|
return std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count();
|
|
};
|
|
std::printf("EDEN_PERF_ALLOC threads=1 pairs=%u ns_per_pair=%.1f\n", ops, run(1) / double(ops));
|
|
for (unsigned threads : {2u, 4u, 8u}) {
|
|
std::vector<long long> elapsed(threads);
|
|
std::latch start{static_cast<std::ptrdiff_t>(threads)};
|
|
std::vector<std::thread> pool;
|
|
for (unsigned index = 0; index < threads; ++index)
|
|
pool.emplace_back([&, index] { start.arrive_and_wait(); elapsed[index] = run(index + 7); });
|
|
for (auto& thread : pool) thread.join();
|
|
long long worst = 0, sum = 0;
|
|
for (auto value : elapsed) { worst = std::max(worst, value); sum += value; }
|
|
std::printf("EDEN_PERF_ALLOC threads=%u pairs=%u ns_per_pair_mean=%.1f ns_per_pair_worst=%.1f\n",
|
|
threads, ops, sum / double(threads) / ops, worst / double(ops));
|
|
}
|
|
}
|
|
|
|
void PlatformChecks() {
|
|
#ifdef PS5_NATIVE
|
|
CheckWorkerTopology();
|
|
#endif
|
|
CheckCpuClocks();
|
|
#ifdef EDEN_DEV_PROFILE
|
|
AllocatorCheck();
|
|
#endif
|
|
using clock = std::chrono::steady_clock;
|
|
const auto ns = [](auto start) {
|
|
return std::chrono::duration_cast<std::chrono::nanoseconds>(clock::now() - start).count();
|
|
};
|
|
std::printf("EDEN_PERF_CLOCK leaf_max=%x invariant=%d tsc_hz=%llu native=%d\n",
|
|
__get_cpuid_max(0, nullptr), Common::g_cpu_caps.invariant_tsc,
|
|
static_cast<unsigned long long>(Common::g_cpu_caps.tsc_frequency),
|
|
Common::g_wall_clock.IsNative());
|
|
unsigned long long sum = 0;
|
|
auto start = clock::now();
|
|
for (unsigned i = 0; i < 100'000; ++i) sum += Common::g_wall_clock.GetCNTPCT();
|
|
std::printf("EDEN_PERF_CLOCK_READ calls=100000 elapsed_ns=%lld checksum=%llu\n",
|
|
static_cast<long long>(ns(start)), sum);
|
|
// Bound the diagnostic overhead comparison; per-block timers use the
|
|
// already-qualified fenced counter, with the upstream steady-clock fallback.
|
|
start = clock::now();
|
|
for (unsigned i = 0; i < 1000; ++i) sum += clock::now().time_since_epoch().count();
|
|
const auto system_clock_ns = ns(start);
|
|
start = clock::now();
|
|
for (unsigned i = 0; i < 1000; ++i) sum += Common::g_wall_clock.GetTimeNS().count();
|
|
std::printf("EDEN_PERF_DIAGNOSTIC_CLOCK calls=1000 system_ns=%lld counter_ns=%lld checksum=%llu\n",
|
|
static_cast<long long>(system_clock_ns), static_cast<long long>(ns(start)), sum);
|
|
for (const unsigned requested_us : {1000u, 16667u, 50000u}) {
|
|
const auto wall = Common::g_wall_clock.GetTimeNS();
|
|
start = clock::now();
|
|
std::this_thread::sleep_for(std::chrono::microseconds(requested_us));
|
|
const auto mono_delta = ns(start);
|
|
const auto wall_delta = (Common::g_wall_clock.GetTimeNS() - wall).count();
|
|
if (mono_delta <= 0 || wall_delta <= 0) throw std::runtime_error("Nonmonotonic clock");
|
|
if (std::abs(wall_delta - mono_delta) > std::max<decltype(mono_delta)>(2'000'000, mono_delta / 20))
|
|
throw std::runtime_error("Guest clock disagrees with monotonic time");
|
|
std::printf("EDEN_PERF_SLEEP requested_us=%u mono_ns=%lld wall_ns=%lld\n",
|
|
requested_us, static_cast<long long>(mono_delta), static_cast<long long>(wall_delta));
|
|
}
|
|
constexpr std::size_t bytes = 16 * 1024 * 1024;
|
|
auto* memory = static_cast<unsigned long long*>(Common::AllocateMemoryPages(bytes));
|
|
if (!memory) throw std::bad_alloc{};
|
|
struct Release {
|
|
void* memory;
|
|
~Release() { Common::FreeMemoryPages(memory, bytes); }
|
|
} release{memory};
|
|
constexpr std::size_t words = bytes / sizeof(*memory);
|
|
for (std::size_t i = 0; i < words; ++i) memory[i] = i;
|
|
const volatile auto* reads = memory;
|
|
sum = 0;
|
|
start = clock::now();
|
|
for (unsigned repeat = 0; repeat < 4; ++repeat)
|
|
for (std::size_t i = 0; i < words; ++i) sum += reads[i];
|
|
if (sum != 4ULL * words * (words - 1) / 2) throw std::runtime_error("Memory read checksum");
|
|
std::printf("EDEN_PERF_MEMORY pattern=stream bytes=%zu elapsed_ns=%lld checksum=%llu\n",
|
|
bytes * 4, static_cast<long long>(ns(start)), sum);
|
|
// Full-cycle LCG, one node per cache line. Compare a small warm working set
|
|
// with the same dependent-load loop across the larger owned mapping.
|
|
for (const std::size_t nodes : {std::size_t{512}, bytes / 64}) {
|
|
for (std::size_t i = 0; i < nodes; ++i) memory[i * 8] = (i * 5 + 1) & (nodes - 1);
|
|
std::size_t index = 0;
|
|
constexpr unsigned steps = 1'048'576;
|
|
start = clock::now();
|
|
for (unsigned i = 0; i < steps; ++i) index = reads[index * 8];
|
|
if (index != 0) throw std::runtime_error("Memory chain checksum");
|
|
std::printf("EDEN_PERF_MEMORY pattern=dependent working_bytes=%zu steps=%u elapsed_ns=%lld checksum=%zu\n",
|
|
nodes * 64, steps, static_cast<long long>(ns(start)), index);
|
|
}
|
|
std::fflush(stdout);
|
|
}
|
|
} // namespace Eden::Performance
|
|
|
|
// dynarmic A64 hooks (weak there): whole-cache evacuations and block compilation time.
|
|
extern "C" void eden_jit_evacuation(unsigned core) {
|
|
if (core < Eden::Performance::evacuations.size())
|
|
Eden::Performance::evacuations[core].fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
#ifdef EDEN_DEV_PROFILE
|
|
// Development builds only (the per-phase and duplicate counters exist there).
|
|
extern "C" void eden_jit_phases(unsigned core, unsigned long long translate, unsigned long long optimize,
|
|
unsigned long long emit, unsigned long long location) {
|
|
if (core >= Eden::Performance::jit_phase_ns.size()) return;
|
|
auto& phases = Eden::Performance::jit_phase_ns[core];
|
|
phases[0].fetch_add(translate, std::memory_order_relaxed);
|
|
phases[1].fetch_add(optimize, std::memory_order_relaxed);
|
|
phases[2].fetch_add(emit, std::memory_order_relaxed);
|
|
if (!Eden::Performance::jit_duplicate_tracking.load(std::memory_order_relaxed)) return;
|
|
// Which cores compiled each location, and when the first one did.
|
|
struct First { unsigned cores; long long ns; };
|
|
static std::mutex mutex;
|
|
static auto* compiled = [] {
|
|
auto* map = new std::unordered_map<unsigned long long, First>;
|
|
map->reserve(1u << 21);
|
|
return map;
|
|
}();
|
|
const long long now = Common::g_wall_clock.GetTimeNS().count();
|
|
unsigned index = 0;
|
|
{
|
|
std::lock_guard lock{mutex};
|
|
auto [it, inserted] = compiled->try_emplace(location, First{1u << core, now});
|
|
if (!inserted) {
|
|
if (it->second.cores & (1u << core)) {
|
|
index = 1;
|
|
} else {
|
|
it->second.cores |= 1u << core;
|
|
const long long lead = now - it->second.ns;
|
|
index = 2;
|
|
for (long long bound = 1'000'000; index < 7 && lead >= bound; bound *= 10) ++index;
|
|
}
|
|
}
|
|
}
|
|
Eden::Performance::jit_duplicates[core][index].fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
extern "C" void eden_jit_ranges(unsigned core, unsigned long long ns) {
|
|
if (core < Eden::Performance::jit_phase_ns.size())
|
|
Eden::Performance::jit_phase_ns[core][3].fetch_add(ns, std::memory_order_relaxed);
|
|
}
|
|
#endif
|
|
// The shared JIT's compile timers (headless/dynarmic/jit_impl.inc): the qualified invariant-TSC
|
|
// clock, about 26 ns per read against about 0.9 µs for the steady clock's system call.
|
|
extern "C" unsigned long long eden_jit_clock_ns() {
|
|
return static_cast<unsigned long long>(Common::g_wall_clock.GetTimeNS().count());
|
|
}
|
|
extern "C" void eden_jit_compile(unsigned core, unsigned long long ns) {
|
|
if (core >= Eden::Performance::compilation.size()) return;
|
|
Eden::Performance::compilation[core].calls.fetch_add(1, std::memory_order_relaxed);
|
|
Eden::Performance::compilation[core].nanoseconds.fetch_add(ns, std::memory_order_relaxed);
|
|
}
|