From e8a11591a8b423ad3730846c1b39a4441b4e8d03 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Mon, 3 Nov 2025 21:45:46 -0800 Subject: [PATCH] ui: add render loop profiling (#36558) --- system/ui/lib/application.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index bca836334f..f9d7b0c9c1 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -31,6 +31,7 @@ SHOW_FPS = os.getenv("SHOW_FPS") == "1" SHOW_TOUCHES = os.getenv("SHOW_TOUCHES") == "1" STRICT_MODE = os.getenv("STRICT_MODE") == "1" SCALE = float(os.getenv("SCALE", "1.0")) +PROFILE_RENDER = int(os.getenv("PROFILE_RENDER", "0")) DEFAULT_TEXT_SIZE = 60 DEFAULT_TEXT_COLOR = rl.WHITE @@ -172,6 +173,9 @@ class GuiApplication: self._mouse_history: deque[MousePosWithTime] = deque(maxlen=MOUSE_THREAD_RATE) self._show_touches = SHOW_TOUCHES self._show_fps = SHOW_FPS + self._profile_render_frames = PROFILE_RENDER + self._render_profiler = None + self._render_profile_start_time = None @property def frame(self): @@ -345,6 +349,12 @@ class GuiApplication: def render(self): try: + if self._profile_render_frames > 0: + import cProfile + self._render_profiler = cProfile.Profile() + self._render_profile_start_time = time.monotonic() + self._render_profiler.enable() + while not (self._window_close_requested or rl.window_should_close()): if PC: # Thread is not used on PC, need to manually add mouse events @@ -429,6 +439,9 @@ class GuiApplication: rl.end_drawing() self._monitor_fps() self._frame += 1 + + if self._profile_render_frames > 0 and self._frame >= self._profile_render_frames: + self._output_render_profile() except KeyboardInterrupt: pass @@ -526,6 +539,25 @@ class GuiApplication: cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.") os._exit(1) + def _output_render_profile(self): + import io + import pstats + + self._render_profiler.disable() + elapsed_ms = (time.monotonic() - self._render_profile_start_time) * 1e3 + avg_frame_time = elapsed_ms / self._frame if self._frame > 0 else 0 + + stats_stream = io.StringIO() + pstats.Stats(self._render_profiler, stream=stats_stream).sort_stats("cumtime").print_stats(25) + print("\n=== Render loop profile ===") + print(stats_stream.getvalue().rstrip()) + + green = "\033[92m" + reset = "\033[0m" + print(f"\n{green}Rendered {self._frame} frames in {elapsed_ms:.1f} ms{reset}") + print(f"{green}Average frame time: {avg_frame_time:.2f} ms ({1000/avg_frame_time:.1f} FPS){reset}") + sys.exit(0) + def _calculate_auto_scale(self) -> float: # Create temporary window to query monitor info rl.init_window(1, 1, "")