Add real-time system performance charts to dashboard
Backend: GET /api/v1/system/performance endpoint using psutil (CPU/RAM) and nvidia-ml-py (GPU utilization, memory, temperature) with graceful fallback. Frontend: Chart.js line charts with rolling 60-sample history persisted to sessionStorage, flicker-free updates via persistent DOM and diff-based dynamic section refresh. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
180
server/src/wled_controller/static/js/features/perf-charts.js
Normal file
180
server/src/wled_controller/static/js/features/perf-charts.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Performance charts — real-time CPU, RAM, GPU usage with Chart.js.
|
||||
*/
|
||||
|
||||
import { API_BASE, getHeaders } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
|
||||
const MAX_SAMPLES = 60;
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
const STORAGE_KEY = 'perf_history';
|
||||
|
||||
let _pollTimer = null;
|
||||
let _charts = {}; // { cpu: Chart, ram: Chart, gpu: Chart }
|
||||
let _history = _loadHistory();
|
||||
let _hasGpu = null; // null = unknown, true/false after first fetch
|
||||
|
||||
function _loadHistory() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed.cpu && parsed.ram && parsed.gpu) return parsed;
|
||||
}
|
||||
} catch {}
|
||||
return { cpu: [], ram: [], gpu: [] };
|
||||
}
|
||||
|
||||
function _saveHistory() {
|
||||
try { sessionStorage.setItem(STORAGE_KEY, JSON.stringify(_history)); }
|
||||
catch {}
|
||||
}
|
||||
|
||||
/** Returns the static HTML for the perf section (canvas placeholders). */
|
||||
export function renderPerfSection() {
|
||||
return `<div class="perf-charts-grid">
|
||||
<div class="perf-chart-card">
|
||||
<div class="perf-chart-header">
|
||||
<span class="perf-chart-label">${t('dashboard.perf.cpu')}</span>
|
||||
<span class="perf-chart-value cpu" id="perf-cpu-value">-</span>
|
||||
</div>
|
||||
<div class="perf-chart-wrap"><canvas id="perf-chart-cpu"></canvas></div>
|
||||
</div>
|
||||
<div class="perf-chart-card">
|
||||
<div class="perf-chart-header">
|
||||
<span class="perf-chart-label">${t('dashboard.perf.ram')}</span>
|
||||
<span class="perf-chart-value ram" id="perf-ram-value">-</span>
|
||||
</div>
|
||||
<div class="perf-chart-wrap"><canvas id="perf-chart-ram"></canvas></div>
|
||||
</div>
|
||||
<div class="perf-chart-card" id="perf-gpu-card">
|
||||
<div class="perf-chart-header">
|
||||
<span class="perf-chart-label">${t('dashboard.perf.gpu')}</span>
|
||||
<span class="perf-chart-value gpu" id="perf-gpu-value">-</span>
|
||||
</div>
|
||||
<div class="perf-chart-wrap"><canvas id="perf-chart-gpu"></canvas></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _createChart(canvasId, color, fillColor) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return null;
|
||||
return new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: Array(MAX_SAMPLES).fill(''),
|
||||
datasets: [{
|
||||
data: [],
|
||||
borderColor: color,
|
||||
backgroundColor: fillColor,
|
||||
borderWidth: 1.5,
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
pointRadius: 0,
|
||||
}],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
||||
scales: {
|
||||
x: { display: false },
|
||||
y: { min: 0, max: 100, display: false },
|
||||
},
|
||||
layout: { padding: 0 },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Initialize Chart.js instances on the already-mounted canvases. */
|
||||
export function initPerfCharts() {
|
||||
_destroyCharts();
|
||||
_charts.cpu = _createChart('perf-chart-cpu', '#2196F3', 'rgba(33,150,243,0.15)');
|
||||
_charts.ram = _createChart('perf-chart-ram', '#4CAF50', 'rgba(76,175,80,0.15)');
|
||||
_charts.gpu = _createChart('perf-chart-gpu', '#FF9800', 'rgba(255,152,0,0.15)');
|
||||
// Restore any existing history data into the freshly created charts
|
||||
for (const key of ['cpu', 'ram', 'gpu']) {
|
||||
if (_charts[key] && _history[key].length > 0) {
|
||||
_charts[key].data.datasets[0].data = [..._history[key]];
|
||||
_charts[key].data.labels = _history[key].map(() => '');
|
||||
_charts[key].update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _destroyCharts() {
|
||||
for (const key of Object.keys(_charts)) {
|
||||
if (_charts[key]) { _charts[key].destroy(); _charts[key] = null; }
|
||||
}
|
||||
}
|
||||
|
||||
function _pushSample(key, value) {
|
||||
_history[key].push(value);
|
||||
if (_history[key].length > MAX_SAMPLES) _history[key].shift();
|
||||
const chart = _charts[key];
|
||||
if (!chart) return;
|
||||
chart.data.datasets[0].data = [..._history[key]];
|
||||
chart.data.labels = _history[key].map(() => '');
|
||||
chart.update();
|
||||
}
|
||||
|
||||
async function _fetchPerformance() {
|
||||
try {
|
||||
const resp = await fetch(`${API_BASE}/system/performance`, { headers: getHeaders() });
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
|
||||
// CPU
|
||||
_pushSample('cpu', data.cpu_percent);
|
||||
const cpuEl = document.getElementById('perf-cpu-value');
|
||||
if (cpuEl) cpuEl.textContent = `${data.cpu_percent.toFixed(0)}%`;
|
||||
|
||||
// RAM
|
||||
_pushSample('ram', data.ram_percent);
|
||||
const ramEl = document.getElementById('perf-ram-value');
|
||||
if (ramEl) {
|
||||
const usedGb = (data.ram_used_mb / 1024).toFixed(1);
|
||||
const totalGb = (data.ram_total_mb / 1024).toFixed(1);
|
||||
ramEl.textContent = `${usedGb}/${totalGb} GB`;
|
||||
}
|
||||
|
||||
// GPU
|
||||
if (data.gpu) {
|
||||
_hasGpu = true;
|
||||
_pushSample('gpu', data.gpu.utilization);
|
||||
const gpuEl = document.getElementById('perf-gpu-value');
|
||||
if (gpuEl) gpuEl.textContent = `${data.gpu.utilization.toFixed(0)}% · ${data.gpu.temperature_c}°C`;
|
||||
} else if (_hasGpu === null) {
|
||||
_hasGpu = false;
|
||||
const card = document.getElementById('perf-gpu-card');
|
||||
if (card) {
|
||||
const canvas = card.querySelector('canvas');
|
||||
if (canvas) canvas.style.display = 'none';
|
||||
const noGpu = document.createElement('div');
|
||||
noGpu.className = 'perf-chart-unavailable';
|
||||
noGpu.textContent = t('dashboard.perf.unavailable');
|
||||
card.appendChild(noGpu);
|
||||
}
|
||||
}
|
||||
|
||||
_saveHistory();
|
||||
} catch {
|
||||
// Silently ignore fetch errors (e.g., network issues, tab hidden)
|
||||
}
|
||||
}
|
||||
|
||||
export function startPerfPolling() {
|
||||
if (_pollTimer) return;
|
||||
_fetchPerformance();
|
||||
_pollTimer = setInterval(_fetchPerformance, POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
export function stopPerfPolling() {
|
||||
if (_pollTimer) {
|
||||
clearInterval(_pollTimer);
|
||||
_pollTimer = null;
|
||||
}
|
||||
_saveHistory();
|
||||
}
|
||||
Reference in New Issue
Block a user