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/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/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 - - ("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/system/GeneralMetrics.tsx b/web/src/views/system/GeneralMetrics.tsx new file mode 100644 index 000000000..ba486b2e6 --- /dev/null +++ b/web/src/views/system/GeneralMetrics.tsx @@ -0,0 +1,441 @@ +import useSWR from "swr"; +import { FrigateStats } from "@/types/stats"; +import { useEffect, useMemo, useState } from "react"; +import { useFrigateStats } from "@/api/ws"; +import { + DetectorCpuThreshold, + DetectorMemThreshold, + GPUMemThreshold, + GPUUsageThreshold, + InferenceThreshold, +} from "@/types/graph"; +import { Button } from "@/components/ui/button"; +import VainfoDialog from "@/components/overlay/VainfoDialog"; +import { ThresholdBarGraph } from "@/components/graph/SystemGraph"; +import { Skeleton } from "@/components/ui/skeleton"; + +type GeneralMetricsProps = { + lastUpdated: number; + setLastUpdated: (last: number) => 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("_", " ")}
+ +
+ ))} +
+
+ ); +}