Compare commits

...

2 Commits

Author SHA1 Message Date
Nicolas Mowen
80a13e43e9
Add support for NPU statistics in metrics page (#17806)
* Add npu usages as a statistic

* Support showing NPU stats in dashboard

* Add sys volume mount for npu usages

* Fix type

* Simplify check

* Cleanup

* Cleanup
2025-04-19 08:20:22 -06:00
GuoQing Liu
c8e22a3653
Fix some page i18n wrong (#17682)
* fix: some pages can't translation object label.

* revert: revert wrong label fix

* feat: add openai base_url setting

* fix: fix classification modelSize i18n error

* revert: revert openai base_url setting

* fix: fix enrichments pages i18n keys wrong

* fix: fix mobile bottom bar reindexing embeddings i18n wrong

* feat: add more system stats i18n keys

* fix: fix review filter objects i18n

* chore: remove frigate+ label i18n
2025-04-19 09:02:15 -05:00
19 changed files with 301 additions and 123 deletions

View File

@ -165,6 +165,8 @@ devices:
- /dev/dma_heap
- /dev/rga
- /dev/mpp_service
volumes:
- /sys/:/sys/:ro
```
or add these options to your `docker run` command:
@ -175,7 +177,8 @@ or add these options to your `docker run` command:
--device /dev/dri \
--device /dev/dma_heap \
--device /dev/rga \
--device /dev/mpp_service
--device /dev/mpp_service \
--volume /sys/:/sys/:ro
```
#### Configuration

View File

@ -24,6 +24,7 @@ from frigate.util.services import (
get_intel_gpu_stats,
get_jetson_stats,
get_nvidia_gpu_stats,
get_rockchip_npu_stats,
is_vaapi_amd_driver,
)
from frigate.version import VERSION
@ -109,6 +110,7 @@ def get_processing_stats(
stats_tasks = [
asyncio.create_task(set_gpu_stats(config, stats, hwaccel_errors)),
asyncio.create_task(set_cpu_stats(stats)),
asyncio.create_task(set_npu_usages(config, stats)),
]
if config.telemetry.stats.network_bandwidth:
@ -238,6 +240,19 @@ async def set_gpu_stats(
all_stats["gpu_usages"] = stats
async def set_npu_usages(config: FrigateConfig, all_stats: dict[str, Any]) -> None:
stats: dict[str, dict] = {}
for detector in config.detectors.values():
if detector.type == "rknn":
# Rockchip NPU usage
rk_usage = get_rockchip_npu_stats()
stats["rockchip"] = rk_usage
if stats:
all_stats["npu_usages"] = stats
def stats_snapshot(
config: FrigateConfig, stats_tracking: StatsTrackingTypes, hwaccel_errors: list[str]
) -> dict[str, Any]:

View File

@ -382,6 +382,23 @@ def get_intel_gpu_stats(sriov: bool) -> dict[str, str]:
return results
def get_rockchip_npu_stats() -> dict[str, str]:
"""Get stats using rk."""
try:
with open("/sys/kernel/debug/rknpu/load", "r") as f:
npu_output = f.read()
core_loads = re.findall(r"Core\d+:\s*(\d+)%", npu_output)
except FileNotFoundError:
core_loads = None
if not core_loads:
return None
percentages = [int(load) for load in core_loads]
mean = round(sum(percentages) / len(percentages), 2)
return {"npu": mean, "mem": "-"}
def try_get_info(f, h, default="N/A"):
try:
if h:

View File

@ -54,7 +54,12 @@
},
"gone": "{{label}} left",
"heard": "{{label}} heard",
"external": "{{label}} detected"
"external": "{{label}} detected",
"header": {
"zones": "Zones",
"ratio": "Ratio",
"area": "Area"
}
},
"annotationSettings": {
"title": "Annotation Settings",

View File

@ -72,7 +72,9 @@
"toast": {
"success": "Copied GPU info to clipboard"
}
}
},
"npuUsage": "NPU Usage",
"npuMemory": "NPU Memory"
},
"otherProcesses": {
"title": "Other Processes",
@ -142,7 +144,10 @@
"ffmpegHighCpuUsage": "{{camera}} has high FFMPEG CPU usage ({{ffmpegAvg}}%)",
"detectHighCpuUsage": "{{camera}} has high detect CPU usage ({{detectAvg}}%)",
"healthy": "System is healthy",
"reindexingEmbeddings": "Reindexing embeddings ({{processed}}% complete)"
"reindexingEmbeddings": "Reindexing embeddings ({{processed}}% complete)",
"cameraIsOffline": "{{camera}} is offline",
"detectIsSlow": "{{detect}} is slow ({{speed}} ms)",
"detectIsVerySlow": "{{detect}} is very slow ({{speed}} ms)"
},
"enrichments": {
"title": "Enrichments",
@ -150,6 +155,7 @@
"embeddings": {
"image_embedding_speed": "Image Embedding Speed",
"face_embedding_speed": "Face Embedding Speed",
"face_recognition_speed": "Face Recognition Speed",
"plate_recognition_speed": "Plate Recognition Speed",
"text_embedding_speed": "Text Embedding Speed"
}

View File

@ -498,7 +498,7 @@ export function GeneralFilterContent({
{allLabels.map((item) => (
<FilterSwitch
key={item}
label={item.replaceAll("_", " ")}
label={t(item, { ns: "objects" })}
isChecked={filter.labels?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
if (isChecked) {

View File

@ -263,7 +263,9 @@ function GeneralFilterButton({
}
if (selectedLabels.length == 1) {
return t(selectedLabels[0], { ns: "objects" });
return t(selectedLabels[0], {
ns: "objects",
});
}
return t("labels.count", {

View File

@ -419,7 +419,9 @@ export default function InputWithTags({
? t("button.yes", { ns: "common" })
: t("button.no", { ns: "common" });
} else if (filterType === "labels") {
return t(filterValues as string, { ns: "objects" });
return t(filterValues as string, {
ns: "objects",
});
} else if (filterType === "search_type") {
return t("filter.searchType." + (filterValues as string));
} else {
@ -817,7 +819,11 @@ export default function InputWithTags({
className="inline-flex items-center whitespace-nowrap rounded-full bg-green-100 px-2 py-0.5 text-sm capitalize text-green-800"
>
{t("filter.label." + filterType)}:{" "}
{value.replaceAll("_", " ")}
{filterType === "labels"
? t(value, {
ns: "objects",
})
: value.replaceAll("_", " ")}
<button
onClick={() =>
removeFilter(filterType as FilterType, value)

View File

@ -16,6 +16,7 @@ import { Link } from "react-router-dom";
import { cn } from "@/lib/utils";
import { isIOS, isMobile } from "react-device-detect";
import { isPWA } from "@/utils/isPWA";
import { useTranslation } from "react-i18next";
function Bottombar() {
const navItems = useNavigation("secondary");
@ -43,6 +44,7 @@ type StatusAlertNavProps = {
className?: string;
};
function StatusAlertNav({ className }: StatusAlertNavProps) {
const { t } = useTranslation(["views/system"]);
const { data: initialStats } = useSWR<FrigateStats>("stats", {
revalidateOnFocus: false,
});
@ -82,14 +84,19 @@ function StatusAlertNav({ className }: StatusAlertNavProps) {
clearMessages("embeddings-reindex");
addMessage(
"embeddings-reindex",
`Reindexing embeddings (${Math.floor((reindexState.processed_objects / reindexState.total_objects) * 100)}% complete)`,
t("stats.reindexingEmbeddings", {
processed: Math.floor(
(reindexState.processed_objects / reindexState.total_objects) *
100,
),
}),
);
}
if (reindexState.status === "completed") {
clearMessages("embeddings-reindex");
}
}
}, [reindexState, addMessage, clearMessages]);
}, [reindexState, addMessage, clearMessages, t]);
if (!messages || Object.keys(messages).length === 0) {
return;

View File

@ -596,7 +596,9 @@ export default function ObjectLifecycle({
<div className="text-md mr-2 w-1/3">
<div className="flex flex-col items-end justify-start">
<p className="mb-1.5 text-sm text-primary-variant">
Zones
{t(
"objectLifecycle.lifecycleItemDesc.header.zones",
)}
</p>
{item.class_type === "entered_zone"
? item.data.zones.map((zone, index) => (
@ -627,7 +629,9 @@ export default function ObjectLifecycle({
<div className="text-md mr-2 w-1/3">
<div className="flex flex-col items-end justify-start">
<p className="mb-1.5 text-sm text-primary-variant">
Ratio
{t(
"objectLifecycle.lifecycleItemDesc.header.ratio",
)}
</p>
{Array.isArray(item.data.box) &&
item.data.box.length >= 4
@ -641,7 +645,7 @@ export default function ObjectLifecycle({
<div className="text-md mr-2 w-1/3">
<div className="flex flex-col items-end justify-start">
<p className="mb-1.5 text-sm text-primary-variant">
Area
{t("objectLifecycle.lifecycleItemDesc.header.area")}
</p>
{Array.isArray(item.data.box) &&
item.data.box.length >= 4 ? (

View File

@ -320,7 +320,11 @@ export default function ReviewDetailDialog({
ns="views/explore"
values={{
objects: missingObjects
.map((x) => t(x, { ns: "objects" }))
.map((x) =>
t(x, {
ns: "objects",
}),
)
.join(", "),
}}
>

View File

@ -709,7 +709,9 @@ function ObjectDetailsTab({
<div className="text-sm text-primary/40">{t("details.label")}</div>
<div className="flex flex-row items-center gap-2 text-sm capitalize">
{getIconForLabel(search.label, "size-4 text-primary")}
{t(search.label, { ns: "objects" })}
{t(search.label, {
ns: "objects",
})}
{search.sub_label && ` (${search.sub_label})`}
{isAdmin && (
<Tooltip>
@ -980,7 +982,7 @@ function ObjectDetailsTab({
description={
search.label
? t("details.editSubLabel.desc", {
label: t(search.label, { ns: "objects" }),
label: search.label,
})
: t("details.editSubLabel.descNoLabel")
}
@ -995,7 +997,7 @@ function ObjectDetailsTab({
description={
search.label
? t("details.editLPR.desc", {
label: t(search.label, { ns: "objects" }),
label: search.label,
})
: t("details.editLPR.descNoLabel")
}

View File

@ -36,13 +36,19 @@ export default function useStats(stats: FrigateStats | undefined) {
Object.entries(memoizedStats["detectors"]).forEach(([key, det]) => {
if (det["inference_speed"] > InferenceThreshold.error) {
problems.push({
text: `${capitalizeFirstLetter(key)} is very slow (${det["inference_speed"]} ms)`,
text: t("stats.detectIsVerySlow", {
detect: capitalizeFirstLetter(key),
speed: det["inference_speed"],
}),
color: "text-danger",
relevantLink: "/system#general",
});
} else if (det["inference_speed"] > InferenceThreshold.warning) {
problems.push({
text: `${capitalizeFirstLetter(key)} is slow (${det["inference_speed"]} ms)`,
text: t("stats.detectIsSlow", {
detect: capitalizeFirstLetter(key),
speed: det["inference_speed"],
}),
color: "text-orange-400",
relevantLink: "/system#general",
});
@ -57,7 +63,9 @@ export default function useStats(stats: FrigateStats | undefined) {
if (config.cameras[name].enabled && cam["camera_fps"] == 0) {
problems.push({
text: `${capitalizeFirstLetter(name.replaceAll("_", " "))} is offline`,
text: t("stats.cameraIsOffline", {
camera: capitalizeFirstLetter(name.replaceAll("_", " ")),
}),
color: "text-danger",
relevantLink: "logs",
});

View File

@ -4,6 +4,7 @@ export interface FrigateStats {
detectors: { [detectorKey: string]: DetectorStats };
embeddings?: EmbeddingsStats;
gpu_usages?: { [gpuKey: string]: GpuStats };
npu_usages?: { [npuKey: string]: NpuStats };
processes: { [processKey: string]: ExtraProcessStats };
service: ServiceStats;
detection_fps: number;
@ -54,6 +55,11 @@ export type GpuStats = {
pstate?: string;
};
export type NpuStats = {
npu: number;
mem: string;
};
export type GpuInfo = "vainfo" | "nvinfo";
export type ServiceStats = {

View File

@ -5,11 +5,16 @@ export function getLifecycleItemDescription(
lifecycleItem: ObjectLifecycleSequence,
) {
// can't use useTranslation here
const label = (
const label = t(
(
(Array.isArray(lifecycleItem.data.sub_label)
? lifecycleItem.data.sub_label[0]
: lifecycleItem.data.sub_label) || lifecycleItem.data.label
).replaceAll("_", " ");
)
.replace(" ", "_")
.toLowerCase(),
{ ns: "objects" },
);
switch (lifecycleItem.class_type) {
case "visible":

View File

@ -336,7 +336,9 @@ export default function ClassificationSettingsView({
}
>
<SelectTrigger className="w-20">
{classificationSettings.search.model_size}
{t(
`classification.semanticSearch.modelSize.${classificationSettings.search.model_size}.title`,
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
@ -466,7 +468,9 @@ export default function ClassificationSettingsView({
}
>
<SelectTrigger className="w-20">
{classificationSettings.face.model_size}
{t(
`classification.faceRecognition.modelSize.${classificationSettings.face.model_size}.title`,
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>

View File

@ -366,7 +366,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
{getIconForLabel(obj.label, "size-5 text-white")}
</div>
<div className="ml-3 text-lg">
{capitalizeFirstLetter(obj.label.replaceAll("_", " "))}
{t(obj.label, { ns: "objects" })}
</div>
</div>
<div className="flex w-8/12 flex-row items-center justify-end">

View File

@ -77,7 +77,10 @@ export default function EnrichmentMetrics({
const key = rawKey.replaceAll("_", " ");
if (!(key in series)) {
series[key] = { name: t("features.embeddings." + rawKey), data: [] };
series[key] = {
name: t("enrichments.embeddings." + rawKey),
data: [],
};
}
series[key].data.push({ x: statsIdx + 1, y: stat });
@ -90,7 +93,7 @@ export default function EnrichmentMetrics({
<>
<div className="scrollbar-container mt-4 flex size-full flex-col overflow-y-auto">
<div className="text-sm font-medium text-muted-foreground">
{t("features.title")}
{t("enrichments.title")}
</div>
<div
className={cn(

View File

@ -34,7 +34,7 @@ export default function GeneralMetrics({
const { data: initialStats } = useSWR<FrigateStats[]>(
[
"stats/history",
{ keys: "cpu_usages,detectors,gpu_usages,processes,service" },
{ keys: "cpu_usages,detectors,gpu_usages,npu_usages,processes,service" },
],
{
revalidateOnFocus: false,
@ -369,8 +369,57 @@ export default function GeneralMetrics({
return Object.keys(series).length > 0 ? Object.values(series) : undefined;
}, [statsHistory]);
// npu stats
const npuSeries = useMemo(() => {
if (!statsHistory) {
return [];
}
const series: {
[key: string]: { name: string; data: { x: number; y: number }[] };
} = {};
let hasValidNpu = false;
statsHistory.forEach((stats, statsIdx) => {
if (!stats) {
return;
}
Object.entries(stats.npu_usages || []).forEach(([key, stats]) => {
if (!(key in series)) {
series[key] = { name: key, data: [] };
}
if (stats.npu) {
hasValidNpu = true;
series[key].data.push({ x: statsIdx + 1, y: stats.npu });
}
});
});
if (!hasValidNpu) {
return [];
}
return Object.keys(series).length > 0 ? Object.values(series) : [];
}, [statsHistory]);
// other processes stats
const hardwareType = useMemo(() => {
const hasGpu = gpuSeries.length > 0;
const hasNpu = npuSeries.length > 0;
if (hasGpu && !hasNpu) {
return "GPUs";
} else if (!hasGpu && hasNpu) {
return "NPUs";
} else {
return "GPUs / NPUs";
}
}, [gpuSeries, npuSeries]);
const otherProcessCpuSeries = useMemo(() => {
if (!statsHistory) {
return [];
@ -533,11 +582,13 @@ export default function GeneralMetrics({
)}
</div>
{(statsHistory.length == 0 || statsHistory[0].gpu_usages) && (
{(statsHistory.length == 0 ||
statsHistory[0].gpu_usages ||
statsHistory[0].npu_usages) && (
<>
<div className="mt-4 flex items-center justify-between">
<div className="text-sm font-medium text-muted-foreground">
GPUs
{hardwareType}
</div>
{canGetGpuInfo && (
<Button
@ -556,6 +607,8 @@ export default function GeneralMetrics({
gpuEncSeries?.length && "md:grid-cols-4",
)}
>
{statsHistory[0]?.gpu_usages && (
<>
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
@ -648,6 +701,34 @@ export default function GeneralMetrics({
) : (
<Skeleton className="aspect-video w-full" />
)}
</>
)}
{statsHistory[0]?.npu_usages && (
<div
className={cn("mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2")}
>
{statsHistory.length != 0 ? (
<div className="rounded-lg bg-background_alt p-2.5 md:rounded-2xl">
<div className="mb-5">
{t("general.hardwareInfo.npuUsage")}
</div>
{npuSeries.map((series) => (
<ThresholdBarGraph
key={series.name}
graphId={`${series.name}-npu`}
name={series.name}
unit="%"
threshold={GPUUsageThreshold}
updateTimes={updateTimes}
data={[series]}
/>
))}
</div>
) : (
<Skeleton className="aspect-video w-full" />
)}
</div>
)}
</div>
</>
)}