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