diff --git a/server/src/wled_controller/core/processing/metrics_history.py b/server/src/wled_controller/core/processing/metrics_history.py index 3870e73..571f336 100644 --- a/server/src/wled_controller/core/processing/metrics_history.py +++ b/server/src/wled_controller/core/processing/metrics_history.py @@ -105,6 +105,7 @@ class MetricsHistory: self._targets[target_id].append({ "t": now, "fps": state.get("fps_actual"), + "fps_current": state.get("fps_current"), "fps_target": state.get("fps_target"), "timing": state.get("timing_total_ms"), "errors": state.get("errors_count", 0), diff --git a/server/src/wled_controller/static/css/cards.css b/server/src/wled_controller/static/css/cards.css index 05c1629..f2d06b1 100644 --- a/server/src/wled_controller/static/css/cards.css +++ b/server/src/wled_controller/static/css/cards.css @@ -540,6 +540,15 @@ ul.section-tip li { font-size: 0.75rem; } +.target-fps-avg { + display: block; + font-size: 0.65rem; + font-weight: 400; + opacity: 0.45; + line-height: 1.1; + color: #4CAF50; +} + /* Timing breakdown bar */ .timing-breakdown { margin-top: 8px; diff --git a/server/src/wled_controller/static/css/dashboard.css b/server/src/wled_controller/static/css/dashboard.css index 789cde0..996ee7e 100644 --- a/server/src/wled_controller/static/css/dashboard.css +++ b/server/src/wled_controller/static/css/dashboard.css @@ -171,6 +171,15 @@ font-size: 0.75rem; } +.dashboard-fps-avg { + display: block; + font-size: 0.6rem; + font-weight: 400; + opacity: 0.45; + line-height: 1.1; + color: #4CAF50; +} + .dashboard-target-actions { display: flex; align-items: center; diff --git a/server/src/wled_controller/static/js/features/dashboard.js b/server/src/wled_controller/static/js/features/dashboard.js index b9c399d..9fe5b59 100644 --- a/server/src/wled_controller/static/js/features/dashboard.js +++ b/server/src/wled_controller/static/js/features/dashboard.js @@ -17,7 +17,8 @@ import { const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed'; const MAX_FPS_SAMPLES = 120; -let _fpsHistory = {}; // { targetId: number[] } +let _fpsHistory = {}; // { targetId: number[] } — fps_actual +let _fpsCurrentHistory = {}; // { targetId: number[] } — fps_current let _fpsCharts = {}; // { targetId: Chart } let _lastRunningIds = []; // sorted target IDs from previous render let _uptimeBase = {}; // { targetId: { seconds, timestamp } } @@ -25,10 +26,14 @@ let _uptimeTimer = null; let _uptimeElements = {}; // { targetId: HTMLElement } — cached DOM refs let _metricsElements = new Map(); -function _pushFps(targetId, value) { +function _pushFps(targetId, actual, current) { if (!_fpsHistory[targetId]) _fpsHistory[targetId] = []; - _fpsHistory[targetId].push(value); + _fpsHistory[targetId].push(actual); if (_fpsHistory[targetId].length > MAX_FPS_SAMPLES) _fpsHistory[targetId].shift(); + + if (!_fpsCurrentHistory[targetId]) _fpsCurrentHistory[targetId] = []; + _fpsCurrentHistory[targetId].push(current); + if (_fpsCurrentHistory[targetId].length > MAX_FPS_SAMPLES) _fpsCurrentHistory[targetId].shift(); } function _setUptimeBase(targetId, seconds) { @@ -80,22 +85,32 @@ function _destroyFpsCharts() { _fpsCharts = {}; } -function _createFpsChart(canvasId, history, fpsTarget) { +function _createFpsChart(canvasId, actualHistory, currentHistory, fpsTarget) { const canvas = document.getElementById(canvasId); if (!canvas) return null; return new Chart(canvas, { type: 'line', data: { - labels: history.map(() => ''), - datasets: [{ - data: [...history], - borderColor: '#2196F3', - backgroundColor: 'rgba(33,150,243,0.12)', - borderWidth: 1.5, - tension: 0.3, - fill: true, - pointRadius: 0, - }], + labels: actualHistory.map(() => ''), + datasets: [ + { + data: [...actualHistory], + borderColor: '#2196F3', + backgroundColor: 'rgba(33,150,243,0.12)', + borderWidth: 1.5, + tension: 0.3, + fill: true, + pointRadius: 0, + }, + { + data: [...currentHistory], + borderColor: '#4CAF50', + borderWidth: 1.5, + tension: 0.3, + fill: false, + pointRadius: 0, + }, + ], }, options: { responsive: true, @@ -124,6 +139,7 @@ async function _initFpsCharts(runningTargetIds) { for (const id of runningTargetIds) { const samples = serverTargets[id] || []; _fpsHistory[id] = samples.map(s => s.fps).filter(v => v != null); + _fpsCurrentHistory[id] = samples.map(s => s.fps_current).filter(v => v != null); } } } catch { @@ -133,14 +149,15 @@ async function _initFpsCharts(runningTargetIds) { // Clean up history for targets that are no longer running for (const id of Object.keys(_fpsHistory)) { - if (!runningTargetIds.includes(id)) delete _fpsHistory[id]; + if (!runningTargetIds.includes(id)) { delete _fpsHistory[id]; delete _fpsCurrentHistory[id]; } } for (const id of runningTargetIds) { const canvas = document.getElementById(`dashboard-fps-${id}`); if (!canvas) continue; - const history = _fpsHistory[id] || []; + const actualH = _fpsHistory[id] || []; + const currentH = _fpsCurrentHistory[id] || []; const fpsTarget = parseFloat(canvas.dataset.fpsTarget) || 30; - _fpsCharts[id] = _createFpsChart(`dashboard-fps-${id}`, history, fpsTarget); + _fpsCharts[id] = _createFpsChart(`dashboard-fps-${id}`, actualH, currentH, fpsTarget); } _cacheMetricsElements(runningTargetIds); @@ -162,19 +179,22 @@ function _updateRunningMetrics(enrichedRunning) { for (const target of enrichedRunning) { const state = target.state || {}; const metrics = target.metrics || {}; + const fpsCurrent = state.fps_current ?? 0; const fpsActual = state.fps_actual != null ? state.fps_actual.toFixed(1) : '-'; const fpsTarget = state.fps_target || (target.settings || target.key_colors_settings || {}).fps || '-'; const errors = metrics.errors_count || 0; // Push FPS and update chart if (state.fps_actual != null) { - _pushFps(target.id, state.fps_actual); + _pushFps(target.id, state.fps_actual, fpsCurrent); } const chart = _fpsCharts[target.id]; if (chart) { - const history = _fpsHistory[target.id] || []; - chart.data.datasets[0].data = [...history]; - chart.data.labels = history.map(() => ''); + const actualH = _fpsHistory[target.id] || []; + const currentH = _fpsCurrentHistory[target.id] || []; + chart.data.datasets[0].data = [...actualH]; + chart.data.datasets[1].data = [...currentH]; + chart.data.labels = actualH.map(() => ''); chart.update(); } @@ -186,7 +206,8 @@ function _updateRunningMetrics(enrichedRunning) { // Update text values (use cached refs, fallback to querySelector) const cached = _metricsElements.get(target.id); const fpsEl = cached?.fps || document.querySelector(`[data-fps-text="${target.id}"]`); - if (fpsEl) fpsEl.innerHTML = `${fpsActual}/${fpsTarget}`; + if (fpsEl) fpsEl.innerHTML = `${fpsCurrent}/${fpsTarget}` + + `avg ${fpsActual}`; const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`); if (errorsEl) errorsEl.textContent = `${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}`; @@ -509,6 +530,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap } if (isRunning) { + const fpsCurrent = state.fps_current ?? 0; const fpsActual = state.fps_actual != null ? state.fps_actual.toFixed(1) : '-'; const fpsTarget = state.fps_target || (target.settings || target.key_colors_settings || {}).fps || '-'; const uptime = formatUptime(metrics.uptime_seconds); @@ -519,9 +541,9 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap _setUptimeBase(target.id, metrics.uptime_seconds); } - // Push FPS sample to history + // Push FPS samples to history if (state.fps_actual != null) { - _pushFps(target.id, state.fps_actual); + _pushFps(target.id, state.fps_actual, fpsCurrent); } let healthDot = ''; @@ -544,7 +566,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
- ${fpsActual}/${fpsTarget} + ${fpsCurrent}/${fpsTarget}avg ${fpsActual}
diff --git a/server/src/wled_controller/static/js/features/targets.js b/server/src/wled_controller/static/js/features/targets.js index 86e0286..b4b069e 100644 --- a/server/src/wled_controller/static/js/features/targets.js +++ b/server/src/wled_controller/static/js/features/targets.js @@ -42,32 +42,49 @@ document.addEventListener('languageChanged', () => { // --- FPS sparkline history and chart instances for target cards --- const _TARGET_MAX_FPS_SAMPLES = 30; -const _targetFpsHistory = {}; +const _targetFpsHistory = {}; // fps_actual (rolling avg) +const _targetFpsCurrentHistory = {}; // fps_current (sends/sec) const _targetFpsCharts = {}; -function _pushTargetFps(targetId, value) { +function _pushTargetFps(targetId, actual, current) { if (!_targetFpsHistory[targetId]) _targetFpsHistory[targetId] = []; const h = _targetFpsHistory[targetId]; - h.push(value); + h.push(actual); if (h.length > _TARGET_MAX_FPS_SAMPLES) h.shift(); + + if (!_targetFpsCurrentHistory[targetId]) _targetFpsCurrentHistory[targetId] = []; + const c = _targetFpsCurrentHistory[targetId]; + c.push(current); + if (c.length > _TARGET_MAX_FPS_SAMPLES) c.shift(); } -function _createTargetFpsChart(canvasId, history, fpsTarget, maxHwFps) { +function _createTargetFpsChart(canvasId, actualHistory, currentHistory, fpsTarget, maxHwFps) { const canvas = document.getElementById(canvasId); if (!canvas) return null; - const datasets = [{ - data: [...history], - borderColor: '#2196F3', - backgroundColor: 'rgba(33,150,243,0.12)', - borderWidth: 1.5, - tension: 0.3, - fill: true, - pointRadius: 0, - }]; + const labels = actualHistory.map(() => ''); + const datasets = [ + { + data: [...actualHistory], + borderColor: '#2196F3', + backgroundColor: 'rgba(33,150,243,0.12)', + borderWidth: 1.5, + tension: 0.3, + fill: true, + pointRadius: 0, + }, + { + data: [...currentHistory], + borderColor: '#4CAF50', + borderWidth: 1.5, + tension: 0.3, + fill: false, + pointRadius: 0, + }, + ]; // Flat line showing hardware max FPS if (maxHwFps && maxHwFps < fpsTarget * 1.15) { datasets.push({ - data: history.map(() => maxHwFps), + data: actualHistory.map(() => maxHwFps), borderColor: 'rgba(255,152,0,0.5)', borderWidth: 1, borderDash: [4, 3], @@ -77,7 +94,7 @@ function _createTargetFpsChart(canvasId, history, fpsTarget, maxHwFps) { } return new Chart(canvas, { type: 'line', - data: { labels: history.map(() => ''), datasets }, + data: { labels, datasets }, options: { responsive: true, maintainAspectRatio: false, animation: false, @@ -93,9 +110,11 @@ function _createTargetFpsChart(canvasId, history, fpsTarget, maxHwFps) { function _updateTargetFpsChart(targetId, fpsTarget) { const chart = _targetFpsCharts[targetId]; if (!chart) return; - const history = _targetFpsHistory[targetId] || []; - chart.data.labels = history.map(() => ''); - chart.data.datasets[0].data = [...history]; + const actualH = _targetFpsHistory[targetId] || []; + const currentH = _targetFpsCurrentHistory[targetId] || []; + chart.data.labels = actualH.map(() => ''); + chart.data.datasets[0].data = [...actualH]; + chart.data.datasets[1].data = [...currentH]; chart.options.scales.y.max = fpsTarget * 1.15; chart.update('none'); } @@ -630,15 +649,16 @@ export async function loadTargetsTab() { if (target.state && target.state.processing) { runningIds.add(target.id); if (target.state.fps_actual != null) { - _pushTargetFps(target.id, target.state.fps_actual); + _pushTargetFps(target.id, target.state.fps_actual, target.state.fps_current ?? 0); } // Create chart if it doesn't exist (new or replaced card) if (!_targetFpsCharts[target.id]) { - const history = _targetFpsHistory[target.id] || []; + const actualH = _targetFpsHistory[target.id] || []; + const currentH = _targetFpsCurrentHistory[target.id] || []; const fpsTarget = target.state.fps_target || 30; const device = devices.find(d => d.id === target.device_id); const maxHwFps = device ? _computeMaxFps(device.baud_rate, device.led_count, device.device_type) : null; - const chart = _createTargetFpsChart(`target-fps-${target.id}`, history, fpsTarget, maxHwFps); + const chart = _createTargetFpsChart(`target-fps-${target.id}`, actualH, currentH, fpsTarget, maxHwFps); if (chart) _targetFpsCharts[target.id] = chart; } else { // Chart survived reconcile — just update data @@ -648,7 +668,7 @@ export async function loadTargetsTab() { }); // Clean up history and charts for targets no longer running Object.keys(_targetFpsHistory).forEach(id => { - if (!runningIds.has(id)) delete _targetFpsHistory[id]; + if (!runningIds.has(id)) { delete _targetFpsHistory[id]; delete _targetFpsCurrentHistory[id]; } }); Object.keys(_targetFpsCharts).forEach(id => { if (!runningIds.has(id)) { @@ -715,7 +735,8 @@ function _patchTargetMetrics(target) { const metrics = target.metrics || {}; const fps = card.querySelector('[data-tm="fps"]'); - if (fps) fps.innerHTML = `${state.fps_actual?.toFixed(1) || '0.0'}/${state.fps_target || 0}`; + if (fps) fps.innerHTML = `${state.fps_current ?? 0}/${state.fps_target || 0}` + + `avg ${state.fps_actual?.toFixed(1) || '0.0'}`; const timing = card.querySelector('[data-tm="timing"]'); if (timing && state.timing_total_ms != null) timing.innerHTML = _buildLedTimingHTML(state);