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.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()

View File

@ -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("/<camera_name>")
@ -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({})

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

View File

@ -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(