Display warning in frontend if shm size is too low (#19712)

* backend

refactor shm calculation to utility function so it can be used in frontend stats

* frontend

* fix check

* clean up
This commit is contained in:
Josh Hawkins 2025-08-22 14:48:27 -05:00 committed by GitHub
parent ee48d6782d
commit a88760efa1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 131 additions and 54 deletions

View File

@ -2,8 +2,6 @@
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import os
import shutil
import threading import threading
from multiprocessing import Queue from multiprocessing import Queue
from multiprocessing.managers import DictProxy, SyncManager from multiprocessing.managers import DictProxy, SyncManager
@ -16,11 +14,11 @@ from frigate.config.camera.updater import (
CameraConfigUpdateEnum, CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber, CameraConfigUpdateSubscriber,
) )
from frigate.const import SHM_FRAMES_VAR
from frigate.models import Regions from frigate.models import Regions
from frigate.util.builtin import empty_and_close_queue from frigate.util.builtin import empty_and_close_queue
from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory from frigate.util.image import SharedMemoryFrameManager, UntrackedSharedMemory
from frigate.util.object import get_camera_regions_grid from frigate.util.object import get_camera_regions_grid
from frigate.util.services import calculate_shm_requirements
from frigate.video import CameraCapture, CameraTracker from frigate.video import CameraCapture, CameraTracker
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -74,53 +72,25 @@ class CameraMaintainer(threading.Thread):
) )
def __calculate_shm_frame_count(self) -> int: def __calculate_shm_frame_count(self) -> int:
total_shm = round(shutil.disk_usage("/dev/shm").total / pow(2, 20), 1) shm_stats = calculate_shm_requirements(self.config)
# required for log files + nginx cache if not shm_stats:
min_req_shm = 40 + 10 # /dev/shm not available
if self.config.birdseye.restream:
min_req_shm += 8
available_shm = total_shm - min_req_shm
cam_total_frame_size = 0.0
for camera in self.config.cameras.values():
if (
camera.enabled_in_config
and camera.detect.width
and camera.detect.height
):
cam_total_frame_size += round(
(camera.detect.width * camera.detect.height * 1.5 + 270480)
/ 1048576,
1,
)
# leave room for 2 cameras that are added dynamically, if a user wants to add more cameras they may need to increase the SHM size and restart after adding them.
cam_total_frame_size += 2 * round(
(1280 * 720 * 1.5 + 270480) / 1048576,
1,
)
if cam_total_frame_size == 0.0:
return 0 return 0
shm_frame_count = min(
int(os.environ.get(SHM_FRAMES_VAR, "50")),
int(available_shm / (cam_total_frame_size)),
)
logger.debug( logger.debug(
f"Calculated total camera size {available_shm} / {cam_total_frame_size} :: {shm_frame_count} frames for each camera in SHM" f"Calculated total camera size {shm_stats['available']} / "
f"{shm_stats['camera_frame_size']} :: {shm_stats['shm_frame_count']} "
f"frames for each camera in SHM"
) )
if shm_frame_count < 20: if shm_stats["shm_frame_count"] < 20:
logger.warning( logger.warning(
f"The current SHM size of {total_shm}MB is too small, recommend increasing it to at least {round(min_req_shm + cam_total_frame_size * 20)}MB." f"The current SHM size of {shm_stats['total']}MB is too small, "
f"recommend increasing it to at least {shm_stats['min_shm']}MB."
) )
return shm_frame_count return shm_stats["shm_frame_count"]
def __start_camera_processor( def __start_camera_processor(
self, name: str, config: CameraConfig, runtime: bool = False self, name: str, config: CameraConfig, runtime: bool = False

View File

@ -8,7 +8,6 @@ from json import JSONDecodeError
from multiprocessing.managers import DictProxy from multiprocessing.managers import DictProxy
from typing import Any, Optional from typing import Any, Optional
import psutil
import requests import requests
from requests.exceptions import RequestException from requests.exceptions import RequestException
@ -18,9 +17,11 @@ from frigate.data_processing.types import DataProcessorMetrics
from frigate.object_detection.base import ObjectDetectProcess from frigate.object_detection.base import ObjectDetectProcess
from frigate.types import StatsTrackingTypes from frigate.types import StatsTrackingTypes
from frigate.util.services import ( from frigate.util.services import (
calculate_shm_requirements,
get_amd_gpu_stats, get_amd_gpu_stats,
get_bandwidth_stats, get_bandwidth_stats,
get_cpu_stats, get_cpu_stats,
get_fs_type,
get_intel_gpu_stats, get_intel_gpu_stats,
get_jetson_stats, get_jetson_stats,
get_nvidia_gpu_stats, get_nvidia_gpu_stats,
@ -70,16 +71,6 @@ def stats_init(
return stats_tracking return stats_tracking
def get_fs_type(path: str) -> str:
bestMatch = ""
fsType = ""
for part in psutil.disk_partitions(all=True):
if path.startswith(part.mountpoint) and len(bestMatch) < len(part.mountpoint):
fsType = part.fstype
bestMatch = part.mountpoint
return fsType
def read_temperature(path: str) -> Optional[float]: def read_temperature(path: str) -> Optional[float]:
if os.path.isfile(path): if os.path.isfile(path):
with open(path) as f: with open(path) as f:
@ -389,7 +380,7 @@ def stats_snapshot(
"last_updated": int(time.time()), "last_updated": int(time.time()),
} }
for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR, "/dev/shm"]: for path in [RECORD_DIR, CLIPS_DIR, CACHE_DIR]:
try: try:
storage_stats = shutil.disk_usage(path) storage_stats = shutil.disk_usage(path)
except (FileNotFoundError, OSError): except (FileNotFoundError, OSError):
@ -403,6 +394,8 @@ def stats_snapshot(
"mount_type": get_fs_type(path), "mount_type": get_fs_type(path),
} }
stats["service"]["storage"]["/dev/shm"] = calculate_shm_requirements(config)
stats["processes"] = {} stats["processes"] = {}
for name, pid in stats_tracking["processes"].items(): for name, pid in stats_tracking["processes"].items():
stats["processes"][name] = { stats["processes"][name] = {

View File

@ -6,6 +6,7 @@ import logging
import os import os
import re import re
import resource import resource
import shutil
import signal import signal
import subprocess as sp import subprocess as sp
import traceback import traceback
@ -22,6 +23,7 @@ from frigate.const import (
DRIVER_ENV_VAR, DRIVER_ENV_VAR,
FFMPEG_HWACCEL_NVIDIA, FFMPEG_HWACCEL_NVIDIA,
FFMPEG_HWACCEL_VAAPI, FFMPEG_HWACCEL_VAAPI,
SHM_FRAMES_VAR,
) )
from frigate.util.builtin import clean_camera_user_pass, escape_special_characters from frigate.util.builtin import clean_camera_user_pass, escape_special_characters
@ -768,3 +770,65 @@ def set_file_limit() -> None:
logger.debug( logger.debug(
f"File limit set. New soft limit: {new_soft}, Hard limit remains: {current_hard}" f"File limit set. New soft limit: {new_soft}, Hard limit remains: {current_hard}"
) )
def get_fs_type(path: str) -> str:
bestMatch = ""
fsType = ""
for part in psutil.disk_partitions(all=True):
if path.startswith(part.mountpoint) and len(bestMatch) < len(part.mountpoint):
fsType = part.fstype
bestMatch = part.mountpoint
return fsType
def calculate_shm_requirements(config) -> dict:
try:
storage_stats = shutil.disk_usage("/dev/shm")
except (FileNotFoundError, OSError):
return {}
total_mb = round(storage_stats.total / pow(2, 20), 1)
used_mb = round(storage_stats.used / pow(2, 20), 1)
free_mb = round(storage_stats.free / pow(2, 20), 1)
# required for log files + nginx cache
min_req_shm = 40 + 10
if config.birdseye.restream:
min_req_shm += 8
available_shm = total_mb - min_req_shm
cam_total_frame_size = 0.0
for camera in config.cameras.values():
if camera.enabled_in_config and camera.detect.width and camera.detect.height:
cam_total_frame_size += round(
(camera.detect.width * camera.detect.height * 1.5 + 270480) / 1048576,
1,
)
# leave room for 2 cameras that are added dynamically, if a user wants to add more cameras they may need to increase the SHM size and restart after adding them.
cam_total_frame_size += 2 * round(
(1280 * 720 * 1.5 + 270480) / 1048576,
1,
)
shm_frame_count = min(
int(os.environ.get(SHM_FRAMES_VAR, "50")),
int(available_shm / cam_total_frame_size),
)
# minimum required shm recommendation
min_shm = round(min_req_shm + cam_total_frame_size * 20)
return {
"total": total_mb,
"used": used_mb,
"free": free_mb,
"mount_type": get_fs_type("/dev/shm"),
"available": round(available_shm, 1),
"camera_frame_size": cam_total_frame_size,
"shm_frame_count": shm_frame_count,
"min_shm": min_shm,
}

View File

@ -91,6 +91,11 @@
"tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk.", "tips": "This value represents the total storage used by the recordings in Frigate's database. Frigate does not track storage usage for all files on your disk.",
"earliestRecording": "Earliest recording available:" "earliestRecording": "Earliest recording available:"
}, },
"shm": {
"title": "SHM (shared memory) allocation",
"warning": "The current SHM size of {{total}}MB is too small. Increase it to at least {{min_shm}}MB.",
"readTheDocumentation": "Read the documentation"
},
"cameraStorage": { "cameraStorage": {
"title": "Camera Storage", "title": "Camera Storage",
"camera": "Camera", "camera": "Camera",

View File

@ -79,6 +79,7 @@ export type StorageStats = {
total: number; total: number;
used: number; used: number;
mount_type: string; mount_type: string;
min_shm?: number;
}; };
export type PotentialProblem = { export type PotentialProblem = {

View File

@ -14,6 +14,10 @@ import { useFormattedTimestamp, useTimezone } from "@/hooks/use-date-utils";
import { RecordingsSummary } from "@/types/review"; import { RecordingsSummary } from "@/types/review";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TZDate } from "react-day-picker"; import { TZDate } from "react-day-picker";
import { Link } from "react-router-dom";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { LuExternalLink } from "react-icons/lu";
import { FaExclamationTriangle } from "react-icons/fa";
type CameraStorage = { type CameraStorage = {
[key: string]: { [key: string]: {
@ -36,6 +40,7 @@ export default function StorageMetrics({
}); });
const { t } = useTranslation(["views/system"]); const { t } = useTranslation(["views/system"]);
const timezone = useTimezone(config); const timezone = useTimezone(config);
const { getLocaleDocUrl } = useDocDomain();
const totalStorage = useMemo(() => { const totalStorage = useMemo(() => {
if (!cameraStorage || !stats) { if (!cameraStorage || !stats) {
@ -142,7 +147,46 @@ export default function StorageMetrics({
/> />
</div> </div>
<div className="flex-col rounded-lg bg-background_alt p-2.5 md:rounded-2xl"> <div className="flex-col rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">/dev/shm</div> <div className="mb-5 flex flex-row items-center justify-between">
/dev/shm
{stats.service.storage["/dev/shm"]["total"] <
(stats.service.storage["/dev/shm"]["min_shm"] ?? 0) && (
<Popover>
<PopoverTrigger asChild>
<button
className="focus:outline-none"
aria-label={t("storage.shm.title")}
>
<FaExclamationTriangle
className="size-5 text-danger"
aria-label={t("storage.shm.title")}
/>
</button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-2">
{t("storage.shm.warning", {
total: stats.service.storage["/dev/shm"]["total"],
min_shm: stats.service.storage["/dev/shm"]["min_shm"],
})}
<div className="mt-2 flex items-center text-primary">
<Link
to={getLocaleDocUrl(
"frigate/installation#calculating-required-shm-size",
)}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("storage.shm.readTheDocumentation")}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</div>
</PopoverContent>
</Popover>
)}
</div>
<StorageGraph <StorageGraph
graphId="general-shared-memory" graphId="general-shared-memory"
used={stats.service.storage["/dev/shm"]["used"]} used={stats.service.storage["/dev/shm"]["used"]}