mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 17:12:16 +03:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import useSWR from "swr";
|
||||
import { FrigateStats } from "@/types/stats";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
startTransition,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useFrigateStats } from "@/api/ws";
|
||||
import { EmbeddingThreshold, GenAIThreshold, Threshold } from "@/types/graph";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -12,16 +18,18 @@ import { EventsPerSecondsLineGraph } from "@/components/graph/LineGraph";
|
||||
type EnrichmentMetricsProps = {
|
||||
lastUpdated: number;
|
||||
setLastUpdated: (last: number) => void;
|
||||
isActive: boolean;
|
||||
};
|
||||
export default function EnrichmentMetrics({
|
||||
lastUpdated,
|
||||
setLastUpdated,
|
||||
isActive,
|
||||
}: EnrichmentMetricsProps) {
|
||||
// stats
|
||||
const { t } = useTranslation(["views/system"]);
|
||||
|
||||
const { data: initialStats } = useSWR<FrigateStats[]>(
|
||||
["stats/history", { keys: "embeddings,service" }],
|
||||
const { data: initialStats, mutate: refreshStats } = useSWR<FrigateStats[]>(
|
||||
["stats/history", { keys: "embeddings,service.last_updated" }],
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
@@ -36,19 +44,38 @@ export default function EnrichmentMetrics({
|
||||
}
|
||||
|
||||
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 getThreshold = useCallback((key: string) => {
|
||||
if (key.includes("description")) {
|
||||
@@ -205,6 +232,7 @@ export default function EnrichmentMetrics({
|
||||
threshold={group.speedSeries.metrics}
|
||||
updateTimes={updateTimes}
|
||||
data={[group.speedSeries]}
|
||||
isActive={isActive}
|
||||
/>
|
||||
)}
|
||||
{group.eventsSeries && (
|
||||
@@ -215,6 +243,7 @@ export default function EnrichmentMetrics({
|
||||
name={t("enrichments.infPerSecond")}
|
||||
updateTimes={updateTimes}
|
||||
data={[group.eventsSeries]}
|
||||
isActive={isActive}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CombinedStorageGraph } from "@/components/graph/CombinedStorageGraph";
|
||||
import { StorageGraph } from "@/components/graph/StorageGraph";
|
||||
import { FrigateStats } from "@/types/stats";
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -56,9 +56,14 @@ export default function StorageMetrics({
|
||||
Object.values(cameraStorage).forEach(
|
||||
(cam) => (totalStorage.camera += cam.usage),
|
||||
);
|
||||
setLastUpdated(Date.now() / 1000);
|
||||
return totalStorage;
|
||||
}, [cameraStorage, stats, setLastUpdated]);
|
||||
}, [cameraStorage, stats]);
|
||||
|
||||
useEffect(() => {
|
||||
if (totalStorage) {
|
||||
setLastUpdated(Math.floor(Date.now() / 1000));
|
||||
}
|
||||
}, [totalStorage, setLastUpdated]);
|
||||
|
||||
// recordings summary
|
||||
|
||||
|
||||
Reference in New Issue
Block a user