From 21c792c22b3707ac0a9d87d741f97e0bfb59c9fb Mon Sep 17 00:00:00 2001 From: Daniel-dev22 <47092714+Daniel-dev22@users.noreply.github.com> Date: Sat, 20 Jun 2026 09:42:22 -0400 Subject: [PATCH] perf(track): avoid per-frame allocations and list lookups in tracker Two small per-object-per-frame improvements in the tracker hot path (match_and_update), both bit-identical: - get_stationary_threshold returned a freshly constructed StationaryThresholds (a dataclass plus a list) on every call for any label not in the three known lists - i.e. for common labels like person/dog. The default thresholds are constant and never mutated, so return a shared module-level singleton, as the other three cases already do. - untracked_object_boxes membership used `box not in [list of boxes]` (O(n)); build a set of box tuples for O(1) membership. Boxes are hashable as tuples and output is unchanged. get_stationary_threshold appeared in a live py-spy --gil profile of a camera process_frames worker. Adds tests for the threshold lookups. Co-Authored-By: Claude Opus 4.8 (1M context) --- frigate/test/test_stationary_classifier.py | 39 ++++++++++++++++++++++ frigate/track/norfair_tracker.py | 6 ++-- frigate/track/stationary_classifier.py | 5 ++- 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 frigate/test/test_stationary_classifier.py diff --git a/frigate/test/test_stationary_classifier.py b/frigate/test/test_stationary_classifier.py new file mode 100644 index 0000000000..bd5e2c41e2 --- /dev/null +++ b/frigate/test/test_stationary_classifier.py @@ -0,0 +1,39 @@ +"""Tests for stationary object classification thresholds.""" + +import unittest + +from frigate.track.stationary_classifier import ( + DEFAULT_OBJECT_THRESHOLDS, + DYNAMIC_OBJECT_THRESHOLDS, + NON_STATIONARY_OBJECT_THRESHOLDS, + STATIONARY_OBJECT_THRESHOLDS, + StationaryThresholds, + get_stationary_threshold, +) + + +class TestStationaryThresholds(unittest.TestCase): + def test_known_labels_return_expected_singletons(self) -> None: + self.assertIs(get_stationary_threshold("package"), STATIONARY_OBJECT_THRESHOLDS) + self.assertIs(get_stationary_threshold("car"), DYNAMIC_OBJECT_THRESHOLDS) + self.assertIs( + get_stationary_threshold("license_plate"), + NON_STATIONARY_OBJECT_THRESHOLDS, + ) + + def test_unknown_label_returns_shared_default(self) -> None: + # an unknown label must reuse the shared default instance, not allocate + # a fresh one on every call (this runs per object per frame) + first = get_stationary_threshold("person") + second = get_stationary_threshold("dog") + self.assertIs(first, DEFAULT_OBJECT_THRESHOLDS) + self.assertIs(second, DEFAULT_OBJECT_THRESHOLDS) + + def test_default_matches_a_fresh_instance(self) -> None: + # the shared default must be value-equivalent to the previous + # per-call StationaryThresholds() + self.assertEqual(DEFAULT_OBJECT_THRESHOLDS, StationaryThresholds()) + + +if __name__ == "__main__": + unittest.main() diff --git a/frigate/track/norfair_tracker.py b/frigate/track/norfair_tracker.py index 3f11823572..7c695a7a7c 100644 --- a/frigate/track/norfair_tracker.py +++ b/frigate/track/norfair_tracker.py @@ -641,9 +641,11 @@ class NorfairTracker(ObjectTracker): self.deregister(self.track_id_map[e_id], e_id) # update list of object boxes that don't have a tracked object yet - tracked_object_boxes = [obj["box"] for obj in self.tracked_objects.values()] + tracked_object_boxes = { + tuple(obj["box"]) for obj in self.tracked_objects.values() + } self.untracked_object_boxes = [ - o[2] for o in detections if o[2] not in tracked_object_boxes + o[2] for o in detections if tuple(o[2]) not in tracked_object_boxes ] def print_objects_as_table(self, tracked_objects: Sequence) -> None: diff --git a/frigate/track/stationary_classifier.py b/frigate/track/stationary_classifier.py index bea37f641b..1e22ec6f6d 100644 --- a/frigate/track/stationary_classifier.py +++ b/frigate/track/stationary_classifier.py @@ -63,6 +63,9 @@ NON_STATIONARY_OBJECT_THRESHOLDS = StationaryThresholds( max_stationary_history=4, ) +# Default thresholds for any other object label +DEFAULT_OBJECT_THRESHOLDS = StationaryThresholds() + def get_stationary_threshold(label: str) -> StationaryThresholds: """Get the stationary thresholds for a given object label.""" @@ -76,7 +79,7 @@ def get_stationary_threshold(label: str) -> StationaryThresholds: if label in NON_STATIONARY_OBJECT_THRESHOLDS.objects: return NON_STATIONARY_OBJECT_THRESHOLDS - return StationaryThresholds() + return DEFAULT_OBJECT_THRESHOLDS class StationaryMotionClassifier: