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
+73 -42
View File
@@ -1,6 +1,6 @@
import useSWR from "swr";
import { FrigateStats, GpuInfo } from "@/types/stats";
import { useEffect, useMemo, useState } from "react";
import { startTransition, useEffect, useMemo, useState } from "react";
import { useFrigateStats } from "@/api/ws";
import {
DetectorCpuThreshold,
@@ -26,10 +26,12 @@ import { CiCircleAlert } from "react-icons/ci";
type GeneralMetricsProps = {
lastUpdated: number;
setLastUpdated: (last: number) => void;
isActive: boolean;
};
export default function GeneralMetrics({
lastUpdated,
setLastUpdated,
isActive,
}: GeneralMetricsProps) {
// extra info
const { t } = useTranslation(["views/system"]);
@@ -37,10 +39,12 @@ export default function GeneralMetrics({
// stats
const { data: initialStats } = useSWR<FrigateStats[]>(
const { data: initialStats, mutate: refreshStats } = useSWR<FrigateStats[]>(
[
"stats/history",
{ keys: "cpu_usages,detectors,gpu_usages,npu_usages,processes,service" },
{
keys: "detectors.inference_speed,detectors.temperature,detectors.cpu,detectors.mem,gpu_usages,npu_usages,processes.cpu,processes.mem,service.last_updated",
},
],
{
revalidateOnFocus: false,
@@ -56,19 +60,38 @@ export default function GeneralMetrics({
}
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]);
const [canGetGpuInfo, gpuType] = useMemo<[boolean, GpuInfo]>(() => {
let vaCount = 0;
@@ -181,7 +204,7 @@ export default function GeneralMetrics({
series[key] = { name: key, data: [] };
}
const data = stats.cpu_usages[detStats.pid.toString()]?.cpu;
const data = detStats.cpu;
if (data != undefined) {
series[key].data.push({
@@ -213,10 +236,12 @@ export default function GeneralMetrics({
series[key] = { name: key, data: [] };
}
series[key].data.push({
x: statsIdx + 1,
y: stats.cpu_usages[detStats.pid.toString()].mem,
});
if (detStats.mem != undefined) {
series[key].data.push({
x: statsIdx + 1,
y: detStats.mem,
});
}
});
});
return Object.values(series);
@@ -581,22 +606,18 @@ export default function GeneralMetrics({
}
Object.entries(stats.processes).forEach(([key, procStats]) => {
if (procStats.pid.toString() in stats.cpu_usages) {
if (!(key in series)) {
series[key] = {
name: t(`general.otherProcesses.series.${key}`),
data: [],
};
}
if (!(key in series)) {
series[key] = {
name: t(`general.otherProcesses.series.${key}`),
data: [],
};
}
const data = stats.cpu_usages[procStats.pid.toString()]?.cpu;
if (data != undefined) {
series[key].data.push({
x: statsIdx + 1,
y: data,
});
}
if (procStats.cpu != undefined) {
series[key].data.push({
x: statsIdx + 1,
y: procStats.cpu,
});
}
});
});
@@ -618,22 +639,18 @@ export default function GeneralMetrics({
}
Object.entries(stats.processes).forEach(([key, procStats]) => {
if (procStats.pid.toString() in stats.cpu_usages) {
if (!(key in series)) {
series[key] = {
name: t(`general.otherProcesses.series.${key}`),
data: [],
};
}
if (!(key in series)) {
series[key] = {
name: t(`general.otherProcesses.series.${key}`),
data: [],
};
}
const data = stats.cpu_usages[procStats.pid.toString()]?.mem;
if (data) {
series[key].data.push({
x: statsIdx + 1,
y: data,
});
}
if (procStats.mem) {
series[key].data.push({
x: statsIdx + 1,
y: procStats.mem,
});
}
});
});
@@ -670,6 +687,7 @@ export default function GeneralMetrics({
threshold={InferenceThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -692,6 +710,7 @@ export default function GeneralMetrics({
threshold={DetectorTempThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -730,6 +749,7 @@ export default function GeneralMetrics({
threshold={DetectorCpuThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -748,6 +768,7 @@ export default function GeneralMetrics({
threshold={DetectorMemThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -840,6 +861,7 @@ export default function GeneralMetrics({
threshold={GPUUsageThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -862,6 +884,7 @@ export default function GeneralMetrics({
threshold={GPUMemThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -886,6 +909,7 @@ export default function GeneralMetrics({
threshold={GPUMemThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -910,6 +934,7 @@ export default function GeneralMetrics({
threshold={GPUMemThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -934,6 +959,7 @@ export default function GeneralMetrics({
threshold={GPUMemThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -958,6 +984,7 @@ export default function GeneralMetrics({
threshold={DetectorTempThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -983,6 +1010,7 @@ export default function GeneralMetrics({
threshold={GPUUsageThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -1005,6 +1033,7 @@ export default function GeneralMetrics({
threshold={DetectorTempThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -1038,6 +1067,7 @@ export default function GeneralMetrics({
threshold={DetectorCpuThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>
@@ -1057,6 +1087,7 @@ export default function GeneralMetrics({
threshold={DetectorMemThreshold}
updateTimes={updateTimes}
data={[series]}
isActive={isActive}
/>
))}
</div>