mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 12:57:01 +02:00
The streamed-texture reading is one of the perf readings, and perf textures is where it gets looked for. It is now an alias, so Tab under perf offers it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
548 lines
33 KiB
C++
548 lines
33 KiB
C++
#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 <Windows.h>
|
|
#include <Psapi.h>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <format>
|
|
#include <map>
|
|
#include <mutex>
|
|
#include <vector>
|
|
|
|
// 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<char>(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<std::uintptr_t, std::uint64_t> 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<std::uintptr_t>(system.lpMinimumApplicationAddress);
|
|
const auto end = reinterpret_cast<std::uintptr_t>(system.lpMaximumApplicationAddress);
|
|
MEMORY_BASIC_INFORMATION info{};
|
|
while (at < end && VirtualQuery(reinterpret_cast<void*>(at), &info, sizeof(info)) == sizeof(info)) {
|
|
if (info.State == MEM_COMMIT) {
|
|
if (info.Type == MEM_PRIVATE) {
|
|
map.allocations[reinterpret_cast<std::uintptr_t>(info.AllocationBase)] += info.RegionSize;
|
|
map.committed += info.RegionSize;
|
|
} else (info.Type == MEM_IMAGE ? map.image : map.mapped) += info.RegionSize;
|
|
}
|
|
at = reinterpret_cast<std::uintptr_t>(info.BaseAddress) + info.RegionSize;
|
|
}
|
|
return map;
|
|
}
|
|
std::string megabytes(std::uint64_t bytes) {
|
|
return bytes >= (1ull << 30) ? std::format("{:.2f} GB", static_cast<double>(bytes) / (1ull << 30))
|
|
: std::format("{:.1f} MB", static_cast<double>(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<PSAPI_WORKING_SET_EX_INFORMATION> pages(batch);
|
|
std::uint64_t resident{};
|
|
for (std::uint64_t done = 0; done < bytes;) {
|
|
const auto count = static_cast<std::size_t>(std::min<std::uint64_t>(batch, (bytes - done + 4095) / 4096));
|
|
for (std::size_t i = 0; i < count; ++i) pages[i].VirtualAddress = reinterpret_cast<void*>(base + done + i * 4096);
|
|
if (!QueryWorkingSetEx(GetCurrentProcess(), pages.data(), static_cast<DWORD>(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<std::uint64_t>& sizes, const Output& out, std::size_t most) {
|
|
std::map<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>> 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<std::pair<std::uint64_t, std::pair<std::uint64_t, std::uint64_t>>> 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=<megabytes>: 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<std::uint64_t>(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 <id>)" : 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<std::string>(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 <id>");
|
|
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<int>(std::clamp<std::int64_t>(std::get<std::int64_t>(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<int>(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<float>(std::clamp<std::int64_t>(std::get<std::int64_t>(args[0]), 0, 1600)) / 100.0f);
|
|
out(std::format("Music through the game: {}% loud.", static_cast<int>(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<bool>(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<bool>(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|<thread id>", Type::text, true);
|
|
target.complete = [](const Model&, auto) { return std::vector<std::string>{"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<double>(args[0]);
|
|
if (args.size() > 1) {
|
|
const auto& which = std::get<std::string>(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::uint32_t>(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<unsigned>(std::get<std::uint64_t>(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::string>(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<double>(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<bool>(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<double>(args[0]) : (std::get<bool>(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<std::string>{"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<std::string>{"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<std::string>(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<std::string>(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<std::string>(args[0]), "big")) {
|
|
// The largest blocks one by one: where each is, its exact size, and how much of it is used.
|
|
std::vector<std::pair<std::uint64_t, std::uintptr_t>> 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<void*>(at), &info, sizeof(info)) == sizeof(info) &&
|
|
reinterpret_cast<std::uintptr_t>(info.AllocationBase) == order[i].second;
|
|
at = reinterpret_cast<std::uintptr_t>(info.BaseAddress) + info.RegionSize)
|
|
span = reinterpret_cast<std::uintptr_t>(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<void*>(order[i].second), &info, sizeof(info));
|
|
PSAPI_WORKING_SET_EX_INFORMATION page{reinterpret_cast<void*>(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<std::uint64_t>(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<void*>(at)};
|
|
if (!QueryWorkingSetEx(GetCurrentProcess(), &here, sizeof(here)) || !here.VirtualAttributes.Valid) continue;
|
|
std::array<unsigned char, 4096> bytes{};
|
|
SIZE_T got{};
|
|
if (!ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<void*>(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<std::string>(args[0]), "trace");
|
|
if (!args.empty() && equal(std::get<std::string>(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<std::string>(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<std::uint64_t> 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<std::uint64_t> 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
|