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:
Josh Hawkins
2026-03-29 12:58:47 -05:00
committed by GitHub
parent f002513d36
commit f44f485f48
10 changed files with 330 additions and 93 deletions

View File

@@ -52,18 +52,66 @@ class StatsEmitter(threading.Thread):
def get_stats_history(
self, keys: Optional[list[str]] = None
) -> 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:
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]] = []
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)
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)
return selected_stats

View File

@@ -498,4 +498,30 @@ def stats_snapshot(
"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