controller: FrigateBridge bbox throttling — rate-limit + delta filter

Frigate publishes 5-10 detection events/sec при motion → каждое emit ZMQ
overlay update к pipeline (mutex acquire, atlas rebuild). Это блокирует
render thread и вызывает TV stutter (см. cuda-grid-filter-stutter memory
2026-05-21 диагностика).

Throttle config:
  bbox_min_interval_sec: 0.25  # max 4 updates/sec на event_id
  bbox_delta_threshold: 0.02   # skip если все coords changed < 2% camera dim

State в self._event_bbox_last: event_id → (timestamp, nx, ny, nw, nh).
Cleanup в _remove_event_overlay (event end).

С default 0.25s + 0.02 threshold:
  5-10 ev/sec → ~2-4 ev/sec applied (rate-limit), плюс stationary objects
  не апдейтятся вообще (delta filter). Render-thread load на bbox flow
  снижается 60-80%.

Эффект — можно вернуть bbox_overlay=true в controller.yaml без risk
TV stutter. Diagnostic-disable из 2026-05-21 теперь не нужен.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gx
2026-05-22 09:38:00 +01:00
parent e773066867
commit 976aed52e9
@@ -17,6 +17,7 @@ from __future__ import annotations
import asyncio
import json
import time
from typing import TYPE_CHECKING
import structlog
@@ -82,6 +83,15 @@ class FrigateBridgeCfg(BaseModel):
auto_hysteresis_sec: float = Field(default=3.0, ge=0.0, le=60.0,
description="Debounce — apply layout change только если state стабилен N sec. "
"0 = немедленно. Защищает от short motion blips дёргающих layout")
bbox_min_interval_sec: float = Field(default=0.25, ge=0.0, le=5.0,
description="Min interval между bbox update ZMQ commands per event_id. "
"Frigate publishes 5-10 events/sec при motion → throttle reduces "
"render-thread pause (см. cuda-grid-filter-stutter memory). "
"0 = no throttle.")
bbox_delta_threshold: float = Field(default=0.02, ge=0.0, le=1.0,
description="Min relative bbox shift (fraction of camera dim) для new update. "
"Skip update если все coords changed < threshold от last. "
"0.02 = 2% → 38px на 1920 cam. 0 = no delta filter.")
class FrigateBridge:
@@ -98,6 +108,8 @@ class FrigateBridge:
self._by_camera = {m.frigate_camera: m for m in cfg.mappings if m.enabled}
self._motion_overlays: dict[str, str] = {} # legacy — unused if border theme active
self._event_overlays: dict[str, tuple[str, str]] = {}
# Throttling state per event_id: (last_apply_ts, last_nx, last_ny, last_nw, last_nh)
self._event_bbox_last: dict[str, tuple[float, float, float, float, float]] = {}
self._borders_initialized: dict[str, bool] = {} # target_instance → bool
self._cell_states: dict[str, set[str]] = {} # "<inst>:<cell>" → set of active cameras с motion
self._focus_dims: dict[str, set[int]] = {} # instance → set of cells which сейчас затемнены
@@ -359,6 +371,22 @@ class FrigateBridge:
nw = max(0.01, min(1.0 - nx, nw))
nh = max(0.01, min(1.0 - ny, nh))
# Throttle: skip update if too frequent OR bbox barely moved.
# First emission всегда applies (regardless of throttle).
now = time.monotonic()
prev = self._event_bbox_last.get(event_id)
if prev is not None:
prev_ts, px, py, pw, ph = prev
if self.cfg.bbox_min_interval_sec > 0 and \
(now - prev_ts) < self.cfg.bbox_min_interval_sec:
return # rate-limit
if self.cfg.bbox_delta_threshold > 0:
dt = self.cfg.bbox_delta_threshold
if abs(nx - px) < dt and abs(ny - py) < dt and \
abs(nw - pw) < dt and abs(nh - ph) < dt:
return # delta filter — barely changed
self._event_bbox_last[event_id] = (now, nx, ny, nw, nh)
# Short id'ы — кутаем event_id чтобы влезть в 32 chars
eid_short = event_id.replace("-", "")[:8]
rect_id = f"e{eid_short}r"
@@ -390,6 +418,7 @@ class FrigateBridge:
await self.dispatcher.handle(mapping.target_instance, "overlay.add", text.model_dump_json())
async def _remove_event_overlay(self, mapping, event_id: str) -> None:
self._event_bbox_last.pop(event_id, None) # cleanup throttle state
stored = self._event_overlays.pop(event_id, None)
if not stored:
return