feat(agent): Status shows smoothed jobs/min + downloads/min (replace jumpy gauges)
The buffer / on-GPU / downloader counts flip many times a second, so a 3s
status poll only ever samples noise — the tiles looked frozen (same value
twice) or random (wildly different), reading as "the Status section doesn't
update" when the backend was in fact live (operator-flagged 2026-07-01).
Replace the three instantaneous gauge tiles with two derived RATE tiles:
- jobs / min — GPU throughput, from the monotonic `processed` counter
- downloads / min — fetch throughput, from a new monotonic `downloaded`
counter (bumped when a job is decoded into the buffer)
Together they also show pipeline balance (dl/min > j/min ⇒ GPU-bound; the
reverse ⇒ GPU starved). Both are EWMA-smoothed over the poll deltas, clamped
at 0 (agent restart resets the counters), and skip a backgrounded-tab gap.
The still-useful instantaneous state is demoted, not lost: buffer stays as
the occupancy bar; downloaders/consumers/on-GPU move to the sub-line. `waited
out` (transient) gets promoted to a tile.
backend: worker.status() gains `downloaded`; `_bump(downloaded=)`.
frontend: retiled Status + rate math in applyStatus; VERSION → .7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+31
-9
@@ -17,7 +17,7 @@ from .worker import Worker
|
|||||||
# Bump on every agent change. The page embeds this and /status reports it; the UI
|
# Bump on every agent change. The page embeds this and /status reports it; the UI
|
||||||
# warns to reload when they differ — so a stale browser-cached page can't be
|
# warns to reload when they differ — so a stale browser-cached page can't be
|
||||||
# mistaken for "the new image didn't deploy". (Belt-and-braces with no-store.)
|
# mistaken for "the new image didn't deploy". (Belt-and-braces with no-store.)
|
||||||
VERSION = "2026-07-01.6 · stream videos via ffmpeg-from-URL (no full download)"
|
VERSION = "2026-07-01.7 · status: smoothed jobs/min + downloads/min rates"
|
||||||
|
|
||||||
logbuf.install()
|
logbuf.install()
|
||||||
cfg = Config.from_env()
|
cfg = Config.from_env()
|
||||||
@@ -218,11 +218,11 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|||||||
<div class=card-h>Status</div>
|
<div class=card-h>Status</div>
|
||||||
<div class=tiles>
|
<div class=tiles>
|
||||||
<div class=tile><div class=n id=state>—</div><div class=l>state</div></div>
|
<div class=tile><div class=n id=state>—</div><div class=l>state</div></div>
|
||||||
<div class=tile><div class=n id=dln>0</div><div class=l>downloaders</div></div>
|
<div class=tile><div class=n id=jpm>—</div><div class=l>jobs / min</div></div>
|
||||||
<div class=tile><div class=n id=bufn>—</div><div class=l>buffer</div></div>
|
<div class=tile><div class=n id=dpm>—</div><div class=l>downloads / min</div></div>
|
||||||
<div class=tile><div class=n id=active>0</div><div class=l>on GPU</div></div>
|
|
||||||
<div class=tile><div class="n ok" id=done>0</div><div class=l>processed</div></div>
|
<div class=tile><div class="n ok" id=done>0</div><div class=l>processed</div></div>
|
||||||
<div class=tile><div class=n id=err>0</div><div class=l>errors</div></div>
|
<div class=tile><div class=n id=err>0</div><div class=l>errors</div></div>
|
||||||
|
<div class=tile><div class=n id=waited>0</div><div class=l>waited out</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class=meters>
|
<div class=meters>
|
||||||
<div class=meter><div class=meter-h><span>GPU util</span><b id=utillbl>—</b></div>
|
<div class=meter><div class=meter-h><span>GPU util</span><b id=utillbl>—</b></div>
|
||||||
@@ -232,7 +232,7 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|||||||
<div class=meter><div class=meter-h><span>buffer occupancy</span><b id=buflbl>—</b></div>
|
<div class=meter><div class=meter-h><span>buffer occupancy</span><b id=buflbl>—</b></div>
|
||||||
<div class=bar><i id=bufbar></i></div></div>
|
<div class=bar><i id=bufbar></i></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class=queue id=pipe>consumers — · waited out 0</div>
|
<div class=queue id=pipe>downloaders — · consumers — · on GPU 0</div>
|
||||||
<div class=queue id=queue>queue —</div>
|
<div class=queue id=queue>queue —</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -246,6 +246,11 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|||||||
<script>
|
<script>
|
||||||
const PAGE_BUILD="__BUILD__"
|
const PAGE_BUILD="__BUILD__"
|
||||||
let CAP=8
|
let CAP=8
|
||||||
|
// Smoothed derived rates (jobs/min, downloads/min) + the deltas they're built
|
||||||
|
// from. The raw buffer/on-GPU/downloader counts change many times a second, so
|
||||||
|
// a poll only ever samples noise; these EWMA the two monotonic counters into a
|
||||||
|
// stable, readable throughput instead. Reset to null on load → re-converge.
|
||||||
|
let jpmv=null, dpmv=null, lastP=null, lastD=null, lastRateT=null
|
||||||
// Optimistic transitional state on click, then apply the POST's own status
|
// Optimistic transitional state on click, then apply the POST's own status
|
||||||
// response (it returns worker.status()) for instant feedback — don't wait on the
|
// response (it returns worker.status()) for instant feedback — don't wait on the
|
||||||
// separate /status poll, which can lag behind the curator queue call.
|
// separate /status poll, which can lag behind the curator queue call.
|
||||||
@@ -288,12 +293,29 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
|||||||
const draining=!running && s.active>0
|
const draining=!running && s.active>0
|
||||||
state.textContent=draining?'stopping':s.state
|
state.textContent=draining?'stopping':s.state
|
||||||
state.className='n'+(draining?' busy':'')
|
state.className='n'+(draining?' busy':'')
|
||||||
dln.textContent=(s.downloaders!=null?s.downloaders:'—')
|
// Derived rates over the poll deltas — j/min ≈ GPU throughput, dl/min ≈ fetch
|
||||||
bufn.textContent=(s.buffer!=null?(s.buffer+'/'+s.buffer_max):'—')
|
// throughput. EWMA-smoothed so a lull between jobs doesn't blank them; clamped
|
||||||
active.textContent=s.active; active.className='n'+(s.active>0?' busy':'')
|
// at 0 so an agent restart (counters reset to 0 → negative delta) can't spike
|
||||||
|
// them; the dt<30s guard drops a backgrounded-tab gap that would read as huge.
|
||||||
|
const now=performance.now()
|
||||||
|
if(lastRateT!=null && s.processed!=null){
|
||||||
|
const dt=(now-lastRateT)/1000
|
||||||
|
if(dt>0.5 && dt<30){
|
||||||
|
const jp=Math.max(0,60*(s.processed-lastP)/dt)
|
||||||
|
const dp=Math.max(0,60*((s.downloaded||0)-lastD)/dt)
|
||||||
|
jpmv=(jpmv==null)?jp:0.4*jp+0.6*jpmv
|
||||||
|
dpmv=(dpmv==null)?dp:0.4*dp+0.6*dpmv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastP=s.processed; lastD=(s.downloaded||0); lastRateT=now
|
||||||
|
jpm.textContent=(jpmv==null)?'—':Math.round(jpmv)
|
||||||
|
dpm.textContent=(dpmv==null)?'—':Math.round(dpmv)
|
||||||
done.textContent=s.processed
|
done.textContent=s.processed
|
||||||
err.textContent=s.errors; err.className='n'+(s.errors>0?' warn':'')
|
err.textContent=s.errors; err.className='n'+(s.errors>0?' warn':'')
|
||||||
pipe.textContent='consumers '+(s.consumers!=null?s.consumers:'—')+' · waited out '+(s.transient||0)
|
waited.textContent=s.transient||0
|
||||||
|
// Instantaneous pool state → demoted to the sub-line, where its jumpiness reads
|
||||||
|
// as live churn rather than a "broken" headline metric.
|
||||||
|
pipe.textContent='downloaders '+(s.downloaders!=null?s.downloaders:'—')+' · consumers '+(s.consumers!=null?s.consumers:'—')+' · on GPU '+(s.active||0)
|
||||||
// Buffer occupancy bar (also driven here so it tracks the /status cadence).
|
// Buffer occupancy bar (also driven here so it tracks the /status cadence).
|
||||||
if(s.buffer!=null && s.buffer_max){ const p=Math.round(100*s.buffer/s.buffer_max)
|
if(s.buffer!=null && s.buffer_max){ const p=Math.round(100*s.buffer/s.buffer_max)
|
||||||
buflbl.textContent=s.buffer+' / '+s.buffer_max; bufbar.style.width=p+'%' }
|
buflbl.textContent=s.buffer+' / '+s.buffer_max; bufbar.style.width=p+'%' }
|
||||||
|
|||||||
@@ -162,6 +162,10 @@ class Worker:
|
|||||||
self._held: set[int] = set()
|
self._held: set[int] = set()
|
||||||
self._held_lock = threading.Lock()
|
self._held_lock = threading.Lock()
|
||||||
self.processed = 0
|
self.processed = 0
|
||||||
|
self.downloaded = 0 # jobs fetched+decoded into the buffer (monotonic)
|
||||||
|
# — the UI derives a smoothed downloads/min from it,
|
||||||
|
# since the instantaneous buffer/active gauges move
|
||||||
|
# faster than any sane poll can show.
|
||||||
self.errors = 0
|
self.errors = 0
|
||||||
self.transient = 0 # jobs handed back due to a server outage (NOT
|
self.transient = 0 # jobs handed back due to a server outage (NOT
|
||||||
# failed) — the "waiting out curator" counter
|
# failed) — the "waiting out curator" counter
|
||||||
@@ -425,13 +429,15 @@ class Worker:
|
|||||||
"buffer_max": BUFFER_MAX,
|
"buffer_max": BUFFER_MAX,
|
||||||
"active": self._active,
|
"active": self._active,
|
||||||
"processed": self.processed,
|
"processed": self.processed,
|
||||||
|
"downloaded": self.downloaded,
|
||||||
"errors": self.errors,
|
"errors": self.errors,
|
||||||
"transient": self.transient,
|
"transient": self.transient,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _bump(self, *, processed=0, errors=0, active=0, transient=0):
|
def _bump(self, *, processed=0, downloaded=0, errors=0, active=0, transient=0):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.processed += processed
|
self.processed += processed
|
||||||
|
self.downloaded += downloaded
|
||||||
self.errors += errors
|
self.errors += errors
|
||||||
self.transient += transient
|
self.transient += transient
|
||||||
# Clamp at 0: a Stop resets _active to 0, so a consumer that was
|
# Clamp at 0: a Stop resets _active to 0, so a consumer that was
|
||||||
@@ -497,6 +503,7 @@ class Worker:
|
|||||||
continue
|
continue
|
||||||
# Blocks on a full buffer (backpressure) but wakes promptly on stop.
|
# Blocks on a full buffer (backpressure) but wakes promptly on stop.
|
||||||
if self._put((job, frames), stop_evt):
|
if self._put((job, frames), stop_evt):
|
||||||
|
self._bump(downloaded=1) # fetched+decoded into the buffer
|
||||||
owned.remove(jid) # ownership handed to the buffer/consumer
|
owned.remove(jid) # ownership handed to the buffer/consumer
|
||||||
else:
|
else:
|
||||||
break # stopped while waiting for buffer space
|
break # stopped while waiting for buffer space
|
||||||
|
|||||||
Reference in New Issue
Block a user