From 2e8a15841970fc916c1094e0671408b68269b061 Mon Sep 17 00:00:00 2001 From: Danny Date: Mon, 7 Sep 2026 16:55:07 -0700 Subject: [PATCH] Leaky Frame Mems --- starpilot/system/speed_limit_vision.py | 1 + .../system/tests/test_speed_limit_vision.py | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/starpilot/system/speed_limit_vision.py b/starpilot/system/speed_limit_vision.py index 520e4d4118..c92f0b76f6 100644 --- a/starpilot/system/speed_limit_vision.py +++ b/starpilot/system/speed_limit_vision.py @@ -2825,6 +2825,7 @@ class SpeedLimitVisionDaemon: self._maybe_commit_training_capture(now) self._maybe_capture_map_transition_miss(now) + frame_bgr = None ratekeeper.keep_time() diff --git a/starpilot/system/tests/test_speed_limit_vision.py b/starpilot/system/tests/test_speed_limit_vision.py index ae8959e5b6..541e9dbb57 100644 --- a/starpilot/system/tests/test_speed_limit_vision.py +++ b/starpilot/system/tests/test_speed_limit_vision.py @@ -1,5 +1,6 @@ from collections import deque import gc +from types import SimpleNamespace import weakref import numpy as np @@ -238,6 +239,97 @@ def test_receive_frame_does_not_retain_vision_buffer(monkeypatch): assert buffer_refs[0]() is None +def test_run_releases_local_frame_after_offroad_cleanup(): + class StopLoop(Exception): + pass + + class FakeSubMaster: + def __init__(self): + self.updated = {"userBookmark": False, "livePose": False} + self.valid = {"starpilotCarState": True} + self.states = { + "deviceState": SimpleNamespace(started=False), + "mapdOut": SimpleNamespace(roadName=""), + "livePose": SimpleNamespace(inputsOK=True), + "starpilotCarState": SimpleNamespace(isParked=False), + } + self.update_count = 0 + + def update(self, _timeout): + self.update_count += 1 + self.states["deviceState"].started = self.update_count == 1 + + def __getitem__(self, key): + return self.states[key] + + class FakeRatekeeper: + def __init__(self, frame_ref): + self.frame_ref = frame_ref + self.keep_count = 0 + self.frame_alive_at_offroad_keep = None + + def keep_time(self): + self.keep_count += 1 + if self.keep_count == 2: + self.frame_alive_at_offroad_keep = self.frame_ref() is not None + raise StopLoop + + received_frames = [np.ones((2, 2, 3), dtype=np.uint8)] + frame_ref = weakref.ref(received_frames[0]) + ratekeeper = FakeRatekeeper(frame_ref) + daemon = SpeedLimitVisionDaemon.__new__(SpeedLimitVisionDaemon) + daemon.use_runtime = True + daemon.sm = FakeSubMaster() + daemon.Ratekeeper = lambda *_args: ratekeeper + daemon.net = object() + daemon.last_error = "" + daemon.started_prev = False + daemon.parked_prev = False + daemon.last_road_name = "" + daemon.last_inference_at = -float("inf") + daemon.stream_name = "road camera" + daemon.published_speed_limit_mph = 0 + daemon.published_confidence = 0.0 + daemon.pending_auto_bookmark = None + daemon.memory_pressure_state = "normal" + daemon.last_cpu_busy = False + daemon.interval_skip_count = 0 + daemon.busy_skip_count = 0 + daemon.camera_unavailable_count = 0 + daemon.empty_frame_count = 0 + daemon.loop_count = 0 + daemon.inference_count = 0 + daemon.detector_inference_count = 0 + daemon.detection_count = 0 + daemon.current_frame_bgr = None + + daemon._update_memory_pressure = lambda: None + daemon._start_debug_session = lambda: False + daemon._publish_runtime_telemetry = lambda *_args, **_kwargs: None + daemon._write_debug_event = lambda *_args, **_kwargs: None + daemon._close_debug_session = lambda: None + daemon._disconnect_camera = lambda: None + daemon._connect_camera = lambda: True + daemon._update_coexistence_mode = lambda _now: None + daemon._inference_interval = lambda _now: 0.0 + daemon._track_classification_due = lambda _now: False + daemon._detector_interval = lambda _interval: 0.0 + daemon._receive_frame_bgr = lambda: received_frames.pop() + daemon._detect_sign = lambda _frame: None + daemon._start_latest_detector_track = lambda *_args: None + daemon._clear_published_detection_if_stale = lambda *_args: False + daemon._publish_status = lambda *_args, **_kwargs: None + daemon._maybe_commit_auto_bookmark = lambda _now: None + daemon._maybe_commit_training_capture = lambda _now: None + daemon._maybe_capture_map_transition_miss = lambda _now: None + + with pytest.raises(StopLoop): + daemon.run() + + assert not ratekeeper.frame_alive_at_offroad_keep + assert frame_ref() is None + + def test_publish_status_only_writes_changed_values(): daemon = SpeedLimitVisionDaemon.__new__(SpeedLimitVisionDaemon) daemon.params_memory = MemoryParams()