diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py index 2c29548e9..a90be5271 100644 --- a/frigate/stats/emitter.py +++ b/frigate/stats/emitter.py @@ -16,7 +16,8 @@ from frigate.types import StatsTrackingTypes logger = logging.getLogger(__name__) -MAX_STATS_POINTS = 120 +MAX_STATS_POINTS = 80 +FREQUENCY_STATS_POINTS = 15 class StatsEmitter(threading.Thread): @@ -70,9 +71,9 @@ class StatsEmitter(threading.Thread): def run(self) -> None: time.sleep(10) for counter in itertools.cycle( - range(int(self.config.mqtt.stats_interval / 10)) + range(int(self.config.mqtt.stats_interval / FREQUENCY_STATS_POINTS)) ): - if self.stop_event.wait(10): + if self.stop_event.wait(FREQUENCY_STATS_POINTS): break logger.debug("Starting stats collection") diff --git a/web/src/App.tsx b/web/src/App.tsx index 98385fc20..2f0853200 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -11,7 +11,6 @@ import { Suspense, lazy } from "react"; const Live = lazy(() => import("@/pages/Live")); const Events = lazy(() => import("@/pages/Events")); const Export = lazy(() => import("@/pages/Export")); -const Storage = lazy(() => import("@/pages/Storage")); const SubmitPlus = lazy(() => import("@/pages/SubmitPlus")); const ConfigEditor = lazy(() => import("@/pages/ConfigEditor")); const System = lazy(() => import("@/pages/System")); @@ -38,7 +37,6 @@ function App() { } /> } /> } /> - } /> } /> } /> } /> diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index 4d2e1db8e..f5b09836b 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -2,7 +2,7 @@ import { Button } from "../ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; import useSWR from "swr"; import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { DropdownMenu, DropdownMenuContent, @@ -29,6 +29,7 @@ import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar"; import MobileReviewSettingsDrawer, { DrawerFeatures, } from "../overlay/MobileReviewSettingsDrawer"; +import useOptimisticState from "@/hooks/use-optimistic-state"; const REVIEW_FILTERS = [ "cameras", @@ -361,13 +362,19 @@ function ShowReviewFilter({ showReviewed, setShowReviewed, }: ShowReviewedFilterProps) { + const [showReviewedSwitch, setShowReviewedSwitch] = useOptimisticState( + showReviewed, + setShowReviewed, + ); return ( <>
setShowReviewed(showReviewed == 0 ? 1 : 0)} + checked={showReviewedSwitch == 1} + onCheckedChange={() => + setShowReviewedSwitch(showReviewedSwitch == 0 ? 1 : 0) + } />
diff --git a/web/src/components/graph/SystemGraph.tsx b/web/src/components/graph/SystemGraph.tsx index ec750dceb..a6ca219b0 100644 --- a/web/src/components/graph/SystemGraph.tsx +++ b/web/src/components/graph/SystemGraph.tsx @@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo } from "react"; import Chart from "react-apexcharts"; import useSWR from "swr"; -type SystemGraphProps = { +type ThresholdBarGraphProps = { graphId: string; name: string; unit: string; @@ -13,14 +13,14 @@ type SystemGraphProps = { updateTimes: number[]; data: ApexAxisChartSeries; }; -export default function SystemGraph({ +export function ThresholdBarGraph({ graphId, name, unit, threshold, updateTimes, data, -}: SystemGraphProps) { +}: ThresholdBarGraphProps) { const { data: config } = useSWR("config", { revalidateOnFocus: false, }); @@ -87,8 +87,12 @@ export default function SystemGraph({ tooltip: { theme: systemTheme || theme, }, + markers: { + size: 0, + }, xaxis: { - tickAmount: 6, + tickAmount: 4, + tickPlacement: "on", labels: { formatter: formatTime, }, @@ -104,7 +108,7 @@ export default function SystemGraph({ min: 0, max: threshold.warning + 10, }, - }; + } as ApexCharts.ApexOptions; }, [graphId, threshold, systemTheme, theme, formatTime]); useEffect(() => { @@ -124,3 +128,110 @@ export default function SystemGraph({ ); } + +const getUnitSize = (MB: number) => { + if (isNaN(MB) || MB < 0) return "Invalid number"; + if (MB < 1024) return `${MB} MiB`; + if (MB < 1048576) return `${(MB / 1024).toFixed(2)} GiB`; + + return `${(MB / 1048576).toFixed(2)} TiB`; +}; + +type StorageGraphProps = { + graphId: string; + used: number; + total: number; +}; +export function StorageGraph({ graphId, used, total }: StorageGraphProps) { + const { theme, systemTheme } = useTheme(); + + const options = useMemo(() => { + return { + chart: { + id: graphId, + background: (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5", + selection: { + enabled: false, + }, + toolbar: { + show: false, + }, + zoom: { + enabled: false, + }, + }, + grid: { + show: false, + padding: { + bottom: -40, + top: -60, + left: -20, + right: 0, + }, + }, + legend: { + show: false, + }, + dataLabels: { + enabled: false, + }, + plotOptions: { + bar: { + horizontal: true, + }, + }, + tooltip: { + theme: systemTheme || theme, + }, + xaxis: { + axisBorder: { + show: false, + }, + axisTicks: { + show: false, + }, + labels: { + show: false, + }, + }, + yaxis: { + show: false, + min: 0, + max: 100, + }, + }; + }, [graphId, systemTheme, theme]); + + useEffect(() => { + ApexCharts.exec(graphId, "updateOptions", options, true, true); + }, [graphId, options]); + + return ( +
+
+
+
+ {getUnitSize(used)} +
+
/
+
+ {getUnitSize(total)} +
+
+
+ {Math.round((used / total) * 100)}% +
+
+
+ +
+
+ ); +} diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index a9d597f83..5475dd00c 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -216,11 +216,11 @@ export function ExportContent({ Export - + )} onSelectTime(value as ExportOption)} > {EXPORT_OPTIONS.map((opt) => { @@ -254,13 +254,13 @@ export function ExportContent({ /> )} setName(e.target.value)} /> - {isDesktop && } + {isDesktop && } @@ -371,7 +371,7 @@ function CustomTimeSelector({ return (
{ const cameraConfig = config.cameras[camera]; cameraConfig.objects.track.forEach((label) => { - if (!ATTRIBUTES.includes(label)) { - labels.add(label); - } + labels.add(label); }); if (cameraConfig.audio.enabled_in_config) { diff --git a/web/src/components/settings/GeneralSettings.tsx b/web/src/components/settings/GeneralSettings.tsx index e225257f3..b13051ea4 100644 --- a/web/src/components/settings/GeneralSettings.tsx +++ b/web/src/components/settings/GeneralSettings.tsx @@ -1,7 +1,6 @@ import { LuActivity, LuGithub, - LuHardDrive, LuLifeBuoy, LuList, LuMoon, @@ -138,18 +137,6 @@ export default function GeneralSettings({ className }: GeneralSettings) { System - - - - Storage - - = [T, (newValue: T) => void]; + +const useOptimisticState = ( + initialState: T, + setState: (newValue: T) => void, + delay: number = 20, +): OptimisticStateResult => { + const [optimisticValue, setOptimisticValue] = useState(initialState); + const debounceTimeout = useRef | null>(null); + + const handleValueChange = useCallback( + (newValue: T) => { + // Update the optimistic value immediately + setOptimisticValue(newValue); + + // Clear any pending debounce timeout + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + + // Set a new debounce timeout + debounceTimeout.current = setTimeout(() => { + // Update the actual value using the provided setter function + setState(newValue); + }, delay); + }, + [delay, setState], + ); + + useEffect(() => { + return () => { + if (debounceTimeout.current) { + clearTimeout(debounceTimeout.current); + } + }; + }, []); + + return [optimisticValue, handleValueChange]; +}; + +export default useOptimisticState; diff --git a/web/src/pages/Storage.tsx b/web/src/pages/Storage.tsx deleted file mode 100644 index ad0d7774d..000000000 --- a/web/src/pages/Storage.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useWs } from "@/api/ws"; -import ActivityIndicator from "@/components/indicators/activity-indicator"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import Heading from "@/components/ui/heading"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { useMemo } from "react"; -import { LuAlertCircle } from "react-icons/lu"; -import useSWR from "swr"; - -type CameraStorage = { - [key: string]: { - bandwidth: number; - usage: number; - usage_percent: number; - }; -}; - -const emptyObject = Object.freeze({}); - -function Storage() { - const { data: storage } = useSWR("recordings/storage"); - - const { - value: { payload: stats }, - } = useWs("stats", ""); - const { data: initialStats } = useSWR("stats"); - - const { service } = stats || initialStats || emptyObject; - - const hasSeparateMedia = useMemo(() => { - return ( - service && - service["storage"]["/media/frigate/recordings"]["total"] != - service["storage"]["/media/frigate/clips"]["total"] - ); - }, [service]); - - const getUnitSize = (MB: number) => { - if (isNaN(MB) || MB < 0) return "Invalid number"; - if (MB < 1024) return `${MB} MiB`; - if (MB < 1048576) return `${(MB / 1024).toFixed(2)} GiB`; - - return `${(MB / 1048576).toFixed(2)} TiB`; - }; - - if (!service || !storage) { - return ; - } - - return ( - <> - Storage - - - Overview - -
- - -
- Data - - - - - - -

- Overview of total used storage and total capacity of the - drives that hold the recordings and snapshots directories. -

-
-
-
-
-
- - - - - Location - Used - Total - - - - - - {hasSeparateMedia ? "Recordings" : "Recordings & Snapshots"} - - - {getUnitSize( - service["storage"]["/media/frigate/recordings"]["used"], - )} - - - {getUnitSize( - service["storage"]["/media/frigate/recordings"]["total"], - )} - - - {hasSeparateMedia && ( - - Snapshots - - {getUnitSize( - service["storage"]["/media/frigate/clips"]["used"], - )} - - - {getUnitSize( - service["storage"]["/media/frigate/clips"]["total"], - )} - - - )} - -
-
-
- - - -
- Memory - - - - - - -

Overview of used and total memory in frigate process.

-
-
-
-
-
- - - - - Location - Used - Total - - - - - /dev/shm - - {getUnitSize(service["storage"]["/dev/shm"]["used"])} - - - {getUnitSize(service["storage"]["/dev/shm"]["total"])} - - - - /tmp/cache - - {getUnitSize(service["storage"]["/tmp/cache"]["used"])} - - - {getUnitSize(service["storage"]["/tmp/cache"]["total"])} - - - -
-
-
-
- -
- Cameras - - - - - - -

Overview of per-camera storage usage and bandwidth.

-
-
-
-
- -
- {Object.entries(storage).map(([name, camera]) => ( - - -
- - - - Usage - Stream Bandwidth - - - - - - {Math.round(camera["usage_percent"] ?? 0)}% - - - {camera["bandwidth"] - ? `${getUnitSize(camera["bandwidth"])}/hr` - : "Calculating..."} - - - -
-
-
- ))} -
- - ); -} - -export default Storage; diff --git a/web/src/pages/System.tsx b/web/src/pages/System.tsx index 9412ad1c1..a0fd69b95 100644 --- a/web/src/pages/System.tsx +++ b/web/src/pages/System.tsx @@ -1,19 +1,15 @@ import useSWR from "swr"; import { FrigateStats } from "@/types/stats"; -import { useEffect, useMemo, useState } from "react"; -import SystemGraph from "@/components/graph/SystemGraph"; -import { useFrigateStats } from "@/api/ws"; +import { useState } from "react"; import TimeAgo from "@/components/dynamic/TimeAgo"; -import { - DetectorCpuThreshold, - DetectorMemThreshold, - GPUMemThreshold, - GPUUsageThreshold, - InferenceThreshold, -} from "@/types/graph"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; -import { Button } from "@/components/ui/button"; -import VainfoDialog from "@/components/overlay/VainfoDialog"; +import { isDesktop, isMobile } from "react-device-detect"; +import GeneralMetrics from "@/views/system/GeneralMetrics"; +import StorageMetrics from "@/views/system/StorageMetrics"; +import { LuActivity, LuHardDrive } from "react-icons/lu"; +import { FaVideo } from "react-icons/fa"; +import Logo from "@/components/Logo"; +import useOptimisticState from "@/hooks/use-optimistic-state"; const metrics = ["general", "storage", "cameras"] as const; type SystemMetric = (typeof metrics)[number]; @@ -22,6 +18,7 @@ function System() { // stats page const [page, setPage] = useState("general"); + const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const [lastUpdated, setLastUpdated] = useState(Date.now() / 1000); // stats collection @@ -32,26 +29,32 @@ function System() { return (
-
+
+ {isMobile && ( + + )} { if (value) { - setPage(value); + setPageToggle(value); } }} // don't allow the severity to be unselected > {Object.values(metrics).map((item) => ( -
{item}
+ {item == "general" && } + {item == "storage" && } + {item == "cameras" && } + {isDesktop &&
{item}
}
))}
@@ -78,6 +81,7 @@ function System() { setLastUpdated={setLastUpdated} /> )} + {page == "storage" && }
); } @@ -182,13 +186,13 @@ export default System; if (camera.enabled) { return (
- - */ - -type GeneralMetricsProps = { - lastUpdated: number; - setLastUpdated: (last: number) => void; -}; -function GeneralMetrics({ lastUpdated, setLastUpdated }: GeneralMetricsProps) { - // extra info - - const [showVainfo, setShowVainfo] = useState(false); - - // stats - - const { data: initialStats } = useSWR( - [ - "stats/history", - { keys: "cpu_usages,detectors,gpu_usages,processes,service" }, - ], - { - revalidateOnFocus: false, - }, - ); - - const [statsHistory, setStatsHistory] = useState([]); - const { payload: updatedStats } = useFrigateStats(); - - useEffect(() => { - if (initialStats == undefined || initialStats.length == 0) { - return; - } - - if (statsHistory.length == 0) { - setStatsHistory(initialStats); - return; - } - - if (!updatedStats) { - return; - } - - if (updatedStats.service.last_updated > lastUpdated) { - setStatsHistory([...statsHistory, updatedStats]); - setLastUpdated(Date.now() / 1000); - } - }, [initialStats, updatedStats, statsHistory, lastUpdated, setLastUpdated]); - - // timestamps - - const updateTimes = useMemo( - () => statsHistory.map((stats) => stats.service.last_updated), - [statsHistory], - ); - - // detectors stats - - const detInferenceTimeSeries = useMemo(() => { - if (!statsHistory) { - return []; - } - - const series: { - [key: string]: { name: string; data: { x: number; y: number }[] }; - } = {}; - - statsHistory.forEach((stats, statsIdx) => { - if (!stats) { - return; - } - - Object.entries(stats.detectors).forEach(([key, stats]) => { - if (!(key in series)) { - series[key] = { name: key, data: [] }; - } - - series[key].data.push({ x: statsIdx, y: stats.inference_speed }); - }); - }); - return Object.values(series); - }, [statsHistory]); - - const detCpuSeries = useMemo(() => { - if (!statsHistory) { - return []; - } - - const series: { - [key: string]: { name: string; data: { x: number; y: string }[] }; - } = {}; - - statsHistory.forEach((stats, statsIdx) => { - if (!stats) { - return; - } - - Object.entries(stats.detectors).forEach(([key, detStats]) => { - if (!(key in series)) { - series[key] = { name: key, data: [] }; - } - - series[key].data.push({ - x: statsIdx, - y: stats.cpu_usages[detStats.pid.toString()].cpu, - }); - }); - }); - return Object.values(series); - }, [statsHistory]); - - const detMemSeries = useMemo(() => { - if (!statsHistory) { - return []; - } - - const series: { - [key: string]: { name: string; data: { x: number; y: string }[] }; - } = {}; - - statsHistory.forEach((stats, statsIdx) => { - if (!stats) { - return; - } - - Object.entries(stats.detectors).forEach(([key, detStats]) => { - if (!(key in series)) { - series[key] = { name: key, data: [] }; - } - - series[key].data.push({ - x: statsIdx, - y: stats.cpu_usages[detStats.pid.toString()].mem, - }); - }); - }); - return Object.values(series); - }, [statsHistory]); - - // gpu stats - - const gpuSeries = useMemo(() => { - if (!statsHistory) { - return []; - } - - const series: { - [key: string]: { name: string; data: { x: number; y: string }[] }; - } = {}; - - statsHistory.forEach((stats, statsIdx) => { - if (!stats) { - return; - } - - Object.entries(stats.gpu_usages || []).forEach(([key, stats]) => { - if (!(key in series)) { - series[key] = { name: key, data: [] }; - } - - series[key].data.push({ x: statsIdx, y: stats.gpu }); - }); - }); - return Object.keys(series).length > 0 ? Object.values(series) : []; - }, [statsHistory]); - - const gpuMemSeries = useMemo(() => { - if (!statsHistory) { - return []; - } - - const series: { - [key: string]: { name: string; data: { x: number; y: string }[] }; - } = {}; - - statsHistory.forEach((stats, statsIdx) => { - if (!stats) { - return; - } - - Object.entries(stats.gpu_usages || {}).forEach(([key, stats]) => { - if (!(key in series)) { - series[key] = { name: key, data: [] }; - } - - series[key].data.push({ x: statsIdx, y: stats.mem }); - }); - }); - return Object.values(series); - }, [statsHistory]); - - // other processes stats - - const otherProcessCpuSeries = useMemo(() => { - if (!statsHistory) { - return []; - } - - const series: { - [key: string]: { name: string; data: { x: number; y: string }[] }; - } = {}; - - statsHistory.forEach((stats, statsIdx) => { - if (!stats) { - return; - } - - Object.entries(stats.processes).forEach(([key, procStats]) => { - if (procStats.pid.toString() in stats.cpu_usages) { - if (!(key in series)) { - series[key] = { name: key, data: [] }; - } - - series[key].data.push({ - x: statsIdx, - y: stats.cpu_usages[procStats.pid.toString()].cpu, - }); - } - }); - }); - return Object.keys(series).length > 0 ? Object.values(series) : []; - }, [statsHistory]); - - const otherProcessMemSeries = useMemo(() => { - if (!statsHistory) { - return []; - } - - const series: { - [key: string]: { name: string; data: { x: number; y: string }[] }; - } = {}; - - statsHistory.forEach((stats, statsIdx) => { - if (!stats) { - return; - } - - Object.entries(stats.processes).forEach(([key, procStats]) => { - if (procStats.pid.toString() in stats.cpu_usages) { - if (!(key in series)) { - series[key] = { name: key, data: [] }; - } - - series[key].data.push({ - x: statsIdx, - y: stats.cpu_usages[procStats.pid.toString()].mem, - }); - } - }); - }); - return Object.values(series); - }, [statsHistory]); - - if (statsHistory.length == 0) { - return; - } - - return ( - <> - - -
-
- Detectors -
-
-
-
Detector Inference Speed
- {detInferenceTimeSeries.map((series) => ( - - ))} -
-
-
Detector CPU Usage
- {detCpuSeries.map((series) => ( - - ))} -
-
-
Detector Memory Usage
- {detMemSeries.map((series) => ( - - ))} -
-
- - {statsHistory.length > 0 && statsHistory[0].gpu_usages && ( - <> -
-
- GPUs -
- {Object.keys(statsHistory[0].gpu_usages).filter( - (key) => - key == "amd-vaapi" || - key == "intel-vaapi" || - key == "intel-qsv", - ).length > 0 && ( - - )} -
-
-
-
GPU Usage
- {gpuSeries.map((series) => ( - - ))} -
-
-
GPU Memory
- {gpuMemSeries.map((series) => ( - - ))} -
-
- - )} - -
- Other Processes -
-
-
-
Process CPU Usage
- {otherProcessCpuSeries.map((series) => ( - - ))} -
-
-
Process Memory Usage
- {otherProcessMemSeries.map((series) => ( - - ))} -
-
-
- - ); -} diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 05cfa182c..8a12ec347 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -41,6 +41,7 @@ import { RecordingStartingPoint } from "@/types/record"; import VideoControls from "@/components/player/VideoControls"; import { TimeRange } from "@/types/timeline"; import { useCameraMotionNextTimestamp } from "@/hooks/use-camera-activity"; +import useOptimisticState from "@/hooks/use-optimistic-state"; type EventViewProps = { reviews?: ReviewSegment[]; @@ -199,6 +200,11 @@ export default function EventView({ ); const [motionOnly, setMotionOnly] = useState(false); + const [severityToggle, setSeverityToggle] = useOptimisticState( + severity, + setSeverity, + 100, + ); if (!config) { return ; @@ -214,13 +220,13 @@ export default function EventView({ className="*:px-3 *:py-4 *:rounded-md" type="single" size="sm" - value={severity} + value={severityToggle} onValueChange={(value: ReviewSeverity) => - value ? setSeverity(value) : null + value ? setSeverityToggle(value) : null } // don't allow the severity to be unselected > @@ -230,7 +236,7 @@ export default function EventView({
@@ -242,7 +248,7 @@ export default function EventView({ onOpenRecording({ camera: camera.name, - startTime: currentTime, + startTime: Math.min(currentTime, Date.now() / 1000 - 10), severity: "significant_motion", }) } diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index c6cc866e9..dbcc37752 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -142,7 +142,7 @@ export function RecordingView({ ); useEffect(() => { - if (scrubbing) { + if (scrubbing || exportRange) { if ( currentTime > currentTimeRange.before + 60 || currentTime < currentTimeRange.after - 60 @@ -157,6 +157,8 @@ export function RecordingView({ controller.scrubToTimestamp(currentTime); }); } + // we only want to seek when current time updates + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ currentTime, scrubbing, @@ -486,7 +488,9 @@ function Timeline({ setExportRange({ after: exportStart, before: exportEnd }); } - }, [exportRange, exportStart, exportEnd, setExportRange, setCurrentTime]); + // we only want to update when the export parts change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [exportStart, exportEnd, setExportRange, setCurrentTime]); return (
void; +}; +export default function GeneralMetrics({ + lastUpdated, + setLastUpdated, +}: GeneralMetricsProps) { + // extra info + + const [showVainfo, setShowVainfo] = useState(false); + + // stats + + const { data: initialStats } = useSWR( + [ + "stats/history", + { keys: "cpu_usages,detectors,gpu_usages,processes,service" }, + ], + { + revalidateOnFocus: false, + }, + ); + + const [statsHistory, setStatsHistory] = useState([]); + const { payload: updatedStats } = useFrigateStats(); + + useEffect(() => { + if (initialStats == undefined || initialStats.length == 0) { + return; + } + + if (statsHistory.length == 0) { + setStatsHistory(initialStats); + return; + } + + if (!updatedStats) { + return; + } + + if (updatedStats.service.last_updated > lastUpdated) { + setStatsHistory([...statsHistory, updatedStats]); + setLastUpdated(Date.now() / 1000); + } + }, [initialStats, updatedStats, statsHistory, lastUpdated, setLastUpdated]); + + // timestamps + + const updateTimes = useMemo( + () => statsHistory.map((stats) => stats.service.last_updated), + [statsHistory], + ); + + // detectors stats + + const detInferenceTimeSeries = useMemo(() => { + if (!statsHistory) { + return []; + } + + const series: { + [key: string]: { name: string; data: { x: number; y: number }[] }; + } = {}; + + statsHistory.forEach((stats, statsIdx) => { + if (!stats) { + return; + } + + Object.entries(stats.detectors).forEach(([key, stats]) => { + if (!(key in series)) { + series[key] = { name: key, data: [] }; + } + + series[key].data.push({ x: statsIdx, y: stats.inference_speed }); + }); + }); + return Object.values(series); + }, [statsHistory]); + + const detCpuSeries = useMemo(() => { + if (!statsHistory) { + return []; + } + + const series: { + [key: string]: { name: string; data: { x: number; y: string }[] }; + } = {}; + + statsHistory.forEach((stats, statsIdx) => { + if (!stats) { + return; + } + + Object.entries(stats.detectors).forEach(([key, detStats]) => { + if (!(key in series)) { + series[key] = { name: key, data: [] }; + } + + series[key].data.push({ + x: statsIdx, + y: stats.cpu_usages[detStats.pid.toString()].cpu, + }); + }); + }); + return Object.values(series); + }, [statsHistory]); + + const detMemSeries = useMemo(() => { + if (!statsHistory) { + return []; + } + + const series: { + [key: string]: { name: string; data: { x: number; y: string }[] }; + } = {}; + + statsHistory.forEach((stats, statsIdx) => { + if (!stats) { + return; + } + + Object.entries(stats.detectors).forEach(([key, detStats]) => { + if (!(key in series)) { + series[key] = { name: key, data: [] }; + } + + series[key].data.push({ + x: statsIdx, + y: stats.cpu_usages[detStats.pid.toString()].mem, + }); + }); + }); + return Object.values(series); + }, [statsHistory]); + + // gpu stats + + const gpuSeries = useMemo(() => { + if (!statsHistory) { + return []; + } + + const series: { + [key: string]: { name: string; data: { x: number; y: string }[] }; + } = {}; + + statsHistory.forEach((stats, statsIdx) => { + if (!stats) { + return; + } + + Object.entries(stats.gpu_usages || []).forEach(([key, stats]) => { + if (!(key in series)) { + series[key] = { name: key, data: [] }; + } + + series[key].data.push({ x: statsIdx, y: stats.gpu }); + }); + }); + return Object.keys(series).length > 0 ? Object.values(series) : []; + }, [statsHistory]); + + const gpuMemSeries = useMemo(() => { + if (!statsHistory) { + return []; + } + + const series: { + [key: string]: { name: string; data: { x: number; y: string }[] }; + } = {}; + + statsHistory.forEach((stats, statsIdx) => { + if (!stats) { + return; + } + + Object.entries(stats.gpu_usages || {}).forEach(([key, stats]) => { + if (!(key in series)) { + series[key] = { name: key, data: [] }; + } + + series[key].data.push({ x: statsIdx, y: stats.mem }); + }); + }); + return Object.values(series); + }, [statsHistory]); + + // other processes stats + + const otherProcessCpuSeries = useMemo(() => { + if (!statsHistory) { + return []; + } + + const series: { + [key: string]: { name: string; data: { x: number; y: string }[] }; + } = {}; + + statsHistory.forEach((stats, statsIdx) => { + if (!stats) { + return; + } + + Object.entries(stats.processes).forEach(([key, procStats]) => { + if (procStats.pid.toString() in stats.cpu_usages) { + if (!(key in series)) { + series[key] = { name: key, data: [] }; + } + + series[key].data.push({ + x: statsIdx, + y: stats.cpu_usages[procStats.pid.toString()].cpu, + }); + } + }); + }); + return Object.keys(series).length > 0 ? Object.values(series) : []; + }, [statsHistory]); + + const otherProcessMemSeries = useMemo(() => { + if (!statsHistory) { + return []; + } + + const series: { + [key: string]: { name: string; data: { x: number; y: string }[] }; + } = {}; + + statsHistory.forEach((stats, statsIdx) => { + if (!stats) { + return; + } + + Object.entries(stats.processes).forEach(([key, procStats]) => { + if (procStats.pid.toString() in stats.cpu_usages) { + if (!(key in series)) { + series[key] = { name: key, data: [] }; + } + + series[key].data.push({ + x: statsIdx, + y: stats.cpu_usages[procStats.pid.toString()].mem, + }); + } + }); + }); + return Object.values(series); + }, [statsHistory]); + + return ( + <> + + +
+
+ Detectors +
+
+ {detInferenceTimeSeries.length != 0 ? ( +
+
Detector Inference Speed
+ {detInferenceTimeSeries.map((series) => ( + + ))} +
+ ) : ( + + )} + {statsHistory.length != 0 ? ( +
+
Detector CPU Usage
+ {detCpuSeries.map((series) => ( + + ))} +
+ ) : ( + + )} + {statsHistory.length != 0 ? ( +
+
Detector Memory Usage
+ {detMemSeries.map((series) => ( + + ))} +
+ ) : ( + + )} +
+ + {(statsHistory.length == 0 || statsHistory[0].gpu_usages) && ( + <> +
+
+ GPUs +
+ {statsHistory.length > 0 && + Object.keys(statsHistory[0].gpu_usages ?? {}).filter( + (key) => + key == "amd-vaapi" || + key == "intel-vaapi" || + key == "intel-qsv", + ).length > 0 && ( + + )} +
+
+ {statsHistory.length != 0 ? ( +
+
GPU Usage
+ {gpuSeries.map((series) => ( + + ))} +
+ ) : ( + + )} + {statsHistory.length != 0 ? ( +
+
GPU Memory
+ {gpuMemSeries.map((series) => ( + + ))} +
+ ) : ( + + )} +
+ + )} + +
+ Other Processes +
+
+ {statsHistory.length != 0 ? ( +
+
Process CPU Usage
+ {otherProcessCpuSeries.map((series) => ( + + ))} +
+ ) : ( + + )} + {statsHistory.length != 0 ? ( +
+
Process Memory Usage
+ {otherProcessMemSeries.map((series) => ( + + ))} +
+ ) : ( + + )} +
+
+ + ); +} diff --git a/web/src/views/system/StorageMetrics.tsx b/web/src/views/system/StorageMetrics.tsx new file mode 100644 index 000000000..d275e72fe --- /dev/null +++ b/web/src/views/system/StorageMetrics.tsx @@ -0,0 +1,92 @@ +import { StorageGraph } from "@/components/graph/SystemGraph"; +import { FrigateStats } from "@/types/stats"; +import { useMemo } from "react"; +import useSWR from "swr"; + +type CameraStorage = { + [key: string]: { + bandwidth: number; + usage: number; + usage_percent: number; + }; +}; + +type StorageMetricsProps = { + setLastUpdated: (last: number) => void; +}; +export default function StorageMetrics({ + setLastUpdated, +}: StorageMetricsProps) { + const { data: cameraStorage } = useSWR("recordings/storage"); + const { data: stats } = useSWR("stats"); + + const totalStorage = useMemo(() => { + if (!cameraStorage || !stats) { + return undefined; + } + + const totalStorage = { + used: 0, + total: stats.service.storage["/media/frigate/recordings"]["total"], + }; + + Object.values(cameraStorage).forEach( + (cam) => (totalStorage.used += cam.usage), + ); + setLastUpdated(Date.now() / 1000); + return totalStorage; + }, [cameraStorage, stats, setLastUpdated]); + + if (!cameraStorage || !stats || !totalStorage) { + return; + } + + return ( +
+
+ General Storage +
+
+
+
Recordings
+ +
+
+
/tmp/cache
+ +
+
+
/dev/shm
+ +
+
+
+ Camera Storage +
+
+ {Object.keys(cameraStorage).map((camera) => ( +
+
{camera.replaceAll("_", " ")}
+ +
+ ))} +
+
+ ); +}