Skip to content

Commit 4201ede

Browse files
authored
Feature/update the palette (#59)
* update the palette * add encoder / decoder --------- Co-authored-by: Panos <>
1 parent 0e75131 commit 4201ede

File tree

17 files changed

+407
-275
lines changed

17 files changed

+407
-275
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Real-time NVIDIA GPU monitoring dashboard. Lightweight, web-based, and self-host
99
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
1010
[![NVIDIA](https://img.shields.io/badge/NVIDIA-GPU-76B900?style=flat-square&logo=nvidia&logoColor=white)](https://www.nvidia.com/)
1111

12-
<img src="gpu-hot.jpg" alt="GPU Hot Dashboard" width="800" />
12+
<img src="gpu-hot.png" alt="GPU Hot Dashboard" width="800" />
1313

1414
<p>
1515
<a href="https://psalias2006.github.io/gpu-hot/demo.html">

core/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
Real-time NVIDIA GPU monitoring application
44
"""
55

6-
__version__ = '1.0.0'
7-
__author__ = 'GPU Hot Team'
8-
96
from .monitor import GPUMonitor
107
from . import config
118

core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os
66
import socket
77

8-
# Flask Configuration
8+
# Server Configuration
99
SECRET_KEY = 'gpu_hot_secret'
1010
HOST = '0.0.0.0'
1111
PORT = 1312

core/metrics/collector.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""GPU metrics collector using NVML"""
22

33
import time
4+
import logging
45
import pynvml
56
from datetime import datetime
67
from .utils import safe_get, decode_bytes, to_mib, to_watts
78

9+
logger = logging.getLogger(__name__)
10+
811

912
class MetricsCollector:
1013
"""Collect all available GPU metrics via NVML"""
@@ -267,29 +270,54 @@ def _add_connectivity(self, handle, data):
267270

268271
def _add_media_engines(self, handle, data):
269272
"""Encoder/decoder metrics"""
270-
# Encoder
271-
if enc := safe_get(pynvml.nvmlDeviceGetEncoderUtilization, handle):
272-
if isinstance(enc, tuple) and len(enc) >= 2:
273-
data['encoder_utilization'] = float(enc[0])
273+
if not hasattr(self, '_media_logged'):
274+
self._media_logged = False
274275

276+
# Encoder utilization — only set when GPU supports it
277+
try:
278+
enc_util, enc_period = pynvml.nvmlDeviceGetEncoderUtilization(handle)
279+
data['encoder_utilization'] = float(enc_util)
280+
except pynvml.NVMLError as e:
281+
if not self._media_logged:
282+
logger.info(f"Encoder utilization not available: {e}")
283+
except Exception as e:
284+
if not self._media_logged:
285+
logger.info(f"Encoder utilization error: {e}")
286+
287+
# Encoder sessions
275288
try:
276-
if sessions := pynvml.nvmlDeviceGetEncoderSessions(handle):
277-
data['encoder_sessions'] = len(sessions)
278-
if fps := [s.averageFps for s in sessions if hasattr(s, 'averageFps')]:
289+
sessions = pynvml.nvmlDeviceGetEncoderSessions(handle)
290+
data['encoder_sessions'] = len(sessions) if sessions else 0
291+
if sessions:
292+
fps = [s.averageFps for s in sessions if hasattr(s, 'averageFps')]
293+
if fps:
279294
data['encoder_fps'] = float(sum(fps) / len(fps))
280-
except:
295+
except pynvml.NVMLError:
296+
pass
297+
except Exception:
281298
pass
282299

283-
# Decoder
284-
if dec := safe_get(pynvml.nvmlDeviceGetDecoderUtilization, handle):
285-
if isinstance(dec, tuple) and len(dec) >= 2:
286-
data['decoder_utilization'] = float(dec[0])
287-
300+
# Decoder utilization — only set when GPU supports it
288301
try:
289-
if sessions := pynvml.nvmlDeviceGetDecoderSessions(handle):
290-
data['decoder_sessions'] = len(sessions)
291-
except:
302+
dec_util, dec_period = pynvml.nvmlDeviceGetDecoderUtilization(handle)
303+
data['decoder_utilization'] = float(dec_util)
304+
except pynvml.NVMLError as e:
305+
if not self._media_logged:
306+
logger.info(f"Decoder utilization not available: {e}")
307+
except Exception as e:
308+
if not self._media_logged:
309+
logger.info(f"Decoder utilization error: {e}")
310+
311+
# Decoder sessions
312+
try:
313+
sessions = pynvml.nvmlDeviceGetDecoderSessions(handle)
314+
data['decoder_sessions'] = len(sessions) if sessions else 0
315+
except pynvml.NVMLError:
292316
pass
317+
except Exception:
318+
pass
319+
320+
self._media_logged = True
293321

294322
def _add_health_status(self, handle, data):
295323
"""ECC and health metrics"""

core/nvidia_smi_fallback.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def parse_nvidia_smi():
2222
'clocks.max.gr,clocks.max.sm,clocks.max.mem,'
2323
'pcie.link.gen.current,pcie.link.gen.max,pcie.link.width.current,pcie.link.width.max,'
2424
'encoder.stats.sessionCount,encoder.stats.averageFps,encoder.stats.averageLatency,'
25-
'pstate,compute_mode',
25+
'pstate,compute_mode,'
26+
'utilization.encoder,utilization.decoder',
2627
'--format=csv,noheader,nounits'
2728
], capture_output=True, text=True, timeout=10)
2829

@@ -74,6 +75,8 @@ def parse_nvidia_smi():
7475
'decoder_latency': 0,
7576
'performance_state': parts[27] if len(parts) > 27 and parts[27] not in ['N/A', '[N/A]', ''] else 'N/A',
7677
'compute_mode': parts[28] if len(parts) > 28 and parts[28] not in ['N/A', '[N/A]', ''] else 'N/A',
78+
'encoder_utilization': float(parts[29]) if len(parts) > 29 and parts[29] not in ['N/A', '[N/A]', ''] else 0,
79+
'decoder_utilization': float(parts[30]) if len(parts) > 30 and parts[30] not in ['N/A', '[N/A]', ''] else 0,
7780
'throttle_reasons': 'None',
7881
'timestamp': datetime.now().isoformat(),
7982
'_fallback_mode': True
@@ -146,9 +149,11 @@ def parse_nvidia_smi_fallback():
146149
'encoder_sessions': 0,
147150
'encoder_fps': 0,
148151
'encoder_latency': 0,
152+
'encoder_utilization': 0,
149153
'decoder_sessions': 0,
150154
'decoder_fps': 0,
151155
'decoder_latency': 0,
156+
'decoder_utilization': 0,
152157
'performance_state': parts[13] if parts[13] not in ['N/A', '[N/A]', ''] else 'N/A',
153158
'compute_mode': 'N/A',
154159
'throttle_reasons': 'None',

gpu-hot.jpg

-364 KB
Binary file not shown.

gpu-hot.png

286 KB
Loading

static/css/components.css

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
gap: var(--space-lg);
1616
padding: var(--space-sm) 0;
1717
cursor: pointer;
18-
transition: background 0.1s;
18+
transition: background var(--transition-fast), transform var(--transition-fast);
1919
min-width: 0;
2020
}
2121

