From 89f843cf953d0a839fbb3100ed80822232cafda0 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 30 Mar 2024 12:45:13 -0600 Subject: [PATCH] Implement alerts when a potential problem is detected (#10734) * Implement alerts on statusbar when a potential problem is detected * Add alert to mobile --- web/src/components/Statusbar.tsx | 104 +++++++++++--------- web/src/components/navigation/Bottombar.tsx | 45 ++++++++- web/src/hooks/use-stats.ts | 63 ++++++++++++ web/src/types/stats.ts | 5 + 4 files changed, 171 insertions(+), 46 deletions(-) create mode 100644 web/src/hooks/use-stats.ts diff --git a/web/src/components/Statusbar.tsx b/web/src/components/Statusbar.tsx index e5564eecf..52bbe3114 100644 --- a/web/src/components/Statusbar.tsx +++ b/web/src/components/Statusbar.tsx @@ -1,6 +1,8 @@ import { useFrigateStats } from "@/api/ws"; +import useStats from "@/hooks/use-stats"; import { FrigateStats } from "@/types/stats"; import { useMemo } from "react"; +import { IoIosWarning } from "react-icons/io"; import { MdCircle } from "react-icons/md"; import useSWR from "swr"; @@ -27,58 +29,70 @@ export default function Statusbar() { return parseInt(systemCpu); }, [stats]); + const { potentialProblems } = useStats(stats); + return ( -
- {cpuPercent && ( -
- - CPU {cpuPercent}% -
- )} - {Object.entries(stats?.gpu_usages || {}).map(([name, stats]) => { - if (name == "error-gpu") { - return; - } - - let gpuTitle; - switch (name) { - case "amd-vaapi": - gpuTitle = "AMD GPU"; - break; - case "intel-vaapi": - case "intel-qsv": - gpuTitle = "Intel GPU"; - break; - default: - gpuTitle = name; - break; - } - - const gpu = parseInt(stats.gpu); - - return ( -
+
+
+ {cpuPercent && ( +
- {gpuTitle} {gpu}% + CPU {cpuPercent}%
- ); - })} + )} + {Object.entries(stats?.gpu_usages || {}).map(([name, stats]) => { + if (name == "error-gpu") { + return; + } + + let gpuTitle; + switch (name) { + case "amd-vaapi": + gpuTitle = "AMD GPU"; + break; + case "intel-vaapi": + case "intel-qsv": + gpuTitle = "Intel GPU"; + break; + default: + gpuTitle = name; + break; + } + + const gpu = parseInt(stats.gpu); + + return ( +
+ + {gpuTitle} {gpu}% +
+ ); + })} +
+
+ {potentialProblems.map((prob) => ( +
+ + {prob.text} +
+ ))} +
); } diff --git a/web/src/components/navigation/Bottombar.tsx b/web/src/components/navigation/Bottombar.tsx index d4e1e0760..68b28d22a 100644 --- a/web/src/components/navigation/Bottombar.tsx +++ b/web/src/components/navigation/Bottombar.tsx @@ -1,6 +1,13 @@ import { navbarLinks } from "@/pages/site-navigation"; import NavItem from "./NavItem"; import SettingsNavItems from "../settings/SettingsNavItems"; +import { IoIosWarning } from "react-icons/io"; +import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; +import useSWR from "swr"; +import { FrigateStats } from "@/types/stats"; +import { useFrigateStats } from "@/api/ws"; +import { useMemo } from "react"; +import useStats from "@/hooks/use-stats"; function Bottombar() { return ( @@ -17,10 +24,46 @@ function Bottombar() { /> ))} +
); } -// +function StatusAlertNav() { + const { data: initialStats } = useSWR("stats", { + revalidateOnFocus: false, + }); + const { payload: latestStats } = useFrigateStats(); + const stats = useMemo(() => { + if (latestStats) { + return latestStats; + } + + return initialStats; + }, [initialStats, latestStats]); + const { potentialProblems } = useStats(stats); + + if (!potentialProblems || potentialProblems.length == 0) { + return; + } + + return ( + + + + + +
+ {potentialProblems.map((prob) => ( +
+ + {prob.text} +
+ ))} +
+
+
+ ); +} export default Bottombar; diff --git a/web/src/hooks/use-stats.ts b/web/src/hooks/use-stats.ts new file mode 100644 index 000000000..b5e0407a6 --- /dev/null +++ b/web/src/hooks/use-stats.ts @@ -0,0 +1,63 @@ +import { FrigateStats, PotentialProblem } from "@/types/stats"; +import { useMemo } from "react"; + +export default function useStats(stats: FrigateStats | undefined) { + const potentialProblems = useMemo(() => { + const problems: PotentialProblem[] = []; + + if (!stats) { + return problems; + } + + // check detectors for high inference speeds + Object.entries(stats["detectors"]).forEach(([key, det]) => { + if (det["inference_speed"] > 100) { + problems.push({ + text: `${key} is very slow (${det["inference_speed"]} ms)`, + color: "text-danger", + }); + } else if (det["inference_speed"] > 50) { + problems.push({ + text: `${key} is slow (${det["inference_speed"]} ms)`, + color: "text-orange-400", + }); + } + }); + + // check for offline cameras + Object.entries(stats["cameras"]).forEach(([name, cam]) => { + if (cam["camera_fps"] == 0) { + problems.push({ + text: `${name.replaceAll("_", " ")} is offline`, + color: "text-danger", + }); + } + }); + + // check camera cpu usages + Object.entries(stats["cameras"]).forEach(([name, cam]) => { + const ffmpegAvg = parseFloat( + stats["cpu_usages"][cam["ffmpeg_pid"]].cpu_average, + ); + const detectAvg = parseFloat(stats["cpu_usages"][cam["pid"]].cpu_average); + + if (!isNaN(ffmpegAvg) && ffmpegAvg >= 20.0) { + problems.push({ + text: `${name.replaceAll("_", " ")} has high FFMPEG CPU usage (${ffmpegAvg}%)`, + color: "text-danger", + }); + } + + if (!isNaN(detectAvg) && detectAvg >= 40.0) { + problems.push({ + text: `${name.replaceAll("_", " ")} has high detect CPU usage (${detectAvg}%)`, + color: "text-danger", + }); + } + }); + + return problems; + }, [stats]); + + return { potentialProblems }; +} diff --git a/web/src/types/stats.ts b/web/src/types/stats.ts index 1ae1199c0..831e2e639 100644 --- a/web/src/types/stats.ts +++ b/web/src/types/stats.ts @@ -58,3 +58,8 @@ export type StorageStats = { used: number; mount_type: string; }; + +export type PotentialProblem = { + text: string; + color: string; +};