Files
wled-screen-controller-mixed/server/src/wled_controller/static/js/features/perf-charts.js
alexei.dolgolyov 755077607a Optimize frontend rendering: delta updates, rAF debouncing, cached DOM refs
- Disable Chart.js animations on real-time FPS and perf charts
- Dashboard: delta-update profile badges on state changes instead of full DOM rebuild
- Dashboard: cache querySelector results in Map for metrics update loop
- Dashboard: debounce poll interval slider restart (300ms)
- Calibration: debounce ResizeObserver and span drag via requestAnimationFrame
- Calibration: batch updateCalibrationPreview canvas render into rAF

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:06:39 +03:00

181 lines
6.2 KiB
JavaScript

/**
* 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';
import { dashboardPollInterval } from '../core/state.js';
const MAX_SAMPLES = 60;
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, dashboardPollInterval);
}
export function stopPerfPolling() {
if (_pollTimer) {
clearInterval(_pollTimer);
_pollTimer = null;
}
_saveHistory();
}