@@ -64,6 +64,89 @@ class RobotMode(Enum):
6464 DAMPING = "damping" # Emergency stop / recovery
6565
6666
67+ class _LoopTimingReporter :
68+ """Aggregate best-effort control-loop timing stats and emit periodic logs."""
69+
70+ def __init__ (self , * , target_period_s : float , log_interval_s : float = 1.0 ) -> None :
71+ self ._target_period_s = float (target_period_s )
72+ self ._log_interval_s = float (log_interval_s )
73+ self ._window_start_s : float | None = None
74+ self ._loop_ms : list [float ] = []
75+ self ._work_ms : list [float ] = []
76+ self ._pico_age_ms : list [float ] = []
77+ self ._overrun_count = 0
78+
79+ def record (
80+ self ,
81+ * ,
82+ loop_start_s : float ,
83+ work_elapsed_s : float ,
84+ cycle_elapsed_s : float ,
85+ pico_age_s : float | None ,
86+ ) -> None :
87+ if self ._window_start_s is None :
88+ self ._window_start_s = float (loop_start_s )
89+
90+ self ._loop_ms .append (float (cycle_elapsed_s ) * 1000.0 )
91+ self ._work_ms .append (float (work_elapsed_s ) * 1000.0 )
92+ if pico_age_s is not None :
93+ self ._pico_age_ms .append (float (pico_age_s ) * 1000.0 )
94+ if cycle_elapsed_s > self ._target_period_s + 1e-9 :
95+ self ._overrun_count += 1
96+
97+ if loop_start_s - self ._window_start_s >= self ._log_interval_s :
98+ self ._emit (loop_start_s )
99+
100+ def _emit (self , end_s : float ) -> None :
101+ sample_count = len (self ._loop_ms )
102+ if sample_count <= 0 :
103+ self ._reset (end_s )
104+ return
105+
106+ loop_summary = self ._summarize (self ._loop_ms )
107+ work_summary = self ._summarize (self ._work_ms )
108+ message = (
109+ "Timing stats | samples=%d window=%.1fs | "
110+ "loop_ms p50=%.2f p95=%.2f p99=%.2f max=%.2f overrun=%d/%d | "
111+ "work_ms p50=%.2f p95=%.2f p99=%.2f max=%.2f"
112+ )
113+ args : list [object ] = [
114+ sample_count ,
115+ end_s - float (self ._window_start_s ),
116+ loop_summary [0 ],
117+ loop_summary [1 ],
118+ loop_summary [2 ],
119+ loop_summary [3 ],
120+ self ._overrun_count ,
121+ sample_count ,
122+ work_summary [0 ],
123+ work_summary [1 ],
124+ work_summary [2 ],
125+ work_summary [3 ],
126+ ]
127+ if self ._pico_age_ms :
128+ pico_summary = self ._summarize (self ._pico_age_ms )
129+ message += " | pico_age_ms p50=%.2f p95=%.2f p99=%.2f max=%.2f"
130+ args .extend ([pico_summary [0 ], pico_summary [1 ], pico_summary [2 ], pico_summary [3 ]])
131+ logger .info (message , * args )
132+ self ._reset (end_s )
133+
134+ def _reset (self , window_start_s : float ) -> None :
135+ self ._window_start_s = float (window_start_s )
136+ self ._loop_ms .clear ()
137+ self ._work_ms .clear ()
138+ self ._pico_age_ms .clear ()
139+ self ._overrun_count = 0
140+
141+ @staticmethod
142+ def _summarize (samples : list [float ]) -> tuple [float , float , float , float ]:
143+ values = np .asarray (samples , dtype = np .float64 )
144+ if values .size <= 0 :
145+ return 0.0 , 0.0 , 0.0 , 0.0
146+ p50 , p95 , p99 = np .percentile (values , [50.0 , 95.0 , 99.0 ])
147+ return float (p50 ), float (p95 ), float (p99 ), float (np .max (values ))
148+
149+
67150def _parse_sim2real_viewers (cfg : Any ) -> set [str ]:
68151 viewers = parse_viewers (cfg )
69152 unsupported = viewers .difference ({"retarget" })
@@ -266,6 +349,7 @@ def run(self) -> None:
266349 "Control loop started | mode=IDLE | press Start to enter STANDING"
267350 )
268351 dt = 1.0 / self .policy_hz
352+ timing = _LoopTimingReporter (target_period_s = dt )
269353
270354 try :
271355 self ._video_runtime .start ()
@@ -284,22 +368,26 @@ def run(self) -> None:
284368 logger .warning ("EMERGENCY STOP (L1+R1)" )
285369 self ._enter_damping ()
286370 self ._tick_dexterous_hand ()
287- self ._sleep_until (t0 , dt )
288- continue
289-
290- # 3. Mode transitions
291- self ._handle_transitions ()
371+ else :
372+ # 3. Mode transitions
373+ self ._handle_transitions ()
292374
293- # 5. Execute current mode
294- if self .mode == RobotMode .STANDING :
295- self ._standing_step ()
296- elif self .mode == RobotMode .MOCAP :
297- self ._mocap_step ()
375+ # 5. Execute current mode
376+ if self .mode == RobotMode .STANDING :
377+ self ._standing_step ()
378+ elif self .mode == RobotMode .MOCAP :
379+ self ._mocap_step ()
298380
299- self ._tick_dexterous_hand ()
381+ self ._tick_dexterous_hand ()
300382
301- # 6. Rate control
302- self ._sleep_until (t0 , dt )
383+ work_elapsed_s = time .monotonic () - t0
384+ cycle_elapsed_s = self ._sleep_until (t0 , dt )
385+ timing .record (
386+ loop_start_s = t0 ,
387+ work_elapsed_s = work_elapsed_s ,
388+ cycle_elapsed_s = cycle_elapsed_s ,
389+ pico_age_s = self ._sample_pico_frame_age_s (),
390+ )
303391
304392 except KeyboardInterrupt :
305393 logger .info ("KeyboardInterrupt -- shutting down" )
@@ -944,12 +1032,26 @@ def _write_retarget_viewer(self, qpos: Float64Array) -> None:
9441032 logger .exception ("Sim2real retarget viewer update failed; control continues" )
9451033
9461034 @staticmethod
947- def _sleep_until (t0 : float , dt : float ) -> None :
1035+ def _sleep_until (t0 : float , dt : float ) -> float :
9481036 """Sleep to maintain control frequency."""
9491037 elapsed = time .monotonic () - t0
9501038 remaining = dt - elapsed
9511039 if remaining > 0 :
9521040 time .sleep (remaining )
1041+ return time .monotonic () - t0
1042+
1043+ def _sample_pico_frame_age_s (self ) -> float | None :
1044+ has_frame = getattr (self .input_provider , "has_frame" , None )
1045+ get_frame_packet = getattr (self .input_provider , "get_frame_packet" , None )
1046+ if not callable (has_frame ) or not callable (get_frame_packet ):
1047+ return None
1048+ try :
1049+ if not has_frame ():
1050+ return None
1051+ _ , frame_timestamp_s , _ = get_frame_packet ()
1052+ except Exception :
1053+ return None
1054+ return max (0.0 , time .monotonic () - float (frame_timestamp_s ))
9531055
9541056 # ------------------------------------------------------------------
9551057 # Lifecycle
0 commit comments