Reorganize stats (#9960)

* Reorganize stats

* Fix tests
This commit is contained in:
Nicolas Mowen 2024-02-21 13:10:28 -07:00 committed by GitHub
parent 509e46adc8
commit 33c77d03c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 100 additions and 78 deletions

View File

@ -57,7 +57,8 @@ from frigate.ptz.onvif import OnvifController
from frigate.record.cleanup import RecordingCleanup from frigate.record.cleanup import RecordingCleanup
from frigate.record.record import manage_recordings from frigate.record.record import manage_recordings
from frigate.review.review import manage_review_segments 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.storage import StorageMaintainer
from frigate.timeline import TimelineProcessor from frigate.timeline import TimelineProcessor
from frigate.types import CameraMetricsTypes, PTZMetricsTypes from frigate.types import CameraMetricsTypes, PTZMetricsTypes
@ -322,11 +323,6 @@ class FrigateApp:
] ]
self.db.bind(models) 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: def init_external_event_processor(self) -> None:
self.external_event_processor = ExternalEventProcessor( self.external_event_processor = ExternalEventProcessor(
self.config, self.event_queue self.config, self.event_queue
@ -341,12 +337,12 @@ class FrigateApp:
self.flask_app = create_app( self.flask_app = create_app(
self.config, self.config,
self.db, self.db,
self.stats_tracking,
self.detected_frames_processor, self.detected_frames_processor,
self.storage_maintainer, self.storage_maintainer,
self.onvif_controller, self.onvif_controller,
self.external_event_processor, self.external_event_processor,
self.plus_api, self.plus_api,
self.stats_emitter,
) )
def init_onvif(self) -> None: def init_onvif(self) -> None:
@ -542,8 +538,9 @@ class FrigateApp:
def start_stats_emitter(self) -> None: def start_stats_emitter(self) -> None:
self.stats_emitter = StatsEmitter( self.stats_emitter = StatsEmitter(
self.config, self.config,
self.stats_tracking, stats_init(
self.dispatcher, self.config, self.camera_metrics, self.detectors, self.processes
),
self.stop_event, self.stop_event,
) )
self.stats_emitter.start() self.stats_emitter.start()
@ -648,14 +645,13 @@ class FrigateApp:
self.start_camera_capture_processes() self.start_camera_capture_processes()
self.start_audio_processors() self.start_audio_processors()
self.start_storage_maintainer() self.start_storage_maintainer()
self.init_stats()
self.init_external_event_processor() self.init_external_event_processor()
self.start_stats_emitter()
self.init_web_server() self.init_web_server()
self.start_timeline_processor() self.start_timeline_processor()
self.start_event_processor() self.start_event_processor()
self.start_event_cleanup() self.start_event_cleanup()
self.start_record_cleanup() self.start_record_cleanup()
self.start_stats_emitter()
self.start_watchdog() self.start_watchdog()
self.check_shm() self.check_shm()

View File

@ -50,7 +50,7 @@ from frigate.object_processing import TrackedObject
from frigate.plus import PlusApi from frigate.plus import PlusApi
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.record.export import PlaybackFactorEnum, RecordingExporter 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.storage import StorageMaintainer
from frigate.util.builtin import ( from frigate.util.builtin import (
clean_camera_user_pass, clean_camera_user_pass,
@ -70,12 +70,12 @@ bp = Blueprint("frigate", __name__)
def create_app( def create_app(
frigate_config, frigate_config,
database: SqliteQueueDatabase, database: SqliteQueueDatabase,
stats_tracking,
detected_frames_processor, detected_frames_processor,
storage_maintainer: StorageMaintainer, storage_maintainer: StorageMaintainer,
onvif: OnvifController, onvif: OnvifController,
external_processor: ExternalEventProcessor, external_processor: ExternalEventProcessor,
plus_api: PlusApi, plus_api: PlusApi,
stats_emitter: StatsEmitter,
): ):
app = Flask(__name__) app = Flask(__name__)
@ -97,14 +97,13 @@ def create_app(
database.close() database.close()
app.frigate_config = frigate_config app.frigate_config = frigate_config
app.stats_tracking = stats_tracking
app.detected_frames_processor = detected_frames_processor app.detected_frames_processor = detected_frames_processor
app.storage_maintainer = storage_maintainer app.storage_maintainer = storage_maintainer
app.onvif = onvif app.onvif = onvif
app.external_processor = external_processor app.external_processor = external_processor
app.plus_api = plus_api app.plus_api = plus_api
app.camera_error_image = None app.camera_error_image = None
app.hwaccel_errors = [] app.stats_emitter = stats_emitter
app.register_blueprint(bp) app.register_blueprint(bp)
@ -1739,12 +1738,12 @@ def version():
@bp.route("/stats") @bp.route("/stats")
def stats(): def stats():
stats = stats_snapshot( return jsonify(current_app.stats_emitter.get_latest_stats())
current_app.frigate_config,
current_app.stats_tracking,
current_app.hwaccel_errors, @bp.route("/stats/history")
) def stats_history():
return jsonify(stats) return jsonify(current_app.stats_emitter.get_stats_history())
@bp.route("/<camera_name>") @bp.route("/<camera_name>")
@ -1941,11 +1940,9 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
@bp.route("/recordings/storage", methods=["GET"]) @bp.route("/recordings/storage", methods=["GET"])
def get_recordings_storage_usage(): def get_recordings_storage_usage():
recording_stats = stats_snapshot( recording_stats = current_app.stats_emitter.get_latest_stats()["service"][
current_app.frigate_config, "storage"
current_app.stats_tracking, ][RECORD_DIR]
current_app.hwaccel_errors,
)["service"]["storage"][RECORD_DIR]
if not recording_stats: if not recording_stats:
return jsonify({}) return jsonify({})

View File

61
frigate/stats/emitter.py Normal file
View File

@ -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...")

View File

@ -1,18 +1,15 @@
"""Utilities for stats."""
import asyncio import asyncio
import json
import logging
import os import os
import shutil import shutil
import threading
import time import time
from multiprocessing.synchronize import Event as MpEvent
from typing import Any, Optional from typing import Any, Optional
import psutil import psutil
import requests import requests
from requests.exceptions import RequestException from requests.exceptions import RequestException
from frigate.comms.dispatcher import Dispatcher
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR from frigate.const import CACHE_DIR, CLIPS_DIR, RECORD_DIR
from frigate.object_detection import ObjectDetectProcess from frigate.object_detection import ObjectDetectProcess
@ -28,8 +25,6 @@ from frigate.util.services import (
) )
from frigate.version import VERSION from frigate.version import VERSION
logger = logging.getLogger(__name__)
def get_latest_version(config: FrigateConfig) -> str: def get_latest_version(config: FrigateConfig) -> str:
if not config.telemetry.version_check: if not config.telemetry.version_check:
@ -318,31 +313,3 @@ def stats_snapshot(
} }
return stats 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...")

View File

@ -3,7 +3,7 @@ import json
import logging import logging
import os import os
import unittest import unittest
from unittest.mock import patch from unittest.mock import Mock
from peewee_migrate import Router from peewee_migrate import Router
from playhouse.shortcuts import model_to_dict from playhouse.shortcuts import model_to_dict
@ -14,6 +14,7 @@ from frigate.config import FrigateConfig
from frigate.http import create_app from frigate.http import create_app
from frigate.models import Event, Recordings from frigate.models import Event, Recordings
from frigate.plus import PlusApi from frigate.plus import PlusApi
from frigate.stats.emitter import StatsEmitter
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
@ -119,8 +120,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
id = "123456.random" id = "123456.random"
id2 = "7890.random" id2 = "7890.random"
@ -155,8 +156,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
id = "123456.random" id = "123456.random"
@ -176,8 +177,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
id = "123456.random" id = "123456.random"
bad_id = "654321.other" bad_id = "654321.other"
@ -196,8 +197,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
id = "123456.random" id = "123456.random"
@ -218,8 +219,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
id = "123456.random" id = "123456.random"
@ -244,8 +245,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
morning_id = "123456.random" morning_id = "123456.random"
evening_id = "654321.random" evening_id = "654321.random"
@ -282,8 +283,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
id = "123456.random" id = "123456.random"
sub_label = "sub" sub_label = "sub"
@ -317,8 +318,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
id = "123456.random" id = "123456.random"
sub_label = "sub" sub_label = "sub"
@ -342,8 +343,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
with app.test_client() as client: with app.test_client() as client:
@ -359,8 +360,8 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
None,
) )
id = "123456.random" id = "123456.random"
@ -370,8 +371,9 @@ class TestHttp(unittest.TestCase):
assert recording assert recording
assert recording[0]["id"] == id assert recording[0]["id"] == id
@patch("frigate.http.stats_snapshot") def test_stats(self):
def test_stats(self, mock_stats): stats = Mock(spec=StatsEmitter)
stats.get_latest_stats.return_value = self.test_stats
app = create_app( app = create_app(
FrigateConfig(**self.minimal_config).runtime_config(), FrigateConfig(**self.minimal_config).runtime_config(),
self.db, self.db,
@ -379,14 +381,13 @@ class TestHttp(unittest.TestCase):
None, None,
None, None,
None, None,
None,
PlusApi(), PlusApi(),
stats,
) )
mock_stats.return_value = self.test_stats
with app.test_client() as client: with app.test_client() as client:
stats = client.get("/stats").json full_stats = client.get("/stats").json
assert stats == self.test_stats assert full_stats == self.test_stats
def _insert_mock_event( def _insert_mock_event(