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 = '';