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
This commit is contained in:
Josh Hawkins 2026-03-29 07:24:47 -05:00
parent 20b3cdf5e7
commit 31a26f9e0a
6 changed files with 237 additions and 87 deletions

View File

@ -2,7 +2,7 @@ import { useTheme } from "@/context/theme-provider";
import { useDateLocale } from "@/hooks/use-date-locale"; import { useDateLocale } from "@/hooks/use-date-locale";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import Chart from "react-apexcharts"; import Chart from "react-apexcharts";
import { isMobileOnly } from "react-device-detect"; import { isMobileOnly } from "react-device-detect";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -17,6 +17,7 @@ type CameraLineGraphProps = {
dataLabels: string[]; dataLabels: string[];
updateTimes: number[]; updateTimes: number[];
data: ApexAxisChartSeries; data: ApexAxisChartSeries;
isActive?: boolean;
}; };
export function CameraLineGraph({ export function CameraLineGraph({
graphId, graphId,
@ -24,6 +25,7 @@ export function CameraLineGraph({
dataLabels, dataLabels,
updateTimes, updateTimes,
data, data,
isActive = true,
}: CameraLineGraphProps) { }: CameraLineGraphProps) {
const { t } = useTranslation(["views/system", "common"]); const { t } = useTranslation(["views/system", "common"]);
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
@ -134,6 +136,16 @@ export function CameraLineGraph({
ApexCharts.exec(graphId, "updateOptions", options, true, true); ApexCharts.exec(graphId, "updateOptions", options, true, true);
}, [graphId, options]); }, [graphId, options]);
const hasBeenActive = useRef(isActive);
useEffect(() => {
if (isActive && hasBeenActive.current === false) {
ApexCharts.exec(graphId, "updateSeries", data, true);
}
hasBeenActive.current = isActive;
// only replay animation on visibility change, not data updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, graphId]);
return ( return (
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
{lastValues && ( {lastValues && (
@ -166,6 +178,7 @@ type EventsPerSecondLineGraphProps = {
name: string; name: string;
updateTimes: number[]; updateTimes: number[];
data: ApexAxisChartSeries; data: ApexAxisChartSeries;
isActive?: boolean;
}; };
export function EventsPerSecondsLineGraph({ export function EventsPerSecondsLineGraph({
graphId, graphId,
@ -173,6 +186,7 @@ export function EventsPerSecondsLineGraph({
name, name,
updateTimes, updateTimes,
data, data,
isActive = true,
}: EventsPerSecondLineGraphProps) { }: EventsPerSecondLineGraphProps) {
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false, revalidateOnFocus: false,
@ -277,6 +291,16 @@ export function EventsPerSecondsLineGraph({
ApexCharts.exec(graphId, "updateOptions", options, true, true); ApexCharts.exec(graphId, "updateOptions", options, true, true);
}, [graphId, options]); }, [graphId, options]);
const hasBeenActive = useRef(isActive);
useEffect(() => {
if (isActive && hasBeenActive.current === false) {
ApexCharts.exec(graphId, "updateSeries", data, true);
}
hasBeenActive.current = isActive;
// only replay animation on visibility change, not data updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, graphId]);
return ( return (
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">

View File

@ -3,7 +3,7 @@ import { useDateLocale } from "@/hooks/use-date-locale";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { Threshold } from "@/types/graph"; import { Threshold } from "@/types/graph";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import Chart from "react-apexcharts"; import Chart from "react-apexcharts";
import { isMobileOnly } from "react-device-detect"; import { isMobileOnly } from "react-device-detect";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -16,6 +16,7 @@ type ThresholdBarGraphProps = {
threshold: Threshold; threshold: Threshold;
updateTimes: number[]; updateTimes: number[];
data: ApexAxisChartSeries; data: ApexAxisChartSeries;
isActive?: boolean;
}; };
export function ThresholdBarGraph({ export function ThresholdBarGraph({
graphId, graphId,
@ -24,6 +25,7 @@ export function ThresholdBarGraph({
threshold, threshold,
updateTimes, updateTimes,
data, data,
isActive = true,
}: ThresholdBarGraphProps) { }: ThresholdBarGraphProps) {
const displayName = name || data[0]?.name || ""; const displayName = name || data[0]?.name || "";
const { data: config } = useSWR<FrigateConfig>("config", { const { data: config } = useSWR<FrigateConfig>("config", {
@ -173,17 +175,29 @@ export function ThresholdBarGraph({
return data; return data;
} }
const copiedData = [...data]; const dataPointCount = data[0].data.length;
const fakeData = []; const fakeData = [];
for (let i = data.length; i < 30; i++) { for (let i = dataPointCount; i < 30; i++) {
fakeData.push({ x: i - 30, y: 0 }); fakeData.push({ x: i - 30, y: 0 });
} }
// @ts-expect-error data types are not obvious const paddedFirst = {
copiedData[0].data = [...fakeData, ...data[0].data]; ...data[0],
return copiedData; data: [...fakeData, ...data[0].data],
};
return [paddedFirst, ...data.slice(1)] as ApexAxisChartSeries;
}, [data]); }, [data]);
const hasBeenActive = useRef(isActive);
useEffect(() => {
if (isActive && hasBeenActive.current === false) {
ApexCharts.exec(graphId, "updateSeries", chartData, true);
}
hasBeenActive.current = isActive;
// only replay animation on visibility change, not data updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, graphId]);
return ( return (
<div className="flex w-full flex-col"> <div className="flex w-full flex-col">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">

View File

@ -1,6 +1,6 @@
import useSWR from "swr"; import useSWR from "swr";
import { FrigateStats } from "@/types/stats"; import { FrigateStats } from "@/types/stats";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import TimeAgo from "@/components/dynamic/TimeAgo"; import TimeAgo from "@/components/dynamic/TimeAgo";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { isDesktop, isMobile } from "react-device-detect"; import { isDesktop, isMobile } from "react-device-detect";
@ -49,7 +49,17 @@ function System() {
setPage, setPage,
100, 100,
); );
const [lastUpdated, setLastUpdated] = useState<number>(Date.now() / 1000); const [lastUpdated, setLastUpdated] = useState<number>(
Math.floor(Date.now() / 1000),
);
// Track which tabs have been visited so we can keep them mounted after first visit.
// Using a ref updated during render avoids extra render cycles from state/effects.
const visitedTabsRef = useRef(new Set<string>());
if (page) {
visitedTabsRef.current.add(page);
}
const visitedTabs = visitedTabsRef.current;
useEffect(() => { useEffect(() => {
if (pageToggle) { if (pageToggle) {
@ -116,24 +126,37 @@ function System() {
</div> </div>
)} )}
</div> </div>
{page == "general" && ( {visitedTabs.has("general") && (
<GeneralMetrics <div className={page == "general" ? "contents" : "hidden"}>
lastUpdated={lastUpdated} <GeneralMetrics
setLastUpdated={setLastUpdated} lastUpdated={lastUpdated}
/> setLastUpdated={setLastUpdated}
isActive={page == "general"}
/>
</div>
)} )}
{page == "enrichments" && ( {metrics.includes("enrichments") && visitedTabs.has("enrichments") && (
<EnrichmentMetrics <div className={page == "enrichments" ? "contents" : "hidden"}>
lastUpdated={lastUpdated} <EnrichmentMetrics
setLastUpdated={setLastUpdated} lastUpdated={lastUpdated}
/> setLastUpdated={setLastUpdated}
isActive={page == "enrichments"}
/>
</div>
)} )}
{page == "storage" && <StorageMetrics setLastUpdated={setLastUpdated} />} {visitedTabs.has("storage") && (
{page == "cameras" && ( <div className={page == "storage" ? "contents" : "hidden"}>
<CameraMetrics <StorageMetrics setLastUpdated={setLastUpdated} />
lastUpdated={lastUpdated} </div>
setLastUpdated={setLastUpdated} )}
/> {visitedTabs.has("cameras") && (
<div className={page == "cameras" ? "contents" : "hidden"}>
<CameraMetrics
lastUpdated={lastUpdated}
setLastUpdated={setLastUpdated}
isActive={page == "cameras"}
/>
</div>
)} )}
</div> </div>
); );

View File

@ -5,7 +5,14 @@ import { ConnectionQualityIndicator } from "@/components/camera/ConnectionQualit
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { FrigateStats } from "@/types/stats"; 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 { MdInfo } from "react-icons/md";
import { import {
Tooltip, Tooltip,
@ -20,10 +27,12 @@ import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
type CameraMetricsProps = { type CameraMetricsProps = {
lastUpdated: number; lastUpdated: number;
setLastUpdated: (last: number) => void; setLastUpdated: (last: number) => void;
isActive: boolean;
}; };
export default function CameraMetrics({ export default function CameraMetrics({
lastUpdated, lastUpdated,
setLastUpdated, setLastUpdated,
isActive,
}: CameraMetricsProps) { }: CameraMetricsProps) {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const { t } = useTranslation(["views/system"]); const { t } = useTranslation(["views/system"]);
@ -39,11 +48,11 @@ export default function CameraMetrics({
// stats // stats
const { data: initialStats } = useSWR<FrigateStats[]>( const { data: initialStats, mutate: refreshStats } = useSWR<FrigateStats[]>(
[ [
"stats/history", "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) { if (statsHistory.length == 0) {
setStatsHistory(initialStats); startTransition(() => setStatsHistory(initialStats));
return; return;
} }
if (!updatedStats) { if (!isActive || !updatedStats) {
return; return;
} }
if (updatedStats.service.last_updated > lastUpdated) { if (updatedStats.service.last_updated > lastUpdated) {
setStatsHistory([...statsHistory.slice(1), updatedStats]); 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 // timestamps
@ -168,15 +196,15 @@ export default function CameraMetrics({
series[key]["ffmpeg"].data.push({ series[key]["ffmpeg"].data.push({
x: statsIdx, x: statsIdx,
y: stats.cpu_usages[camStats.ffmpeg_pid.toString()]?.cpu ?? 0.0, y: camStats.ffmpeg_cpu ?? "0",
}); });
series[key]["capture"].data.push({ series[key]["capture"].data.push({
x: statsIdx, x: statsIdx,
y: stats.cpu_usages[camStats.capture_pid?.toString()]?.cpu ?? 0, y: camStats.capture_cpu ?? "0",
}); });
series[key]["detect"].data.push({ series[key]["detect"].data.push({
x: statsIdx, 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"]} dataLabels={["camera", "detect", "skipped"]}
updateTimes={updateTimes} updateTimes={updateTimes}
data={overallFpsSeries} data={overallFpsSeries}
isActive={isActive}
/> />
</div> </div>
) : ( ) : (
@ -272,10 +301,9 @@ export default function CameraMetrics({
Object.values(config.cameras).map((camera) => { Object.values(config.cameras).map((camera) => {
if (camera.enabled) { if (camera.enabled) {
return ( return (
<> <Fragment key={camera.name}>
{probeCameraName == camera.name && ( {probeCameraName == camera.name && (
<CameraInfoDialog <CameraInfoDialog
key={camera.name}
camera={camera} camera={camera}
showCameraInfoDialog={showCameraInfoDialog} showCameraInfoDialog={showCameraInfoDialog}
setShowCameraInfoDialog={setShowCameraInfoDialog} setShowCameraInfoDialog={setShowCameraInfoDialog}
@ -345,6 +373,7 @@ export default function CameraMetrics({
data={Object.values( data={Object.values(
cameraCpuSeries[camera.name] || {}, cameraCpuSeries[camera.name] || {},
)} )}
isActive={isActive}
/> />
</div> </div>
) : ( ) : (
@ -363,6 +392,7 @@ export default function CameraMetrics({
data={Object.values( data={Object.values(
cameraFpsSeries[camera.name] || {}, cameraFpsSeries[camera.name] || {},
)} )}
isActive={isActive}
/> />
</div> </div>
) : ( ) : (
@ -370,7 +400,7 @@ export default function CameraMetrics({
)} )}
</div> </div>
</div> </div>
</> </Fragment>
); );
} }

View File

@ -1,6 +1,12 @@
import useSWR from "swr"; import useSWR from "swr";
import { FrigateStats } from "@/types/stats"; 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 { useFrigateStats } from "@/api/ws";
import { EmbeddingThreshold, GenAIThreshold, Threshold } from "@/types/graph"; import { EmbeddingThreshold, GenAIThreshold, Threshold } from "@/types/graph";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@ -12,16 +18,18 @@ import { EventsPerSecondsLineGraph } from "@/components/graph/LineGraph";
type EnrichmentMetricsProps = { type EnrichmentMetricsProps = {
lastUpdated: number; lastUpdated: number;
setLastUpdated: (last: number) => void; setLastUpdated: (last: number) => void;
isActive: boolean;
}; };
export default function EnrichmentMetrics({ export default function EnrichmentMetrics({
lastUpdated, lastUpdated,
setLastUpdated, setLastUpdated,
isActive,
}: EnrichmentMetricsProps) { }: EnrichmentMetricsProps) {
// stats // stats
const { t } = useTranslation(["views/system"]); const { t } = useTranslation(["views/system"]);
const { data: initialStats } = useSWR<FrigateStats[]>( const { data: initialStats, mutate: refreshStats } = useSWR<FrigateStats[]>(
["stats/history", { keys: "embeddings,service" }], ["stats/history", { keys: "embeddings,service.last_updated" }],
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
}, },
@ -36,19 +44,38 @@ export default function EnrichmentMetrics({
} }
if (statsHistory.length == 0) { if (statsHistory.length == 0) {
setStatsHistory(initialStats); startTransition(() => setStatsHistory(initialStats));
return; return;
} }
if (!updatedStats) { if (!isActive || !updatedStats) {
return; return;
} }
if (updatedStats.service.last_updated > lastUpdated) { if (updatedStats.service.last_updated > lastUpdated) {
setStatsHistory([...statsHistory.slice(1), updatedStats]); 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) => { const getThreshold = useCallback((key: string) => {
if (key.includes("description")) { if (key.includes("description")) {
@ -205,6 +232,7 @@ export default function EnrichmentMetrics({
threshold={group.speedSeries.metrics} threshold={group.speedSeries.metrics}
updateTimes={updateTimes} updateTimes={updateTimes}
data={[group.speedSeries]} data={[group.speedSeries]}
isActive={isActive}
/> />
)} )}
{group.eventsSeries && ( {group.eventsSeries && (
@ -215,6 +243,7 @@ export default function EnrichmentMetrics({
name={t("enrichments.infPerSecond")} name={t("enrichments.infPerSecond")}
updateTimes={updateTimes} updateTimes={updateTimes}
data={[group.eventsSeries]} data={[group.eventsSeries]}
isActive={isActive}
/> />
)} )}
</div> </div>

View File

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