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