Rate events over at least one second (#24455)

* Rate events over at least one second

EventsPerSecond.eps() divided the event count by the time since start(),
which can be a few milliseconds right after a restart. Frames buffered
during an ffmpeg restart then report as 100+ fps, and the same happens to
the detector fps. Use a window of at least one second.

* Keep sub-second windows consistent

Floor the divisor at the window length when the window is shorter than a
second, so a caller with a sub-second window still gets its true rate.
This commit is contained in:
markfrancisonly
2026-09-24 06:28:00 -06:00
committed by GitHub
parent af0ba19196
commit 397f5253a5
2 changed files with 32 additions and 4 deletions
+25
View File
@@ -36,6 +36,31 @@ class TestEventsPerSecond(unittest.TestCase):
clock[0] += 100.0 clock[0] += 100.0
self.assertEqual(eps.eps(), 0.0) self.assertEqual(eps.eps(), 0.0)
def test_burst_after_start_is_not_divided_by_a_tiny_window(self) -> None:
eps = EventsPerSecond(last_n_seconds=10)
clock = [1000.0]
with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
eps.start()
# eleven buffered frames arrive within 100 ms of starting
for _ in range(11):
clock[0] += 0.01
eps.update()
# 11 events over less than a second is at most 11 per second
self.assertLessEqual(eps.eps(), 11.0)
def test_subsecond_window_keeps_its_rate(self) -> None:
eps = EventsPerSecond(last_n_seconds=0.5)
clock = [1000.0]
with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
eps.start()
# twenty events per second for two seconds
for _ in range(40):
clock[0] += 0.05
eps.update()
# read between events, so none sits exactly on the window edge
clock[0] += 0.01
self.assertAlmostEqual(eps.eps(), 20.0)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+7 -4
View File
@@ -56,10 +56,13 @@ class EventsPerSecond:
self._start = now self._start = now
# compute the (approximate) events in the last n seconds # compute the (approximate) events in the last n seconds
self.expire_timestamps(now) self.expire_timestamps(now)
seconds = min(now - self._start, self._last_n_seconds) # rate over at least one second (or the whole window, if shorter),
# avoid divide by zero # so a burst of events right after start() is not divided by a
if seconds == 0: # tiny window
seconds = 1 seconds = max(
min(now - self._start, self._last_n_seconds),
min(1.0, self._last_n_seconds),
)
return len(self._timestamps) / seconds return len(self._timestamps) / seconds
# remove aged out timestamps # remove aged out timestamps