diff --git a/crates/runtime-dashboard/src/dashboard_html.rs b/crates/runtime-dashboard/src/dashboard_html.rs
index 8004023..5c544aa 100644
--- a/crates/runtime-dashboard/src/dashboard_html.rs
+++ b/crates/runtime-dashboard/src/dashboard_html.rs
@@ -77,6 +77,17 @@ pub const DASHBOARD_HTML: &str = r##"
.chart-panel { grid-row: span 2; }
canvas#workerChart { width: 100%; height: 200px; }
+ .worker-cards { display: flex; flex-direction: column; gap: 6px; }
+ .worker-card {
+ display: flex; align-items: center; gap: 10px;
+ background: #1c1f2e; border-radius: 4px; padding: 6px 10px;
+ }
+ .worker-card .wc-id { font-weight: 700; min-width: 32px; }
+ .worker-card .wc-bar-wrap { flex: 1; height: 14px; background: #0f1117; border-radius: 2px; overflow: hidden; display: flex; }
+ .worker-card .wc-bar-seg { height: 100%; }
+ .worker-card .wc-stats { font-size: 11px; color: #888; min-width: 200px; text-align: right; }
+ .worker-card .wc-spark { display: inline-flex; gap: 4px; margin-left: 6px; }
+
.actor-table-wrap { max-height: 200px; overflow-y: auto; }
.actor-table-wrap table { width: 100%; border-collapse: collapse; }
.actor-table-wrap th, .actor-table-wrap td {
@@ -156,9 +167,14 @@ pub const DASHBOARD_HTML: &str = r##"
-
Worker Distribution
-
-
+
Worker Utilization
+
+
+ \u25A0 processing
+ \u25A0 delivery
+ \u25A0 spawns
+ \u25A0 overhead
+
@@ -243,48 +259,83 @@ pub const DASHBOARD_HTML: &str = r##"
}
setInterval(updateUptime, 1000);
- var canvas = document.getElementById('workerChart');
- var ctx = canvas.getContext('2d');
var colors = ['#4caf50','#2196f3','#ff9800','#f44336','#9c27b0','#00bcd4','#ffeb3b','#e91e63'];
- function drawWorkerChart(workers) {
- var dpr = window.devicePixelRatio || 1;
- var rect = canvas.getBoundingClientRect();
- canvas.width = rect.width * dpr;
- canvas.height = rect.height * dpr;
- ctx.scale(dpr, dpr);
- var W = rect.width, H = rect.height;
- ctx.clearRect(0, 0, W, H);
+ var phaseColors = ['#4caf50', '#2196f3', '#00bcd4', '#f44336'];
+ // Group tick phases: processing=2, delivery=1+4, spawns=0+3, overhead=5
- if (!workers || workers.length === 0) return;
+ function computePhases(timings) {
+ if (!timings || timings.length === 0) return [0.25, 0.25, 0.25, 0.25];
+ var sums = [0,0,0,0,0,0];
+ var active = 0;
+ for (var i = 0; i < timings.length; i++) {
+ var t = timings[i];
+ if (t.did_work) active++;
+ for (var p = 0; p < 6 && p < t.phase_us.length; p++) sums[p] += t.phase_us[p];
+ }
+ var total = sums.reduce(function(a,b) { return a+b; }, 0);
+ if (total === 0) return [0.25, 0.25, 0.25, 0.25];
+ var processing = sums[2] / total;
+ var delivery = (sums[1] + sums[4]) / total;
+ var spawns = (sums[0] + sums[3]) / total;
+ var overhead = sums[5] / total;
+ var load = timings.length > 0 ? active / timings.length : 0;
+ return { fracs: [processing, delivery, spawns, overhead], load: load };
+ }
- var maxActors = Math.max(1, Math.max.apply(null, workers.map(function(w) { return w.num_actors; })));
- var barW = Math.max(8, Math.floor((W - 40) / workers.length) - 6);
- var chartH = H - 30;
+ function renderWorkerCards(data) {
+ var container = document.getElementById('workerCards');
+ if (!data.workers) return;
+ container.innerHTML = '';
- workers.forEach(function(w, i) {
- var x = 20 + i * (barW + 6);
- var h = (w.num_actors / maxActors) * (chartH * 0.45);
- ctx.fillStyle = colors[i % colors.length];
- ctx.globalAlpha = 0.8;
- ctx.fillRect(x, chartH * 0.5 - h, barW, h);
+ data.workers.forEach(function(w, idx) {
+ var timings = data.tick_timings ? data.tick_timings[idx] : null;
+ var phases = computePhases(timings);
+ var load = phases.load || 0;
+ var fracs = phases.fracs || [0.25, 0.25, 0.25, 0.25];
- var mh = Math.min(w.mailbox_depth * 2, chartH * 0.4);
- ctx.globalAlpha = 0.4;
- ctx.fillRect(x, chartH * 0.55, barW, mh);
+ var card = document.createElement('div');
+ card.className = 'worker-card';
- ctx.globalAlpha = 1;
- ctx.fillStyle = '#888';
- ctx.font = '10px monospace';
- ctx.textAlign = 'center';
- ctx.fillText('W' + w.id, x + barW / 2, H - 2);
+ // ID
+ var idSpan = document.createElement('span');
+ idSpan.className = 'wc-id';
+ idSpan.style.color = colors[w.id % colors.length];
+ idSpan.textContent = 'W' + w.id;
+ card.appendChild(idSpan);
+
+ // Phase bar
+ var barWrap = document.createElement('span');
+ barWrap.className = 'wc-bar-wrap';
+ var filledPct = Math.round(load * 100);
+ for (var p = 0; p < 4; p++) {
+ var seg = document.createElement('span');
+ seg.className = 'wc-bar-seg';
+ seg.style.width = (fracs[p] * filledPct) + '%';
+ seg.style.background = phaseColors[p];
+ barWrap.appendChild(seg);
+ }
+ card.appendChild(barWrap);
+
+ // Sparklines
+ var sparkWrap = document.createElement('span');
+ sparkWrap.className = 'wc-spark';
+ var wh = workerHistory[w.id];
+ if (wh) {
+ sparkWrap.innerHTML = renderSparklineSvg(wh.message_rates, 60, 14, '#4caf50');
+ }
+ card.appendChild(sparkWrap);
+
+ // Stats
+ var statsSpan = document.createElement('span');
+ statsSpan.className = 'wc-stats';
+ statsSpan.textContent = w.num_actors + ' actors ' +
+ w.messages_processed.toLocaleString() + ' msgs mbox ' + w.mailbox_depth +
+ ' ' + Math.round(load * 100) + '%';
+ card.appendChild(statsSpan);
+
+ container.appendChild(card);
});
-
- var legend = document.getElementById('workerLegend');
- legend.innerHTML = workers.map(function(w, i) {
- return 'W' + w.id +
- ': ' + w.num_actors + ' actors, ' + w.messages_processed + ' msgs, mbox ' + w.mailbox_depth + '';
- }).join(' | ');
}
function updateStats(data) {
@@ -301,7 +352,7 @@ pub const DASHBOARD_HTML: &str = r##"
document.getElementById('statWorkers').textContent = data.num_workers || 0;
document.getElementById('statMailbox').textContent = totalMailbox;
- drawWorkerChart(data.workers);
+ renderWorkerCards(data);
var tbody = document.getElementById('actorTableBody');
tbody.innerHTML = '';