Files
ObsidianDragon/src/util/perf_log.h
DanS 3aee55b49c ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency.
Built with Dear ImGui, SDL3, and OpenGL3/DX11.

Features:
- Send/receive shielded and transparent transactions
- Autoshield with merged transaction display
- Built-in CPU mining (xmrig)
- Peer management and network monitoring
- Wallet encryption with PIN lock
- QR code generation for receive addresses
- Transaction history with pagination
- Console for direct RPC commands
- Cross-platform (Linux, Windows)
2026-02-27 00:26:01 -06:00

200 lines
6.1 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <chrono>
#include <fstream>
#include <mutex>
#include <vector>
#include <unordered_map>
#include <cstdint>
#include <algorithm>
#include <cstdio>
#include "../util/logger.h"
namespace dragonx {
namespace util {
/**
* @brief Lightweight per-frame performance profiler that writes periodic
* summaries to a log file.
*
* Usage:
* PerfLog::instance().beginFrame();
* { PERF_SCOPE("EventPoll"); ... }
* { PERF_SCOPE("AppUpdate"); ... }
* { PERF_SCOPE("AppRender"); ... }
* PerfLog::instance().endFrame();
*
* Every N frames (default 300 ≈ 5s at 60fps) it appends a summary block
* to <ObsidianDragonDir>/perf.log with min/avg/max/p95 for each zone.
*/
class PerfLog {
public:
static PerfLog& instance() {
static PerfLog s;
return s;
}
/// Open (or re-open) the log file. Called once at startup.
bool init(const std::string& path, int flushIntervalFrames = 300) {
std::lock_guard<std::mutex> lk(mutex_);
flushInterval_ = flushIntervalFrames;
file_.open(path, std::ios::out | std::ios::app);
if (!file_.is_open()) {
DEBUG_LOGF("[PerfLog] Failed to open %s\n", path.c_str());
return false;
}
DEBUG_LOGF("[PerfLog] Logging to %s (flush every %d frames)\n",
path.c_str(), flushInterval_);
// Header
file_ << "\n========== PerfLog started ==========\n";
file_.flush();
ready_ = true;
return true;
}
/// Call at the very start of the frame.
void beginFrame() {
if (!ready_) return;
frameStart_ = Clock::now();
}
/// Call at the very end of the frame (after present/swap).
void endFrame() {
if (!ready_) return;
double totalUs = usFrom(frameStart_);
record("FRAME_TOTAL", totalUs);
frameCount_++;
if (frameCount_ >= flushInterval_) {
flush();
frameCount_ = 0;
}
}
/// Begin a named zone. Returns an opaque time point.
std::chrono::steady_clock::time_point beginZone() {
return Clock::now();
}
/// End a named zone. Pass the time point from beginZone().
void endZone(const char* name, std::chrono::steady_clock::time_point start) {
if (!ready_) return;
record(name, usFrom(start));
}
private:
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
PerfLog() = default;
~PerfLog() {
if (ready_ && frameCount_ > 0) flush();
}
PerfLog(const PerfLog&) = delete;
PerfLog& operator=(const PerfLog&) = delete;
static double usFrom(TimePoint start) {
return std::chrono::duration<double, std::micro>(Clock::now() - start).count();
}
/// Record a single timing sample (microseconds).
void record(const char* name, double us) {
auto& bucket = buckets_[name];
bucket.samples.push_back(us);
}
/// Write accumulated stats and reset.
void flush() {
std::lock_guard<std::mutex> lk(mutex_);
if (!file_.is_open()) return;
auto now = std::chrono::system_clock::now();
auto tt = std::chrono::system_clock::to_time_t(now);
char timeBuf[64];
std::strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", std::localtime(&tt));
file_ << "\n--- " << timeBuf << " (" << frameCount_ << " frames) ---\n";
// Sort zone names for consistent output
std::vector<std::string> names;
names.reserve(buckets_.size());
for (auto& kv : buckets_) names.push_back(kv.first);
std::sort(names.begin(), names.end());
for (auto& name : names) {
auto& b = buckets_[name];
if (b.samples.empty()) continue;
std::sort(b.samples.begin(), b.samples.end());
size_t n = b.samples.size();
double sum = 0;
for (double v : b.samples) sum += v;
double avg = sum / n;
double mn = b.samples.front();
double mx = b.samples.back();
double p95 = b.samples[std::min(n - 1, (size_t)(n * 0.95))];
double p99 = b.samples[std::min(n - 1, (size_t)(n * 0.99))];
char line[256];
std::snprintf(line, sizeof(line),
" %-28s min=%7.0f avg=%7.0f p95=%7.0f p99=%7.0f max=%7.0f us\n",
name.c_str(), mn, avg, p95, p99, mx);
file_ << line;
}
// FPS summary from FRAME_TOTAL
auto it = buckets_.find("FRAME_TOTAL");
if (it != buckets_.end() && !it->second.samples.empty()) {
auto& s = it->second.samples;
double avgFrame = 0;
for (double v : s) avgFrame += v;
avgFrame /= s.size();
double fps = (avgFrame > 0) ? 1000000.0 / avgFrame : 0;
char fpsLine[128];
std::snprintf(fpsLine, sizeof(fpsLine),
" FPS: %.1f (avg frame %.1f us = %.2f ms)\n",
fps, avgFrame, avgFrame / 1000.0);
file_ << fpsLine;
}
file_.flush();
// Reset all buckets
for (auto& kv : buckets_) kv.second.samples.clear();
}
struct Bucket {
std::vector<double> samples; // microseconds
};
std::mutex mutex_;
std::ofstream file_;
bool ready_ = false;
int flushInterval_ = 300;
int frameCount_ = 0;
TimePoint frameStart_;
std::unordered_map<std::string, Bucket> buckets_;
};
/// RAII scope timer — records duration under a named zone.
struct PerfScope {
const char* name;
std::chrono::steady_clock::time_point start;
PerfScope(const char* n) : name(n), start(PerfLog::instance().beginZone()) {}
~PerfScope() { PerfLog::instance().endZone(name, start); }
};
#define PERF_SCOPE(name) ::dragonx::util::PerfScope _perf_##__LINE__(name)
// Manual begin/end for zones that span across scopes
#define PERF_BEGIN(var) auto var = ::dragonx::util::PerfLog::instance().beginZone()
#define PERF_END(name, var) ::dragonx::util::PerfLog::instance().endZone(name, var)
} // namespace util
} // namespace dragonx