feat: add basic profiling/metrics system (#104)
- Add ProfilingMetrics struct to track performance data - Track frame time (current and 60-frame rolling average) - Calculate FPS from average frame time - Count draw calls, UI elements, and visible elements per frame - Track total runtime and current frame number - PyScene counts elements during render - Expose metrics via mcrfpy.getMetrics() returning dict This provides basic performance monitoring capabilities for identifying bottlenecks and optimizing rendering performance. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1c7195a748
commit
f23aa784f2
|
@ -140,6 +140,9 @@ void GameEngine::run()
|
||||||
clock.restart();
|
clock.restart();
|
||||||
while (running)
|
while (running)
|
||||||
{
|
{
|
||||||
|
// Reset per-frame metrics
|
||||||
|
metrics.resetPerFrame();
|
||||||
|
|
||||||
currentScene()->update();
|
currentScene()->update();
|
||||||
testTimers();
|
testTimers();
|
||||||
|
|
||||||
|
@ -171,8 +174,12 @@ void GameEngine::run()
|
||||||
currentFrame++;
|
currentFrame++;
|
||||||
frameTime = clock.restart().asSeconds();
|
frameTime = clock.restart().asSeconds();
|
||||||
fps = 1 / frameTime;
|
fps = 1 / frameTime;
|
||||||
int whole_fps = (int)fps;
|
|
||||||
int tenth_fps = int(fps * 100) % 10;
|
// Update profiling metrics
|
||||||
|
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
|
||||||
|
|
||||||
|
int whole_fps = metrics.fps;
|
||||||
|
int tenth_fps = (metrics.fps * 10) % 10;
|
||||||
|
|
||||||
if (!headless && window) {
|
if (!headless && window) {
|
||||||
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
||||||
|
|
|
@ -37,6 +37,41 @@ public:
|
||||||
//std::map<std::string, Timer> timers;
|
//std::map<std::string, Timer> timers;
|
||||||
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
||||||
std::string scene;
|
std::string scene;
|
||||||
|
|
||||||
|
// Profiling metrics
|
||||||
|
struct ProfilingMetrics {
|
||||||
|
float frameTime = 0.0f; // Current frame time in milliseconds
|
||||||
|
float avgFrameTime = 0.0f; // Average frame time over last N frames
|
||||||
|
int fps = 0; // Frames per second
|
||||||
|
int drawCalls = 0; // Draw calls per frame
|
||||||
|
int uiElements = 0; // Number of UI elements rendered
|
||||||
|
int visibleElements = 0; // Number of visible elements
|
||||||
|
|
||||||
|
// Frame time history for averaging
|
||||||
|
static constexpr int HISTORY_SIZE = 60;
|
||||||
|
float frameTimeHistory[HISTORY_SIZE] = {0};
|
||||||
|
int historyIndex = 0;
|
||||||
|
|
||||||
|
void updateFrameTime(float deltaMs) {
|
||||||
|
frameTime = deltaMs;
|
||||||
|
frameTimeHistory[historyIndex] = deltaMs;
|
||||||
|
historyIndex = (historyIndex + 1) % HISTORY_SIZE;
|
||||||
|
|
||||||
|
// Calculate average
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (int i = 0; i < HISTORY_SIZE; ++i) {
|
||||||
|
sum += frameTimeHistory[i];
|
||||||
|
}
|
||||||
|
avgFrameTime = sum / HISTORY_SIZE;
|
||||||
|
fps = avgFrameTime > 0 ? static_cast<int>(1000.0f / avgFrameTime) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetPerFrame() {
|
||||||
|
drawCalls = 0;
|
||||||
|
uiElements = 0;
|
||||||
|
visibleElements = 0;
|
||||||
|
}
|
||||||
|
} metrics;
|
||||||
GameEngine();
|
GameEngine();
|
||||||
GameEngine(const McRogueFaceConfig& cfg);
|
GameEngine(const McRogueFaceConfig& cfg);
|
||||||
~GameEngine();
|
~GameEngine();
|
||||||
|
|
|
@ -45,6 +45,8 @@ static PyMethodDef mcrfpyMethods[] = {
|
||||||
{"find", McRFPy_API::_find, METH_VARARGS, "find(name, scene=None) - find first UI element with given name"},
|
{"find", McRFPy_API::_find, METH_VARARGS, "find(name, scene=None) - find first UI element with given name"},
|
||||||
{"findAll", McRFPy_API::_findAll, METH_VARARGS, "findAll(pattern, scene=None) - find all UI elements matching name pattern (supports * wildcards)"},
|
{"findAll", McRFPy_API::_findAll, METH_VARARGS, "findAll(pattern, scene=None) - find all UI elements matching name pattern (supports * wildcards)"},
|
||||||
|
|
||||||
|
{"getMetrics", McRFPy_API::_getMetrics, METH_VARARGS, "getMetrics() - get performance metrics (returns dict)"},
|
||||||
|
|
||||||
{NULL, NULL, 0, NULL}
|
{NULL, NULL, 0, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -882,3 +884,25 @@ PyObject* McRFPy_API::_findAll(PyObject* self, PyObject* args) {
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject* McRFPy_API::_getMetrics(PyObject* self, PyObject* args) {
|
||||||
|
// Create a dictionary with metrics
|
||||||
|
PyObject* dict = PyDict_New();
|
||||||
|
if (!dict) return NULL;
|
||||||
|
|
||||||
|
// Add frame time metrics
|
||||||
|
PyDict_SetItemString(dict, "frame_time", PyFloat_FromDouble(game->metrics.frameTime));
|
||||||
|
PyDict_SetItemString(dict, "avg_frame_time", PyFloat_FromDouble(game->metrics.avgFrameTime));
|
||||||
|
PyDict_SetItemString(dict, "fps", PyLong_FromLong(game->metrics.fps));
|
||||||
|
|
||||||
|
// Add draw call metrics
|
||||||
|
PyDict_SetItemString(dict, "draw_calls", PyLong_FromLong(game->metrics.drawCalls));
|
||||||
|
PyDict_SetItemString(dict, "ui_elements", PyLong_FromLong(game->metrics.uiElements));
|
||||||
|
PyDict_SetItemString(dict, "visible_elements", PyLong_FromLong(game->metrics.visibleElements));
|
||||||
|
|
||||||
|
// Add general metrics
|
||||||
|
PyDict_SetItemString(dict, "current_frame", PyLong_FromLong(game->getFrame()));
|
||||||
|
PyDict_SetItemString(dict, "runtime", PyFloat_FromDouble(game->runtime.getElapsedTime().asSeconds()));
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
|
@ -77,4 +77,7 @@ public:
|
||||||
// Name-based finding methods
|
// Name-based finding methods
|
||||||
static PyObject* _find(PyObject*, PyObject*);
|
static PyObject* _find(PyObject*, PyObject*);
|
||||||
static PyObject* _findAll(PyObject*, PyObject*);
|
static PyObject* _findAll(PyObject*, PyObject*);
|
||||||
|
|
||||||
|
// Profiling/metrics
|
||||||
|
static PyObject* _getMetrics(PyObject*, PyObject*);
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,9 +72,17 @@ void PyScene::render()
|
||||||
// Render in sorted order (no need to copy anymore)
|
// Render in sorted order (no need to copy anymore)
|
||||||
for (auto e: *ui_elements)
|
for (auto e: *ui_elements)
|
||||||
{
|
{
|
||||||
if (e)
|
if (e) {
|
||||||
|
// Track metrics
|
||||||
|
game->metrics.uiElements++;
|
||||||
|
if (e->visible) {
|
||||||
|
game->metrics.visibleElements++;
|
||||||
|
// Count this as a draw call (each visible element = 1+ draw calls)
|
||||||
|
game->metrics.drawCalls++;
|
||||||
|
}
|
||||||
e->render();
|
e->render();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Display is handled by GameEngine
|
// Display is handled by GameEngine
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue