perf(downloads): de-prioritize worker transcodes + idle-back-off polls to stop LAN slowdown

The download/edit child (yt-dlp + ffmpeg) shares one LAN box with the API,
Postgres, and Plex playback. A CPU re-encode pegged every core for ~30s and
starved web requests + playback until it finished. Full-scope S4 mitigations:

- Child self-de-prioritizes at startup (inherited by every ffmpeg/yt-dlp
  descendant): CPU nice increment + best-effort idle ionice class + an
  OMP_NUM_THREADS budget, all config-gated and independently disable-able.
- Cap the browser-safe re-encode's encoder threads (`-threads`) so a transcode
  reserves a core for foreground work instead of claiming all of them.
- Throttle the retention GC's whole-tree orphan sweep (yield IO after each
  batch of files) so its rglob can't monopolize disk IO in the API process.
- Idle-back-off the Download Center (2s→15s when nothing queued/running) and
  UsageBar (2s→15s when running_jobs is 0) polls; both snap back to 2s on the
  next fetch after an enqueue (invalidateDownloads refetches immediately).

Tests: encode_thread_cap (pin/reserve-core/edge), -threads flag presence,
_deprioritize (nice/ionice/OMP branches), GC sweep throttle batching.
This commit is contained in:
2026-08-28 02:37:56 +02:00
parent b7a6451dba
commit 9ae6c47d6c
9 changed files with 235 additions and 6 deletions
+37
View File
@@ -5,6 +5,8 @@ import os
import time
from pathlib import Path
from app.config import settings
from app.downloads import gc as gcmod
from app.downloads.gc import _sweep_orphan_files
@@ -69,3 +71,38 @@ def test_sweep_removes_orphans_but_keeps_owned_system_and_fresh(tmp_path: Path):
assert not (root / "OldChan").exists()
assert sysfile.exists() # system tree untouched
assert fresh.exists() # too new to sweep
def test_sweep_throttles_after_each_batch(tmp_path: Path, monkeypatch):
"""The whole-tree walk yields the IO channel after every `batch` files so it can't monopolize
disk IO in the API process. batch=2 over 5 touched files → 2 pauses (at file 2 and 4)."""
root = tmp_path
d = root / "Chan"
d.mkdir()
for i in range(5):
(d / f"orphan_{i}.dat").write_bytes(b"x")
for f in d.iterdir():
_aged(f, 5) # old enough that every file is "touched" by the walk
monkeypatch.setattr(settings, "download_gc_sweep_pause_ms", 10)
monkeypatch.setattr(settings, "download_gc_sweep_batch", 2)
pauses = []
monkeypatch.setattr(gcmod.time, "sleep", lambda s: pauses.append(s))
_sweep_orphan_files(_StubDB([]), str(root))
assert pauses == [0.01, 0.01] # 5 files, batch 2 → yield at #2 and #4
def test_sweep_no_throttle_when_pause_disabled(tmp_path: Path, monkeypatch):
(tmp_path / "Chan").mkdir()
f = tmp_path / "Chan" / "orphan.dat"
f.write_bytes(b"x")
_aged(f, 5)
monkeypatch.setattr(settings, "download_gc_sweep_pause_ms", 0)
pauses = []
monkeypatch.setattr(gcmod.time, "sleep", lambda s: pauses.append(s))
_sweep_orphan_files(_StubDB([]), str(tmp_path))
assert pauses == []