mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-30 20:04:54 +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:
parent
f002513d36
commit
f44f485f48
@ -52,18 +52,66 @@ class StatsEmitter(threading.Thread):
|
|||||||
def get_stats_history(
|
def get_stats_history(
|
||||||
self, keys: Optional[list[str]] = None
|
self, keys: Optional[list[str]] = None
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""Get stats history."""
|
"""Get stats history.
|
||||||
|
|
||||||
|
Supports dot-notation for nested keys to avoid returning large objects
|
||||||
|
when only specific subfields are needed. Handles two patterns:
|
||||||
|
|
||||||
|
- Flat dict: "service.last_updated" returns {"service": {"last_updated": ...}}
|
||||||
|
- Dict-of-dicts: "cameras.camera_fps" returns each camera entry filtered
|
||||||
|
to only include "camera_fps"
|
||||||
|
"""
|
||||||
if not keys:
|
if not keys:
|
||||||
return self.stats_history
|
return self.stats_history
|
||||||
|
|
||||||
|
# Pre-parse keys into top-level keys and dot-notation fields
|
||||||
|
top_level_keys: list[str] = []
|
||||||
|
nested_keys: dict[str, list[str]] = {}
|
||||||
|
|
||||||
|
for k in keys:
|
||||||
|
if "." in k:
|
||||||
|
parent_key, child_key = k.split(".", 1)
|
||||||
|
nested_keys.setdefault(parent_key, []).append(child_key)
|
||||||
|
else:
|
||||||
|
top_level_keys.append(k)
|
||||||
|
|
||||||
selected_stats: list[dict[str, Any]] = []
|
selected_stats: list[dict[str, Any]] = []
|
||||||
|
|
||||||
for s in self.stats_history:
|
for s in self.stats_history:
|
||||||
selected = {}
|
selected: dict[str, Any] = {}
|
||||||
|
|
||||||
for k in keys:
|
for k in top_level_keys:
|
||||||
selected[k] = s.get(k)
|
selected[k] = s.get(k)
|
||||||
|
|
||||||
|
for parent_key, child_keys in nested_keys.items():
|
||||||
|
parent = s.get(parent_key)
|
||||||
|
|
||||||
|
if not isinstance(parent, dict):
|
||||||
|
selected[parent_key] = parent
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if values are dicts (dict-of-dicts like cameras/detectors)
|
||||||
|
first_value = next(iter(parent.values()), None)
|
||||||
|
|
||||||
|
if isinstance(first_value, dict):
|
||||||
|
# Filter each nested entry to only requested fields,
|
||||||
|
# omitting None values to preserve key-absence semantics
|
||||||
|
selected[parent_key] = {
|
||||||
|
entry_key: {
|
||||||
|
field: val
|
||||||
|
for field in child_keys
|
||||||
|
if (val := entry.get(field)) is not None
|
||||||
|
}
|
||||||
|
for entry_key, entry in parent.items()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Flat dict (like service) - pick individual fields
|
||||||
|
if parent_key not in selected:
|
||||||
|
selected[parent_key] = {}
|
||||||
|
|
||||||
|
for child_key in child_keys:
|
||||||
|
selected[parent_key][child_key] = parent.get(child_key)
|
||||||
|
|
||||||
selected_stats.append(selected)
|
selected_stats.append(selected)
|
||||||
|
|
||||||
return selected_stats
|
return selected_stats
|
||||||
|
|||||||
@ -498,4 +498,30 @@ def stats_snapshot(
|
|||||||
"pid": pid,
|
"pid": pid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Embed cpu/mem stats into detectors, cameras, and processes
|
||||||
|
# so history consumers don't need the full cpu_usages dict
|
||||||
|
cpu_usages = stats.get("cpu_usages", {})
|
||||||
|
|
||||||
|
for det_stats in stats["detectors"].values():
|
||||||
|
pid_str = str(det_stats.get("pid", ""))
|
||||||
|
usage = cpu_usages.get(pid_str, {})
|
||||||
|
det_stats["cpu"] = usage.get("cpu")
|
||||||
|
det_stats["mem"] = usage.get("mem")
|
||||||
|
|
||||||
|
for cam_stats in stats["cameras"].values():
|
||||||
|
for pid_key, field in [
|
||||||
|
("ffmpeg_pid", "ffmpeg_cpu"),
|
||||||
|
("capture_pid", "capture_cpu"),
|
||||||
|
("pid", "detect_cpu"),
|
||||||
|
]:
|
||||||
|
pid_str = str(cam_stats.get(pid_key, ""))
|
||||||
|
usage = cpu_usages.get(pid_str, {})
|
||||||
|
cam_stats[field] = usage.get("cpu")
|
||||||
|
|
||||||
|
for proc_stats in stats["processes"].values():
|
||||||
|
pid_str = str(proc_stats.get("pid", ""))
|
||||||
|
usage = cpu_usages.get(pid_str, {})
|
||||||
|
proc_stats["cpu"] = usage.get("cpu")
|
||||||
|
proc_stats["mem"] = usage.get("mem")
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -28,6 +28,9 @@ export type CameraStats = {
|
|||||||
expected_fps: number;
|
expected_fps: number;
|
||||||
reconnects_last_hour: number;
|
reconnects_last_hour: number;
|
||||||
stalls_last_hour: number;
|
stalls_last_hour: number;
|
||||||
|
ffmpeg_cpu?: string;
|
||||||
|
capture_cpu?: string;
|
||||||
|
detect_cpu?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CpuStats = {
|
export type CpuStats = {
|
||||||
@ -42,6 +45,8 @@ export type DetectorStats = {
|
|||||||
inference_speed: number;
|
inference_speed: number;
|
||||||
pid: number;
|
pid: number;
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
|
cpu?: string;
|
||||||
|
mem?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EmbeddingsStats = {
|
export type EmbeddingsStats = {
|
||||||
@ -53,6 +58,8 @@ export type EmbeddingsStats = {
|
|||||||
|
|
||||||
export type ExtraProcessStats = {
|
export type ExtraProcessStats = {
|
||||||
pid: number;
|
pid: number;
|
||||||
|
cpu?: string;
|
||||||
|
mem?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GpuStats = {
|
export type GpuStats = {
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -910,6 +934,7 @@ export default function GeneralMetrics({
|
|||||||
threshold={GPUMemThreshold}
|
threshold={GPUMemThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={[series]}
|
data={[series]}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -934,6 +959,7 @@ export default function GeneralMetrics({
|
|||||||
threshold={GPUMemThreshold}
|
threshold={GPUMemThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={[series]}
|
data={[series]}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -958,6 +984,7 @@ export default function GeneralMetrics({
|
|||||||
threshold={DetectorTempThreshold}
|
threshold={DetectorTempThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={[series]}
|
data={[series]}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -983,6 +1010,7 @@ export default function GeneralMetrics({
|
|||||||
threshold={GPUUsageThreshold}
|
threshold={GPUUsageThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={[series]}
|
data={[series]}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -1005,6 +1033,7 @@ export default function GeneralMetrics({
|
|||||||
threshold={DetectorTempThreshold}
|
threshold={DetectorTempThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={[series]}
|
data={[series]}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -1038,6 +1067,7 @@ export default function GeneralMetrics({
|
|||||||
threshold={DetectorCpuThreshold}
|
threshold={DetectorCpuThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={[series]}
|
data={[series]}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -1057,6 +1087,7 @@ export default function GeneralMetrics({
|
|||||||
threshold={DetectorMemThreshold}
|
threshold={DetectorMemThreshold}
|
||||||
updateTimes={updateTimes}
|
updateTimes={updateTimes}
|
||||||
data={[series]}
|
data={[series]}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { CombinedStorageGraph } from "@/components/graph/CombinedStorageGraph";
|
import { CombinedStorageGraph } from "@/components/graph/CombinedStorageGraph";
|
||||||
import { StorageGraph } from "@/components/graph/StorageGraph";
|
import { StorageGraph } from "@/components/graph/StorageGraph";
|
||||||
import { FrigateStats } from "@/types/stats";
|
import { FrigateStats } from "@/types/stats";
|
||||||
import { useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@ -56,9 +56,14 @@ export default function StorageMetrics({
|
|||||||
Object.values(cameraStorage).forEach(
|
Object.values(cameraStorage).forEach(
|
||||||
(cam) => (totalStorage.camera += cam.usage),
|
(cam) => (totalStorage.camera += cam.usage),
|
||||||
);
|
);
|
||||||
setLastUpdated(Date.now() / 1000);
|
|
||||||
return totalStorage;
|
return totalStorage;
|
||||||
}, [cameraStorage, stats, setLastUpdated]);
|
}, [cameraStorage, stats]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (totalStorage) {
|
||||||
|
setLastUpdated(Math.floor(Date.now() / 1000));
|
||||||
|
}
|
||||||
|
}, [totalStorage, setLastUpdated]);
|
||||||
|
|
||||||
// recordings summary
|
// recordings summary
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user