From 41d99890c1e57d724ace40e09f281267c2613886 Mon Sep 17 00:00:00 2001 From: catbot Date: Tue, 16 Jun 2026 15:25:22 +0000 Subject: [PATCH] perf(window): ring buffer for CRAFTER_TIMING frame times (#44) Replace the std::vector frameTimes plus erase(begin()) with a fixed-size std::array ring buffer (head + count). The previous code memmoved ~99 elements left every frame once the window filled. LogTiming computes order-independent sum/avg/min/max, so the ring's write order is irrelevant to the reported stats. Behind CRAFTER_TIMING only; shipping builds are unaffected. Co-Authored-By: Claude Opus 4.8 --- implementations/Crafter.Graphics-Window.cpp | 34 +++++++++++---------- interfaces/Crafter.Graphics-Window.cppm | 9 +++++- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/implementations/Crafter.Graphics-Window.cpp b/implementations/Crafter.Graphics-Window.cpp index 1388a03..411ffb1 100644 --- a/implementations/Crafter.Graphics-Window.cpp +++ b/implementations/Crafter.Graphics-Window.cpp @@ -657,11 +657,12 @@ void Window::Update() { #ifdef CRAFTER_TIMING frameEnd = std::chrono::high_resolution_clock::now(); - frameTimes.push_back(totalUpdate+totalRender); - - // Keep only the last 100 frame times - if (frameTimes.size() > 100) { - frameTimes.erase(frameTimes.begin()); + // Ring buffer: overwrite the oldest entry once full. Order doesn't matter + // to LogTiming, so we never need to shift elements. + frameTimes[frameTimesHead] = totalUpdate+totalRender; + frameTimesHead = (frameTimesHead + 1) % frameTimeCapacity; + if (frameTimesCount < frameTimeCapacity) { + ++frameTimesCount; } #endif lastFrameBegin = startTime; @@ -843,21 +844,22 @@ void Window::LogTiming() { std::cout << std::format("Total: {}", duration_cast(totalUpdate+totalRender)) << std::endl; std::cout << std::format("Vblank: {}", duration_cast(vblank)) << std::endl; - // Add 100-frame average and min-max timing info - if (!frameTimes.empty()) { + // Add 100-frame average and min-max timing info. Only the first + // frameTimesCount entries of the ring buffer hold valid samples. + if (frameTimesCount != 0) { // Calculate average std::chrono::nanoseconds sum(0); - for (const auto& frameTime : frameTimes) { - sum += frameTime; + for (std::size_t i = 0; i < frameTimesCount; ++i) { + sum += frameTimes[i]; } - auto average = sum / frameTimes.size(); - + auto average = sum / frameTimesCount; + // Find min and max - auto min = frameTimes.front(); - auto max = frameTimes.front(); - for (const auto& frameTime : frameTimes) { - if (frameTime < min) min = frameTime; - if (frameTime > max) max = frameTime; + auto min = frameTimes[0]; + auto max = frameTimes[0]; + for (std::size_t i = 0; i < frameTimesCount; ++i) { + if (frameTimes[i] < min) min = frameTimes[i]; + if (frameTimes[i] > max) max = frameTimes[i]; } std::cout << std::format("Last 100 Frame Times - Avg: {}, Min: {}, Max: {}", diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index b957882..9068267 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -164,7 +164,14 @@ export namespace Crafter { std::chrono::nanoseconds vblank; std::chrono::nanoseconds totalFrame; std::chrono::time_point frameEnd; - std::vector frameTimes; + // Fixed-size ring buffer of the most recent frame times. LogTiming does + // order-independent sum/avg/min/max, so head position is irrelevant to + // the reported stats; this avoids the per-frame memmove a vector::erase + // at the front would incur once full. + static constexpr std::size_t frameTimeCapacity = 100; + std::array frameTimes{}; + std::size_t frameTimesHead = 0; + std::size_t frameTimesCount = 0; void LogTiming(); #endif