diff --git a/frigate/app.py b/frigate/app.py index 95a742515..be01bd0ab 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -57,7 +57,8 @@ from frigate.ptz.onvif import OnvifController from frigate.record.cleanup import RecordingCleanup from frigate.record.record import manage_recordings from frigate.review.review import manage_review_segments -from frigate.stats import StatsEmitter, stats_init +from frigate.stats.emitter import StatsEmitter +from frigate.stats.util import stats_init from frigate.storage import StorageMaintainer from frigate.timeline import TimelineProcessor from frigate.types import CameraMetricsTypes, PTZMetricsTypes @@ -322,11 +323,6 @@ class FrigateApp: ] self.db.bind(models) - def init_stats(self) -> None: - self.stats_tracking = stats_init( - self.config, self.camera_metrics, self.detectors, self.processes - ) - def init_external_event_processor(self) -> None: self.external_event_processor = ExternalEventProcessor( self.config, self.event_queue @@ -341,12 +337,12 @@ class FrigateApp: self.flask_app = create_app( self.config, self.db, - self.stats_tracking, self.detected_frames_processor, self.storage_maintainer, self.onvif_controller, self.external_event_processor, self.plus_api, + self.stats_emitter, ) def init_onvif(self) -> None: @@ -542,8 +538,9 @@ class FrigateApp: def start_stats_emitter(self) -> None: self.stats_emitter = StatsEmitter( self.config, - self.stats_tracking, - self.dispatcher, + stats_init( + self.config, self.camera_metrics, self.detectors, self.processes + ), self.stop_event, ) self.stats_emitter.start() @@ -648,14 +645,13 @@ class FrigateApp: self.start_camera_capture_processes() self.start_audio_processors() self.start_storage_maintainer() - self.init_stats() self.init_external_event_processor() + self.start_stats_emitter() self.init_web_server() self.start_timeline_processor() self.start_event_processor() self.start_event_cleanup() self.start_record_cleanup() - self.start_stats_emitter() self.start_watchdog() self.check_shm() diff --git a/frigate/http.py b/frigate/http.py index 38a16b299..a4bc5ffc1 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -50,7 +50,7 @@ from frigate.object_processing import TrackedObject from frigate.plus import PlusApi from frigate.ptz.onvif import OnvifController from frigate.record.export import PlaybackFactorEnum, RecordingExporter -from frigate.stats import stats_snapshot +from frigate.stats.emitter import StatsEmitter from frigate.storage import StorageMaintainer from frigate.util.builtin import ( clean_camera_user_pass, @@ -70,12 +70,12 @@ bp = Blueprint("frigate", __name__) def create_app( frigate_config, database: SqliteQueueDatabase, - stats_tracking, detected_frames_processor, storage_maintainer: StorageMaintainer, onvif: OnvifController, external_processor: ExternalEventProcessor, plus_api: PlusApi, + stats_emitter: StatsEmitter, ): app = Flask(__name__) @@ -97,14 +97,13 @@ def create_app( database.close() app.frigate_config = frigate_config - app.stats_tracking = stats_tracking app.detected_frames_processor = detected_frames_processor app.storage_maintainer = storage_maintainer app.onvif = onvif app.external_processor = external_processor app.plus_api = plus_api app.camera_error_image = None - app.hwaccel_errors = [] + app.stats_emitter = stats_emitter app.register_blueprint(bp) @@ -1739,12 +1738,12 @@ def version(): @bp.route("/stats") def stats(): - stats = stats_snapshot( - current_app.frigate_config, - current_app.stats_tracking, - current_app.hwaccel_errors, - ) - return jsonify(stats) + return jsonify(current_app.stats_emitter.get_latest_stats()) + + +@bp.route("/stats/history") +def stats_history(): + return jsonify(current_app.stats_emitter.get_stats_history()) @bp.route("/") @@ -1941,11 +1940,9 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str): @bp.route("/recordings/storage", methods=["GET"]) def get_recordings_storage_usage(): - recording_stats = stats_snapshot( - current_app.frigate_config, - current_app.stats_tracking, - current_app.hwaccel_errors, - )["service"]["storage"][RECORD_DIR] + recording_stats = current_app.stats_emitter.get_latest_stats()["service"][ + "storage" + ][RECORD_DIR] if not recording_stats: return jsonify({}) diff --git a/frigate/stats/__init__.py b/frigate/stats/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py new file mode 100644 index 000000000..048436514 --- /dev/null +++ b/frigate/stats/emitter.py @@ -0,0 +1,61 @@ +"""Emit stats to listeners.""" + +import json +import logging +import threading +import time +from multiprocessing.synchronize import Event as MpEvent + +from frigate.comms.inter_process import InterProcessRequestor +from frigate.config import FrigateConfig +from frigate.stats.util import stats_snapshot +from frigate.types import StatsTrackingTypes + +logger = logging.getLogger(__name__) + + +class StatsEmitter(threading.Thread): + def __init__( + self, + config: FrigateConfig, + stats_tracking: StatsTrackingTypes, + stop_event: MpEvent, + ): + threading.Thread.__init__(self) + self.name = "frigate_stats_emitter" + self.config = config + self.stats_tracking = stats_tracking + self.stop_event = stop_event + self.hwaccel_errors: list[str] = [] + self.stats_history: list[dict[str, any]] = [] + + # create communication for stats + self.requestor = InterProcessRequestor() + + def get_latest_stats(self) -> dict[str, any]: + """Get latest stats.""" + if len(self.stats_history) > 0: + return self.stats_history[-1] + else: + stats = stats_snapshot( + self.config, self.stats_tracking, self.hwaccel_errors + ) + self.stats_history.append(stats) + return stats + + def get_stats_history(self) -> list[dict[str, any]]: + """Get stats history.""" + return self.stats_history + + def run(self) -> None: + time.sleep(10) + while not self.stop_event.wait(self.config.mqtt.stats_interval): + logger.debug("Starting stats collection") + stats = stats_snapshot( + self.config, self.stats_tracking, self.hwaccel_errors + ) + self.stats_history.append(stats) + self.stats_history = self.stats_history[-10:] + self.requestor.send_data("stats", json.dumps(stats)) + logger.debug("Finished stats collection") + logger.info("Exiting stats emitter...") diff --git a/frigate/stats.py b/frigate/stats/util.py similarity index 89% rename from frigate/stats.py rename to frigate/stats/util.py index 185d532fe..8eb59e464 100644 --- a/frigate/stats.py +++ b/frigate/stats/util.py @@ -1,18 +1,15 @@ +"""Utilities for stats.""" + import asyncio -import json -import logging import os import shutil -import threading import time -from multiprocessing.synchronize import Event as MpEvent from typing import Any, Optional import psutil import requests from requests.exceptions import RequestException -from frigate.comms.dispatcher import Dispatcher from frigate.config import FrigateConfig from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR from frigate.object_detection import ObjectDetectProcess @@ -28,8 +25,6 @@ from frigate.util.services import ( ) from frigate.version import VERSION -logger = logging.getLogger(__name__) - def get_latest_version(config: FrigateConfig) -> str: if not config.telemetry.version_check: @@ -318,31 +313,3 @@ def stats_snapshot( } return stats - - -class StatsEmitter(threading.Thread): - def __init__( - self, - config: FrigateConfig, - stats_tracking: StatsTrackingTypes, - dispatcher: Dispatcher, - stop_event: MpEvent, - ): - threading.Thread.__init__(self) - self.name = "frigate_stats_emitter" - self.config = config - self.stats_tracking = stats_tracking - self.dispatcher = dispatcher - self.stop_event = stop_event - self.hwaccel_errors: list[str] = [] - - def run(self) -> None: - time.sleep(10) - while not self.stop_event.wait(self.config.mqtt.stats_interval): - logger.debug("Starting stats collection") - stats = stats_snapshot( - self.config, self.stats_tracking, self.hwaccel_errors - ) - self.dispatcher.publish("stats", json.dumps(stats), retain=False) - logger.debug("Finished stats collection") - logger.info("Exiting stats emitter...") diff --git a/frigate/test/test_http.py b/frigate/test/test_http.py index 932a468a3..40f5b2677 100644 --- a/frigate/test/test_http.py +++ b/frigate/test/test_http.py @@ -3,7 +3,7 @@ import json import logging import os import unittest -from unittest.mock import patch +from unittest.mock import Mock from peewee_migrate import Router from playhouse.shortcuts import model_to_dict @@ -14,6 +14,7 @@ from frigate.config import FrigateConfig from frigate.http import create_app from frigate.models import Event, Recordings from frigate.plus import PlusApi +from frigate.stats.emitter import StatsEmitter from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS @@ -119,8 +120,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) id = "123456.random" id2 = "7890.random" @@ -155,8 +156,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) id = "123456.random" @@ -176,8 +177,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) id = "123456.random" bad_id = "654321.other" @@ -196,8 +197,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) id = "123456.random" @@ -218,8 +219,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) id = "123456.random" @@ -244,8 +245,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) morning_id = "123456.random" evening_id = "654321.random" @@ -282,8 +283,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) id = "123456.random" sub_label = "sub" @@ -317,8 +318,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) id = "123456.random" sub_label = "sub" @@ -342,8 +343,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) with app.test_client() as client: @@ -359,8 +360,8 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + None, ) id = "123456.random" @@ -370,8 +371,9 @@ class TestHttp(unittest.TestCase): assert recording assert recording[0]["id"] == id - @patch("frigate.http.stats_snapshot") - def test_stats(self, mock_stats): + def test_stats(self): + stats = Mock(spec=StatsEmitter) + stats.get_latest_stats.return_value = self.test_stats app = create_app( FrigateConfig(**self.minimal_config).runtime_config(), self.db, @@ -379,14 +381,13 @@ class TestHttp(unittest.TestCase): None, None, None, - None, PlusApi(), + stats, ) - mock_stats.return_value = self.test_stats with app.test_client() as client: - stats = client.get("/stats").json - assert stats == self.test_stats + full_stats = client.get("/stats").json + assert full_stats == self.test_stats def _insert_mock_event(