#include "Extension/Console/commands.h" #include "Engine/Core/Profiling/profiler.h" #include "Extension/Settings/engine_tweaks.h" #include "Extension/Settings/job_spin.h" #include "Extension/UI/Overlay/overlay.h" #include "Extension/Music/spotify_station.h" #include "Extension/Music/music_inject.h" #include "Extension/Music/spotify_web.h" #include "Extension/Music/spotify_device.h" #include "Extension/Music/spotify_dashboard.h" #include "Extension/UI/ui_pointer_skip.h" #include "Extension/World/texture_handle_limit.h" #include "Engine/Core/Hooks/hooks.h" #include #include #include #include #include #include #include #include #include // The performance profiler's console commands (Engine/Core/Profiling/profiler.h). Everything but // the saved HUD switch runs on the overlay's thread: the profiler is thread-safe. namespace dingosdk::console { namespace { std::string narrow(const std::filesystem::path& path) { std::string result; for (const auto c : path.wstring()) result.push_back(c < 128 ? static_cast(c) : '?'); return result; } void print_summary(const Output& out) { if (!profiler::active()) { out("The profiler is off: `perf hud on` or `perf window on` starts it (figures follow a second later)."); return; } const auto s = profiler::summary(); if (!s || s->seconds <= 0) { out("No figures yet; try again in a second."); return; } out(std::format("Client update: {:.1f}/s, every {:.1f} ms (longest {:.1f}); client frame {:.2f} ms (longest {:.1f}); " "ReSkate tick {:.2f} ms (longest {:.1f}) + hooks {:.2f} ms per frame", s->client_updates_per_second, s->client_interval_average_ms, s->client_interval_longest_ms, s->client_frame_average_ms, s->client_frame_longest_ms, s->client_own_average_ms, s->client_own_longest_ms, s->hooks_milliseconds_per_update)); out(std::format("Frames: {:.1f}/s, {:.2f} ms (longest {:.1f}); process CPU {:.0f}% of {} cores", s->presents_per_second, s->present_interval_average_ms, s->present_interval_longest_ms, s->process_cpu_percent, s->cores)); out("ReSkate zones (busiest first):"); std::size_t shown{}; for (const auto& zone : s->zones) { if (shown++ == 12) break; out(std::format(" {:<48} {:7.2f} ms/s {:7.0f}/s {:8.1f} us avg {:6.2f} ms longest", zone.name, zone.milliseconds_per_second, zone.calls_per_second, zone.average_microseconds, zone.longest_milliseconds)); } out("Threads (100% = one core):"); shown = 0; for (const auto& thread : s->threads) { if (shown++ == 10) break; out(std::format(" {:>6} {:<26} {:5.1f}%", thread.id, thread.label, thread.cpu_percent)); } } void print_report(const Output& out) { const auto r = profiler::sample_report(); if (!r || (!r->running && !r->samples && r->error.empty())) { out("No sample yet: `perf sample` takes one."); return; } if (r->running) { out(std::format("Sampling {}: {:.0f}%, {} samples so far.", r->target, r->progress * 100, r->samples)); return; } if (!r->error.empty()) out("error: " + r->error); if (!r->samples) return; out(std::format("{} (thread {}): {} samples over {:.1f} s, busy {:.1f}%.", r->target, r->thread_id, r->samples, r->seconds, r->busy_percent)); for (const auto& thread : r->threads) out(std::format(" {:5.1f}% thread {} {}", thread.cpu_percent, thread.id, thread.label)); out(" Self Total Function"); std::size_t shown{}; for (const auto& f : r->functions) { if (shown++ == 20) break; out(std::format("{:5.1f}% {:5.1f}% {}", 100.0 * f.self / r->samples, 100.0 * f.total / r->samples, f.name)); } out("Report: " + narrow(r->report)); out("Flame graph (open at speedscope.app): " + narrow(r->folded)); } } // `perf memory`: what the game's memory is made of, by allocation, and what was added since a // mark: for finding what a full server's memory goes on. An allocation is one VirtualAlloc // reservation (the engine's arenas and pools are each one or a few); its size here is what of // it is committed, which is what counts against the PC's memory. struct MemoryMap { std::map allocations; // private memory: base -> committed bytes std::uint64_t committed{}, mapped{}, image{}; }; MemoryMap read_memory_map() { MemoryMap map; SYSTEM_INFO system{}; GetSystemInfo(&system); auto at = reinterpret_cast(system.lpMinimumApplicationAddress); const auto end = reinterpret_cast(system.lpMaximumApplicationAddress); MEMORY_BASIC_INFORMATION info{}; while (at < end && VirtualQuery(reinterpret_cast(at), &info, sizeof(info)) == sizeof(info)) { if (info.State == MEM_COMMIT) { if (info.Type == MEM_PRIVATE) { map.allocations[reinterpret_cast(info.AllocationBase)] += info.RegionSize; map.committed += info.RegionSize; } else (info.Type == MEM_IMAGE ? map.image : map.mapped) += info.RegionSize; } at = reinterpret_cast(info.BaseAddress) + info.RegionSize; } return map; } std::string megabytes(std::uint64_t bytes) { return bytes >= (1ull << 30) ? std::format("{:.2f} GB", static_cast(bytes) / (1ull << 30)) : std::format("{:.1f} MB", static_cast(bytes) / (1ull << 20)); } // How much of [base, base + bytes) is in RAM now: the pages the game has written to and // Windows has not moved out. The rest of a committed block is promised and unused. std::uint64_t resident_bytes(std::uintptr_t base, std::uint64_t bytes) { constexpr std::size_t batch = 16384; std::vector pages(batch); std::uint64_t resident{}; for (std::uint64_t done = 0; done < bytes;) { const auto count = static_cast(std::min(batch, (bytes - done + 4095) / 4096)); for (std::size_t i = 0; i < count; ++i) pages[i].VirtualAddress = reinterpret_cast(base + done + i * 4096); if (!QueryWorkingSetEx(GetCurrentProcess(), pages.data(), static_cast(count * sizeof(pages[0])))) break; for (std::size_t i = 0; i < count; ++i) resident += pages[i].VirtualAttributes.Valid ? 4096 : 0; done += count * 4096ull; } return resident; } // Allocations by size, the sizes that add up to most first: "126 x 32.0 MB = 3.94 GB". void print_by_size(const std::vector& sizes, const Output& out, std::size_t most) { std::map> groups; // size rounded to 64 KB -> count, bytes for (const auto size : sizes) { auto& group = groups[(size + 0xffff) & ~0xffffull]; ++group.first; group.second += size; } std::vector>> order(groups.begin(), groups.end()); std::sort(order.begin(), order.end(), [](const auto& a, const auto& b) { return a.second.second > b.second.second; }); for (std::size_t i = 0; i < order.size() && i < most; ++i) out(std::format(" {} x {} = {}", order[i].second.first, megabytes(order[i].first), megabytes(order[i].second.second))); } // `perf memory trace`: who asks Windows for the middle-sized blocks (2 to 64 MB) while it is on, // by the code that called for each: where a block of a size `perf memory` showed many of comes // from. Every commit of memory in the process passes the hook while it is on, so it is a // research aid and off until asked for. namespace trace { constexpr std::size_t callers = 8192, depth = 14; struct Caller { std::uint64_t hash{}, count{}, bytes{}, size{}, largest{}; // size: the smallest such block; largest: the largest std::array frames{}; unsigned frame_count{}; }; std::mutex mutex; std::vector table(callers); // found by hash: a commit of any size can be traced std::atomic on{}; // The sizes traced: 2 to 64 MB from the console; the largest blocks alone from the start of // the game (RESKATE_MEMORY_TRACE), where nearly everything is being allocated. std::atomic least{2ull << 20}, most{64ull << 20}; bool prepared{}; using Allocate = LONG (NTAPI*)(HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG); Allocate original{}; // The newer call for the same (VirtualAlloc2, which graphics drivers and the D3D12 runtime use). using AllocateEx = LONG (NTAPI*)(HANDLE, PVOID*, PSIZE_T, ULONG, ULONG, void*, ULONG); AllocateEx original_ex{}; void note(HANDLE process, SIZE_T asked, ULONG type) noexcept; LONG NTAPI hooked(HANDLE process, PVOID* base, ULONG_PTR zero_bits, PSIZE_T size, ULONG type, ULONG protect) { const auto asked = size ? *size : 0; const auto status = original(process, base, zero_bits, size, type, protect); if (status >= 0) note(process, asked, type); return status; } LONG NTAPI hooked_ex(HANDLE process, PVOID* base, PSIZE_T size, ULONG type, ULONG protect, void* parameters, ULONG parameter_count) { const auto asked = size ? *size : 0; const auto status = original_ex(process, base, size, type, protect, parameters, parameter_count); if (status >= 0) note(process, asked, type); return status; } void note(HANDLE process, SIZE_T asked, ULONG type) noexcept { if (!on.load(std::memory_order_relaxed) || !(type & (MEM_COMMIT | MEM_RESERVE)) || process != GetCurrentProcess() || asked < least.load(std::memory_order_relaxed) || asked > most.load(std::memory_order_relaxed)) return; // (Reservations count too: a block reserved whole and committed a piece at a time is only // this large when it is reserved.) Caller seen; seen.frame_count = CaptureStackBackTrace(2, static_cast(depth), seen.frames.data(), nullptr); // By who asked alone: one caller's blocks of every size add up together. std::uint64_t hash = 1469598103934665603ull; for (unsigned i = 0; i < seen.frame_count; ++i) hash = hash * 1099511628211ull ^ reinterpret_cast(seen.frames[i]); if (!hash) hash = 1; std::lock_guard lock(mutex); for (std::size_t probe = 0; probe < 128; ++probe) { auto& caller = table[(hash + probe) % callers]; if (caller.count && caller.hash != hash) continue; if (!caller.count) { caller = seen; caller.hash = hash; caller.size = asked; } ++caller.count; caller.bytes += asked; caller.size = std::min(caller.size, asked); caller.largest = std::max(caller.largest, asked); break; } } bool start(std::string& error) { if (!prepared) { const auto target = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtAllocateVirtualMemory")); if (!target || hook_prepare(target, reinterpret_cast(&hooked), reinterpret_cast(&original)) != HookOk) { error = "the allocation hook could not be prepared"; return false; } // (Missing on an older Windows: then only the first is traced.) if (const auto ex = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtAllocateVirtualMemoryEx")); ex && hook_prepare(ex, reinterpret_cast(&hooked_ex), reinterpret_cast(&original_ex)) != HookOk) original_ex = nullptr; prepared = true; } { std::lock_guard lock(mutex); std::fill(table.begin(), table.end(), Caller{}); } const auto target = reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtAllocateVirtualMemory")); if (!on.load() && hook_enable(target) != HookOk) { error = "the allocation hook could not be attached"; return false; } if (!on.load() && original_ex) hook_enable(reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtAllocateVirtualMemoryEx"))); on.store(true); return true; } void stop() { if (!on.exchange(false)) return; hook_disable(reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtAllocateVirtualMemory"))); if (original_ex) hook_disable(reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtAllocateVirtualMemoryEx"))); } std::string place(void* address) { HMODULE module{}; if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, reinterpret_cast(address), &module) || !module) return std::format("{:#x}", reinterpret_cast(address)); wchar_t path[MAX_PATH]{}; GetModuleFileNameW(module, path, MAX_PATH); const wchar_t* name = path; for (const wchar_t* c = path; *c; ++c) if (*c == L'\\') name = c + 1; std::string text; for (; *name; ++name) text += static_cast(*name); return std::format("{}+{:#x}", text, reinterpret_cast(address) - reinterpret_cast(module)); } void report(const Output& out) { std::vector seen; { std::lock_guard lock(mutex); for (const auto& caller : table) if (caller.count) seen.push_back(caller); } std::sort(seen.begin(), seen.end(), [](const Caller& a, const Caller& b) { return a.bytes > b.bytes; }); out(std::format("Memory asked for while tracing (blocks of {} and up), by who asked ({} callers), the most first:", megabytes(least.load()), seen.size())); for (std::size_t i = 0; i < seen.size() && i < 16; ++i) { out(seen[i].size == seen[i].largest ? std::format(" {} x {} bytes = {}", seen[i].count, seen[i].size, megabytes(seen[i].bytes)) : std::format(" {} blocks of {} to {} bytes = {}", seen[i].count, seen[i].size, seen[i].largest, megabytes(seen[i].bytes))); std::string line = " "; for (unsigned frame = 0; frame < seen[i].frame_count; ++frame) line += " < " + place(seen[i].frames[frame]); out(line); } } } // namespace trace // RESKATE_MEMORY_TRACE=: from the start of the game, who asks for each block of at // least that size (`perf memory` then lists them). A research aid for what a map's load commits. void start_memory_trace_from_environment() noexcept { wchar_t value[16]{}; const auto length = GetEnvironmentVariableW(L"RESKATE_MEMORY_TRACE", value, 16); if (!length || length >= 16) return; // (In megabytes; "all" for every block, however small.) const bool all = value[0] == L'a'; const auto megabytes_least = std::wcstoul(value, nullptr, 10); if (!megabytes_least && !all) return; try { trace::least.store(all ? 1 : static_cast(megabytes_least) << 20); trace::most.store(UINT64_MAX); std::string error; (void)trace::start(error); } catch (...) {} } void register_perf_commands(Commands& registry) { // The music sources in the music screen (Spotify, YouTube Music, the player's files, radio, // another application): what is on, what is playing, and the Spotify account. auto spotify = action("music", "The music sources (Spotify, YouTube Music, local files, radio, external audio): what is on, what is playing, and the Spotify account", Group::console); spotify.execution = Execution::local; spotify.run = [](const Model&, const Values&, const Output& out) { out(spotify_station::status()); out(music_inject::status()); out(spotify_device::status()); const auto account = spotify_web::state(); out(std::format("Spotify account: {}{}; {} playlist(s){}{}.", !account.has_client ? "no client id set (music spotify client )" : account.signed_in ? "signed in" : "not signed in (music spotify login)", account.user.empty() ? std::string() : " as " + account.user, account.playlists, account.busy ? "; busy" : "", account.problem.empty() ? std::string() : "; " + account.problem)); }; registry.add(std::move(spotify)); auto setup = action("music setup", "Open the page in your browser that sets the music sources up (Spotify, YouTube Music, local files, radio, external audio, and how it sounds)", Group::console); setup.execution = Execution::local; setup.run = [](const Model&, const Values&, const Output& out) { if (spotify_dashboard::open()) out("Opening " + spotify_dashboard::address() + " in your browser."); else out("The music setup page is not being served."); }; registry.add(std::move(setup)); auto client = action("music spotify client", "The client id of your Spotify application (its redirect address: http://127.0.0.1:47824/callback)", Group::console, {argument("id")}); client.execution = Execution::local; client.run = [](const Model&, const Values& args, const Output& out) { spotify_web::set_client(std::get(args[0])); out("Spotify client id set. Sign in with: music spotify login"); }; registry.add(std::move(client)); auto login = action("music spotify login", "Sign in to Spotify in your browser, for your playlists in the music screen", Group::console); login.execution = Execution::local; login.run = [](const Model&, const Values&, const Output& out) { if (!spotify_web::state().has_client) return out("Set your Spotify application's client id first: music spotify client "); spotify_web::login(); out("Your browser is opening Spotify's sign-in. Your playlists are in the music screen the next time the game starts."); }; registry.add(std::move(login)); auto logout = action("music spotify logout", "Forget the Spotify sign-in", Group::console); logout.execution = Execution::local; logout.run = [](const Model&, const Values&, const Output& out) { spotify_web::logout(); out("The Spotify sign-in is forgotten."); }; registry.add(std::move(logout)); auto through = variable("music through", "The music sources' sound through the game while one of their tiles is on: 0 off, 1 in place of songs, 2 in place of every stereo sound file", Group::console, argument("0-2", Type::integer)); through.execution = Execution::local; through.inspect = [](const Model&) { return State{true, std::to_string(music_inject::mode()), {}, "0 off, 1 songs, 2 every stereo sound file", false}; }; through.run = [](const Model&, const Values& args, const Output& out) { music_inject::set_mode(static_cast(std::clamp(std::get(args[0]), 0, 3))); out(std::format("Music through the game: mode {}.", music_inject::mode())); }; registry.add(std::move(through)); auto gain = variable("music gain", "How loud the music sources are through the game, in percent (100 as it is, up to 1600)", Group::console, argument("percent", Type::integer)); gain.execution = Execution::local; gain.inspect = [](const Model&) { return State{true, std::to_string(static_cast(music_inject::gain() * 100.0f + 0.5f)), {}, "percent", false}; }; gain.run = [](const Model&, const Values& args, const Output& out) { music_inject::set_gain(static_cast(std::clamp(std::get(args[0]), 0, 1600)) / 100.0f); out(std::format("Music through the game: {}% loud.", static_cast(music_inject::gain() * 100.0f + 0.5f))); }; registry.add(std::move(gain)); // The message a kicked or banned player is shown (Extension/UI/Overlay/message_card.cpp), to // look at and to answer with a controller without being kicked for it. auto popup = action("popup test", "Show the message a kicked player sees, to try answering it (A / Enter / click OK)", Group::console); popup.execution = Execution::local; popup.run = [](const Model&, const Values&, const Output& out) { overlay::show_message("YOU WERE KICKED", "You were kicked from this server by a vote for 5 minutes."); out("Close the console to answer it."); }; registry.add(std::move(popup)); // How close the game is to its limit of streamed textures (texture_handle_limit). auto textures = action("textures", "Streamed textures in use, and the most the game can hold", Group::console); textures.execution = Execution::local; textures.run = [](const Model&, const Values&, const Output& out) { const auto state = texture_handle_limit::use(); if (!state.known) return out("The game's texture streaming is not running yet."); out(std::format("{} of {} streamed textures in use ({}%). A warning shows at 90%; the game freezes at the limit.", state.taken, state.limit, std::uint64_t{state.taken} * 100 / state.limit)); }; registry.add(std::move(textures)); // The HUD starts off every launch; these turn it on for the session. auto toggle = action("perf", "Show or hide the performance HUD", Group::console); toggle.execution = Execution::local; toggle.run = [](const Model&, const Values&, const Output& out) { const bool on = !profiler::hud(); profiler::set_hud(on); out(on ? "Performance HUD on. `perf window on` opens the profiler; `perf sample` takes a stack sample." : "Performance HUD off."); }; registry.add(std::move(toggle)); auto hud = variable("perf hud", "Performance HUD: client update, frame time, ReSkate's cost, CPU per thread", Group::console, argument("on|off", Type::boolean)); hud.execution = Execution::local; hud.inspect = [](const Model&) { return boolean_state(true, profiler::hud()); }; hud.run = [](const Model&, const Values& args, const Output& out) { const bool on = std::get(args[0]); profiler::set_hud(on); out(on ? "Performance HUD on." : "Performance HUD off."); }; registry.add(std::move(hud)); auto window = variable("perf window", "The profiler window (shown while the menu or console is open)", Group::console, argument("on|off", Type::boolean)); window.execution = Execution::local; window.inspect = [](const Model&) { return boolean_state(true, profiler::window()); }; window.run = [](const Model&, const Values& args, const Output& out) { profiler::set_window(std::get(args[0])); out(profiler::window() ? "Profiler window on: it shows while the menu or console is open." : "Profiler window off."); }; registry.add(std::move(window)); auto seconds = argument("seconds", Type::number, true); seconds.minimum = 0.5; seconds.maximum = 60; auto target = argument("client|present|all|", Type::text, true); target.complete = [](const Model&, auto) { return std::vector{"client", "present", "all"}; }; auto rate = argument("rate", Type::unsigned_integer, true); rate.minimum = 50; rate.maximum = 4000; auto sample = action("perf sample", "Sample stacks: the client update thread (default, 10 s at 1000 Hz), present, all busy threads, or a thread id", Group::console, {seconds, target, rate}); sample.execution = Execution::local; sample.run = [](const Model&, const Values& args, const Output& out) { profiler::SampleRequest request; if (args.size() > 0) request.seconds = std::get(args[0]); if (args.size() > 1) { const auto& which = std::get(args[1]); if (equal(which, "client")) request.target = profiler::Target::client; else if (equal(which, "present")) request.target = profiler::Target::present; else if (equal(which, "all")) { request.target = profiler::Target::all; request.rate = 250; } else { request.target = profiler::Target::thread; try { request.thread_id = static_cast(std::stoul(which, nullptr, 0)); } catch (...) { out("error: The target is client, present, all or a thread id."); return; } } } if (args.size() > 2) request.rate = static_cast(std::get(args[2])); std::string error; if (!profiler::start_sampling(request, error)) { out("error: " + error); return; } out(std::format("Sampling for {:.1f} s at {} Hz. `perf report` shows the result; the files go next to ReSkate.log.", request.seconds, request.rate)); }; registry.add(std::move(sample)); auto stop = action("perf stop", "Stop the running sample early (it still reports)", Group::console); stop.execution = Execution::local; stop.run = [](const Model&, const Values&, const Output& out) { profiler::stop_sampling(); out("Stopping the sample."); }; registry.add(std::move(stop)); auto report = action("perf report", "The last sample's busiest functions and its report files", Group::console); report.execution = Execution::local; report.run = [](const Model&, const Values&, const Output& out) { print_report(out); }; registry.add(std::move(report)); auto microseconds = argument("microseconds", Type::number); microseconds.minimum = 0; microseconds.maximum = 5000; auto spin = variable("perf jobspin", "How long idle engine job threads poll for work before sleeping (ReSkate 50 us, stock 250 us)", Group::engine, microseconds); spin.execution = Execution::local; spin.inspect = [](const Model&) { const auto value = job_spin::microseconds(); return State{value.has_value(), value ? std::optional(std::format("{:.0f}", *value)) : std::nullopt, "The job system's spin setting is unavailable for this build.", std::format("ReSkate default {:.0f} us; stock {:.0f} us", job_spin::default_microseconds, job_spin::stock_microseconds()), value && *value != job_spin::default_microseconds}; }; spin.run = [](const Model&, const Values& args, const Output& out) { const double wanted = std::get(args[0]); if (!job_spin::set_microseconds(wanted)) { out("error: The job system's spin setting is unavailable for this build."); return; } out(std::format("Job threads now poll for {:.0f} us before sleeping (ReSkate default {:.0f}, stock {:.0f}; not saved).", wanted, job_spin::default_microseconds, job_spin::stock_microseconds())); }; spin.reset = [](const Model&, const Output& out) { if (!job_spin::set_microseconds(job_spin::default_microseconds)) { out("error: The job system's spin setting is unavailable."); return; } out(std::format("Job threads poll for {:.0f} us again.", job_spin::default_microseconds)); }; registry.add(std::move(spin)); auto pointer = variable("perf uipointer", "Skip the UI's per-frame mouse hit-test while the game shows no cursor (on by default)", Group::engine, argument("on|off", Type::boolean)); pointer.execution = Execution::local; pointer.inspect = [](const Model&) { return boolean_state(true, ui_pointer::skip(), {}, ui_pointer::status()); }; pointer.run = [](const Model&, const Values& args, const Output& out) { ui_pointer::set_skip(std::get(args[0])); out(ui_pointer::status()); }; registry.add(std::move(pointer)); // Engine tweaks (Extension/Settings/engine_tweaks.h): mainsleep and dirtyskip start on, the // others off; nothing is saved. Measure each against stock with `perf sample 8 all`. const auto tweak_variable = [&](std::string name, std::string description, engine_tweaks::Tweak tweak, Argument value) { auto entry = variable(std::move(name), std::move(description), Group::engine, std::move(value)); entry.execution = Execution::local; const bool numeric = tweak == engine_tweaks::Tweak::gi || tweak == engine_tweaks::Tweak::job_wake; entry.inspect = [tweak, numeric](const Model&) { const auto now = engine_tweaks::value(tweak); const bool changed = now != engine_tweaks::default_value(tweak); if (!numeric) { auto state = boolean_state(engine_tweaks::available(tweak), now > 0, engine_tweaks::status(tweak), engine_tweaks::status(tweak)); state.overridden = changed; return state; } return State{engine_tweaks::available(tweak), std::format("{:.0f}", now), engine_tweaks::status(tweak), engine_tweaks::status(tweak), changed}; }; entry.run = [tweak, numeric](const Model&, const Values& args, const Output& out) { const double wanted = numeric ? std::get(args[0]) : (std::get(args[0]) ? 1.0 : 0.0); std::string error; if (!engine_tweaks::set(tweak, wanted, error)) { out("error: " + error); return; } out(engine_tweaks::status(tweak)); }; entry.reset = [tweak](const Model&, const Output& out) { std::string error; if (!engine_tweaks::set(tweak, engine_tweaks::default_value(tweak), error)) { out("error: " + error); return; } out(engine_tweaks::status(tweak)); }; registry.add(std::move(entry)); }; auto updates = argument("updates per second", Type::number); updates.minimum = 0; updates.maximum = 1000; tweak_variable("perf gi", "Throttle Enlighten GI's per-frame update to this rate (0 = every frame, stock; try 60)", engine_tweaks::Tweak::gi, updates); tweak_variable("perf meshtree", "Cull meshes through the octree the game ships off (MeshCullTreeEnabled)", engine_tweaks::Tweak::mesh_tree, argument("on|off", Type::boolean)); auto wait = argument("microseconds", Type::number); wait.minimum = 0; wait.maximum = 1000; tweak_variable("perf jobwake", "Idle job workers wait this long where a push wakes them cheaply, and pushes prefer them (0 = stock; measured to cost more CPU than it saves)", engine_tweaks::Tweak::job_wake, wait); tweak_variable("perf mainsleep", "Pace the main loop on a high-resolution timer with a 0.25 ms spin instead of a 1 ms one (on by default)", engine_tweaks::Tweak::main_sleep, argument("on|off", Type::boolean)); tweak_variable("perf dirtyskip", "Skip the per-section spinlock that clears dirty bits when none are set (on by default)", engine_tweaks::Tweak::dirty_skip, argument("on|off", Type::boolean)); auto tweaks = action("perf tweaks", "What each engine tweak is doing (perf gi, meshtree, jobwake, mainsleep, dirtyskip)", Group::console); tweaks.execution = Execution::local; tweaks.run = [](const Model&, const Values&, const Output& out) { for (const auto tweak : {engine_tweaks::Tweak::gi, engine_tweaks::Tweak::mesh_tree, engine_tweaks::Tweak::job_wake, engine_tweaks::Tweak::main_sleep, engine_tweaks::Tweak::dirty_skip}) out(engine_tweaks::status(tweak)); }; registry.add(std::move(tweaks)); auto what = argument("mark", Type::text, true); what.complete = [](const Model&, auto) { return std::vector{"mark", "trace", "stop", "big"}; }; auto memory = action("perf memory", "The game's memory by allocation; `perf memory mark` remembers it and the next `perf memory` says what was added since; " "`perf memory trace` then also says who asked for the middle-sized blocks, until `perf memory stop`; `perf memory big` lists the largest blocks and how much of each is in use", Group::console, {std::move(what)}); memory.execution = Execution::local; memory.run = [](const Model&, const Values& args, const Output& out) { static std::mutex mutex; static MemoryMap marked; static bool has_mark{}; std::lock_guard lock(mutex); const auto now = read_memory_map(); out(std::format("Memory: {} committed in {} allocations, {} of files mapped in, {} of programs.", megabytes(now.committed), now.allocations.size(), megabytes(now.mapped), megabytes(now.image))); if (!args.empty() && equal(std::get(args[0]), "big")) { // The largest blocks one by one: where each is, its exact size, and how much of it is used. std::vector> order; for (const auto& [base, size] : now.allocations) order.emplace_back(size, base); std::sort(order.rbegin(), order.rend()); out("The largest blocks (committed, of which in RAM now):"); for (std::size_t i = 0; i < order.size() && i < 24; ++i) { // (The whole reservation is walked: what is not committed in it is not in RAM either.) MEMORY_BASIC_INFORMATION info{}; std::uint64_t span = order[i].first; for (auto at = order[i].second; VirtualQuery(reinterpret_cast(at), &info, sizeof(info)) == sizeof(info) && reinterpret_cast(info.AllocationBase) == order[i].second; at = reinterpret_cast(info.BaseAddress) + info.RegionSize) span = reinterpret_cast(info.BaseAddress) + info.RegionSize - order[i].second; out(std::format(" {:#x}: {} bytes ({}), {} in RAM", order[i].second, order[i].first, megabytes(order[i].first), megabytes(resident_bytes(order[i].second, span)))); if (i >= 3) continue; // What kind of memory the three largest are, and a look at what is in them: how // they are protected and held, and the bytes at a few places that are in RAM. VirtualQuery(reinterpret_cast(order[i].second), &info, sizeof(info)); PSAPI_WORKING_SET_EX_INFORMATION page{reinterpret_cast(order[i].second)}; QueryWorkingSetEx(GetCurrentProcess(), &page, sizeof(page)); out(std::format(" reserved {:#x} bytes, protection {:#x} (as reserved {:#x}), first region {:#x} bytes; first page: {}{}{}{}", span, info.Protect, info.AllocationProtect, static_cast(info.RegionSize), page.VirtualAttributes.Valid ? "in RAM" : "not in RAM", page.VirtualAttributes.Shared ? ", shared" : "", page.VirtualAttributes.Locked ? ", locked" : "", page.VirtualAttributes.LargePage ? ", large pages" : "")); std::size_t looked{}, zero_pages{}; for (std::uint64_t part = 0; part < 64; ++part) { const auto at = order[i].second + ((span / 64) * part & ~0xfffull); PSAPI_WORKING_SET_EX_INFORMATION here{reinterpret_cast(at)}; if (!QueryWorkingSetEx(GetCurrentProcess(), &here, sizeof(here)) || !here.VirtualAttributes.Valid) continue; std::array bytes{}; SIZE_T got{}; if (!ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(at), bytes.data(), bytes.size(), &got) || got != bytes.size()) continue; ++looked; const bool zero = std::all_of(bytes.begin(), bytes.end(), [](unsigned char byte) { return byte == 0; }); zero_pages += zero; if (zero || looked - zero_pages > 4) continue; std::string hex; for (std::size_t byte = 0; byte < 48; ++byte) hex += std::format("{:02x}{}", bytes[byte], byte % 8 == 7 ? " " : ""); out(std::format(" +{:#x}: {}", at - order[i].second, hex)); } out(std::format(" of {} sampled pages in RAM, {} are all zero", looked, zero_pages)); } return; } const bool tracing = !args.empty() && equal(std::get(args[0]), "trace"); if (!args.empty() && equal(std::get(args[0]), "stop")) { trace::stop(); out("Tracing stopped."); return; } if (tracing) { std::string error; // Every size: a large block is often set aside whole and filled a page at a time. trace::least.store(1); trace::most.store(UINT64_MAX); if (!trace::start(error)) { out("error: " + error); return; } } if (tracing || (!args.empty() && equal(std::get(args[0]), "mark"))) { marked = now; has_mark = true; out(tracing ? "Marked, and tracing who asks for memory. Change what you want to measure, then run `perf memory`; `perf memory stop` ends the tracing." : "Marked. Change what you want to measure, then run `perf memory`."); return; } if (!has_mark) { std::vector sizes; for (const auto& [base, size] : now.allocations) sizes.push_back(size); out("The allocation sizes that add up to most:"); print_by_size(sizes, out, 20); if (trace::on.load()) trace::report(out); return; } std::vector added, grown; std::uint64_t added_bytes{}, grown_bytes{}, freed_bytes{}; for (const auto& [base, size] : now.allocations) { const auto before = marked.allocations.find(base); if (before == marked.allocations.end()) added.push_back(size), added_bytes += size; else if (size > before->second) grown.push_back(size - before->second), grown_bytes += size - before->second; else freed_bytes += before->second - size; } for (const auto& [base, size] : marked.allocations) if (!now.allocations.contains(base)) freed_bytes += size; out(std::format("Since the mark: {} to {} ({}{}).", megabytes(marked.committed), megabytes(now.committed), now.committed >= marked.committed ? "+" : "-", megabytes(now.committed >= marked.committed ? now.committed - marked.committed : marked.committed - now.committed))); out(std::format("New allocations: {} in {}. By size:", megabytes(added_bytes), added.size())); print_by_size(added, out, 20); out(std::format("Growth of allocations that were there: {} in {}. By how much each grew:", megabytes(grown_bytes), grown.size())); print_by_size(grown, out, 12); out(std::format("Given back: {}.", megabytes(freed_bytes))); if (trace::on.load()) trace::report(out); }; registry.add(std::move(memory)); auto status = action("perf status", "Client update and frame timing, ReSkate's zones, CPU per thread", Group::console); status.execution = Execution::local; status.run = [](const Model&, const Values&, const Output& out) { print_summary(out); }; registry.add(std::move(status)); } } // namespace dingosdk::console