feat(agent): Status shows smoothed jobs/min + downloads/min (replace jumpy gauges)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 18s
CI / backend-lint-and-test (push) Successful in 37s
CI / integration (push) Successful in 3m26s

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:
2026-07-01 16:01:29 -04:00
parent 98b2ac90dd
commit 91ea06be79
2 changed files with 39 additions and 10 deletions
+8 -1
View File
@@ -162,6 +162,10 @@ class Worker:
self._held: set[int] = set()
self._held_lock = threading.Lock()
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.transient = 0 # jobs handed back due to a server outage (NOT
# failed) — the "waiting out curator" counter
@@ -425,13 +429,15 @@ class Worker:
"buffer_max": BUFFER_MAX,
"active": self._active,
"processed": self.processed,
"downloaded": self.downloaded,
"errors": self.errors,
"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:
self.processed += processed
self.downloaded += downloaded
self.errors += errors
self.transient += transient
# Clamp at 0: a Stop resets _active to 0, so a consumer that was
@@ -497,6 +503,7 @@ class Worker:
continue
# Blocks on a full buffer (backpressure) but wakes promptly on stop.
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
else:
break # stopped while waiting for buffer space