// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "Common/PerformanceTrace.h" #include #include #include #ifdef _WIN32 #include #include #pragma comment(lib, "advapi32.lib") TRACELOGGING_DEFINE_PROVIDER(s_dolphin_perf, "Dolphin.Performance", (0x54ee9231, 0x28be, 0x4ab0, 0x98, 0x1e, 0xb2, 0x6f, 0x97, 0x9c, 0x56, 0xa4)); #endif namespace Common::PerformanceTrace { namespace { constexpr u32 SCHEMA = 1; constexpr u32 SAMPLE_MASK = 1023; struct ThreadState { bool sampling = false; u32 rng = 0x9e3779b9; u64 roots = 0; u64 selected = 0; u64 last_snapshot = 0; ScopeStack stack; std::array(Counter::Count)> counters{}; }; thread_local ThreadState s_thread; #ifdef _WIN32 struct Registration { Registration() : status(TraceLoggingRegister(s_dolphin_perf)) {} ~Registration() { if (status == ERROR_SUCCESS) TraceLoggingUnregister(s_dolphin_perf); } ULONG status; }; #endif } // namespace bool ScopeStack::Begin(Region region, u64 now) { if (m_depth == m_stack.size() || static_cast(region) >= m_timings.size()) { ++m_errors; return false; } m_stack[m_depth++] = {region, now, 0}; return true; } bool ScopeStack::End(u64 now) { if (m_depth == 0) { ++m_errors; return false; } const Entry entry = m_stack[--m_depth]; if (now < entry.start || now - entry.start < entry.children) { ++m_errors; return false; } const u64 elapsed = now - entry.start; auto& timing = m_timings[static_cast(entry.region)]; ++timing.calls; timing.inclusive += elapsed; timing.exclusive += elapsed - entry.children; if (m_depth != 0) m_stack[m_depth - 1].children += elapsed; return true; } void ScopeStack::Reset() { m_depth = 0; m_errors = 0; m_timings = {}; } bool Requested() { #ifdef _WIN32 static const bool requested = [] { const char* value = std::getenv("DOLPHIN_PERF_TRACE"); return value && std::strcmp(value, "1") == 0; }(); return requested; #else return false; #endif } bool Enabled() { #ifdef _WIN32 if (!Requested()) return false; static Registration registration; return registration.status == ERROR_SUCCESS && TraceLoggingProviderEnabled(s_dolphin_perf, 0, 0); #else return false; #endif } u64 Now() { #ifdef _WIN32 LARGE_INTEGER value; QueryPerformanceCounter(&value); return static_cast(value.QuadPart); #else return 0; #endif } u64 Frequency() { #ifdef _WIN32 static const u64 frequency = [] { LARGE_INTEGER value; QueryPerformanceFrequency(&value); return static_cast(value.QuadPart); }(); return frequency; #else return 0; #endif } void Add(Counter counter, u64 value) { if (Requested() && static_cast(counter) < s_thread.counters.size()) s_thread.counters[static_cast(counter)] += value; } u64 NewGeneration() { static std::atomic generation{0}; return Requested() ? generation.fetch_add(1, std::memory_order_relaxed) + 1 : 0; } SampleRoot::SampleRoot(bool force_for_test) { m_denominator = force_for_test ? 1 : SAMPLE_MASK + 1; if (!Enabled() || s_thread.sampling) return; ++s_thread.roots; auto& x = s_thread.rng; x ^= x << 13; x ^= x >> 17; x ^= x << 5; if (!force_for_test && (x & SAMPLE_MASK) != 0) return; ++s_thread.selected; s_thread.stack.Reset(); m_start = Now(); m_active = s_thread.stack.Begin(Region::Slice, m_start); s_thread.sampling = m_active; } SampleRoot::~SampleRoot() { if (!m_active) return; const u64 end = Now(); s_thread.stack.End(end); s_thread.sampling = false; #ifdef _WIN32 // Sampled wall times, NOT exact total CPU times. Nested regions are exclusive here. for (size_t i = 0; i < s_thread.stack.Timings().size(); ++i) { const auto& timing = s_thread.stack.Timings()[i]; if (timing.calls == 0) continue; TraceLoggingWrite(s_dolphin_perf, "ScopeSample", TraceLoggingUInt32(SCHEMA, "Schema"), TraceLoggingUInt64(m_start, "BeginQPC"), TraceLoggingUInt64(end, "EndQPC"), TraceLoggingUInt64(s_thread.selected, "Sample"), TraceLoggingUInt32(m_denominator, "SamplingDenominator"), TraceLoggingUInt32(static_cast(i), "Region"), TraceLoggingUInt64(timing.calls, "Calls"), TraceLoggingUInt64(timing.inclusive, "InclusiveQPC"), TraceLoggingUInt64(timing.exclusive, "ExclusiveQPC"), TraceLoggingUInt64(s_thread.stack.Errors(), "AccountingErrors")); } #endif } Scope::Scope(Region region) { if (s_thread.sampling) m_active = s_thread.stack.Begin(region, Now()); } Scope::~Scope() { if (m_active) s_thread.stack.End(Now()); } void Snapshot(u64 core_id, u64 instructions, u64 native_instructions, u64 slow_reads, u64 slow_writes, u64 compiles, u64 fallbacks, u32 pc, bool waiting) { if (!Enabled()) return; const u64 now = Now(); if (now - s_thread.last_snapshot < Frequency()) return; s_thread.last_snapshot = now; #ifdef _WIN32 TraceLoggingWrite( s_dolphin_perf, "Snapshot", TraceLoggingUInt32(SCHEMA, "Schema"), TraceLoggingUInt64(now, "QPC"), TraceLoggingUInt64(Frequency(), "QPCFrequency"), TraceLoggingUInt64(core_id, "CoreId"), TraceLoggingUInt64(instructions, "Instructions"), TraceLoggingUInt64(native_instructions, "NativeInstructions"), TraceLoggingUInt64(slow_reads, "SlowReads"), TraceLoggingUInt64(slow_writes, "SlowWrites"), TraceLoggingUInt64(compiles, "Compiles"), TraceLoggingUInt64(fallbacks, "Fallbacks"), TraceLoggingUInt32(pc, "GuestPC"), TraceLoggingBool(waiting, "Waiting"), TraceLoggingUInt64(s_thread.roots, "Roots"), TraceLoggingUInt64(s_thread.selected, "SelectedRoots")); for (size_t i = 0; i < s_thread.counters.size(); ++i) { TraceLoggingWrite(s_dolphin_perf, "Counter", TraceLoggingUInt32(SCHEMA, "Schema"), TraceLoggingUInt64(now, "QPC"), TraceLoggingUInt32(static_cast(i), "Counter"), TraceLoggingUInt64(s_thread.counters[i], "Value")); } #endif } void CodeMap(const char* domain, const void* owner, const void* begin, const void* end, u32 guest_pc, u64 identity, const char* kind) { if (!begin || reinterpret_cast(end) <= reinterpret_cast(begin) || !Enabled()) return; #ifdef _WIN32 TraceLoggingWrite(s_dolphin_perf, "CodeMap", TraceLoggingUInt32(SCHEMA, "Schema"), TraceLoggingUInt64(Now(), "QPC"), TraceLoggingString(domain, "Domain"), TraceLoggingPointer(owner, "Owner"), TraceLoggingPointer(begin, "Begin"), TraceLoggingPointer(end, "End"), TraceLoggingUInt32(guest_pc, "GuestPC"), TraceLoggingUInt64(identity, "Identity"), TraceLoggingString(kind, "Kind")); #endif } void CodeReset(const char* domain, const void* owner, const void* begin, const void* end) { CodeMap(domain, owner, begin, end, 0, 0, "invalidate"); } void Frame(const char* kind, s64 guest_ticks, u32 guest_frequency) { if (!Enabled()) return; #ifdef _WIN32 TraceLoggingWrite(s_dolphin_perf, "Frame", TraceLoggingUInt32(SCHEMA, "Schema"), TraceLoggingUInt64(Now(), "QPC"), TraceLoggingUInt64(Frequency(), "QPCFrequency"), TraceLoggingString(kind, "Kind"), TraceLoggingInt64(guest_ticks, "GuestTicks"), TraceLoggingUInt32(guest_frequency, "GuestFrequency")); #endif } void Marker(const char* name) { if (!Enabled()) return; #ifdef _WIN32 TraceLoggingWrite(s_dolphin_perf, "Marker", TraceLoggingUInt32(SCHEMA, "Schema"), TraceLoggingUInt64(Now(), "QPC"), TraceLoggingUInt64(Frequency(), "QPCFrequency"), TraceLoggingString(name, "Name")); #endif } void Queue(const void* owner, u64 frames, u64 bytes, u64 oldest_qpc, u64 peak) { if (!Enabled()) return; #ifdef _WIN32 TraceLoggingWrite(s_dolphin_perf, "USBQueue", TraceLoggingUInt32(SCHEMA, "Schema"), TraceLoggingUInt64(Now(), "QPC"), TraceLoggingPointer(owner, "Owner"), TraceLoggingUInt64(frames, "Frames"), TraceLoggingUInt64(bytes, "Bytes"), TraceLoggingUInt64(oldest_qpc, "OldestQPC"), TraceLoggingUInt64(peak, "PeakFrames")); #endif } } // namespace Common::PerformanceTrace