#include "Extension/Console/commands.h" #include "Engine/Core/Profiling/profiler.h" #include "Extension/Console/memory_ledger.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))); } // RESKATE_MEMORY_TRACE=: from the start of the game, who asks for each block of at // least that size (`perf memory` then lists them), and the memory ledger from then on, so // `perf memory owners` can say who made nearly everything. 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 { std::string error; (void)memory_ledger::start_asking(all ? 1 : static_cast(megabytes_least) << 20, UINT64_MAX, 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.aliases = {"perf textures"}; // beside the other perf readings, where it is looked for 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", "owners", "heap", "gpu", "report"}; }; auto heap_mark = argument("mark", Type::text, true); heap_mark.complete = [](const Model&, auto) { return std::vector{"mark"}; }; 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; " "`perf memory owners` says who made what is held and how much of it is used (it starts the memory ledger, which then also says in the log what keeps growing); " "`perf memory heap [mark]` lists the heaps' blocks by size and what was added since a mark; `perf memory gpu` the video memory; " "`perf memory report` writes all of it to a file beside ReSkate.log", Group::console, {std::move(what), std::move(heap_mark)}); 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{}; const memory_ledger::Line line = [&](const std::string& text) { out(text); }; const auto word = args.empty() ? std::string() : std::get(args[0]); if (equal(word, "owners")) return memory_ledger::owners_report(line); if (equal(word, "heap")) return memory_ledger::heap_report(line, args.size() > 1 && equal(std::get(args[1]), "mark")); if (equal(word, "gpu")) return memory_ledger::gpu_report(line); if (equal(word, "report")) { const auto path = memory_ledger::write_report(); out(path.empty() ? std::string("error: the report could not be written") : "Memory report: " + narrow(path)); return; } 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")) { // (The memory ledger stays on: what it missed being given back would be wrong.) memory_ledger::stop_asking(); 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. if (!memory_ledger::start_asking(1, UINT64_MAX, 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 (memory_ledger::asking()) memory_ledger::asked_report(line); 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 (memory_ledger::asking()) memory_ledger::asked_report(line); }; 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