fix: use cumulative CPU average for ffmpeg monitoring

Delta sampling over 2s returns 0-1 integer ticks for low-CPU processes
(ffmpeg copy mode ~0.3% CPU), always rounding to 0.0%. Switch to
cumulative average (total_cpu_ticks / clk_tck / elapsed) — same method
ps uses — which gives stable, accurate readings. Also show 2 decimal
places so values like 0.34% are visible.
This commit is contained in:
joaquin
2026-05-17 23:56:05 +02:00
parent ac06691656
commit 3a450214ab
2 changed files with 29 additions and 27 deletions
+27 -25
View File
@@ -105,9 +105,10 @@ class BroadcastGroup:
self._last_data_at: float = time.time() self._last_data_at: float = time.time()
# ffmpeg process tracking # ffmpeg process tracking
self._ffmpeg_pid: int | None = None self._ffmpeg_pid: int | None = None
self._ffmpeg_cpu: float = 0.0 self._ffmpeg_start_time: float | None = None # wall time when ffmpeg started
self._cpu_task: asyncio.Task | None = None self._ffmpeg_cpu: float = 0.0
self._cpu_task: asyncio.Task | None = None
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Public API # Public API
@@ -210,7 +211,7 @@ class BroadcastGroup:
"stream_info": self.stream_info, "stream_info": self.stream_info,
# ffmpeg process info # ffmpeg process info
"ffmpeg_pid": self._ffmpeg_pid, "ffmpeg_pid": self._ffmpeg_pid,
"ffmpeg_cpu_pct": round(self._ffmpeg_cpu, 1), "ffmpeg_cpu_pct": round(self._ffmpeg_cpu, 2),
} }
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -218,32 +219,31 @@ class BroadcastGroup:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
async def _monitor_cpu(self) -> None: async def _monitor_cpu(self) -> None:
"""Sample /proc/{pid}/stat every 2 s and update self._ffmpeg_cpu (%).""" """Update self._ffmpeg_cpu every 3 s using cumulative average since start.
Delta-based sampling fails for low-CPU processes (copy-mode ffmpeg uses
<1% CPU) because the tick delta in 2 s is 0-1 integer ticks, making the
result always 0.0%. Cumulative average ((total_cpu_ticks / clk_tck) /
elapsed_wall) gives a stable, accurate reading — the same method ps uses.
"""
clk_tck = float(os.sysconf(os.sysconf_names.get("SC_CLK_TCK", 2)) or 100) clk_tck = float(os.sysconf(os.sysconf_names.get("SC_CLK_TCK", 2)) or 100)
prev_ticks: int | None = None
prev_wall: float | None = None
while self._running: while self._running:
await asyncio.sleep(2) await asyncio.sleep(3)
pid = self._ffmpeg_pid pid = self._ffmpeg_pid
if pid is None: start_time = self._ffmpeg_start_time
if pid is None or start_time is None:
self._ffmpeg_cpu = 0.0 self._ffmpeg_cpu = 0.0
prev_ticks = prev_wall = None
continue continue
try: try:
with open(f"/proc/{pid}/stat") as f: with open(f"/proc/{pid}/stat") as f:
parts = f.read().split() parts = f.read().split()
ticks = int(parts[13]) + int(parts[14]) # utime + stime cpu_ticks = int(parts[13]) + int(parts[14]) # utime + stime
wall = time.time() elapsed = time.time() - start_time
if prev_ticks is not None and prev_wall is not None: if elapsed > 0:
dt = wall - prev_wall self._ffmpeg_cpu = cpu_ticks / clk_tck / elapsed * 100
if dt > 0:
self._ffmpeg_cpu = (ticks - prev_ticks) / clk_tck / dt * 100
prev_ticks = ticks
prev_wall = wall
except (FileNotFoundError, IndexError, ValueError, OSError): except (FileNotFoundError, IndexError, ValueError, OSError):
self._ffmpeg_cpu = 0.0 self._ffmpeg_cpu = 0.0
prev_ticks = prev_wall = None
async def _probe_info(self) -> None: async def _probe_info(self) -> None:
from .probe import probe_stream from .probe import probe_stream
@@ -328,9 +328,10 @@ class BroadcastGroup:
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
) )
self._status = "ok" self._status = "ok"
self._last_data_at = time.time() self._last_data_at = time.time()
self._ffmpeg_pid = proc.pid self._ffmpeg_pid = proc.pid
self._ffmpeg_start_time = time.time()
logger.info( logger.info(
f"[ch{self.channel_id}] ffmpeg started pid={proc.pid} " f"[ch{self.channel_id}] ffmpeg started pid={proc.pid} "
f"(reconnects so far: {self._reconnect_count})" f"(reconnects so far: {self._reconnect_count})"
@@ -368,8 +369,9 @@ class BroadcastGroup:
await self._dispatch(chunk, now) await self._dispatch(chunk, now)
finally: finally:
self._ffmpeg_pid = None self._ffmpeg_pid = None
self._ffmpeg_cpu = 0.0 self._ffmpeg_start_time = None
self._ffmpeg_cpu = 0.0
stderr_task.cancel() stderr_task.cancel()
try: try:
proc.kill() proc.kill()
+2 -2
View File
@@ -237,7 +237,7 @@ export default function Dashboard() {
<StatCard <StatCard
icon={<Cpu size={20} className={totalCpu > 80 ? 'text-red-400' : totalCpu > 40 ? 'text-yellow-400' : 'text-cyan-400'} />} icon={<Cpu size={20} className={totalCpu > 80 ? 'text-red-400' : totalCpu > 40 ? 'text-yellow-400' : 'text-cyan-400'} />}
label="ffmpeg CPU total" label="ffmpeg CPU total"
value={`${totalCpu.toFixed(1)}%`} value={`${totalCpu.toFixed(2)}%`}
/> />
</div> </div>
@@ -330,7 +330,7 @@ export default function Dashboard() {
s.ffmpeg_cpu_pct > 40 ? 'text-yellow-400' : s.ffmpeg_cpu_pct > 40 ? 'text-yellow-400' :
'text-gray-400' 'text-gray-400'
}`}> }`}>
{s.ffmpeg_cpu_pct.toFixed(1)}% {s.ffmpeg_cpu_pct.toFixed(2)}%
</span> </span>
)} )}
</div> </div>