// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include #include #include #include #include #include #include #include #include #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 /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 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(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 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 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 samples; // microseconds }; std::mutex mutex_; std::ofstream file_; bool ready_ = false; int flushInterval_ = 300; int frameCount_ = 0; TimePoint frameStart_; std::unordered_map 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