Improve stats (#10911)

* Add overview stats for overall detection and skipped fps

* Fix intel memory stats

* Fix iOS image long pressing

* Cleanup
This commit is contained in:
Nicolas Mowen 2024-04-09 17:08:11 -06:00 committed by GitHub
parent 15e4f5c771
commit 524732ec73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 156 additions and 35 deletions

View File

@ -13,13 +13,14 @@ import { getIconForLabel } from "@/utils/iconUtil";
import TimeAgo from "../dynamic/TimeAgo"; import TimeAgo from "../dynamic/TimeAgo";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { isFirefox, isMobile, isSafari } from "react-device-detect"; import { isFirefox, isIOS, isMobile, isSafari } from "react-device-detect";
import Chip from "@/components/indicators/Chip"; import Chip from "@/components/indicators/Chip";
import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import useImageLoaded from "@/hooks/use-image-loaded"; import useImageLoaded from "@/hooks/use-image-loaded";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
import useContextMenu from "@/hooks/use-contextmenu";
type PreviewPlayerProps = { type PreviewPlayerProps = {
review: ReviewSegment; review: ReviewSegment;
@ -73,6 +74,10 @@ export default function PreviewThumbnailPlayer({
setReviewed(review); setReviewed(review);
}, [review, setReviewed]); }, [review, setReviewed]);
useContextMenu(imgRef, () => {
onClick(review, true);
});
// playback // playback
const relevantPreview = useMemo(() => { const relevantPreview = useMemo(() => {
@ -170,10 +175,6 @@ export default function PreviewThumbnailPlayer({
className="relative size-full cursor-pointer" className="relative size-full cursor-pointer"
onMouseOver={isMobile ? undefined : () => setIsHovered(true)} onMouseOver={isMobile ? undefined : () => setIsHovered(true)}
onMouseLeave={isMobile ? undefined : () => setIsHovered(false)} onMouseLeave={isMobile ? undefined : () => setIsHovered(false)}
onContextMenu={(e) => {
e.preventDefault();
onClick(review, true);
}}
onClick={handleOnClick} onClick={handleOnClick}
{...swipeHandlers} {...swipeHandlers}
> >
@ -196,9 +197,18 @@ export default function PreviewThumbnailPlayer({
<div className={`${imgLoaded ? "visible" : "invisible"}`}> <div className={`${imgLoaded ? "visible" : "invisible"}`}>
<img <img
ref={imgRef} ref={imgRef}
className={`size-full transition-opacity ${ className={`size-full transition-opacity select-none ${
playingBack ? "opacity-0" : "opacity-100" playingBack ? "opacity-0" : "opacity-100"
}`} }`}
style={
isIOS
? {
WebkitUserSelect: "none",
WebkitTouchCallout: "none",
}
: undefined
}
draggable={false}
src={`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`} src={`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`}
loading={isSafari ? "eager" : "lazy"} loading={isSafari ? "eager" : "lazy"}
onLoad={() => { onLoad={() => {

View File

@ -0,0 +1,46 @@
import { MutableRefObject, useEffect } from "react";
import { isIOS } from "react-device-detect";
export default function useContextMenu(
ref: MutableRefObject<HTMLDivElement | null>,
callback: () => void,
) {
useEffect(() => {
if (!ref.current) {
return;
}
const elem = ref.current;
if (isIOS) {
let timeoutId: NodeJS.Timeout;
const touchStart = () => {
timeoutId = setTimeout(() => {
callback();
}, 610);
};
const touchClear = () => {
clearTimeout(timeoutId);
};
elem.addEventListener("touchstart", touchStart);
elem.addEventListener("touchmove", touchClear);
elem.addEventListener("touchend", touchClear);
return () => {
elem.removeEventListener("touchstart", touchStart);
elem.removeEventListener("touchmove", touchClear);
elem.removeEventListener("touchend", touchClear);
};
} else {
const context = (e: MouseEvent) => {
e.preventDefault();
callback();
};
elem.addEventListener("contextmenu", context);
return () => {
elem.removeEventListener("contextmenu", context);
};
}
}, [callback, ref]);
}

View File

@ -19,7 +19,7 @@ export default function CameraMetrics({
// stats // stats
const { data: initialStats } = useSWR<FrigateStats[]>( const { data: initialStats } = useSWR<FrigateStats[]>(
["stats/history", { keys: "cpu_usages,cameras,service" }], ["stats/history", { keys: "cpu_usages,cameras,detection_fps,service" }],
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
}, },
@ -57,6 +57,44 @@ export default function CameraMetrics({
// stats data // stats data
const overallFpsSeries = useMemo(() => {
if (!statsHistory) {
return [];
}
const series: {
[key: string]: { name: string; data: { x: number; y: number }[] };
} = {};
series["overall_dps"] = { name: "overall detections per second", data: [] };
series["overall_skipped_dps"] = {
name: "overall skipped detections per second",
data: [],
};
statsHistory.forEach((stats, statsIdx) => {
if (!stats) {
return;
}
series["overall_dps"].data.push({
x: statsIdx,
y: stats.detection_fps,
});
let skipped = 0;
Object.values(stats.cameras).forEach(
(camStat) => (skipped += camStat.skipped_fps),
);
series["overall_skipped_dps"].data.push({
x: statsIdx,
y: skipped,
});
});
return Object.values(series);
}, [statsHistory]);
const cameraCpuSeries = useMemo(() => { const cameraCpuSeries = useMemo(() => {
if (!statsHistory || statsHistory.length == 0) { if (!statsHistory || statsHistory.length == 0) {
return {}; return {};
@ -147,19 +185,36 @@ export default function CameraMetrics({
}, [statsHistory]); }, [statsHistory]);
return ( return (
<div className="size-full mt-4 flex flex-col overflow-y-auto"> <div className="size-full mt-4 flex flex-col gap-3 overflow-y-auto">
<div className="text-muted-foreground text-sm font-medium">Overview</div>
<div className="grid grid-cols-1 md:grid-cols-3">
{statsHistory.length != 0 ? (
<div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">DPS</div>
<CameraLineGraph
graphId="overall-stats"
unit=" DPS"
dataLabels={["detect", "skipped"]}
updateTimes={updateTimes}
data={overallFpsSeries}
/>
</div>
) : (
<Skeleton className="w-full h-32" />
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{config && {config &&
Object.values(config.cameras).map((camera) => { Object.values(config.cameras).map((camera) => {
if (camera.enabled) { if (camera.enabled) {
return ( return (
<div className="w-full flex flex-col"> <div className="w-full flex flex-col gap-3">
<div className="mb-6 capitalize"> <div className="capitalize text-muted-foreground text-sm font-medium">
{camera.name.replaceAll("_", " ")} {camera.name.replaceAll("_", " ")}
</div> </div>
<div key={camera.name} className="grid sm:grid-cols-2 gap-2"> <div key={camera.name} className="grid sm:grid-cols-2 gap-2">
{Object.keys(cameraCpuSeries).includes(camera.name) ? ( {Object.keys(cameraCpuSeries).includes(camera.name) ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">CPU</div> <div className="mb-5">CPU</div>
<CameraLineGraph <CameraLineGraph
graphId={`${camera.name}-cpu`} graphId={`${camera.name}-cpu`}
@ -175,7 +230,7 @@ export default function CameraMetrics({
<Skeleton className="size-full aspect-video" /> <Skeleton className="size-full aspect-video" />
)} )}
{Object.keys(cameraFpsSeries).includes(camera.name) ? ( {Object.keys(cameraFpsSeries).includes(camera.name) ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">DPS</div> <div className="mb-5">DPS</div>
<CameraLineGraph <CameraLineGraph
graphId={`${camera.name}-dps`} graphId={`${camera.name}-dps`}

View File

@ -193,6 +193,14 @@ export default function GeneralMetrics({
return []; return [];
} }
if (
Object.keys(statsHistory?.at(0)?.gpu_usages ?? {}).length == 1 &&
Object.keys(statsHistory?.at(0)?.gpu_usages ?? {})[0].includes("intel")
) {
// intel gpu stats do not support memory
return undefined;
}
const series: { const series: {
[key: string]: { name: string; data: { x: number; y: string }[] }; [key: string]: { name: string; data: { x: number; y: string }[] };
} = {}; } = {};
@ -285,7 +293,7 @@ export default function GeneralMetrics({
</div> </div>
<div className="w-full mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2"> <div className="w-full mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">Detector Inference Speed</div> <div className="mb-5">Detector Inference Speed</div>
{detInferenceTimeSeries.map((series) => ( {detInferenceTimeSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -303,7 +311,7 @@ export default function GeneralMetrics({
<Skeleton className="w-full aspect-video" /> <Skeleton className="w-full aspect-video" />
)} )}
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">Detector CPU Usage</div> <div className="mb-5">Detector CPU Usage</div>
{detCpuSeries.map((series) => ( {detCpuSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -321,7 +329,7 @@ export default function GeneralMetrics({
<Skeleton className="w-full aspect-video" /> <Skeleton className="w-full aspect-video" />
)} )}
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">Detector Memory Usage</div> <div className="mb-5">Detector Memory Usage</div>
{detMemSeries.map((series) => ( {detMemSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -358,7 +366,7 @@ export default function GeneralMetrics({
</div> </div>
<div className=" mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className=" mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">GPU Usage</div> <div className="mb-5">GPU Usage</div>
{gpuSeries.map((series) => ( {gpuSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -376,7 +384,9 @@ export default function GeneralMetrics({
<Skeleton className="w-full aspect-video" /> <Skeleton className="w-full aspect-video" />
)} )}
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <>
{gpuMemSeries && (
<div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">GPU Memory</div> <div className="mb-5">GPU Memory</div>
{gpuMemSeries.map((series) => ( {gpuMemSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -390,6 +400,8 @@ export default function GeneralMetrics({
/> />
))} ))}
</div> </div>
)}
</>
) : ( ) : (
<Skeleton className="w-full aspect-video" /> <Skeleton className="w-full aspect-video" />
)} )}
@ -402,7 +414,7 @@ export default function GeneralMetrics({
</div> </div>
<div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2"> <div className="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-2">
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">Process CPU Usage</div> <div className="mb-5">Process CPU Usage</div>
{otherProcessCpuSeries.map((series) => ( {otherProcessCpuSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph
@ -420,7 +432,7 @@ export default function GeneralMetrics({
<Skeleton className="w-full aspect-tall" /> <Skeleton className="w-full aspect-tall" />
)} )}
{statsHistory.length != 0 ? ( {statsHistory.length != 0 ? (
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl">
<div className="mb-5">Process Memory Usage</div> <div className="mb-5">Process Memory Usage</div>
{otherProcessMemSeries.map((series) => ( {otherProcessMemSeries.map((series) => (
<ThresholdBarGraph <ThresholdBarGraph

View File

@ -43,9 +43,7 @@ export default function StorageMetrics({
return ( return (
<div className="size-full mt-4 flex flex-col overflow-y-auto"> <div className="size-full mt-4 flex flex-col overflow-y-auto">
<div className="text-muted-foreground text-sm font-medium"> <div className="text-muted-foreground text-sm font-medium">Overview</div>
General Storage
</div>
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2"> <div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="p-2.5 bg-background_alt rounded-2xl flex-col"> <div className="p-2.5 bg-background_alt rounded-2xl flex-col">
<div className="mb-5">Recordings</div> <div className="mb-5">Recordings</div>