2222
.overview-gpu-card:hover {
23-
background: rgba(255, 255, 255, 0.02);
23+
background: rgba(255, 255, 255, 0.03);
24+
transform: translateX(2px);
2425
}
2526

2627
.overview-gpu-name {
@@ -99,11 +100,12 @@
99100
.single-gpu-overview {
100101
cursor: pointer;
101102
padding: 0 0 var(--space-md) 0;
102-
transition: background 0.1s;
103+
transition: background var(--transition-fast), transform var(--transition-fast);
103104
}
104105

105106
.single-gpu-overview:hover {
106-
background: rgba(255, 255, 255, 0.02);
107+
background: rgba(255, 255, 255, 0.03);
108+
transform: translateX(2px);
107109
}
108110

109111
.sgo-header {
@@ -337,21 +339,26 @@
337339
height: 100%;
338340
background: var(--bar-fill);
339341
border-radius: 2px;
342+
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
340343
}
341344

342345
/* Metric-identity colored bullet fills */
343346
.bullet-fill[data-metric="utilization"] {
344347
background: rgba(var(--metric-util), 0.35);
345348
}
349+
346350
.bullet-fill[data-metric="temperature"] {
347351
background: rgba(var(--metric-temp), 0.30);
348352
}
353+
349354
.bullet-fill[data-metric="memory"] {
350355
background: rgba(var(--metric-mem), 0.35);
351356
}
357+
352358
.bullet-fill[data-metric="power"] {
353359
background: rgba(var(--metric-power), 0.30);
354360
}
361+
355362
.bullet-fill[data-metric="fan"] {
356363
background: rgba(var(--metric-fan), 0.30);
357364
}
@@ -441,7 +448,7 @@
441448
}
442449

