feat: Add benchmark logging system for performance analysis (closes #104)
Add Python API for capturing performance data to JSON files: - mcrfpy.start_benchmark() - start capturing frame data - mcrfpy.end_benchmark() - stop and return filename - mcrfpy.log_benchmark(msg) - add log message to current frame The benchmark system captures per-frame data including: - Frame timing (frame_time_ms, fps, timestamp) - Detailed timing breakdown (grid_render, entity_render, python, animation, fov) - Draw call and element counts - User log messages attached to frames Output JSON format supports analysis tools and includes: - Benchmark metadata (PID, timestamps, duration, total frames) - Full frame-by-frame metrics array Also refactors ProfilingMetrics from nested GameEngine struct to top-level struct for easier forward declaration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
219a559c35
commit
a7fef2aeb6
7 changed files with 551 additions and 59 deletions
243
src/BenchmarkLogger.h
Normal file
243
src/BenchmarkLogger.h
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <stdexcept>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
#define getpid _getpid
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// Forward declaration
|
||||
struct ProfilingMetrics;
|
||||
|
||||
/**
|
||||
* @brief Frame data captured during benchmarking
|
||||
*/
|
||||
struct BenchmarkFrame {
|
||||
int frame_number;
|
||||
double timestamp_ms; // Time since benchmark start
|
||||
float frame_time_ms;
|
||||
int fps;
|
||||
|
||||
// Detailed timing breakdown
|
||||
float grid_render_ms;
|
||||
float entity_render_ms;
|
||||
float python_time_ms;
|
||||
float animation_time_ms;
|
||||
float fov_overlay_ms;
|
||||
|
||||
// Counts
|
||||
int draw_calls;
|
||||
int ui_elements;
|
||||
int visible_elements;
|
||||
int grid_cells_rendered;
|
||||
int entities_rendered;
|
||||
int total_entities;
|
||||
|
||||
// User-provided log messages for this frame
|
||||
std::vector<std::string> logs;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Benchmark logging system for capturing performance data to JSON files
|
||||
*
|
||||
* Usage from Python:
|
||||
* mcrfpy.start_benchmark() # Start capturing
|
||||
* mcrfpy.log_benchmark("msg") # Add comment to current frame
|
||||
* filename = mcrfpy.end_benchmark() # Stop and get filename
|
||||
*/
|
||||
class BenchmarkLogger {
|
||||
private:
|
||||
bool running;
|
||||
std::string filename;
|
||||
std::chrono::high_resolution_clock::time_point start_time;
|
||||
std::vector<BenchmarkFrame> frames;
|
||||
std::vector<std::string> pending_logs; // Logs for current frame (before it's recorded)
|
||||
int frame_counter;
|
||||
|
||||
// Generate filename based on PID and timestamp
|
||||
std::string generateFilename() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm = *std::localtime(&time_t);
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "benchmark_" << getpid() << "_"
|
||||
<< std::put_time(&tm, "%Y%m%d_%H%M%S") << ".json";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Get current timestamp as ISO 8601 string
|
||||
std::string getCurrentTimestamp() {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time_t = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm tm = *std::localtime(&time_t);
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S");
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Escape string for JSON
|
||||
std::string escapeJson(const std::string& str) {
|
||||
std::ostringstream oss;
|
||||
for (char c : str) {
|
||||
switch (c) {
|
||||
case '"': oss << "\\\""; break;
|
||||
case '\\': oss << "\\\\"; break;
|
||||
case '\b': oss << "\\b"; break;
|
||||
case '\f': oss << "\\f"; break;
|
||||
case '\n': oss << "\\n"; break;
|
||||
case '\r': oss << "\\r"; break;
|
||||
case '\t': oss << "\\t"; break;
|
||||
default:
|
||||
if ('\x00' <= c && c <= '\x1f') {
|
||||
oss << "\\u" << std::hex << std::setw(4) << std::setfill('0') << (int)c;
|
||||
} else {
|
||||
oss << c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string start_timestamp;
|
||||
|
||||
public:
|
||||
BenchmarkLogger() : running(false), frame_counter(0) {}
|
||||
|
||||
/**
|
||||
* @brief Start benchmark logging
|
||||
* @throws std::runtime_error if already running
|
||||
*/
|
||||
void start() {
|
||||
if (running) {
|
||||
throw std::runtime_error("Benchmark already running. Call end_benchmark() first.");
|
||||
}
|
||||
|
||||
running = true;
|
||||
filename = generateFilename();
|
||||
start_time = std::chrono::high_resolution_clock::now();
|
||||
start_timestamp = getCurrentTimestamp();
|
||||
frames.clear();
|
||||
pending_logs.clear();
|
||||
frame_counter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Stop benchmark logging and write to file
|
||||
* @return The filename that was written
|
||||
* @throws std::runtime_error if not running
|
||||
*/
|
||||
std::string end() {
|
||||
if (!running) {
|
||||
throw std::runtime_error("No benchmark running. Call start_benchmark() first.");
|
||||
}
|
||||
|
||||
running = false;
|
||||
|
||||
// Calculate duration
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
double duration_seconds = std::chrono::duration<double>(end_time - start_time).count();
|
||||
std::string end_timestamp = getCurrentTimestamp();
|
||||
|
||||
// Write JSON file
|
||||
std::ofstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Failed to open benchmark file for writing: " + filename);
|
||||
}
|
||||
|
||||
file << "{\n";
|
||||
file << " \"benchmark\": {\n";
|
||||
file << " \"pid\": " << getpid() << ",\n";
|
||||
file << " \"start_time\": \"" << start_timestamp << "\",\n";
|
||||
file << " \"end_time\": \"" << end_timestamp << "\",\n";
|
||||
file << " \"total_frames\": " << frames.size() << ",\n";
|
||||
file << " \"duration_seconds\": " << std::fixed << std::setprecision(3) << duration_seconds << "\n";
|
||||
file << " },\n";
|
||||
|
||||
file << " \"frames\": [\n";
|
||||
for (size_t i = 0; i < frames.size(); ++i) {
|
||||
const auto& f = frames[i];
|
||||
file << " {\n";
|
||||
file << " \"frame_number\": " << f.frame_number << ",\n";
|
||||
file << " \"timestamp_ms\": " << std::fixed << std::setprecision(3) << f.timestamp_ms << ",\n";
|
||||
file << " \"frame_time_ms\": " << std::setprecision(3) << f.frame_time_ms << ",\n";
|
||||
file << " \"fps\": " << f.fps << ",\n";
|
||||
file << " \"grid_render_ms\": " << std::setprecision(3) << f.grid_render_ms << ",\n";
|
||||
file << " \"entity_render_ms\": " << std::setprecision(3) << f.entity_render_ms << ",\n";
|
||||
file << " \"python_time_ms\": " << std::setprecision(3) << f.python_time_ms << ",\n";
|
||||
file << " \"animation_time_ms\": " << std::setprecision(3) << f.animation_time_ms << ",\n";
|
||||
file << " \"fov_overlay_ms\": " << std::setprecision(3) << f.fov_overlay_ms << ",\n";
|
||||
file << " \"draw_calls\": " << f.draw_calls << ",\n";
|
||||
file << " \"ui_elements\": " << f.ui_elements << ",\n";
|
||||
file << " \"visible_elements\": " << f.visible_elements << ",\n";
|
||||
file << " \"grid_cells_rendered\": " << f.grid_cells_rendered << ",\n";
|
||||
file << " \"entities_rendered\": " << f.entities_rendered << ",\n";
|
||||
file << " \"total_entities\": " << f.total_entities << ",\n";
|
||||
|
||||
// Write logs array
|
||||
file << " \"logs\": [";
|
||||
for (size_t j = 0; j < f.logs.size(); ++j) {
|
||||
file << "\"" << escapeJson(f.logs[j]) << "\"";
|
||||
if (j < f.logs.size() - 1) file << ", ";
|
||||
}
|
||||
file << "]\n";
|
||||
|
||||
file << " }";
|
||||
if (i < frames.size() - 1) file << ",";
|
||||
file << "\n";
|
||||
}
|
||||
file << " ]\n";
|
||||
file << "}\n";
|
||||
|
||||
file.close();
|
||||
|
||||
std::string result = filename;
|
||||
filename.clear();
|
||||
frames.clear();
|
||||
pending_logs.clear();
|
||||
frame_counter = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a log message to the current frame
|
||||
* @param message The message to log
|
||||
* @throws std::runtime_error if not running
|
||||
*/
|
||||
void log(const std::string& message) {
|
||||
if (!running) {
|
||||
throw std::runtime_error("No benchmark running. Call start_benchmark() first.");
|
||||
}
|
||||
pending_logs.push_back(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Record frame data (called by game loop at end of each frame)
|
||||
* @param metrics The current frame's profiling metrics
|
||||
*/
|
||||
void recordFrame(const ProfilingMetrics& metrics);
|
||||
|
||||
/**
|
||||
* @brief Check if benchmark is currently running
|
||||
*/
|
||||
bool isRunning() const { return running; }
|
||||
|
||||
/**
|
||||
* @brief Get current frame count
|
||||
*/
|
||||
int getFrameCount() const { return frame_counter; }
|
||||
};
|
||||
|
||||
// Global benchmark logger instance
|
||||
extern BenchmarkLogger g_benchmarkLogger;
|
||||
Loading…
Add table
Add a link
Reference in a new issue