mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Add GPU stats to the /stats API and debug screen (#3931)
* Add ffprobe endpoint * Get ffprobe for multiple inputs * Copy ffprobe in output * Fix bad if statement * Return full output of ffprobe process * Return full output of ffprobe process * Make ffprobe button show dialog with output and option to copy * Add driver names to consts * Add driver env var name * Setup general tracking for GPU stats * Catch RPi args as well * Add util to get radeontop results * Add real amd GPU stats * Fix missed arg * pass config * Use only the values * Fix vram * Add nvidia gpu stats * Use nvidia stats * Add chart for gpu stats * Format AMD with space between percent * Get correct nvidia % * Start to add support for intel GPU stats * Block out RPi as util is not currently available * Formatting * Fix mypy * Strip for float conversion * Strip for float conversion * Fix percent formatting * Remove name from gpu map * Add tests and fix AMD formatting * Add nvidia gpu stats test * Formatting * Add intel_gpu_top for testing * Formatting * Handle case where hwaccel is not setup * Formatting * Check to remove none * Don't use set * Cleanup and fix types * Handle case where args is list * Fix mypy * Cast to str * Fix type checking * Return none instead of empty * Fix organization * Make keys consistent * Make gpu match style * Get support for vainfo * Add vainfo endpoint * Set vainfo output in error correctly * Remove duplicate function * Fix errors * Do cpu & gpu work asynchonously * Fix async * Fix event loop * Fix crash * Fix naming * Send empty data for gpu if error occurs * Show error if gpu stats could not be retrieved * Fix mypy * Fix test * Don't use json for vainfo * Fix cross references * Strip unicode still * await vainfo response * Add gpu deps * Formatting * remove comments * Use empty string * Add vainfo back in
This commit is contained in:
		
							parent
							
								
									3cb6d43fac
								
							
						
					
					
						commit
						aaedd24f37
					
				| @ -8,7 +8,7 @@ apt-get -qq install --no-install-recommends -y \ | |||||||
|     apt-transport-https \ |     apt-transport-https \ | ||||||
|     gnupg \ |     gnupg \ | ||||||
|     wget \ |     wget \ | ||||||
|     procps \ |     procps vainfo \ | ||||||
|     unzip locales tzdata libxml2 xz-utils \ |     unzip locales tzdata libxml2 xz-utils \ | ||||||
|     python3-pip |     python3-pip | ||||||
| 
 | 
 | ||||||
| @ -53,7 +53,7 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then | |||||||
|     echo 'deb http://deb.debian.org/debian testing main non-free' >/etc/apt/sources.list.d/debian-testing.list |     echo 'deb http://deb.debian.org/debian testing main non-free' >/etc/apt/sources.list.d/debian-testing.list | ||||||
|     apt-get -qq update |     apt-get -qq update | ||||||
|     apt-get -qq install --no-install-recommends --no-install-suggests -y \ |     apt-get -qq install --no-install-recommends --no-install-suggests -y \ | ||||||
|         mesa-va-drivers libva-drm2 intel-media-va-driver-non-free i965-va-driver libmfx1 |         mesa-va-drivers libva-drm2 intel-media-va-driver-non-free i965-va-driver libmfx1 radeontop intel-gpu-tools | ||||||
|     rm -f /etc/apt/sources.list.d/debian-testing.list |     rm -f /etc/apt/sources.list.d/debian-testing.list | ||||||
| fi | fi | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -11,3 +11,10 @@ PLUS_API_HOST = "https://api.frigate.video" | |||||||
| REGEX_CAMERA_NAME = "^[a-zA-Z0-9_-]+$" | REGEX_CAMERA_NAME = "^[a-zA-Z0-9_-]+$" | ||||||
| REGEX_RTSP_CAMERA_USER_PASS = ":\/\/[a-zA-Z0-9_-]+:[\S]+@" | REGEX_RTSP_CAMERA_USER_PASS = ":\/\/[a-zA-Z0-9_-]+:[\S]+@" | ||||||
| REGEX_HTTP_CAMERA_USER_PASS = "user=[a-zA-Z0-9_-]+&password=[\S]+" | REGEX_HTTP_CAMERA_USER_PASS = "user=[a-zA-Z0-9_-]+&password=[\S]+" | ||||||
|  | 
 | ||||||
|  | # Known Driver Names | ||||||
|  | 
 | ||||||
|  | DRIVER_ENV_VAR = "LIBVA_DRIVER_NAME" | ||||||
|  | DRIVER_AMD = "radeonsi" | ||||||
|  | DRIVER_INTEL_i965 = "i965" | ||||||
|  | DRIVER_INTEL_iHD = "iHD" | ||||||
|  | |||||||
| @ -26,11 +26,12 @@ from flask import ( | |||||||
| from peewee import SqliteDatabase, operator, fn, DoesNotExist | from peewee import SqliteDatabase, operator, fn, DoesNotExist | ||||||
| from playhouse.shortcuts import model_to_dict | from playhouse.shortcuts import model_to_dict | ||||||
| 
 | 
 | ||||||
|  | from frigate.config import CameraConfig | ||||||
| from frigate.const import CLIPS_DIR | from frigate.const import CLIPS_DIR | ||||||
| from frigate.models import Event, Recordings | from frigate.models import Event, Recordings | ||||||
| from frigate.object_processing import TrackedObject | from frigate.object_processing import TrackedObject | ||||||
| from frigate.stats import stats_snapshot | from frigate.stats import stats_snapshot | ||||||
| from frigate.util import clean_camera_user_pass, ffprobe_stream | from frigate.util import clean_camera_user_pass, ffprobe_stream, vainfo_hwaccel | ||||||
| from frigate.version import VERSION | from frigate.version import VERSION | ||||||
| 
 | 
 | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @ -608,7 +609,7 @@ def version(): | |||||||
| 
 | 
 | ||||||
| @bp.route("/stats") | @bp.route("/stats") | ||||||
| def stats(): | def stats(): | ||||||
|     stats = stats_snapshot(current_app.stats_tracking) |     stats = stats_snapshot(current_app.frigate_config, current_app.stats_tracking) | ||||||
|     return jsonify(stats) |     return jsonify(stats) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -996,3 +997,19 @@ def ffprobe(): | |||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     return jsonify(output) |     return jsonify(output) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @bp.route("/vainfo", methods=["GET"]) | ||||||
|  | def vainfo(): | ||||||
|  |     vainfo = vainfo_hwaccel() | ||||||
|  |     return jsonify( | ||||||
|  |         { | ||||||
|  |             "return_code": vainfo.returncode, | ||||||
|  |             "stderr": vainfo.stderr.decode("unicode_escape").strip() | ||||||
|  |             if vainfo.stderr.decode() | ||||||
|  |             else "", | ||||||
|  |             "stdout": vainfo.stdout.decode("unicode_escape").strip() | ||||||
|  |             if vainfo.stdout.decode() | ||||||
|  |             else "", | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import asyncio | ||||||
| import json | import json | ||||||
| import logging | import logging | ||||||
| import threading | import threading | ||||||
| @ -11,8 +12,9 @@ from multiprocessing.synchronize import Event as MpEvent | |||||||
| 
 | 
 | ||||||
| from frigate.comms.dispatcher import Dispatcher | from frigate.comms.dispatcher import Dispatcher | ||||||
| from frigate.config import FrigateConfig | from frigate.config import FrigateConfig | ||||||
| from frigate.const import RECORD_DIR, CLIPS_DIR, CACHE_DIR | from frigate.const import DRIVER_AMD, DRIVER_ENV_VAR, RECORD_DIR, CLIPS_DIR, CACHE_DIR | ||||||
| from frigate.types import StatsTrackingTypes, CameraMetricsTypes | from frigate.types import StatsTrackingTypes, CameraMetricsTypes | ||||||
|  | from frigate.util import get_amd_gpu_stats, get_intel_gpu_stats, get_nvidia_gpu_stats | ||||||
| from frigate.version import VERSION | from frigate.version import VERSION | ||||||
| from frigate.util import get_cpu_stats | from frigate.util import get_cpu_stats | ||||||
| from frigate.object_detection import ObjectDetectProcess | from frigate.object_detection import ObjectDetectProcess | ||||||
| @ -82,7 +84,96 @@ def get_temperatures() -> dict[str, float]: | |||||||
|     return temps |     return temps | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def stats_snapshot(stats_tracking: StatsTrackingTypes) -> dict[str, Any]: | def get_processing_stats(config: FrigateConfig, stats: dict[str, str]) -> None: | ||||||
|  |     """Get stats for cpu / gpu.""" | ||||||
|  | 
 | ||||||
|  |     async def run_tasks() -> None: | ||||||
|  |         await asyncio.wait( | ||||||
|  |             [ | ||||||
|  |                 asyncio.create_task(set_gpu_stats(config, stats)), | ||||||
|  |                 asyncio.create_task(set_cpu_stats(stats)), | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     loop = asyncio.new_event_loop() | ||||||
|  |     asyncio.set_event_loop(loop) | ||||||
|  |     loop.run_until_complete(run_tasks()) | ||||||
|  |     loop.close() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def set_cpu_stats(all_stats: dict[str, Any]) -> None: | ||||||
|  |     """Set cpu usage from top.""" | ||||||
|  |     cpu_stats = get_cpu_stats() | ||||||
|  | 
 | ||||||
|  |     if cpu_stats: | ||||||
|  |         all_stats["cpu_usages"] = cpu_stats | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def set_gpu_stats(config: FrigateConfig, all_stats: dict[str, Any]) -> None: | ||||||
|  |     """Parse GPUs from hwaccel args and use for stats.""" | ||||||
|  |     hwaccel_args = [] | ||||||
|  | 
 | ||||||
|  |     for camera in config.cameras.values(): | ||||||
|  |         args = camera.ffmpeg.hwaccel_args | ||||||
|  | 
 | ||||||
|  |         if isinstance(args, list): | ||||||
|  |             args = " ".join(args) | ||||||
|  | 
 | ||||||
|  |         if args and args not in hwaccel_args: | ||||||
|  |             hwaccel_args.append(args) | ||||||
|  | 
 | ||||||
|  |     stats: dict[str, dict] = {} | ||||||
|  | 
 | ||||||
|  |     for args in hwaccel_args: | ||||||
|  |         if "cuvid" in args: | ||||||
|  |             # nvidia GPU | ||||||
|  |             nvidia_usage = get_nvidia_gpu_stats() | ||||||
|  | 
 | ||||||
|  |             if nvidia_usage: | ||||||
|  |                 name = nvidia_usage["name"] | ||||||
|  |                 del nvidia_usage["name"] | ||||||
|  |                 stats[name] = nvidia_usage | ||||||
|  |             else: | ||||||
|  |                 stats["nvidia-gpu"] = {"gpu": -1, "mem": -1} | ||||||
|  |         elif "qsv" in args: | ||||||
|  |             # intel QSV GPU | ||||||
|  |             intel_usage = get_intel_gpu_stats() | ||||||
|  | 
 | ||||||
|  |             if intel_usage: | ||||||
|  |                 stats["intel-qsv"] = intel_usage | ||||||
|  |             else: | ||||||
|  |                 stats["intel-qsv"] = {"gpu": -1, "mem": -1} | ||||||
|  |         elif "vaapi" in args: | ||||||
|  |             driver = os.environ.get(DRIVER_ENV_VAR) | ||||||
|  | 
 | ||||||
|  |             if driver == DRIVER_AMD: | ||||||
|  |                 # AMD VAAPI GPU | ||||||
|  |                 amd_usage = get_amd_gpu_stats() | ||||||
|  | 
 | ||||||
|  |                 if amd_usage: | ||||||
|  |                     stats["amd-vaapi"] = amd_usage | ||||||
|  |                 else: | ||||||
|  |                     stats["amd-vaapi"] = {"gpu": -1, "mem": -1} | ||||||
|  |             else: | ||||||
|  |                 # intel VAAPI GPU | ||||||
|  |                 intel_usage = get_intel_gpu_stats() | ||||||
|  | 
 | ||||||
|  |                 if intel_usage: | ||||||
|  |                     stats["intel-vaapi"] = intel_usage | ||||||
|  |                 else: | ||||||
|  |                     stats["intel-vaapi"] = {"gpu": -1, "mem": -1} | ||||||
|  |         elif "v4l2m2m" in args: | ||||||
|  |             # RPi v4l2m2m is currently not able to get usage stats | ||||||
|  |             stats["rpi-v4l2m2m"] = {"gpu": -1, "mem": -1} | ||||||
|  | 
 | ||||||
|  |     if stats: | ||||||
|  |         all_stats["gpu_usages"] = stats | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def stats_snapshot( | ||||||
|  |     config: FrigateConfig, stats_tracking: StatsTrackingTypes | ||||||
|  | ) -> dict[str, Any]: | ||||||
|  |     """Get a snapshot of the current stats that are being tracked.""" | ||||||
|     camera_metrics = stats_tracking["camera_metrics"] |     camera_metrics = stats_tracking["camera_metrics"] | ||||||
|     stats: dict[str, Any] = {} |     stats: dict[str, Any] = {} | ||||||
| 
 | 
 | ||||||
| @ -119,7 +210,7 @@ def stats_snapshot(stats_tracking: StatsTrackingTypes) -> dict[str, Any]: | |||||||
|         } |         } | ||||||
|     stats["detection_fps"] = round(total_detection_fps, 2) |     stats["detection_fps"] = round(total_detection_fps, 2) | ||||||
| 
 | 
 | ||||||
|     stats["cpu_usages"] = get_cpu_stats() |     get_processing_stats(config, stats) | ||||||
| 
 | 
 | ||||||
|     stats["service"] = { |     stats["service"] = { | ||||||
|         "uptime": (int(time.time()) - stats_tracking["started"]), |         "uptime": (int(time.time()) - stats_tracking["started"]), | ||||||
| @ -159,6 +250,6 @@ class StatsEmitter(threading.Thread): | |||||||
|     def run(self) -> None: |     def run(self) -> None: | ||||||
|         time.sleep(10) |         time.sleep(10) | ||||||
|         while not self.stop_event.wait(self.config.mqtt.stats_interval): |         while not self.stop_event.wait(self.config.mqtt.stats_interval): | ||||||
|             stats = stats_snapshot(self.stats_tracking) |             stats = stats_snapshot(self.config, self.stats_tracking) | ||||||
|             self.dispatcher.publish("stats", json.dumps(stats), retain=False) |             self.dispatcher.publish("stats", json.dumps(stats), retain=False) | ||||||
|         logger.info(f"Exiting watchdog...") |         logger.info(f"Exiting watchdog...") | ||||||
|  | |||||||
							
								
								
									
										45
									
								
								frigate/test/test_gpu_stats.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								frigate/test/test_gpu_stats.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | |||||||
|  | import unittest | ||||||
|  | from unittest.mock import MagicMock, patch | ||||||
|  | 
 | ||||||
|  | from frigate.util import get_amd_gpu_stats, get_intel_gpu_stats, get_nvidia_gpu_stats | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestGpuStats(unittest.TestCase): | ||||||
|  |     def setUp(self): | ||||||
|  |         self.amd_results = "Unknown Radeon card. <= R500 won't work, new cards might.\nDumping to -, line limit 1.\n1664070990.607556: bus 10, gpu 4.17%, ee 0.00%, vgt 0.00%, ta 0.00%, tc 0.00%, sx 0.00%, sh 0.00%, spi 0.83%, smx 0.00%, cr 0.00%, sc 0.00%, pa 0.00%, db 0.00%, cb 0.00%, vram 60.37% 294.04mb, gtt 0.33% 52.21mb, mclk 100.00% 1.800ghz, sclk 26.65% 0.533ghz\n" | ||||||
|  |         self.intel_results = """{"period":{"duration":1.194033,"unit":"ms"},"frequency":{"requested":0.000000,"actual":0.000000,"unit":"MHz"},"interrupts":{"count":3349.991164,"unit":"irq/s"},"rc6":{"value":47.844741,"unit":"%"},"engines":{"Render/3D/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Blitter/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/0":{"busy":4.533124,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/1":{"busy":6.194385,"sema":0.000000,"wait":0.000000,"unit":"%"},"VideoEnhance/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"}}},{"period":{"duration":1.189291,"unit":"ms"},"frequency":{"requested":0.000000,"actual":0.000000,"unit":"MHz"},"interrupts":{"count":0.000000,"unit":"irq/s"},"rc6":{"value":100.000000,"unit":"%"},"engines":{"Render/3D/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Blitter/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"Video/1":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"},"VideoEnhance/0":{"busy":0.000000,"sema":0.000000,"wait":0.000000,"unit":"%"}}}""" | ||||||
|  |         self.nvidia_results = "name, utilization.gpu [%], memory.used [MiB], memory.total [MiB]\nNVIDIA GeForce RTX 3050, 42 %, 5036 MiB, 8192 MiB\n" | ||||||
|  | 
 | ||||||
|  |     @patch("subprocess.run") | ||||||
|  |     def test_amd_gpu_stats(self, sp): | ||||||
|  |         process = MagicMock() | ||||||
|  |         process.returncode = 0 | ||||||
|  |         process.stdout = self.amd_results | ||||||
|  |         sp.return_value = process | ||||||
|  |         amd_stats = get_amd_gpu_stats() | ||||||
|  |         assert amd_stats == {"gpu": "4.17 %", "mem": "60.37 %"} | ||||||
|  | 
 | ||||||
|  |     @patch("subprocess.run") | ||||||
|  |     def test_nvidia_gpu_stats(self, sp): | ||||||
|  |         process = MagicMock() | ||||||
|  |         process.returncode = 0 | ||||||
|  |         process.stdout = self.nvidia_results | ||||||
|  |         sp.return_value = process | ||||||
|  |         nvidia_stats = get_nvidia_gpu_stats() | ||||||
|  |         assert nvidia_stats == { | ||||||
|  |             "name": "NVIDIA GeForce RTX 3050", | ||||||
|  |             "gpu": "42 %", | ||||||
|  |             "mem": "61.5 %", | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     @patch("subprocess.run") | ||||||
|  |     def test_intel_gpu_stats(self, sp): | ||||||
|  |         process = MagicMock() | ||||||
|  |         process.returncode = 0 | ||||||
|  |         process.stdout = self.intel_results | ||||||
|  |         sp.return_value = process | ||||||
|  |         intel_stats = get_intel_gpu_stats() | ||||||
|  |         assert intel_stats == { | ||||||
|  |             "gpu": "10.73 %", | ||||||
|  |             "mem": "- %", | ||||||
|  |         } | ||||||
							
								
								
									
										105
									
								
								frigate/util.py
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								frigate/util.py
									
									
									
									
									
								
							| @ -766,6 +766,105 @@ def get_cpu_stats() -> dict[str, dict]: | |||||||
|         return usages |         return usages | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def get_amd_gpu_stats() -> dict[str, str]: | ||||||
|  |     """Get stats using radeontop.""" | ||||||
|  |     radeontop_command = ["radeontop", "-d", "-", "-l", "1"] | ||||||
|  | 
 | ||||||
|  |     p = sp.run( | ||||||
|  |         radeontop_command, | ||||||
|  |         encoding="ascii", | ||||||
|  |         capture_output=True, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     if p.returncode != 0: | ||||||
|  |         logger.error(p.stderr) | ||||||
|  |         return None | ||||||
|  |     else: | ||||||
|  |         usages = p.stdout.split(",") | ||||||
|  |         results: dict[str, str] = {} | ||||||
|  | 
 | ||||||
|  |         for hw in usages: | ||||||
|  |             if "gpu" in hw: | ||||||
|  |                 results["gpu"] = f"{hw.strip().split(' ')[1].replace('%', '')} %" | ||||||
|  |             elif "vram" in hw: | ||||||
|  |                 results["mem"] = f"{hw.strip().split(' ')[1].replace('%', '')} %" | ||||||
|  | 
 | ||||||
|  |         return results | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_intel_gpu_stats() -> dict[str, str]: | ||||||
|  |     """Get stats using intel_gpu_top.""" | ||||||
|  |     intel_gpu_top_command = [ | ||||||
|  |         "timeout", | ||||||
|  |         "0.1s", | ||||||
|  |         "intel_gpu_top", | ||||||
|  |         "-J", | ||||||
|  |         "-o", | ||||||
|  |         "-", | ||||||
|  |         "-s", | ||||||
|  |         "1", | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     p = sp.run( | ||||||
|  |         intel_gpu_top_command, | ||||||
|  |         encoding="ascii", | ||||||
|  |         capture_output=True, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     if p.returncode != 0: | ||||||
|  |         logger.error(p.stderr) | ||||||
|  |         return None | ||||||
|  |     else: | ||||||
|  |         readings = json.loads(f"[{p.stdout}]") | ||||||
|  |         results: dict[str, str] = {} | ||||||
|  | 
 | ||||||
|  |         for reading in readings: | ||||||
|  |             if reading.get("engines", {}).get("Video/0", {}).get( | ||||||
|  |                 "busy", 0 | ||||||
|  |             ) or reading.get("engines", {}).get("Video/1", {}).get("busy", 0): | ||||||
|  |                 gpu_usage = round( | ||||||
|  |                     float(reading.get("engines", {}).get("Video/0", {}).get("busy", 0)) | ||||||
|  |                     + float( | ||||||
|  |                         reading.get("engines", {}).get("Video/1", {}).get("busy", 0) | ||||||
|  |                     ), | ||||||
|  |                     2, | ||||||
|  |                 ) | ||||||
|  |                 results["gpu"] = f"{gpu_usage} %" | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |         results["mem"] = "- %" | ||||||
|  |         return results | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_nvidia_gpu_stats() -> dict[str, str]: | ||||||
|  |     """Get stats using nvidia-smi.""" | ||||||
|  |     nvidia_smi_command = [ | ||||||
|  |         "nvidia-smi", | ||||||
|  |         "--query-gpu=gpu_name,utilization.gpu,memory.used,memory.total", | ||||||
|  |         "--format=csv", | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     p = sp.run( | ||||||
|  |         nvidia_smi_command, | ||||||
|  |         encoding="ascii", | ||||||
|  |         capture_output=True, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     if p.returncode != 0: | ||||||
|  |         logger.error(p.stderr) | ||||||
|  |         return None | ||||||
|  |     else: | ||||||
|  |         usages = p.stdout.split("\n")[1].strip().split(",") | ||||||
|  |         memory_percent = f"{round(float(usages[2].replace(' MiB', '').strip()) / float(usages[3].replace(' MiB', '').strip()) * 100, 1)} %" | ||||||
|  |         results: dict[str, str] = { | ||||||
|  |             "name": usages[0], | ||||||
|  |             "gpu": usages[1].strip(), | ||||||
|  |             "mem": memory_percent, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return results | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def ffprobe_stream(path: str) -> sp.CompletedProcess: | def ffprobe_stream(path: str) -> sp.CompletedProcess: | ||||||
|     """Run ffprobe on stream.""" |     """Run ffprobe on stream.""" | ||||||
|     ffprobe_cmd = [ |     ffprobe_cmd = [ | ||||||
| @ -781,6 +880,12 @@ def ffprobe_stream(path: str) -> sp.CompletedProcess: | |||||||
|     return sp.run(ffprobe_cmd, capture_output=True) |     return sp.run(ffprobe_cmd, capture_output=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def vainfo_hwaccel() -> sp.CompletedProcess: | ||||||
|  |     """Run vainfo.""" | ||||||
|  |     ffprobe_cmd = ["vainfo"] | ||||||
|  |     return sp.run(ffprobe_cmd, capture_output=True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class FrameManager(ABC): | class FrameManager(ABC): | ||||||
|     @abstractmethod |     @abstractmethod | ||||||
|     def create(self, name, size) -> AnyStr: |     def create(self, name, size) -> AnyStr: | ||||||
|  | |||||||
| @ -21,9 +21,17 @@ export default function System() { | |||||||
|   } = useWs('stats'); |   } = useWs('stats'); | ||||||
|   const { data: initialStats } = useSWR('stats'); |   const { data: initialStats } = useSWR('stats'); | ||||||
| 
 | 
 | ||||||
|   const { cpu_usages, detectors, service = {}, detection_fps: _, ...cameras } = stats || initialStats || emptyObject; |   const { | ||||||
|  |     cpu_usages, | ||||||
|  |     gpu_usages, | ||||||
|  |     detectors, | ||||||
|  |     service = {}, | ||||||
|  |     detection_fps: _, | ||||||
|  |     ...cameras | ||||||
|  |   } = stats || initialStats || emptyObject; | ||||||
| 
 | 
 | ||||||
|   const detectorNames = Object.keys(detectors || emptyObject); |   const detectorNames = Object.keys(detectors || emptyObject); | ||||||
|  |   const gpuNames = Object.keys(gpu_usages || emptyObject); | ||||||
|   const cameraNames = Object.keys(cameras || emptyObject); |   const cameraNames = Object.keys(cameras || emptyObject); | ||||||
| 
 | 
 | ||||||
|   const handleCopyConfig = useCallback(() => { |   const handleCopyConfig = useCallback(() => { | ||||||
| @ -55,9 +63,9 @@ export default function System() { | |||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (response.status === 200) { |     if (response.status === 200) { | ||||||
|       setState({ showFfprobe: true, ffprobe: JSON.stringify(response.data, null, 2) }); |       setState({ ...state, showFfprobe: true, ffprobe: JSON.stringify(response.data, null, 2) }); | ||||||
|     } else { |     } else { | ||||||
|       setState({ ...state, ffprobe: 'There was an error getting the ffprobe output.' }); |       setState({ ...state, showFfprobe: true, ffprobe: 'There was an error getting the ffprobe output.' }); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
| @ -66,11 +74,31 @@ export default function System() { | |||||||
|     setState({ ...state, ffprobe: '', showFfprobe: false }); |     setState({ ...state, ffprobe: '', showFfprobe: false }); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   const onHandleVainfo = async (e) => { | ||||||
|  |     if (e) { | ||||||
|  |       e.stopPropagation(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const response = await axios.get('vainfo'); | ||||||
|  | 
 | ||||||
|  |     if (response.status === 200) { | ||||||
|  |       setState({ ...state, showVainfo: true, vainfo: JSON.stringify(response.data, null, 2) }); | ||||||
|  |     } else { | ||||||
|  |       setState({ ...state, showVainfo: true, vainfo: 'There was an error getting the vainfo output.' }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const onCopyVainfo = async () => { | ||||||
|  |     await window.navigator.clipboard.writeText(JSON.stringify(state.vaifp, null, 2)); | ||||||
|  |     setState({ ...state, vainfo: '', showVainfo: false }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="space-y-4 p-2 px-4"> |     <div className="space-y-4 p-2 px-4"> | ||||||
|       <Heading> |       <Heading> | ||||||
|         System <span className="text-sm">{service.version}</span> |         System <span className="text-sm">{service.version}</span> | ||||||
|       </Heading> |       </Heading> | ||||||
|  | 
 | ||||||
|       {state.showFfprobe && ( |       {state.showFfprobe && ( | ||||||
|         <Dialog> |         <Dialog> | ||||||
|           <div className="p-4"> |           <div className="p-4"> | ||||||
| @ -92,6 +120,23 @@ export default function System() { | |||||||
|         </Dialog> |         </Dialog> | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|  |       {state.showVainfo && ( | ||||||
|  |         <Dialog> | ||||||
|  |           <div className="p-4"> | ||||||
|  |             <Heading size="lg">Vainfo Output</Heading> | ||||||
|  |             {state.vainfo != '' ? <p className="mb-2">{state.vainfo}</p> : <ActivityIndicator />} | ||||||
|  |           </div> | ||||||
|  |           <div className="p-2 flex justify-start flex-row-reverse space-x-2"> | ||||||
|  |             <Button className="ml-2" onClick={() => onCopyVainfo()} type="text"> | ||||||
|  |               Copy | ||||||
|  |             </Button> | ||||||
|  |             <Button className="ml-2" onClick={() => setState({ ...state, vainfo: '', showVainfo: false })} type="text"> | ||||||
|  |               Close | ||||||
|  |             </Button> | ||||||
|  |           </div> | ||||||
|  |         </Dialog> | ||||||
|  |       )} | ||||||
|  | 
 | ||||||
|       {!detectors ? ( |       {!detectors ? ( | ||||||
|         <div> |         <div> | ||||||
|           <ActivityIndicator /> |           <ActivityIndicator /> | ||||||
| @ -125,6 +170,50 @@ export default function System() { | |||||||
|             ))} |             ))} | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|  |           <div className="text-lg flex justify-between p-4"> | ||||||
|  |             <Heading size="lg">GPUs</Heading> | ||||||
|  |             <Button onClick={(e) => onHandleVainfo(e)}>vainfo</Button> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           {!gpu_usages ? ( | ||||||
|  |             <div className="p-4"> | ||||||
|  |               <Link href={'https://docs.frigate.video/configuration/hardware_acceleration'}> | ||||||
|  |                 Hardware acceleration has not been setup, see the docs to setup hardware acceleration. | ||||||
|  |               </Link> | ||||||
|  |             </div> | ||||||
|  |           ) : ( | ||||||
|  |             <div data-testid="gpus" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4"> | ||||||
|  |               {gpuNames.map((gpu) => ( | ||||||
|  |                 <div key={gpu} className="dark:bg-gray-800 shadow-md hover:shadow-lg rounded-lg transition-shadow"> | ||||||
|  |                   <div className="text-lg flex justify-between p-4">{gpu}</div> | ||||||
|  |                   <div className="p-2"> | ||||||
|  |                     {gpu_usages[gpu]['gpu'] == -1 ? ( | ||||||
|  |                       <div className="p-4"> | ||||||
|  |                         There was an error getting usage stats. Either your GPU does not support this or frigate does | ||||||
|  |                         not have proper access. | ||||||
|  |                       </div> | ||||||
|  |                     ) : ( | ||||||
|  |                       <Table className="w-full"> | ||||||
|  |                         <Thead> | ||||||
|  |                           <Tr> | ||||||
|  |                             <Th>Gpu %</Th> | ||||||
|  |                             <Th>Memory %</Th> | ||||||
|  |                           </Tr> | ||||||
|  |                         </Thead> | ||||||
|  |                         <Tbody> | ||||||
|  |                           <Tr> | ||||||
|  |                             <Td>{gpu_usages[gpu]['gpu']}</Td> | ||||||
|  |                             <Td>{gpu_usages[gpu]['mem']}</Td> | ||||||
|  |                           </Tr> | ||||||
|  |                         </Tbody> | ||||||
|  |                       </Table> | ||||||
|  |                     )} | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               ))} | ||||||
|  |             </div> | ||||||
|  |           )} | ||||||
|  | 
 | ||||||
|           <Heading size="lg">Cameras</Heading> |           <Heading size="lg">Cameras</Heading> | ||||||
|           <div data-testid="cameras" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4"> |           <div data-testid="cameras" className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4"> | ||||||
|             {cameraNames.map((camera) => ( |             {cameraNames.map((camera) => ( | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user