dashboard #38
1 changed files with 89 additions and 38 deletions
|
|
@ -77,6 +77,17 @@ pub const DASHBOARD_HTML: &str = r##"<!DOCTYPE html>
|
|||
.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##"<!DOCTYPE html>
|
|||
|
||||
<div class="grid">
|
||||
<div class="panel chart-panel">
|
||||
<h2>Worker Distribution</h2>
|
||||
<canvas id="workerChart"></canvas>
|
||||
<div id="workerLegend" style="margin-top:8px;font-size:11px;color:#888;"></div>
|
||||
<h2>Worker Utilization</h2>
|
||||
<div id="workerCards" class="worker-cards"></div>
|
||||
<div style="margin-top:6px;font-size:10px;color:#555;">
|
||||
<span style="color:#4caf50;">\u25A0</span> processing
|
||||
<span style="color:#2196f3;">\u25A0</span> delivery
|
||||
<span style="color:#00bcd4;">\u25A0</span> spawns
|
||||
<span style="color:#f44336;">\u25A0</span> overhead
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
|
|
@ -243,48 +259,83 @@ pub const DASHBOARD_HTML: &str = r##"<!DOCTYPE html>
|
|||
}
|
||||
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 '<span style="color:' + colors[i % colors.length] + '">W' + w.id +
|
||||
': ' + w.num_actors + ' actors, ' + w.messages_processed + ' msgs, mbox ' + w.mailbox_depth + '</span>';
|
||||
}).join(' | ');
|
||||
}
|
||||
|
||||
function updateStats(data) {
|
||||
|
|
@ -301,7 +352,7 @@ pub const DASHBOARD_HTML: &str = r##"<!DOCTYPE html>
|
|||
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 = '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue