Skip to content

Commit 298c2cf

Browse files
committed
Add performance timing metrics for React reconciliation and ImGui rendering
Implement comprehensive performance monitoring system that measures and displays timing statistics: **JavaScript Measurements:** 1. React Reconciliation (lib/react-imgui-reconciler/): - Measure in host-config.js prepareForCommit → resetAfterCommit - Captures actual reconciliation work (component execution + React diffing + commit) - Calculate statistics in perf-stats.js: * Moving average from last 30 samples * Time-windowed max (last 5 seconds) - Module-scope variables for efficiency 2. ImGui Rendering (lib/imgui-unit/renderer.js): - Measure tree traversal and ImGui FFI calls - Runs every frame - Export timing via globalThis.perfMetrics **C++ Display (lib/imgui-runtime/imgui-runtime.cpp):** - Read metrics from JavaScript globalThis in update_performance_metrics() - Calculate EMA for ImGui render time (α=0.1) - Update display values once per second (prevents blurring) - Render with Sokol Debug Text at bottom-left corner: * FPS: 60 * ImGui: 850us (EMA, per frame) * React: 120/235us (avg/max in microseconds) **CLAUDE.md Updates:** - Add development workflow rule: Do not commit before user review and testing
1 parent 10ea88b commit 298c2cf

File tree

5 files changed

+145
-3
lines changed

5 files changed

+145
-3
lines changed

CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ When modifying the project structure, build system, architecture, or implementat
1111

1212
Always update CLAUDE.md as part of your work - do not defer documentation updates.
1313

14+
## IMPORTANT: Development Workflow
15+
16+
**⚠️ DO NOT COMMIT CODE BEFORE USER REVIEW AND TESTING**
17+
18+
When implementing changes:
19+
1. Write and test the code
20+
2. Wait for the user to review the changes
21+
3. Wait for the user to test the implementation
22+
4. Only commit after explicit user approval
23+
24+
Never commit code immediately after implementation. The user must review and test first.
25+
1426
## Project Overview
1527

1628
This project implements a custom React reconciler that renders to DearImGUI using Static Hermes. The goal is to use React's declarative component model and JSX syntax to describe ImGUI interfaces, while learning how React works internally.

lib/imgui-runtime/imgui-runtime.cpp

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ static uint64_t s_start_time = 0;
103103
static uint64_t s_last_fps_time = 0;
104104
static double s_fps = 0;
105105

