Files
m2dev-client-src/src/PackLib/PackProfile.cpp
server db7ae1f841
Some checks failed
build / Windows Build (push) Has been cancelled
Persist pack profile snapshots during timed captures
2026-04-15 17:06:11 +02:00

555 lines
13 KiB
C++

#include "PackProfile.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <chrono>
#include <condition_variable>
#include <filesystem>
#include <fstream>
#include <map>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include <windows.h>
#include "EterBase/Utils.h"
namespace
{
using Clock = std::chrono::steady_clock;
constexpr const char* kPackProfileEnv = "M2PACK_PROFILE";
constexpr const char* kPackProfileOutput = "log/pack_profile.txt";
constexpr std::size_t kTopListLimit = 12;
constexpr auto kSnapshotFlushInterval = std::chrono::seconds(1);
struct LoadStats
{
std::uint64_t count = 0;
std::uint64_t failures = 0;
std::uint64_t output_bytes = 0;
std::uint64_t elapsed_us = 0;
};
struct MountStats
{
std::uint64_t count = 0;
std::uint64_t failures = 0;
std::uint64_t entry_count = 0;
std::uint64_t elapsed_us = 0;
};
struct StageStats
{
std::uint64_t count = 0;
std::uint64_t input_bytes = 0;
std::uint64_t output_bytes = 0;
std::uint64_t elapsed_us = 0;
};
struct PhaseMarker
{
std::string phase;
std::uint64_t elapsed_us = 0;
};
struct PackProfileState
{
bool initialized = false;
bool enabled = false;
std::string current_phase = "startup";
Clock::time_point start_time {};
std::vector<PhaseMarker> markers;
std::map<std::string, LoadStats> load_by_format;
std::map<std::string, LoadStats> load_by_phase_format;
std::map<std::string, LoadStats> load_by_phase_pack;
std::map<std::string, LoadStats> load_by_extension;
std::map<std::string, MountStats> mount_by_format;
std::map<std::string, MountStats> mount_by_pack;
std::map<std::string, StageStats> stage_by_key;
bool snapshot_dirty = false;
bool stop_snapshot_thread = false;
std::condition_variable snapshot_cv;
std::thread snapshot_thread;
std::mutex mutex;
~PackProfileState()
{
StopSnapshotThread();
if (enabled)
{
std::lock_guard<std::mutex> lock(mutex);
WriteReportLocked("shutdown");
}
}
void StartSnapshotThread()
{
snapshot_thread = std::thread([this]() {
SnapshotLoop();
});
}
void StopSnapshotThread()
{
{
std::lock_guard<std::mutex> lock(mutex);
stop_snapshot_thread = true;
}
snapshot_cv.notify_all();
if (snapshot_thread.joinable())
{
snapshot_thread.join();
}
}
void SnapshotLoop()
{
std::unique_lock<std::mutex> lock(mutex);
for (;;)
{
const bool shouldStop = snapshot_cv.wait_for(
lock,
kSnapshotFlushInterval,
[this]() { return stop_snapshot_thread; });
if (shouldStop)
{
return;
}
if (!enabled || !snapshot_dirty)
{
continue;
}
WriteReportLocked("periodic");
snapshot_dirty = false;
}
}
void WriteReportLocked(std::string_view reason) const
{
std::error_code ec;
std::filesystem::create_directories("log", ec);
std::ofstream output(kPackProfileOutput, std::ios::binary | std::ios::trunc);
if (!output)
{
return;
}
output << "PACK PROFILE REPORT\n";
output << "reason=" << reason << "\n";
output << "current_phase=" << current_phase << "\n";
const auto uptime_us = start_time == Clock::time_point {}
? 0
: static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
Clock::now() - start_time).count());
output << "uptime_ms=" << FormatMs(uptime_us) << "\n\n";
output << "[phase_markers]\n";
for (const auto& marker : markers)
{
output << marker.phase << "\t" << FormatMs(marker.elapsed_us) << " ms\n";
}
output << "\n";
WriteMountSection(output, "[mounts_by_format]", mount_by_format);
WriteMountSection(output, "[mounts_by_pack]", mount_by_pack, kTopListLimit);
WriteLoadSection(output, "[loads_by_format]", load_by_format);
WriteLoadSection(output, "[loads_by_phase_format]", load_by_phase_format);
WriteLoadSection(output, "[top_phase_pack_loads]", load_by_phase_pack, kTopListLimit);
WriteLoadSection(output, "[top_extension_loads]", load_by_extension, kTopListLimit);
WriteStageSection(output, "[loader_stages]", stage_by_key);
}
static std::string FormatMs(std::uint64_t elapsed_us)
{
std::ostringstream out;
out.setf(std::ios::fixed, std::ios::floatfield);
out.precision(3);
out << (static_cast<double>(elapsed_us) / 1000.0);
return out.str();
}
static std::string ToLowerAscii(std::string value)
{
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value;
}
static std::string ParsePackLabel(std::string_view pack_path)
{
const std::filesystem::path path { std::string(pack_path) };
const auto filename = path.filename().string();
if (!filename.empty())
{
return ToLowerAscii(filename);
}
return ToLowerAscii(std::string(pack_path));
}
static std::string ParseExtension(std::string_view request_path)
{
const std::filesystem::path path { std::string(request_path) };
auto extension = path.extension().string();
if (extension.empty())
{
return "<none>";
}
return ToLowerAscii(std::move(extension));
}
static std::string MakePhaseKey(std::string_view phase, std::string_view suffix)
{
std::string key;
key.reserve(phase.size() + suffix.size() + 3);
key.append(phase);
key.append(" | ");
key.append(suffix);
return key;
}
template <typename MapType>
static std::vector<std::pair<std::string, typename MapType::mapped_type>> SortByTimeDesc(const MapType& values)
{
using StatsType = typename MapType::mapped_type;
std::vector<std::pair<std::string, StatsType>> sorted;
sorted.reserve(values.size());
for (const auto& [key, stats] : values)
{
sorted.emplace_back(key, stats);
}
std::sort(sorted.begin(), sorted.end(), [](const auto& left, const auto& right) {
if (left.second.elapsed_us != right.second.elapsed_us)
{
return left.second.elapsed_us > right.second.elapsed_us;
}
return left.first < right.first;
});
return sorted;
}
static void WriteLoadSection(std::ofstream& output, std::string_view header, const std::map<std::string, LoadStats>& values, std::size_t limit = 0)
{
output << header << "\n";
const auto sorted = SortByTimeDesc(values);
const std::size_t count = limit == 0 ? sorted.size() : std::min(limit, sorted.size());
for (std::size_t i = 0; i < count; ++i)
{
const auto& [key, stats] = sorted[i];
output
<< key
<< "\tcount=" << stats.count
<< "\tfail=" << stats.failures
<< "\tbytes=" << stats.output_bytes
<< "\ttime_ms=" << FormatMs(stats.elapsed_us)
<< "\n";
}
output << "\n";
}
static void WriteMountSection(std::ofstream& output, std::string_view header, const std::map<std::string, MountStats>& values, std::size_t limit = 0)
{
output << header << "\n";
std::vector<std::pair<std::string, MountStats>> sorted(values.begin(), values.end());
std::sort(sorted.begin(), sorted.end(), [](const auto& left, const auto& right) {
if (left.second.elapsed_us != right.second.elapsed_us)
{
return left.second.elapsed_us > right.second.elapsed_us;
}
return left.first < right.first;
});
const std::size_t count = limit == 0 ? sorted.size() : std::min(limit, sorted.size());
for (std::size_t i = 0; i < count; ++i)
{
const auto& [key, stats] = sorted[i];
output
<< key
<< "\tcount=" << stats.count
<< "\tfail=" << stats.failures
<< "\tentries=" << stats.entry_count
<< "\ttime_ms=" << FormatMs(stats.elapsed_us)
<< "\n";
}
output << "\n";
}
static void WriteStageSection(std::ofstream& output, std::string_view header, const std::map<std::string, StageStats>& values)
{
output << header << "\n";
std::vector<std::pair<std::string, StageStats>> sorted(values.begin(), values.end());
std::sort(sorted.begin(), sorted.end(), [](const auto& left, const auto& right) {
if (left.second.elapsed_us != right.second.elapsed_us)
{
return left.second.elapsed_us > right.second.elapsed_us;
}
return left.first < right.first;
});
for (const auto& [key, stats] : sorted)
{
output
<< key
<< "\tcount=" << stats.count
<< "\tin=" << stats.input_bytes
<< "\tout=" << stats.output_bytes
<< "\ttime_ms=" << FormatMs(stats.elapsed_us)
<< "\n";
}
output << "\n";
}
};
PackProfileState g_pack_profile;
bool ParseBoolFlag(std::string value, bool& out)
{
value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) {
return std::isspace(ch) != 0;
}), value.end());
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
if (value == "1" || value == "true" || value == "yes" || value == "on")
{
out = true;
return true;
}
if (value == "0" || value == "false" || value == "no" || value == "off")
{
out = false;
return true;
}
return false;
}
std::string GetEnvString(const char* name)
{
char buffer[256];
const DWORD len = GetEnvironmentVariableA(name, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer))
{
return {};
}
return std::string(buffer, buffer + len);
}
std::uint64_t CurrentElapsedUs()
{
if (!g_pack_profile.enabled)
{
return 0;
}
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
Clock::now() - g_pack_profile.start_time).count());
}
} // namespace
void InitializePackProfile(const char* commandLine)
{
if (g_pack_profile.initialized)
{
return;
}
g_pack_profile.initialized = true;
g_pack_profile.enabled = false;
const std::string envProfile = GetEnvString(kPackProfileEnv);
if (!envProfile.empty())
{
bool enabled = false;
if (ParseBoolFlag(envProfile, enabled))
{
g_pack_profile.enabled = enabled;
}
}
int argc = 0;
PCHAR* argv = CommandLineToArgv(const_cast<PCHAR>(commandLine ? commandLine : ""), &argc);
if (argv)
{
for (int i = 0; i < argc; ++i)
{
const std::string arg = argv[i];
if (arg != "--m2pack-profile")
{
continue;
}
bool enabled = true;
if (i + 1 < argc)
{
const std::string next = argv[i + 1];
if (!next.starts_with("--") && ParseBoolFlag(next, enabled))
{
++i;
}
}
g_pack_profile.enabled = enabled;
}
SAFE_FREE_GLOBAL(argv);
}
if (!g_pack_profile.enabled)
{
return;
}
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
g_pack_profile.current_phase = "startup";
g_pack_profile.start_time = Clock::now();
g_pack_profile.markers.push_back(PhaseMarker {
.phase = g_pack_profile.current_phase,
.elapsed_us = 0,
});
g_pack_profile.WriteReportLocked("initialized");
g_pack_profile.snapshot_dirty = false;
g_pack_profile.stop_snapshot_thread = false;
g_pack_profile.StartSnapshotThread();
}
bool IsPackProfileEnabled()
{
return g_pack_profile.enabled;
}
void MarkPackProfilePhase(std::string_view phase)
{
if (!g_pack_profile.enabled)
{
return;
}
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
if (g_pack_profile.current_phase == phase)
{
return;
}
g_pack_profile.current_phase = std::string(phase);
g_pack_profile.markers.push_back(PhaseMarker {
.phase = g_pack_profile.current_phase,
.elapsed_us = CurrentElapsedUs(),
});
g_pack_profile.WriteReportLocked(PackProfileState::MakePhaseKey("phase", phase));
g_pack_profile.snapshot_dirty = false;
}
void RecordPackProfileMount(
std::string_view format,
std::string_view packPath,
bool ok,
std::size_t entryCount,
std::uint64_t elapsedUs)
{
if (!g_pack_profile.enabled)
{
return;
}
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
auto& formatStats = g_pack_profile.mount_by_format[std::string(format)];
formatStats.count += 1;
formatStats.failures += ok ? 0 : 1;
formatStats.entry_count += entryCount;
formatStats.elapsed_us += elapsedUs;
auto& packStats = g_pack_profile.mount_by_pack[PackProfileState::ParsePackLabel(packPath)];
packStats.count += 1;
packStats.failures += ok ? 0 : 1;
packStats.entry_count += entryCount;
packStats.elapsed_us += elapsedUs;
g_pack_profile.snapshot_dirty = true;
}
void RecordPackProfileLoad(
std::string_view format,
std::string_view packPath,
std::string_view requestPath,
bool ok,
std::uint64_t outputBytes,
std::uint64_t elapsedUs)
{
if (!g_pack_profile.enabled)
{
return;
}
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
auto update = [ok, outputBytes, elapsedUs](LoadStats& stats) {
stats.count += 1;
stats.failures += ok ? 0 : 1;
stats.output_bytes += outputBytes;
stats.elapsed_us += elapsedUs;
};
update(g_pack_profile.load_by_format[std::string(format)]);
update(g_pack_profile.load_by_phase_format[
PackProfileState::MakePhaseKey(g_pack_profile.current_phase, format)]);
update(g_pack_profile.load_by_phase_pack[
PackProfileState::MakePhaseKey(g_pack_profile.current_phase, PackProfileState::ParsePackLabel(packPath))]);
update(g_pack_profile.load_by_extension[PackProfileState::ParseExtension(requestPath)]);
g_pack_profile.snapshot_dirty = true;
}
void RecordPackProfileStage(
std::string_view format,
std::string_view stage,
std::uint64_t inputBytes,
std::uint64_t outputBytes,
std::uint64_t elapsedUs)
{
if (!g_pack_profile.enabled)
{
return;
}
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
auto& stats = g_pack_profile.stage_by_key[
PackProfileState::MakePhaseKey(format, stage)];
stats.count += 1;
stats.input_bytes += inputBytes;
stats.output_bytes += outputBytes;
stats.elapsed_us += elapsedUs;
g_pack_profile.snapshot_dirty = true;
}
void FlushPackProfileSnapshot(std::string_view reason)
{
if (!g_pack_profile.enabled)
{
return;
}
std::lock_guard<std::mutex> lock(g_pack_profile.mutex);
g_pack_profile.WriteReportLocked(reason);
g_pack_profile.snapshot_dirty = false;
}