Source code for simvx.core.debug.profiler

"""Frame profiler with ring-buffer timing."""


from __future__ import annotations

import logging
import time

log = logging.getLogger(__name__)

__all__ = ["FrameProfiler"]

_RING_SIZE = 300  # ~5s at 60fps


[docs] class FrameProfiler: """Ring-buffer frame timer. begin()/end() are no-ops when disabled.""" def __init__(self): self.enabled: bool = True self._ring: list[dict[str, float]] = [{}] * _RING_SIZE self._index: int = 0 self._current: dict[str, float] = {} self._starts: dict[str, float] = {} self._node_count: int = 0 self._control_count: int = 0 self._last_frame_time: float = 0.0 self._last_frame_ts: float = time.perf_counter()
[docs] def begin(self, phase: str): if not self.enabled: return self._starts[phase] = time.perf_counter()
[docs] def end(self, phase: str): if not self.enabled: return start = self._starts.pop(phase, None) if start is not None: self._current[phase] = (time.perf_counter() - start) * 1000.0
[docs] def end_frame(self): if not self.enabled: return now = time.perf_counter() self._last_frame_time = (now - self._last_frame_ts) * 1000.0 self._last_frame_ts = now self._ring[self._index] = dict(self._current) self._index = (self._index + 1) % _RING_SIZE self._current.clear() self._starts.clear()
[docs] def count_nodes(self, tree): """Traverse tree and count nodes/controls. Call once per frame.""" if not self.enabled or tree.root is None: self._node_count = 0 self._control_count = 0 return from ..ui import Control nodes = 0 controls = 0 def _walk(node): nonlocal nodes, controls nodes += 1 if isinstance(node, Control): controls += 1 for child in node.children: _walk(child) _walk(tree.root) self._node_count = nodes self._control_count = controls
@property def fps(self) -> float: return 1000.0 / self._last_frame_time if self._last_frame_time > 0 else 0.0 @property def frame_time_ms(self) -> float: return self._last_frame_time @property def node_count(self) -> int: return self._node_count @property def control_count(self) -> int: return self._control_count @property def last_frame(self) -> dict[str, float]: """Phase timings from the most recent completed frame.""" idx = (self._index - 1) % _RING_SIZE return self._ring[idx]
[docs] def phase_avg_ms(self, phase: str, count: int = 60) -> float: """Average time for a phase over the last `count` frames.""" total = 0.0 n = 0 for i in range(count): idx = (self._index - 1 - i) % _RING_SIZE val = self._ring[idx].get(phase) if val is not None: total += val n += 1 return total / n if n > 0 else 0.0
[docs] def frame_times(self, count: int = 120) -> list[float]: """Recent 'total' phase times for graph rendering.""" result = [] for i in range(count): idx = (self._index - 1 - i) % _RING_SIZE t = self._ring[idx].get("total", 0.0) result.append(t) result.reverse() return result