106+
// Performance metrics
107+
static double s_react_avg_ms = 0; // React reconciliation average (accumulated)
108+
static double s_react_max_ms = 0; // React reconciliation max (accumulated)
109+
static double s_imgui_avg_ms = 0; // ImGui render average (EMA, accumulated)
110+
static double s_react_avg_ms_display = 0; // React avg (displayed, updated once/sec)
111+
static double s_react_max_ms_display = 0; // React max (displayed, updated once/sec)
112+
static double s_imgui_avg_ms_display = 0; // ImGui render average (displayed, updated once/sec)
113+
114+
106115
extern "C" int load_image(const char *path) {
107116
s_images.emplace_back(std::make_unique<Image>(path));
108117
return s_images.size() - 1;
@@ -192,6 +201,34 @@ static void app_event(const sapp_event *ev) {
192201
static float s_bg_color[4] = {0.0f, 0.0f, 0.0f, 0.0f};
193202
extern "C" float *get_bg_color() { return s_bg_color; }
194203

204+
static void update_performance_metrics() {
205+
// Read performance metrics from JavaScript
206+
try {
207+
auto global = s_hermesApp->hermes->global();
208+
209+
if (global.hasProperty(*s_hermesApp->hermes, "perfMetrics")) {
210+
auto metrics = global.getPropertyAsObject(*s_hermesApp->hermes, "perfMetrics");
211+
212+
// Read React reconciliation stats (calculated in JS)
213+
if (metrics.hasProperty(*s_hermesApp->hermes, "reconciliationAvg")) {
214+
s_react_avg_ms = metrics.getProperty(*s_hermesApp->hermes, "reconciliationAvg").asNumber();
215+
}
216+
if (metrics.hasProperty(*s_hermesApp->hermes, "reconciliationMax")) {
217+
s_react_max_ms = metrics.getProperty(*s_hermesApp->hermes, "reconciliationMax").asNumber();
218+
}
219+
220+
// Read and calculate EMA for ImGui render time (every frame)
221+
if (metrics.hasProperty(*s_hermesApp->hermes, "renderTime")) {
222+
double renderTime = metrics.getProperty(*s_hermesApp->hermes, "renderTime").asNumber();
223+
const double alpha = 0.1; // Smoothing factor
224+
s_imgui_avg_ms = s_imgui_avg_ms * (1.0 - alpha) + renderTime * alpha;
225+
}
226+
}
227+
} catch (...) {
228+
// Ignore errors reading metrics
229+
}
230+
}
231+
195232
static void app_frame() {
196233
uint64_t now = stm_now();
197234
double curTimeMs = stm_ms(now);
@@ -201,10 +238,13 @@ static void app_frame() {
201238
s_start_time = now;
202239
s_last_fps_time = now;
203240
} else {
204-
// Update FPS every second
241+
// Update FPS and displayed performance metrics every second
205242
uint64_t diff = stm_diff(now, s_last_fps_time);
206243
if (diff > 1000000000) {
207-
s_fps = 1.0 / sapp_frame_duration(); // stm_sec(diff);
244+
s_fps = 1.0 / sapp_frame_duration();
245+
s_imgui_avg_ms_display = s_imgui_avg_ms; // Update displayed value
246+
s_react_avg_ms_display = s_react_avg_ms; // Update displayed value
247+
s_react_max_ms_display = s_react_max_ms; // Update displayed value
208248
s_last_fps_time = now;
209249
}
210250
}
@@ -247,9 +287,24 @@ static void app_frame() {
247287
slog_func("ERROR", 1, 0, e.what(), __LINE__, __FILE__, nullptr);
248288
}
249289

290+
update_performance_metrics();
291+
250292
simgui_render();
251293
sdtx_canvas((float)sapp_width(), (float)sapp_height());
252-
sdtx_printf("FPS: %d", (int)(s_fps + 0.5));
294+
295+
// Position at bottom-left corner
296+
// Each character is 8x8 pixels, calculate rows from bottom
297+
int num_rows = (int)sapp_height() / 8;
298+
int num_lines = s_react_avg_ms_display > 0 ? 3 : 2; // FPS + ImGui [+ React]
299+
sdtx_pos(0.0f, (float)(num_rows - num_lines));
300+
301+
sdtx_printf("FPS: %d\n", (int)(s_fps + 0.5));
302+
sdtx_printf("ImGui: %dus\n", (int)(s_imgui_avg_ms_display * 1000.0 + 0.5));
303+
if (s_react_avg_ms_display > 0) {
304+
sdtx_printf("React: %d/%dus",
305+
(int)(s_react_avg_ms_display * 1000.0 + 0.5),
306+
(int)(s_react_max_ms_display * 1000.0 + 0.5));
307+
}
253308
sdtx_draw();
254309
sg_end_pass();
255310
sg_commit();

lib/imgui-unit/renderer.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,8 @@ function renderNode(node: any): void {
709709
// Export render function
710710
globalThis.imguiUnit = {
711711
renderTree: function(): void {
712+
const startTime = globalThis.performance.now();
713+
712714
const reactApp = globalThis.reactApp;
713715
if (reactApp && reactApp.rootChildren) {
714716
// Validate that only one root component exists
@@ -728,6 +730,14 @@ globalThis.imguiUnit = {
728730
renderNode(reactApp.rootChildren[i]);
729731
}
730732
}
733+
734+
const duration = globalThis.performance.now() - startTime;
735+
736+
// Store for C++ to read
737+
if (!globalThis.perfMetrics) {
738+
globalThis.perfMetrics = {};
739+
}
740+
globalThis.perfMetrics.renderTime = duration;
731741
},
732742

733743
onTreeUpdate: function(): void {

lib/react-imgui-reconciler/host-config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
// See LICENSE file for full license text
44

55
import { TreeNode, TextNode } from './tree-node.js';
6+
import { updateReconciliationStats } from './perf-stats.js';
67

78
// React host config loaded
89

10+
// Timing for reconciliation
11+
let reconciliationStartTime = 0;
12+
913
/**
1014
* Host Config for React Reconciler
1115
*
@@ -351,6 +355,7 @@ const hostConfig = {
351355
* @returns null (we don't need to track anything)
352356
*/
353357
prepareForCommit(containerInfo) {
358+
reconciliationStartTime = performance.now();
354359
return null;
355360
},
356361

@@ -360,6 +365,14 @@ const hostConfig = {
360365
* This syncs our tree to globalThis so the ImGui renderer can access it.
361366
*/
362367
resetAfterCommit(containerInfo) {
368+
// Measure reconciliation time
369+
if (reconciliationStartTime > 0) {
370+
const duration = performance.now() - reconciliationStartTime;
371+
// console.log(`Reconciliation took ${duration.toFixed(3)}ms`);
372+
updateReconciliationStats(duration);
373+
reconciliationStartTime = 0;
374+
}
375+
363376
// Update global reference after every reconciliation
364377
if (globalThis.reactApp) {
365378
globalThis.reactApp.rootChildren = containerInfo.rootChildren || [];
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Tzvetan Mikov and contributors
2+
// SPDX-License-Identifier: MIT
3+
// See LICENSE file for full license text
4+
5+
// Reconciliation timing statistics - module scope for efficiency
6+
const RECONCILIATION_SAMPLE_SIZE = 30;
7+
const MAX_WINDOW_SECONDS = 5;
8+
9+
let reconciliationSamples = []; // Array of {time: timestamp, duration: ms}
10+
let reconciliationAverage = 0;
11+
let reconciliationMax = 0;
12+
13+
/**
14+
* Update reconciliation timing statistics.
15+
* Maintains a moving average of the last N samples and a time-windowed max.
16+
*/
17+
export function updateReconciliationStats(duration) {
18+
const now = performance.now();
19+
20+
// Add new sample
21+
reconciliationSamples.push({ time: now, duration });
22+
23+
// Keep only last N samples for average
24+
if (reconciliationSamples.length > RECONCILIATION_SAMPLE_SIZE) {
25+
reconciliationSamples.shift();
26+
}
27+
28+
// Calculate average from all samples
29+
let sum = 0;
30+
for (let i = 0; i < reconciliationSamples.length; i++) {
31+
sum += reconciliationSamples[i].duration;
32+
}
33+
reconciliationAverage = sum / reconciliationSamples.length;
34+
35+
// Calculate max from samples within time window
36+
const cutoffTime = now - (MAX_WINDOW_SECONDS * 1000);
37+
reconciliationMax = 0;
38+
for (let i = 0; i < reconciliationSamples.length; i++) {
39+
if (reconciliationSamples[i].time >= cutoffTime) {
40+
if (reconciliationSamples[i].duration > reconciliationMax) {
41+
reconciliationMax = reconciliationSamples[i].duration;
42+
}
43+
}
44+
}
45+
46+
// Expose to globalThis for C++ access
47+
if (!globalThis.perfMetrics) {
48+
globalThis.perfMetrics = {};
49+
}
50+
globalThis.perfMetrics.reconciliationAvg = reconciliationAverage;
51+
globalThis.perfMetrics.reconciliationMax = reconciliationMax;
52+
}

0 commit comments

Comments
 (0)