#include "profiler_internal.h" #include #include #include #include #include #include #include #include namespace dingosdk::profiler { std::atomic zones_enabled{false}; namespace detail { std::atomic client_thread{0}, present_thread{0}; std::atomic sampling{false}; } namespace { std::atomic sites{nullptr}; std::atomic hud_visible{false}, window_visible{false}, monitor_running{false}; // Recent values for the HUD's graphs. One writer each (the client update thread, the present // thread); the overlay reads them racily, which at worst shows one value twice. constexpr std::size_t history_size = 512; struct Ring { std::array, history_size> values{}; std::atomic written{0}; void push(float value) noexcept { const auto at = written.load(std::memory_order_relaxed); values[at % history_size].store(value, std::memory_order_relaxed); written.store(at + 1, std::memory_order_release); } }; std::array rings; // Totals since the monitor's last summary, taken with exchange(). struct Totals { std::atomic count{0}, sum{0}, longest{0}; void add(std::uint64_t value) noexcept { count.fetch_add(1, std::memory_order_relaxed); sum.fetch_add(value, std::memory_order_relaxed); auto previous = longest.load(std::memory_order_relaxed); while (value > previous && !longest.compare_exchange_weak(previous, value, std::memory_order_relaxed)) {} } struct Taken { std::uint64_t count, sum, longest; }; Taken take() noexcept { return {count.exchange(0, std::memory_order_relaxed), sum.exchange(0, std::memory_order_relaxed), longest.exchange(0, std::memory_order_relaxed)}; } }; Totals client_interval, client_frame, client_own, present_interval; std::mutex summary_mutex; std::shared_ptr latest_summary = std::make_shared(); bool wanted() noexcept { return hud_visible.load(std::memory_order_acquire) || window_visible.load(std::memory_order_acquire) || detail::sampling.load(std::memory_order_acquire); } std::uint64_t filetime(const FILETIME& value) noexcept { return (static_cast(value.dwHighDateTime) << 32) | value.dwLowDateTime; } // Per-thread CPU use, from GetThreadTimes deltas. The thread list is refreshed every few // seconds: a Toolhelp snapshot walks every thread on the system. class ThreadMonitor { struct Thread { HANDLE handle{}; std::uint64_t last{}; std::string label; bool seen{}; }; std::map threads_; std::uint64_t last_process_{}, last_wall_{}; std::chrono::steady_clock::time_point next_refresh_{}; static std::string module_of(std::uintptr_t address) { HMODULE module{}; if (!address || !GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module)) return {}; wchar_t name[MAX_PATH]{}; if (!GetModuleBaseNameW(GetCurrentProcess(), module, name, MAX_PATH)) return {}; std::string result; for (const wchar_t* c = name; *c; ++c) result.push_back(*c < 128 ? static_cast(*c) : '?'); return result; } static std::string label_of(HANDLE handle) { PWSTR description{}; std::string result; if (SUCCEEDED(GetThreadDescription(handle, &description)) && description) { for (const wchar_t* c = description; *c; ++c) result.push_back(*c < 128 ? static_cast(*c) : '?'); LocalFree(description); } if (!result.empty()) return result; using Query = LONG(NTAPI*)(HANDLE, int, PVOID, ULONG, PULONG); static const auto query = reinterpret_cast( GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationThread")); void* start{}; if (query) query(handle, 9 /* ThreadQuerySetWin32StartAddress */, &start, sizeof(start), nullptr); auto module = module_of(reinterpret_cast(start)); std::string lower = module; for (auto& c : lower) c = static_cast(std::tolower(static_cast(c))); // Frostbite starts its threads through the CRT, so they all begin in ucrtbase. if (lower == "reskate.dll") return "ReSkate worker"; if (lower == "ucrtbase.dll" || lower == "skate.exe") return "Skate"; if (lower.starts_with("steam") || lower.starts_with("tier0") || lower.starts_with("gameoverlay")) return "Steam"; if (lower.starts_with("amd") || lower.starts_with("nv") || lower.starts_with("igd") || lower.starts_with("d3d")) return "Graphics driver"; return module.empty() ? "Thread" : module; } void refresh() { for (auto& [id, thread] : threads_) thread.seen = false; const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (snapshot == INVALID_HANDLE_VALUE) return; const auto self = GetCurrentProcessId(); THREADENTRY32 entry{sizeof(entry)}; for (BOOL ok = Thread32First(snapshot, &entry); ok; ok = Thread32Next(snapshot, &entry)) { if (entry.th32OwnerProcessID != self) continue; auto [it, fresh] = threads_.try_emplace(entry.th32ThreadID); it->second.seen = true; if (fresh) { // Full query rights read the start address (for the label); limited ones still time it. it->second.handle = OpenThread(THREAD_QUERY_INFORMATION, FALSE, entry.th32ThreadID); if (!it->second.handle) it->second.handle = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ThreadID); if (it->second.handle) { FILETIME created, exited, kernel, user; if (GetThreadTimes(it->second.handle, &created, &exited, &kernel, &user)) it->second.last = filetime(kernel) + filetime(user); it->second.label = label_of(it->second.handle); } } } CloseHandle(snapshot); for (auto it = threads_.begin(); it != threads_.end();) { if (it->second.seen) { ++it; continue; } if (it->second.handle) CloseHandle(it->second.handle); it = threads_.erase(it); } } public: static std::string label(HANDLE handle) { return label_of(handle); } void sample(Summary& summary) { const auto now = std::chrono::steady_clock::now(); if (now >= next_refresh_) { refresh(); next_refresh_ = now + std::chrono::seconds(3); } FILETIME wall_time; GetSystemTimePreciseAsFileTime(&wall_time); const auto wall = filetime(wall_time); const double elapsed = last_wall_ ? static_cast(wall - last_wall_) : 0.0; FILETIME created, exited, kernel, user; if (GetProcessTimes(GetCurrentProcess(), &created, &exited, &kernel, &user)) { const auto process = filetime(kernel) + filetime(user); if (elapsed > 0 && last_process_) summary.process_cpu_percent = 100.0 * static_cast(process - last_process_) / elapsed; last_process_ = process; } const auto client = detail::client_thread.load(std::memory_order_relaxed); const auto present = detail::present_thread.load(std::memory_order_relaxed); for (auto& [id, thread] : threads_) { if (!thread.handle || !GetThreadTimes(thread.handle, &created, &exited, &kernel, &user)) continue; const auto total = filetime(kernel) + filetime(user); const double percent = elapsed > 0 ? 100.0 * static_cast(total - thread.last) / elapsed : 0.0; thread.last = total; if (percent < 0.5 && id != client && id != present) continue; std::string label = id == client ? "Client update" : id == present ? "Present" : thread.label; summary.threads.push_back({static_cast(id), std::move(label), percent}); } last_wall_ = wall; std::ranges::sort(summary.threads, [](const ThreadRow& a, const ThreadRow& b) { return a.cpu_percent > b.cpu_percent; }); } ~ThreadMonitor() { for (auto& [id, thread] : threads_) if (thread.handle) CloseHandle(thread.handle); } }; void monitor() { SetThreadDescription(GetCurrentThread(), L"ReSkate profiler"); ThreadMonitor threads; auto last = std::chrono::steady_clock::now(); // Zone ticks to nanoseconds: the timestamp counter measured against the steady clock over // each summary's second. auto last_ticks = __rdtsc(); while (wanted()) { zones_enabled.store(true, std::memory_order_relaxed); Sleep(1000); const auto now = std::chrono::steady_clock::now(); const double seconds = std::max(1e-3, std::chrono::duration(now - last).count()); last = now; const auto now_ticks = __rdtsc(); const double ms_per_tick = seconds * 1e3 / static_cast(std::max(1, now_ticks - last_ticks)); last_ticks = now_ticks; auto next = std::make_shared(); auto& s = *next; s.seconds = seconds; s.cores = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); s.client_thread = detail::client_thread.load(std::memory_order_relaxed); s.present_thread = detail::present_thread.load(std::memory_order_relaxed); const auto ms = [](std::uint64_t ns) { return static_cast(ns) / 1e6; }; const auto interval = client_interval.take(), frame = client_frame.take(), own = client_own.take(); s.client_updates_per_second = static_cast(own.count) / seconds; if (interval.count) { s.client_interval_average_ms = ms(interval.sum) / static_cast(interval.count); s.client_interval_longest_ms = ms(interval.longest); } if (frame.count) { s.client_frame_average_ms = ms(frame.sum) / static_cast(frame.count); s.client_frame_longest_ms = ms(frame.longest); } if (own.count) { s.client_own_average_ms = ms(own.sum) / static_cast(own.count); s.client_own_longest_ms = ms(own.longest); } const auto presents = present_interval.take(); s.presents_per_second = static_cast(presents.count) / seconds; if (presents.count) { s.present_interval_average_ms = ms(presents.sum) / static_cast(presents.count); s.present_interval_longest_ms = ms(presents.longest); } double hooks_ms{}; for (auto* site = sites.load(std::memory_order_acquire); site; site = site->next) { const auto calls = site->calls.exchange(0, std::memory_order_relaxed); const auto total = static_cast(site->ticks.exchange(0, std::memory_order_relaxed)) * ms_per_tick; const auto longest = static_cast(site->longest.exchange(0, std::memory_order_relaxed)) * ms_per_tick; if (!calls) continue; ZoneRow row{site->name, static_cast(calls) / seconds, total / seconds, total * 1e3 / static_cast(calls), longest}; if (std::string_view(site->name).starts_with("hooks/")) hooks_ms += total; // Sites sharing a name (the same work in two hooks) show as one row. const auto same = std::ranges::find(s.zones, row.name, &ZoneRow::name); if (same == s.zones.end()) { s.zones.push_back(std::move(row)); continue; } const auto calls_total = same->calls_per_second + row.calls_per_second; same->average_microseconds = calls_total > 0 ? (same->average_microseconds * same->calls_per_second + row.average_microseconds * row.calls_per_second) / calls_total : 0.0; same->calls_per_second = calls_total; same->milliseconds_per_second += row.milliseconds_per_second; same->longest_milliseconds = std::max(same->longest_milliseconds, row.longest_milliseconds); } if (own.count) s.hooks_milliseconds_per_update = hooks_ms / static_cast(own.count); std::ranges::sort(s.zones, [](const ZoneRow& a, const ZoneRow& b) { return a.milliseconds_per_second > b.milliseconds_per_second; }); threads.sample(s); std::lock_guard lock(summary_mutex); latest_summary = std::move(next); } zones_enabled.store(false, std::memory_order_relaxed); monitor_running.store(false, std::memory_order_release); // Something may have asked again between the last check and the flag being cleared. if (wanted()) detail::wake_monitor(); } } // namespace namespace detail { std::string thread_label(HANDLE thread, std::uint32_t id) { if (id == client_thread.load(std::memory_order_relaxed)) return "Client update"; if (id == present_thread.load(std::memory_order_relaxed)) return "Present"; return ThreadMonitor::label(thread); } void wake_monitor() noexcept { if (!wanted() || monitor_running.exchange(true, std::memory_order_acq_rel)) return; zones_enabled.store(true, std::memory_order_relaxed); try { std::thread(monitor).detach(); } catch (...) { monitor_running.store(false, std::memory_order_release); } } } Site::Site(const char* site_name) noexcept : name(site_name) { auto* head = sites.load(std::memory_order_relaxed); do { next = head; } while (!sites.compare_exchange_weak(head, this, std::memory_order_release, std::memory_order_relaxed)); } void record_client_update(std::uint64_t own_ns) noexcept { // Known even while off, so a sample can target it straight away. detail::client_thread.store(GetCurrentThreadId(), std::memory_order_relaxed); if (!zones_enabled.load(std::memory_order_relaxed)) return; static std::uint64_t last{}; const auto now = now_ns(); if (last && now > last) { client_interval.add(now - last); rings[static_cast(Series::client_interval)].push(static_cast(now - last) / 1e6f); } last = now; client_own.add(own_ns); rings[static_cast(Series::client_own)].push(static_cast(own_ns) / 1e6f); } void record_client_frame(std::uint64_t frame_ns) noexcept { if (!zones_enabled.load(std::memory_order_relaxed)) return; client_frame.add(frame_ns); rings[static_cast(Series::client_frame)].push(static_cast(frame_ns) / 1e6f); } void record_present() noexcept { detail::present_thread.store(GetCurrentThreadId(), std::memory_order_relaxed); if (!zones_enabled.load(std::memory_order_relaxed)) return; static std::uint64_t last{}; const auto now = now_ns(); if (last && now > last) { present_interval.add(now - last); rings[static_cast(Series::present_interval)].push(static_cast(now - last) / 1e6f); } last = now; } std::size_t history(Series series, std::span out) noexcept { auto& ring = rings[static_cast(series)]; const auto written = ring.written.load(std::memory_order_acquire); const auto count = static_cast(std::min({written, history_size, out.size()})); for (std::size_t i = 0; i < count; ++i) out[i] = ring.values[(written - count + i) % history_size].load(std::memory_order_relaxed); return count; } void set_hud(bool visible) noexcept { hud_visible.store(visible, std::memory_order_release); detail::wake_monitor(); } bool hud() noexcept { return hud_visible.load(std::memory_order_acquire); } void set_window(bool visible) noexcept { window_visible.store(visible, std::memory_order_release); detail::wake_monitor(); } bool window() noexcept { return window_visible.load(std::memory_order_acquire); } bool active() noexcept { return wanted(); } std::shared_ptr summary() noexcept { std::lock_guard lock(summary_mutex); return latest_summary; } } // namespace dingosdk::profiler