443450
/* Constrain Chart.js wrapper and canvas so they cannot grow (fixes canvas height runaway) */
444-
.sparkline-canvas-wrap > div {
451+
.sparkline-canvas-wrap>div {
445452
height: 100% !important;
446453
max-height: 90px !important;
447454
min-height: 0;
@@ -541,6 +548,15 @@
541548
padding: var(--space-sm) 0;
542549
align-items: center;
543550
border-bottom: 1px solid var(--border-subtle);
551+
transition: background var(--transition-fast);
552+
}
553+
554+
.process-item:nth-child(even) {
555+
background: rgba(255, 255, 255, 0.015);
556+
}
557+
558+
.process-item:hover {
559+
background: rgba(255, 255, 255, 0.04);
544560
}
545561

546562
.process-item:last-child {
@@ -672,18 +688,34 @@
672688
right: var(--space-lg);
673689
top: var(--space-lg);
674690
width: 280px;
675-
background: var(--bg-surface);
676-
border: 1px solid var(--border-subtle);
691+
background: var(--bg-elevated);
692+
border: 1px solid rgba(255, 183, 77, 0.2);
693+
border-left: 3px solid rgba(255, 183, 77, 0.6);
677694
border-radius: 8px;
678695
padding: var(--space-md);
679696
z-index: 1000;
680-
transition: opacity 0.2s, transform 0.2s;
697+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3), 0 0 40px rgba(255, 183, 77, 0.06);
698+
animation: toast-slide-in 0.35s cubic-bezier(0.4, 0, 0.2, 1) both;
699+
}
700+
701+
@keyframes toast-slide-in {
702+
from {
703+
opacity: 0;
704+
transform: translateY(-12px) scale(0.97);
705+
}
706+
707+
to {
708+
opacity: 1;
709+
transform: translateY(0) scale(1);
710+
}
681711
}
682712

683713
.star-toast.is-hidden {
684714
opacity: 0;
685715
transform: translateY(-8px);
686716
pointer-events: none;
717+
animation: none;
718+
transition: opacity 0.2s, transform 0.2s;
687719
}
688720

689721
.star-toast-close {
@@ -738,7 +770,7 @@
738770

739771
.star-toast-progress-fill {
740772
height: 100%;
741-
background: var(--text-tertiary);
773+
background: rgba(255, 183, 77, 0.5);
742774
border-radius: 1.5px;
743775
transition: width 0.5s ease;
744776
}
@@ -750,28 +782,29 @@
750782

751783
.star-toast-btn {
752784
width: 100%;
753-
border: 1px solid var(--border-subtle);
785+
border: 1px solid rgba(255, 183, 77, 0.3);
754786
border-radius: 6px;
755787
padding: var(--space-sm) var(--space-md);
756788
font-size: 12px;
757789
font-weight: 600;
758790
cursor: pointer;
759-
background: transparent;
760-
color: var(--text-secondary);
791+
background: rgba(255, 183, 77, 0.1);
792+
color: rgba(255, 200, 100, 0.9);
761793
display: inline-flex;
762794
align-items: center;
763795
justify-content: center;
764796
gap: var(--space-sm);
765-
transition: background 0.1s, color 0.1s;
797+
transition: background 0.15s, color 0.15s, border-color 0.15s;
766798
}
767799

768800
.star-toast-btn svg {
769801
flex-shrink: 0;
770802
}
771803

772804
.star-toast-btn:hover {
773-
background: rgba(255, 255, 255, 0.04);
774-
color: var(--text-primary);
805+
background: rgba(255, 183, 77, 0.18);
806+
color: rgba(255, 210, 130, 1);
807+
border-color: rgba(255, 183, 77, 0.45);
775808
}
776809

777810
/* ============================================
@@ -809,6 +842,17 @@
809842
border-left: 1px solid var(--border-subtle);
810843
}
811844

845+
.drawer::before {
846+
content: '';
847+
display: none;
848+
width: 32px;
849+
height: 4px;
850+
background: rgba(255, 255, 255, 0.15);
851+
border-radius: 2px;
852+
margin: var(--space-sm) auto 0;
853+
flex-shrink: 0;
854+
}
855+
812856
.drawer.open {
813857
transform: translateX(0);
814858
}
@@ -1054,6 +1098,10 @@
10541098
max-width: 100vw;
10551099
}
10561100

1101+
.drawer::before {
1102+
display: block;
1103+
}
1104+
10571105
.sgo-metrics-row {
10581106
flex-direction: column;
10591107
align-items: stretch;
@@ -1184,4 +1232,4 @@
11841232
.process-pid {
11851233
display: none;
11861234
}
1187-
}
1235+
}

0 commit comments

Comments
 (0)