Implement general page of system graphs (#10815)

* Reorganize stats and show graphs in system metrics

* Break apart all cpu / mem graphs

* Auto update stats

* Show camera graphs

* Get system graphs working for inference time

* Update stats every 10 seconds, keeping the last 10 minutes

* Use types for thresholds

* Use keys api

* Break system metrics into different pages

* Add dialog for viewing and copying vainfo

* remove unused for now

* Formatting

* Make tooltip match theme

* Make betters color in light mode

* Include gpu

* Make scaling consistent

* Fix name

* address feedback
This commit is contained in:
Nicolas Mowen 2024-04-03 21:22:11 -06:00 committed by GitHub
parent 427c6a6afb
commit 0096a6d778
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 884 additions and 22 deletions

View File

@ -127,7 +127,12 @@ def stats():
@bp.route("/stats/history") @bp.route("/stats/history")
def stats_history(): def stats_history():
return jsonify(current_app.stats_emitter.get_stats_history()) keys = request.args.get("keys", default=None)
if keys:
keys = keys.split(",")
return jsonify(current_app.stats_emitter.get_stats_history(keys))
@bp.route("/config") @bp.route("/config")

View File

@ -1,10 +1,12 @@
"""Emit stats to listeners.""" """Emit stats to listeners."""
import itertools
import json import json
import logging import logging
import threading import threading
import time import time
from multiprocessing.synchronize import Event as MpEvent from multiprocessing.synchronize import Event as MpEvent
from typing import Optional
from frigate.comms.inter_process import InterProcessRequestor from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
@ -14,6 +16,9 @@ from frigate.types import StatsTrackingTypes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_STATS_POINTS = 120
class StatsEmitter(threading.Thread): class StatsEmitter(threading.Thread):
def __init__( def __init__(
self, self,
@ -43,19 +48,43 @@ class StatsEmitter(threading.Thread):
self.stats_history.append(stats) self.stats_history.append(stats)
return stats return stats
def get_stats_history(self) -> list[dict[str, any]]: def get_stats_history(
self, keys: Optional[list[str]] = None
) -> list[dict[str, any]]:
"""Get stats history.""" """Get stats history."""
if not keys:
return self.stats_history return self.stats_history
selected_stats: list[dict[str, any]] = []
for s in self.stats_history:
selected = {}
for k in keys:
selected[k] = s.get(k)
selected_stats.append(selected)
return selected_stats
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): for counter in itertools.cycle(
range(int(self.config.mqtt.stats_interval / 10))
):
if self.stop_event.wait(10):
break
logger.debug("Starting stats collection") logger.debug("Starting stats collection")
stats = stats_snapshot( stats = stats_snapshot(
self.config, self.stats_tracking, self.hwaccel_errors self.config, self.stats_tracking, self.hwaccel_errors
) )
self.stats_history.append(stats) self.stats_history.append(stats)
self.stats_history = self.stats_history[-10:] self.stats_history = self.stats_history[-MAX_STATS_POINTS:]
if counter == 0:
self.requestor.send_data("stats", json.dumps(stats)) self.requestor.send_data("stats", json.dumps(stats))
logger.debug("Finished stats collection") logger.debug("Finished stats collection")
logger.info("Exiting stats emitter...") logger.info("Exiting stats emitter...")

View File

@ -0,0 +1,126 @@
import { useTheme } from "@/context/theme-provider";
import { FrigateConfig } from "@/types/frigateConfig";
import { Threshold } from "@/types/graph";
import { useCallback, useEffect, useMemo } from "react";
import Chart from "react-apexcharts";
import useSWR from "swr";
type SystemGraphProps = {
graphId: string;
name: string;
unit: string;
threshold: Threshold;
updateTimes: number[];
data: ApexAxisChartSeries;
};
export default function SystemGraph({
graphId,
name,
unit,
threshold,
updateTimes,
data,
}: SystemGraphProps) {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
const lastValue = useMemo<number>(
// @ts-expect-error y is valid
() => data[0].data[data[0].data.length - 1]?.y ?? 0,
[data],
);
const { theme, systemTheme } = useTheme();
const formatTime = useCallback(
(val: unknown) => {
const date = new Date(updateTimes[Math.round(val as number)] * 1000);
return date.toLocaleTimeString([], {
hour12: config?.ui.time_format != "24hour",
hour: "2-digit",
minute: "2-digit",
});
},
[config, updateTimes],
);
const options = useMemo(() => {
return {
chart: {
id: graphId,
selection: {
enabled: false,
},
toolbar: {
show: false,
},
zoom: {
enabled: false,
},
},
colors: [
({ value }: { value: number }) => {
if (value >= threshold.error) {
return "#FA5252";
} else if (value >= threshold.warning) {
return "#FF9966";
} else {
return (systemTheme || theme) == "dark" ? "#404040" : "#E5E5E5";
}
},
],
grid: {
show: false,
},
legend: {
show: false,
},
dataLabels: {
enabled: false,
},
plotOptions: {
bar: {
distributed: true,
},
},
tooltip: {
theme: systemTheme || theme,
},
xaxis: {
tickAmount: 6,
labels: {
formatter: formatTime,
},
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
},
yaxis: {
show: false,
min: 0,
max: threshold.warning + 10,
},
};
}, [graphId, threshold, systemTheme, theme, formatTime]);
useEffect(() => {
ApexCharts.exec(graphId, "updateOptions", options, true, true);
}, [graphId, options]);
return (
<div className="w-full flex flex-col">
<div className="flex items-center gap-1">
<div className="text-xs text-muted-foreground">{name}</div>
<div className="text-xs text-primary-foreground">
{lastValue}
{unit}
</div>
</div>
<Chart type="bar" options={options} series={data} height="120" />
</div>
);
}

View File

@ -57,7 +57,10 @@ function StatusAlertNav() {
<DrawerContent className="max-h-[75dvh] px-2 mx-1 rounded-t-2xl overflow-hidden"> <DrawerContent className="max-h-[75dvh] px-2 mx-1 rounded-t-2xl overflow-hidden">
<div className="w-full h-auto py-4 overflow-y-auto overflow-x-hidden flex flex-col items-center gap-2"> <div className="w-full h-auto py-4 overflow-y-auto overflow-x-hidden flex flex-col items-center gap-2">
{potentialProblems.map((prob) => ( {potentialProblems.map((prob) => (
<div className="w-full flex items-center text-xs gap-2 capitalize"> <div
key={prob.text}
className="w-full flex items-center text-xs gap-2 capitalize"
>
<IoIosWarning className={`size-5 ${prob.color}`} /> <IoIosWarning className={`size-5 ${prob.color}`} />
{prob.text} {prob.text}
</div> </div>

View File

@ -0,0 +1,57 @@
import useSWR from "swr";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "../ui/dialog";
import ActivityIndicator from "../indicators/activity-indicator";
import { Vainfo } from "@/types/stats";
import { Button } from "../ui/button";
import copy from "copy-to-clipboard";
type VainfoDialogProps = {
showVainfo: boolean;
setShowVainfo: (show: boolean) => void;
};
export default function VainfoDialog({
showVainfo,
setShowVainfo,
}: VainfoDialogProps) {
const { data: vainfo } = useSWR<Vainfo>(showVainfo ? "vainfo" : null);
const onCopyVainfo = async () => {
copy(JSON.stringify(vainfo).replace(/[\\\s]+/gi, ""));
setShowVainfo(false);
};
return (
<Dialog open={showVainfo} onOpenChange={setShowVainfo}>
<DialogContent>
<DialogHeader>
<DialogTitle>Vainfo Output</DialogTitle>
</DialogHeader>
{vainfo ? (
<div className="mb-2 max-h-96 whitespace-pre-line overflow-y-scroll">
<div>Return Code: {vainfo.return_code}</div>
<br />
<div>Process {vainfo.return_code == 0 ? "Output" : "Error"}:</div>
<br />
<div>{vainfo.return_code == 0 ? vainfo.stdout : vainfo.stderr}</div>
</div>
) : (
<ActivityIndicator />
)}
<DialogFooter>
<Button variant="secondary" onClick={() => setShowVainfo(false)}>
Close
</Button>
<Button variant="select" onClick={() => onCopyVainfo()}>
Copy
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -1,4 +1,9 @@
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import {
CameraDetectThreshold,
CameraFfmpegThreshold,
InferenceThreshold,
} from "@/types/graph";
import { FrigateStats, PotentialProblem } from "@/types/stats"; import { FrigateStats, PotentialProblem } from "@/types/stats";
import { useMemo } from "react"; import { useMemo } from "react";
import useSWR from "swr"; import useSWR from "swr";
@ -15,12 +20,12 @@ export default function useStats(stats: FrigateStats | undefined) {
// check detectors for high inference speeds // check detectors for high inference speeds
Object.entries(stats["detectors"]).forEach(([key, det]) => { Object.entries(stats["detectors"]).forEach(([key, det]) => {
if (det["inference_speed"] > 100) { if (det["inference_speed"] > InferenceThreshold.error) {
problems.push({ problems.push({
text: `${key} is very slow (${det["inference_speed"]} ms)`, text: `${key} is very slow (${det["inference_speed"]} ms)`,
color: "text-danger", color: "text-danger",
}); });
} else if (det["inference_speed"] > 50) { } else if (det["inference_speed"] > InferenceThreshold.warning) {
problems.push({ problems.push({
text: `${key} is slow (${det["inference_speed"]} ms)`, text: `${key} is slow (${det["inference_speed"]} ms)`,
color: "text-orange-400", color: "text-orange-400",
@ -51,14 +56,14 @@ export default function useStats(stats: FrigateStats | undefined) {
stats["cpu_usages"][cam["pid"]]?.cpu_average, stats["cpu_usages"][cam["pid"]]?.cpu_average,
); );
if (!isNaN(ffmpegAvg) && ffmpegAvg >= 20.0) { if (!isNaN(ffmpegAvg) && ffmpegAvg >= CameraFfmpegThreshold.error) {
problems.push({ problems.push({
text: `${name.replaceAll("_", " ")} has high FFMPEG CPU usage (${ffmpegAvg}%)`, text: `${name.replaceAll("_", " ")} has high FFMPEG CPU usage (${ffmpegAvg}%)`,
color: "text-danger", color: "text-danger",
}); });
} }
if (!isNaN(detectAvg) && detectAvg >= 40.0) { if (!isNaN(detectAvg) && detectAvg >= CameraDetectThreshold.error) {
problems.push({ problems.push({
text: `${name.replaceAll("_", " ")} has high detect CPU usage (${detectAvg}%)`, text: `${name.replaceAll("_", " ")} has high detect CPU usage (${detectAvg}%)`,
color: "text-danger", color: "text-danger",

View File

@ -283,7 +283,7 @@ function Logs() {
<div className="size-full p-2 flex flex-col"> <div className="size-full p-2 flex flex-col">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<ToggleGroup <ToggleGroup
className="*:px-3 *:py-4 *:rounded-2xl" className="*:px-3 *:py-4 *:rounded-md"
type="single" type="single"
size="sm" size="sm"
value={logService} value={logService}

View File

@ -1,11 +1,602 @@
import Heading from "@/components/ui/heading"; 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 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";
const metrics = ["general", "storage", "cameras"] as const;
type SystemMetric = (typeof metrics)[number];
function System() { function System() {
// stats page
const [page, setPage] = useState<SystemMetric>("general");
const [lastUpdated, setLastUpdated] = useState<number>(Date.now() / 1000);
// stats collection
const { data: statsSnapshot } = useSWR<FrigateStats>("stats", {
revalidateOnFocus: false,
});
return ( return (
<> <div className="size-full p-2 flex flex-col">
<Heading as="h2">System</Heading> <div className="w-full h-8 flex justify-between items-center">
</> <ToggleGroup
className="*:px-3 *:py-4 *:rounded-md"
type="single"
size="sm"
value={page}
onValueChange={(value: SystemMetric) => {
if (value) {
setPage(value);
}
}} // don't allow the severity to be unselected
>
{Object.values(metrics).map((item) => (
<ToggleGroupItem
key={item}
className={`flex items-center justify-between gap-2 ${page == item ? "" : "text-gray-500"}`}
value={item}
aria-label={`Select ${item}`}
>
<div className="capitalize">{item}</div>
</ToggleGroupItem>
))}
</ToggleGroup>
<div className="h-full flex items-center">
{lastUpdated && (
<div className="h-full text-muted-foreground text-sm content-center">
Last refreshed: <TimeAgo time={lastUpdated * 1000} dense />
</div>
)}
</div>
</div>
<div className="mt-2 flex items-end gap-2">
<div className="h-full font-medium content-center">System</div>
{statsSnapshot && (
<div className="h-full text-muted-foreground text-sm content-center">
{statsSnapshot.service.version}
</div>
)}
</div>
{page == "general" && (
<GeneralMetrics
lastUpdated={lastUpdated}
setLastUpdated={setLastUpdated}
/>
)}
</div>
); );
} }
export default System; export default System;
/**
* const cameraCpuSeries = useMemo(() => {
if (!statsHistory || statsHistory.length == 0) {
return {};
}
const series: {
[cam: string]: {
[key: string]: { name: string; data: { x: object; y: string }[] };
};
} = {};
statsHistory.forEach((stats, statsIdx) => {
if (!stats) {
return;
}
const statTime = new Date(stats.service.last_updated * 1000);
Object.entries(stats.cameras).forEach(([key, camStats]) => {
if (!config?.cameras[key].enabled) {
return;
}
if (!(key in series)) {
const camName = key.replaceAll("_", " ");
series[key] = {};
series[key]["ffmpeg"] = { name: `${camName} ffmpeg`, data: [] };
series[key]["capture"] = { name: `${camName} capture`, data: [] };
series[key]["detect"] = { name: `${camName} detect`, data: [] };
}
series[key]["ffmpeg"].data.push({
x: statsIdx,
y: stats.cpu_usages[camStats.ffmpeg_pid.toString()]?.cpu ?? 0.0,
});
series[key]["capture"].data.push({
x: statsIdx,
y: stats.cpu_usages[camStats.capture_pid?.toString()]?.cpu ?? 0,
});
series[key]["detect"].data.push({
x: statsIdx,
y: stats.cpu_usages[camStats.pid.toString()].cpu,
});
});
});
return series;
}, [statsHistory]);
const cameraFpsSeries = useMemo(() => {
if (!statsHistory) {
return {};
}
const series: {
[cam: string]: {
[key: string]: { name: string; data: { x: object; y: number }[] };
};
} = {};
statsHistory.forEach((stats, statsIdx) => {
if (!stats) {
return;
}
const statTime = new Date(stats.service.last_updated * 1000);
Object.entries(stats.cameras).forEach(([key, camStats]) => {
if (!(key in series)) {
const camName = key.replaceAll("_", " ");
series[key] = {};
series[key]["det"] = { name: `${camName} detections`, data: [] };
series[key]["skip"] = {
name: `${camName} skipped detections`,
data: [],
};
}
series[key]["det"].data.push({
x: statsIdx,
y: camStats.detection_fps,
});
series[key]["skip"].data.push({
x: statsIdx,
y: camStats.skipped_fps,
});
});
});
return series;
}, [statsHistory]);
*
* <div className="bg-primary rounded-2xl flex-col">
<Heading as="h4">Cameras</Heading>
<div className="grid grid-cols-1 sm:grid-cols-2">
{config &&
Object.values(config.cameras).map((camera) => {
if (camera.enabled) {
return (
<div key={camera.name} className="grid grid-cols-2">
<SystemGraph
graphId={`${camera.name}-cpu`}
title={`${camera.name.replaceAll("_", " ")} CPU`}
unit="%"
data={Object.values(cameraCpuSeries[camera.name] || {})}
/>
<SystemGraph
graphId={`${camera.name}-fps`}
title={`${camera.name.replaceAll("_", " ")} FPS`}
unit=""
data={Object.values(cameraFpsSeries[camera.name] || {})}
/>
</div>
);
}
return null;
})}
</div>
*/
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<FrigateStats[]>(
[
"stats/history",
{ keys: "cpu_usages,detectors,gpu_usages,processes,service" },
],
{
revalidateOnFocus: false,
},
);
const [statsHistory, setStatsHistory] = useState<FrigateStats[]>([]);
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 (
<>
<VainfoDialog showVainfo={showVainfo} setShowVainfo={setShowVainfo} />
<div className="size-full mt-4 flex flex-col overflow-y-auto">
<div className="text-muted-foreground text-sm font-medium">
Detectors
</div>
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="p-2.5 bg-primary rounded-2xl flex-col">
<div className="mb-5">Detector Inference Speed</div>
{detInferenceTimeSeries.map((series) => (
<SystemGraph
key={series.name}
graphId={`${series.name}-inference`}
name={series.name}
unit="ms"
threshold={InferenceThreshold}
updateTimes={updateTimes}
data={[series]}
/>
))}
</div>
<div className="p-2.5 bg-primary rounded-2xl flex-col">
<div className="mb-5">Detector CPU Usage</div>
{detCpuSeries.map((series) => (
<SystemGraph
key={series.name}
graphId={`${series.name}-cpu`}
unit="%"
name={series.name}
threshold={DetectorCpuThreshold}
updateTimes={updateTimes}
data={[series]}
/>
))}
</div>
<div className="p-2.5 bg-primary rounded-2xl flex-col">
<div className="mb-5">Detector Memory Usage</div>
{detMemSeries.map((series) => (
<SystemGraph
key={series.name}
graphId={`${series.name}-mem`}
unit="%"
name={series.name}
threshold={DetectorMemThreshold}
updateTimes={updateTimes}
data={[series]}
/>
))}
</div>
</div>
{statsHistory.length > 0 && statsHistory[0].gpu_usages && (
<>
<div className="mt-4 flex items-center justify-between">
<div className="text-muted-foreground text-sm font-medium">
GPUs
</div>
{Object.keys(statsHistory[0].gpu_usages).filter(
(key) =>
key == "amd-vaapi" ||
key == "intel-vaapi" ||
key == "intel-qsv",
).length > 0 && (
<Button
className="cursor-pointer"
variant="secondary"
size="sm"
onClick={() => setShowVainfo(true)}
>
Hardware Info
</Button>
)}
</div>
<div className=" mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
<div className="p-2.5 bg-primary rounded-2xl flex-col">
<div className="mb-5">GPU Usage</div>
{gpuSeries.map((series) => (
<SystemGraph
key={series.name}
graphId={`${series.name}-gpu`}
name={series.name}
unit=""
threshold={GPUUsageThreshold}
updateTimes={updateTimes}
data={[series]}
/>
))}
</div>
<div className="p-2.5 bg-primary rounded-2xl flex-col">
<div className="mb-5">GPU Memory</div>
{gpuMemSeries.map((series) => (
<SystemGraph
key={series.name}
graphId={`${series.name}-mem`}
unit=""
name={series.name}
threshold={GPUMemThreshold}
updateTimes={updateTimes}
data={[series]}
/>
))}
</div>
</div>
</>
)}
<div className="mt-4 text-muted-foreground text-sm font-medium">
Other Processes
</div>
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
<div className="p-2.5 bg-primary rounded-2xl flex-col">
<div className="mb-5">Process CPU Usage</div>
{otherProcessCpuSeries.map((series) => (
<SystemGraph
key={series.name}
graphId={`${series.name}-cpu`}
name={series.name.replaceAll("_", " ")}
unit="%"
threshold={DetectorCpuThreshold}
updateTimes={updateTimes}
data={[series]}
/>
))}
</div>
<div className="p-2.5 bg-primary rounded-2xl flex-col">
<div className="mb-5">Process Memory Usage</div>
{otherProcessMemSeries.map((series) => (
<SystemGraph
key={series.name}
graphId={`${series.name}-mem`}
unit="%"
name={series.name.replaceAll("_", " ")}
threshold={DetectorMemThreshold}
updateTimes={updateTimes}
data={[series]}
/>
))}
</div>
</div>
</div>
</>
);
}

View File

@ -7,3 +7,43 @@ export type GraphData = {
name?: string; name?: string;
data: GraphDataPoint[]; data: GraphDataPoint[];
}; };
export type Threshold = {
warning: number;
error: number;
};
export const InferenceThreshold = {
warning: 50,
error: 100,
} as Threshold;
export const DetectorCpuThreshold = {
warning: 25,
error: 50,
} as Threshold;
export const DetectorMemThreshold = {
warning: 20,
error: 50,
} as Threshold;
export const GPUUsageThreshold = {
warning: 75,
error: 95,
} as Threshold;
export const GPUMemThreshold = {
warning: 75,
error: 95,
} as Threshold;
export const CameraFfmpegThreshold = {
warning: 20,
error: 20,
} as Threshold;
export const CameraDetectThreshold = {
warning: 20,
error: 40,
} as Threshold;

View File

@ -63,3 +63,9 @@ export type PotentialProblem = {
text: string; text: string;
color: string; color: string;
}; };
export type Vainfo = {
return_code: number;
stdout: string;
stderr: string;
};

View File

@ -12,24 +12,24 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
"/api": { "/api": {
target: "http://192.168.50.106:5000", target: "http://localhost:5000",
ws: true, ws: true,
}, },
"/vod": { "/vod": {
target: "http://192.168.50.106:5000", target: "http://localhost:5000",
}, },
"/clips": { "/clips": {
target: "http://192.168.50.106:5000", target: "http://localhost:5000",
}, },
"/exports": { "/exports": {
target: "http://192.168.50.106:5000", target: "http://localhost:5000",
}, },
"/ws": { "/ws": {
target: "ws://192.168.50.106:5000", target: "ws://localhost:5000",
ws: true, ws: true,
}, },
"/live": { "/live": {
target: "ws://192.168.50.106:5000", target: "ws://localhost:5000",
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
}, },