Improve metrics UI performance (#22691)

* embed cpu/mem stats into detectors, cameras, and processes

so history consumers don't need the full cpu_usages dict

* support dot-notation for nested keys

to avoid returning large objects when only specific subfields are needed

* fix setLastUpdated being called inside useMemo

this triggered a setState-during-render warning, so moved to a useEffect

* frontend types

* frontend

hide instead of unmount all graphs - re-rendering is much more expensive and disruptive than the amount of dom memory required

keep track of visited tabs to keep them mounted rather than re-mounting or mounting all tabs

add isActive prop to all charts to re-trigger animation when switching metrics tabs

fix chart data padding bug where the loop used number of series rather than number of data points

fix bug where only a shallow copy of the array was used for mutation

fix missing key prop causing console logs

* add isactive after rebase

* formatting

* skip None values in filtered output for dot notation
This commit is contained in:
Josh Hawkins
2026-03-29 11:58:47 -06:00
committed by GitHub
parent f002513d36
commit f44f485f48
10 changed files with 330 additions and 93 deletions
+43 -13
View File
@@ -5,7 +5,14 @@ import { ConnectionQualityIndicator } from "@/components/camera/ConnectionQualit
import { Skeleton } from "@/components/ui/skeleton";
import { FrigateConfig } from "@/types/frigateConfig";
import { FrigateStats } from "@/types/stats";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
Fragment,
startTransition,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { MdInfo } from "react-icons/md";
import {
Tooltip,
@@ -20,10 +27,12 @@ import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
type CameraMetricsProps = {
lastUpdated: number;
setLastUpdated: (last: number) => void;
isActive: boolean;
};
export default function CameraMetrics({
lastUpdated,
setLastUpdated,
isActive,
}: CameraMetricsProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const { t } = useTranslation(["views/system"]);
@@ -39,11 +48,11 @@ export default function CameraMetrics({
// stats
const { data: initialStats } = useSWR<FrigateStats[]>(
const { data: initialStats, mutate: refreshStats } = useSWR<FrigateStats[]>(
[
"stats/history",
{
keys: "cpu_usages,cameras,camera_fps,detection_fps,skipped_fps,service",
keys: "cameras.camera_fps,cameras.detection_fps,cameras.skipped_fps,cameras.ffmpeg_cpu,cameras.capture_cpu,cameras.detect_cpu,cameras.connection_quality,cameras.expected_fps,cameras.reconnects_last_hour,cameras.stalls_last_hour,camera_fps,detection_fps,skipped_fps,service.last_updated",
},
],
{
@@ -60,19 +69,38 @@ export default function CameraMetrics({
}
if (statsHistory.length == 0) {
setStatsHistory(initialStats);
startTransition(() => setStatsHistory(initialStats));
return;
}
if (!updatedStats) {
if (!isActive || !updatedStats) {
return;
}
if (updatedStats.service.last_updated > lastUpdated) {
setStatsHistory([...statsHistory.slice(1), updatedStats]);
setLastUpdated(Date.now() / 1000);
setLastUpdated(updatedStats.service.last_updated);
}
}, [initialStats, updatedStats, statsHistory, lastUpdated, setLastUpdated]);
}, [
initialStats,
updatedStats,
statsHistory,
lastUpdated,
setLastUpdated,
isActive,
]);
useEffect(() => {
if (isActive && statsHistory.length > 0) {
refreshStats().then((freshStats) => {
if (freshStats && freshStats.length > 0) {
setStatsHistory(freshStats);
}
});
}
// only re-fetch when tab becomes active, not on data changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive]);
// timestamps
@@ -168,15 +196,15 @@ export default function CameraMetrics({
series[key]["ffmpeg"].data.push({
x: statsIdx,
y: stats.cpu_usages[camStats.ffmpeg_pid.toString()]?.cpu ?? 0.0,
y: camStats.ffmpeg_cpu ?? "0",
});
series[key]["capture"].data.push({
x: statsIdx,
y: stats.cpu_usages[camStats.capture_pid?.toString()]?.cpu ?? 0,
y: camStats.capture_cpu ?? "0",
});
series[key]["detect"].data.push({
x: statsIdx,
y: stats.cpu_usages[camStats.pid?.toString()]?.cpu,
y: camStats.detect_cpu ?? "0",
});
});
});
@@ -261,6 +289,7 @@ export default function CameraMetrics({
dataLabels={["camera", "detect", "skipped"]}
updateTimes={updateTimes}
data={overallFpsSeries}
isActive={isActive}
/>
</div>
) : (
@@ -272,10 +301,9 @@ export default function CameraMetrics({
Object.values(config.cameras).map((camera) => {
if (camera.enabled) {
return (
<>
<Fragment key={camera.name}>
{probeCameraName == camera.name && (
<CameraInfoDialog
key={camera.name}
camera={camera}
showCameraInfoDialog={showCameraInfoDialog}
setShowCameraInfoDialog={setShowCameraInfoDialog}
@@ -345,6 +373,7 @@ export default function CameraMetrics({
data={Object.values(
cameraCpuSeries[camera.name] || {},
)}
isActive={isActive}
/>
</div>
) : (
@@ -363,6 +392,7 @@ export default function CameraMetrics({
data={Object.values(
cameraFpsSeries[camera.name] || {},
)}
isActive={isActive}
/>
</div>
) : (
@@ -370,7 +400,7 @@ export default function CameraMetrics({
)}
</div>
</div>
</>
</Fragment>
);
}