"""Utilities for reading real time clocks and keeping soft real time constraints.""" import gc import os import sys import time from setproctitle import getproctitle from openpilot.common.util import MovingAverage from openpilot.system.hardware import PC # time step for each process DT_CTRL = 0.01 # controlsd DT_MDL = 0.05 # model DT_HW = 0.5 # hardwared and manager DT_DMON = 0.05 # driver monitoring class Priority: # CORE 2 # - modeld = 55 # - camerad = 54 CTRL_LOW = 51 # plannerd & radard # CORE 3 # - pandad = 55 CTRL_HIGH = 53 def set_core_affinity(cores: list[int]) -> None: if sys.platform == 'linux' and not PC: try: os.sched_setaffinity(0, cores) except OSError: pass def config_realtime_process(cores: int | list[int], priority: int) -> None: gc.disable() if sys.platform == 'linux' and not PC: os.sched_setscheduler(0, os.SCHED_FIFO, os.sched_param(priority)) c = cores if isinstance(cores, list) else [cores, ] set_core_affinity(c) class Ratekeeper: def __init__(self, rate: float, print_delay_threshold: float | None = 0.0) -> None: """Rate in Hz for ratekeeping. print_delay_threshold must be nonnegative.""" self._interval = 1. / rate self._print_delay_threshold = print_delay_threshold self._frame = 0 self._remaining = 0.0 self._process_name = getproctitle() self._last_monitor_time = -1. self._next_frame_time = -1. self.avg_dt = MovingAverage(100) self.avg_dt.add_value(self._interval) @property def frame(self) -> int: return self._frame @property def remaining(self) -> float: return self._remaining @property def lagging(self) -> bool: expected_dt = self._interval * (1 / 0.9) return self.avg_dt.get_average() > expected_dt # Maintain loop rate by calling this at the end of each loop def keep_time(self) -> bool: lagged = self.monitor_time() if self._remaining > 0: time.sleep(self._remaining) return lagged # Monitors the cumulative lag, but does not enforce a rate def monitor_time(self) -> bool: if self._last_monitor_time < 0: self._next_frame_time = time.monotonic() + self._interval self._last_monitor_time = time.monotonic() prev = self._last_monitor_time self._last_monitor_time = time.monotonic() self.avg_dt.add_value(self._last_monitor_time - prev) lagged = False remaining = self._next_frame_time - time.monotonic() self._next_frame_time += self._interval if self._print_delay_threshold is not None and remaining < -self._print_delay_threshold: print(f"{self._process_name} lagging by {-remaining * 1000:.2f} ms") lagged = True self._frame += 1 self._remaining = remaining return lagged class DurationTimer: def __init__(self, duration=0, step=DT_CTRL) -> None: self.step = step self.duration = duration self.was_reset = False self.timer = 0 self.min = float("-inf") # type: float self.max = float("inf") # type: float def tick_obj(self) -> None: self.timer += self.step # reset on overflow self.timer = 0 if (self.timer == (self.max or self.min)) else self.timer def reset(self) -> None: """Resets this objects timer""" self.timer = 0 self.was_reset = True def active(self) -> bool: """Returns true if time since last reset is less than duration""" return bool(round(self.timer,2) < self.duration) def adjust(self, duration) -> None: """Adjusts the duration of the timer""" self.duration = duration def once_after_reset(self) -> bool: """Returns true only one time after calling reset()""" ret = self.was_reset self.was_reset = False return ret @staticmethod def interval_obj(rate, frame) -> bool: if frame % rate == 0: # Highlighting shows "frame" in white return True return False class ModelTimer(DurationTimer): frame: int = 0 objects: list = [] def __init__(self, duration=0) -> None: self.step = DT_MDL super().__init__(duration, self.step) self.__class__.objects.append(self) @classmethod def tick(cls) -> None: cls.frame += 1 for obj in cls.objects: ModelTimer.tick_obj(obj) @classmethod def reset_all(cls) -> None: for obj in cls.objects: obj.reset() @classmethod def interval(cls, rate) -> bool: return ModelTimer.interval_obj(rate, cls.frame) class ControlsTimer(DurationTimer): frame = 0 objects = [] # type: list[DurationTimer] def __init__(self, duration=0) -> None: self.step = DT_CTRL super().__init__(duration=duration, step=self.step) self.__class__.objects.append(self) @classmethod def tick(cls) -> None: cls.frame += 1 for obj in cls.objects: ControlsTimer.tick_obj(obj) @classmethod def reset_all(cls) -> None: for obj in cls.objects: obj.reset() @classmethod def interval(cls, rate) -> bool: return ControlsTimer.interval_obj(rate, cls.frame)