mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 16:17:06 +02:00
323 lines
16 KiB
C++
323 lines
16 KiB
C++
#include "profiler_internal.h"
|
|
#include <TlHelp32.h>
|
|
#include <psapi.h>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <array>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <thread>
|
|
|
|
namespace dingosdk::profiler {
|
|
std::atomic<bool> zones_enabled{false};
|
|
namespace detail {
|
|
std::atomic<std::uint32_t> client_thread{0}, present_thread{0};
|
|
std::atomic<bool> sampling{false};
|
|
}
|
|
namespace {
|
|
std::atomic<Site*> sites{nullptr};
|
|
std::atomic<bool> 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<std::atomic<float>, history_size> values{};
|
|
std::atomic<std::uint64_t> 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<Ring, 4> rings;
|
|
|
|
// Totals since the monitor's last summary, taken with exchange().
|
|
struct Totals {
|
|
std::atomic<std::uint64_t> 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<const Summary> latest_summary = std::make_shared<Summary>();
|
|
|
|
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<std::uint64_t>(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<DWORD, Thread> 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<LPCWSTR>(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<char>(*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<char>(*c) : '?');
|
|
LocalFree(description);
|
|
}
|
|
if (!result.empty()) return result;
|
|
using Query = LONG(NTAPI*)(HANDLE, int, PVOID, ULONG, PULONG);
|
|
static const auto query = reinterpret_cast<Query>(
|
|
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationThread"));
|
|
void* start{};
|
|
if (query) query(handle, 9 /* ThreadQuerySetWin32StartAddress */, &start, sizeof(start), nullptr);
|
|
auto module = module_of(reinterpret_cast<std::uintptr_t>(start));
|
|
std::string lower = module;
|
|
for (auto& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(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<double>(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<double>(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<double>(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<std::uint32_t>(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<double>(now - last).count());
|
|
last = now;
|
|
const auto now_ticks = __rdtsc();
|
|
const double ms_per_tick = seconds * 1e3 / static_cast<double>(std::max<std::uint64_t>(1, now_ticks - last_ticks));
|
|
last_ticks = now_ticks;
|
|
auto next = std::make_shared<Summary>();
|
|
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<double>(ns) / 1e6; };
|
|
const auto interval = client_interval.take(), frame = client_frame.take(), own = client_own.take();
|
|
s.client_updates_per_second = static_cast<double>(own.count) / seconds;
|
|
if (interval.count) {
|
|
s.client_interval_average_ms = ms(interval.sum) / static_cast<double>(interval.count);
|
|
s.client_interval_longest_ms = ms(interval.longest);
|
|
}
|
|
if (frame.count) {
|
|
s.client_frame_average_ms = ms(frame.sum) / static_cast<double>(frame.count);
|
|
s.client_frame_longest_ms = ms(frame.longest);
|
|
}
|
|
if (own.count) {
|
|
s.client_own_average_ms = ms(own.sum) / static_cast<double>(own.count);
|
|
s.client_own_longest_ms = ms(own.longest);
|
|
}
|
|
const auto presents = present_interval.take();
|
|
s.presents_per_second = static_cast<double>(presents.count) / seconds;
|
|
if (presents.count) {
|
|
s.present_interval_average_ms = ms(presents.sum) / static_cast<double>(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<double>(site->ticks.exchange(0, std::memory_order_relaxed)) * ms_per_tick;
|
|
const auto longest = static_cast<double>(site->longest.exchange(0, std::memory_order_relaxed)) * ms_per_tick;
|
|
if (!calls) continue;
|
|
ZoneRow row{site->name, static_cast<double>(calls) / seconds, total / seconds,
|
|
total * 1e3 / static_cast<double>(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<double>(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<std::size_t>(Series::client_interval)].push(static_cast<float>(now - last) / 1e6f);
|
|
}
|
|
last = now;
|
|
client_own.add(own_ns);
|
|
rings[static_cast<std::size_t>(Series::client_own)].push(static_cast<float>(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<std::size_t>(Series::client_frame)].push(static_cast<float>(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<std::size_t>(Series::present_interval)].push(static_cast<float>(now - last) / 1e6f);
|
|
}
|
|
last = now;
|
|
}
|
|
|
|
std::size_t history(Series series, std::span<float> out) noexcept {
|
|
auto& ring = rings[static_cast<std::size_t>(series)];
|
|
const auto written = ring.written.load(std::memory_order_acquire);
|
|
const auto count = static_cast<std::size_t>(std::min<std::uint64_t>({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<const Summary> summary() noexcept {
|
|
std::lock_guard lock(summary_mutex);
|
|
return latest_summary;
|
|
}
|
|
} // namespace dingosdk::profiler
|