Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
7f183e29be
Merge 66d1e31005 into 5003ab895c 2026-06-21 02:40:45 +08:00
3 changed files with 25 additions and 84 deletions

View File

@ -1,9 +1,7 @@
"""Preview apis.""" """Preview apis."""
import bisect
import logging import logging
import os import os
import threading
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import pytz import pytz
@ -135,32 +133,6 @@ def preview_hour(
return preview_ts(camera_name, start_ts, end_ts, allowed_cameras) return preview_ts(camera_name, start_ts, end_ts, allowed_cameras)
# cache one sorted listing of the shared preview_frames dir
_preview_listing_lock = threading.Lock()
_preview_listing_cache: tuple[float, list[str]] = (-1.0, [])
def _get_preview_frame_listing(preview_dir: str) -> list[str]:
"""Return the sorted preview_frames listing, cached until the dir changes."""
global _preview_listing_cache
# mtime bumps when a frame is added or removed, invalidating the cache
mtime = os.stat(preview_dir).st_mtime
cached_mtime, files = _preview_listing_cache
if mtime == cached_mtime:
return files
with _preview_listing_lock:
# another thread may have refreshed the cache while we waited
cached_mtime, files = _preview_listing_cache
if mtime == cached_mtime:
return files
files = sorted(entry.name for entry in os.scandir(preview_dir))
_preview_listing_cache = (mtime, files)
return files
@router.get( @router.get(
"/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames", "/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames",
response_model=PreviewFramesResponse, response_model=PreviewFramesResponse,
@ -177,15 +149,23 @@ def get_preview_frames_from_cache(camera_name: str, start_ts: float, end_ts: flo
start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}" start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}"
end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}" end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}"
files = _get_preview_frame_listing(preview_dir) camera_files = [
entry.name
# a camera's frames form a contiguous slice of the sorted listing; for entry in os.scandir(preview_dir)
# bisect locates it without scanning the whole directory if entry.name.startswith(file_start)
left = bisect.bisect_left(files, start_file)
right = bisect.bisect_right(files, end_file)
selected_previews = [
file for file in files[left:right] if file.startswith(file_start)
] ]
camera_files.sort()
selected_previews = []
for file in camera_files:
if file < start_file:
continue
if file > end_file:
break
selected_previews.append(file)
return JSONResponse( return JSONResponse(
content=selected_previews, content=selected_previews,

View File

@ -1,41 +0,0 @@
"""Tests for frigate.util.builtin helpers."""
import unittest
from unittest.mock import patch
from frigate.util.builtin import EventsPerSecond
class TestEventsPerSecond(unittest.TestCase):
def test_eps_is_zero_before_any_events(self) -> None:
eps = EventsPerSecond()
with patch("frigate.util.builtin.time.monotonic", return_value=100.0):
self.assertEqual(eps.eps(), 0.0)
def test_eps_counts_events_in_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()
# one event per second for five seconds
for _ in range(5):
clock[0] += 1.0
eps.update()
# five events over the five seconds since start
self.assertAlmostEqual(eps.eps(), 1.0)
def test_old_timestamps_expire_from_window(self) -> None:
eps = EventsPerSecond(last_n_seconds=10)
clock = [0.0]
with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
eps.start()
for _ in range(10):
clock[0] += 1.0
eps.update()
# jump well past the window so every timestamp ages out
clock[0] += 100.0
self.assertEqual(eps.eps(), 0.0)
if __name__ == "__main__":
unittest.main()

View File

@ -2,6 +2,7 @@
import ast import ast
import copy import copy
import datetime
import logging import logging
import math import math
import multiprocessing.queues import multiprocessing.queues
@ -9,9 +10,7 @@ import queue
import re import re
import shlex import shlex
import struct import struct
import time
import urllib.parse import urllib.parse
from collections import deque
from collections.abc import Mapping from collections.abc import Mapping
from multiprocessing.managers import ValueProxy from multiprocessing.managers import ValueProxy
from pathlib import Path from pathlib import Path
@ -33,20 +32,23 @@ class EventsPerSecond:
self._start = None self._start = None
self._max_events = max_events self._max_events = max_events
self._last_n_seconds = last_n_seconds self._last_n_seconds = last_n_seconds
self._timestamps: deque[float] = deque(maxlen=max_events) self._timestamps = []
def start(self) -> None: def start(self) -> None:
self._start = time.monotonic() self._start = datetime.datetime.now().timestamp()
def update(self) -> None: def update(self) -> None:
now = time.monotonic() now = datetime.datetime.now().timestamp()
if self._start is None: if self._start is None:
self._start = now self._start = now
self._timestamps.append(now) self._timestamps.append(now)
# truncate the list when it goes 100 over the max_size
if len(self._timestamps) > self._max_events + 100:
self._timestamps = self._timestamps[(1 - self._max_events) :]
self.expire_timestamps(now) self.expire_timestamps(now)
def eps(self) -> float: def eps(self) -> float:
now = time.monotonic() now = datetime.datetime.now().timestamp()
if self._start is None: if self._start is None:
self._start = now self._start = now
# compute the (approximate) events in the last n seconds # compute the (approximate) events in the last n seconds
@ -61,7 +63,7 @@ class EventsPerSecond:
def expire_timestamps(self, now: float) -> None: def expire_timestamps(self, now: float) -> None:
threshold = now - self._last_n_seconds threshold = now - self._last_n_seconds
while self._timestamps and self._timestamps[0] < threshold: while self._timestamps and self._timestamps[0] < threshold:
self._timestamps.popleft() del self._timestamps[0]
class InferenceSpeed: class InferenceSpeed: