feat(stats): per-container cpu/mem load (#1424 p3)
C0NT41N3R L04D on the SYST3M tab + GET /api/container-resources.
Backend (hive-c0re/src/container_stats.rs): reads cgroup v2 cpu.stat +
memory.{current,peak,max} for each running agent machine
(machine-h\x2d<name>.scope under machine.slice), read-only/world-
readable so no hive-priv. CPU is a two-sample (~200ms) host-normalised
percentage; one shared sleep covers all agents. Skips agents whose
scope dir is absent (= not running). Network omitted: agents share the
host netns, no per-container counter.
Frontend: a polled C0NT41N3R L04D table on SYST3M (agent / cpu / mem /
peak / limit with meter bars), reusing the ST4TS table style. Polls
/api/container-resources every 5s only while the tab is active.
Backend reviewed-in-principle by damocles (path escaping + cpu delta
math); ping for the on-host sign-off.
This commit is contained in:
parent
14c7b0d406
commit
03ea6d1bda
7 changed files with 300 additions and 0 deletions
|
|
@ -1609,3 +1609,29 @@ body.dashboard-shell.has-selection { padding-bottom: 4.5em; }
|
|||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ─── SYST3M › C0NT41N3R L04D: live cgroup cpu/mem ─────────────────────────────
|
||||
Reuses the `.hive-stats-table` styling from the ST4TS tab; only the
|
||||
inline meter bar is new. */
|
||||
.cload-meter {
|
||||
display: inline-block;
|
||||
width: 6em;
|
||||
height: 10px;
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
background: var(--bg-elev);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cload-meter .fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: var(--green);
|
||||
}
|
||||
.cload-meter .fill.warn {
|
||||
background: var(--amber);
|
||||
}
|
||||
.cload-meter .fill.hot {
|
||||
background: var(--red);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,16 @@
|
|||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
<!-- C0NT41N3R L04D: live cpu + memory per agent container, read
|
||||
from cgroup v2 on the host. Polled only while this tab is
|
||||
active (cpu needs a short two-sample read each refresh). -->
|
||||
<h2>◆ C0NT41N3R L04D ◆</h2>
|
||||
<div class="divider">══════════════════════════════════════════════════════════════</div>
|
||||
<p class="meta">live cpu + memory per agent container, from cgroup v2 on the host. cpu is % of total host capacity (all cores), sampled over ~200ms each refresh; polled every 5s while this tab is open. network is omitted on purpose — agents share the host netns, so there is no per-container counter (see <code>docs/network.md</code>).</p>
|
||||
<div id="container-load-section">
|
||||
<p class="meta">loading…</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- P3RM1SS10NS: per-agent capability grants + tool-group
|
||||
|
|
|
|||
|
|
@ -4076,6 +4076,9 @@ window.marked = marked;
|
|||
}
|
||||
// ST4TS: hive-wide rollup is a pull (no SSE) — fetch on activation.
|
||||
if (target === 'stats') { refreshHiveStats(); }
|
||||
// SYST3M › C0NT41N3R L04D: live cgroup poll only while the tab is
|
||||
// open (cpu needs a short two-sample read each refresh).
|
||||
if (target === 'system') { startContainerLoadPolling(); } else { stopContainerLoadPolling(); }
|
||||
}
|
||||
// ─── tabbar overflow menu ────────────────────────────────────────────────
|
||||
// Tabs with `data-overflow="default"` (LOGS, SETTINGS) always live in
|
||||
|
|
@ -4230,6 +4233,100 @@ window.marked = marked;
|
|||
}
|
||||
bindHiveStatsWindows();
|
||||
|
||||
// ─── SYST3M › C0NT41N3R L04D: live per-container cgroup cpu/mem ────────────
|
||||
// Pull-only, polled at 5s ONLY while the SYST3M tab is active (cpu is a
|
||||
// short two-sample read on the server). Data from /api/container-resources.
|
||||
let containerLoadTimer = null;
|
||||
|
||||
function cloadFmtBytes(n) {
|
||||
if (!Number.isFinite(n) || n <= 0) return '0';
|
||||
const u = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
let i = 0; let v = n;
|
||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i += 1; }
|
||||
return v.toFixed(v < 10 && i > 0 ? 1 : 0) + ' ' + u[i];
|
||||
}
|
||||
function cloadMeter(pct) {
|
||||
const p = Math.max(0, Math.min(100, pct));
|
||||
const cls = p >= 90 ? 'hot' : (p >= 70 ? 'warn' : '');
|
||||
const m = document.createElement('span');
|
||||
m.className = 'cload-meter';
|
||||
m.title = p.toFixed(0) + '%';
|
||||
const f = document.createElement('span');
|
||||
f.className = 'fill' + (cls ? ' ' + cls : '');
|
||||
f.style.width = p + '%';
|
||||
m.append(f);
|
||||
return m;
|
||||
}
|
||||
|
||||
function renderContainerLoad(rows) {
|
||||
const root = $('container-load-section');
|
||||
if (!root) return;
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
root.replaceChildren();
|
||||
const p = document.createElement('p'); p.className = 'meta';
|
||||
p.textContent = 'no running agent containers'; root.append(p);
|
||||
return;
|
||||
}
|
||||
const table = document.createElement('table');
|
||||
table.className = 'hive-stats-table';
|
||||
table.innerHTML = '<thead><tr><th>agent</th><th>cpu</th><th>memory</th>'
|
||||
+ '<th>peak</th><th>limit</th></tr></thead>';
|
||||
const tb = document.createElement('tbody');
|
||||
for (const r of rows) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const name = document.createElement('td'); name.textContent = r.name; tr.append(name);
|
||||
|
||||
const cpu = document.createElement('td'); cpu.className = 'num';
|
||||
cpu.append((Number(r.cpu_pct) || 0).toFixed(1) + '%', cloadMeter(Number(r.cpu_pct) || 0));
|
||||
tr.append(cpu);
|
||||
|
||||
const mem = document.createElement('td'); mem.className = 'num';
|
||||
const memCur = Number(r.mem_current_bytes) || 0;
|
||||
if (r.mem_max_bytes) {
|
||||
mem.append(cloadFmtBytes(memCur), cloadMeter(100 * memCur / r.mem_max_bytes));
|
||||
} else {
|
||||
mem.textContent = cloadFmtBytes(memCur);
|
||||
}
|
||||
tr.append(mem);
|
||||
|
||||
const peak = document.createElement('td'); peak.className = 'num';
|
||||
peak.textContent = r.mem_peak_bytes ? cloadFmtBytes(Number(r.mem_peak_bytes)) : '—';
|
||||
tr.append(peak);
|
||||
|
||||
const lim = document.createElement('td'); lim.className = 'num';
|
||||
lim.textContent = r.mem_max_bytes ? cloadFmtBytes(Number(r.mem_max_bytes)) : '∞';
|
||||
tr.append(lim);
|
||||
|
||||
tb.append(tr);
|
||||
}
|
||||
table.append(tb);
|
||||
root.replaceChildren(table);
|
||||
}
|
||||
|
||||
async function refreshContainerLoad() {
|
||||
try {
|
||||
const resp = await fetch('/api/container-resources');
|
||||
if (!resp.ok) throw new Error('http ' + resp.status);
|
||||
renderContainerLoad(await resp.json());
|
||||
} catch (e) {
|
||||
const root = $('container-load-section');
|
||||
if (root) {
|
||||
root.replaceChildren();
|
||||
const p = document.createElement('p'); p.className = 'meta';
|
||||
p.textContent = 'container load fetch failed: ' + e; root.append(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
function startContainerLoadPolling() {
|
||||
refreshContainerLoad();
|
||||
if (containerLoadTimer) return;
|
||||
containerLoadTimer = setInterval(refreshContainerLoad, 5000);
|
||||
}
|
||||
function stopContainerLoadPolling() {
|
||||
if (containerLoadTimer) { clearInterval(containerLoadTimer); containerLoadTimer = null; }
|
||||
}
|
||||
|
||||
function syncTabFromHash() {
|
||||
const h = (window.location.hash || '#swarm').replace(/^#/, '');
|
||||
activateTab(h);
|
||||
|
|
|
|||
Loading…
Reference in a new issue