From f23aa784f2f901f05b4173c846bdd6088dc94a8a Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 6 Jul 2025 12:15:32 -0400 Subject: [PATCH] feat: add basic profiling/metrics system (#104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/GameEngine.cpp | 11 +++++++++-- src/GameEngine.h | 35 +++++++++++++++++++++++++++++++++++ src/McRFPy_API.cpp | 24 ++++++++++++++++++++++++ src/McRFPy_API.h | 3 +++ src/PyScene.cpp | 10 +++++++++- 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index fea99df..0cee428 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -140,6 +140,9 @@ void GameEngine::run() clock.restart(); while (running) { + // Reset per-frame metrics + metrics.resetPerFrame(); + currentScene()->update(); testTimers(); @@ -171,8 +174,12 @@ void GameEngine::run() currentFrame++; frameTime = clock.restart().asSeconds(); 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) { window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS"); diff --git a/src/GameEngine.h b/src/GameEngine.h index 8b7a198..9c668d9 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -37,6 +37,41 @@ public: //std::map timers; std::map> timers; 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(1000.0f / avgFrameTime) : 0; + } + + void resetPerFrame() { + drawCalls = 0; + uiElements = 0; + visibleElements = 0; + } + } metrics; GameEngine(); GameEngine(const McRogueFaceConfig& cfg); ~GameEngine(); diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 18bc7c5..96eb728 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -45,6 +45,8 @@ static PyMethodDef mcrfpyMethods[] = { {"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)"}, + {"getMetrics", McRFPy_API::_getMetrics, METH_VARARGS, "getMetrics() - get performance metrics (returns dict)"}, + {NULL, NULL, 0, NULL} }; @@ -882,3 +884,25 @@ PyObject* McRFPy_API::_findAll(PyObject* self, PyObject* args) { 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; +} diff --git a/src/McRFPy_API.h b/src/McRFPy_API.h index e1f776a..264d15b 100644 --- a/src/McRFPy_API.h +++ b/src/McRFPy_API.h @@ -77,4 +77,7 @@ public: // Name-based finding methods static PyObject* _find(PyObject*, PyObject*); static PyObject* _findAll(PyObject*, PyObject*); + + // Profiling/metrics + static PyObject* _getMetrics(PyObject*, PyObject*); }; diff --git a/src/PyScene.cpp b/src/PyScene.cpp index a05a395..0c4919d 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -72,8 +72,16 @@ void PyScene::render() // Render in sorted order (no need to copy anymore) 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(); + } } // Display is handled by GameEngine