ADD AMD MONITOR BETA

This commit is contained in:
Anonymzx
2026-05-16 05:41:56 +07:00
parent 994eaee55a
commit f1dee05ca8
23 changed files with 3492 additions and 968 deletions
+14 -5
View File
@@ -2,6 +2,7 @@
__pycache__/
*.py[cod]
*$py.class
*.pyc
# Virtual environment
.venv/
@@ -18,8 +19,10 @@ Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
# Python build
# Build
build/
dist/
*.egg-info/
@@ -28,12 +31,18 @@ dist/
temp/
tmp/
# ComfyUI cache
# ComfyUI
*.cache
# Output files
output/
input/
# PyInstaller
*.spec
*.spec
# Bundled LibreHardwareMonitor
monitor/libre_hardware_monitor/
monitor/*.zip
monitor/*.exe
# Test files we don't want in repo
_detect_adlx.py
+287 -207
View File
@@ -1,254 +1,334 @@
# BangtrixToolkit
<div align="center">
Advanced Custom Node Toolkit for ComfyUI focused on:
- AI Prompt Translation
- AMD ROCm Utilities
- Workflow Enhancements
- Performance Utilities
# 🚀 BangtrixToolkit
**Advanced Custom Node Toolkit for ComfyUI**
![GitHub](https://img.shields.io/badge/ComfyUI-Custom_Node-FF6B6B?style=flat-square)
![GitHub](https://img.shields.io/badge/Python-3.10%2B-3776AB?style=flat-square&logo=python)
![GitHub](https://img.shields.io/github/license/Anonymzx/BangtrixToolkit?style=flat-square)
![GitHub](https://img.shields.io/github/stars/Anonymzx/BangtrixToolkit?style=flat-square)
> *Translate your prompts & monitor your AMD GPU — all inside ComfyUI*
---
<details close>
<summary>
# 🌐 Bangtrix Translate Universal
[🌐 Translate](#-bangtrix-translate-universal) •
[🖥️ AMD Monitor](#-amd-monitor--real-time-gpu-overlay-) •
[📦 Installation](#-installation) •
[🚀 Roadmap](#-roadmap)
</summary>
---
</div>
---
<details>
<summary><b>🌐 Bangtrix Translate Universal</b> — Click to expand</summary>
<br>
Bangtrix Translate Universal is an advanced translation node for ComfyUI that automatically translates prompts into English and directly outputs them into CLIP conditioning.
Designed for:
- Stable Diffusion
- Flux
- SDXL
- Pony
- Anime Models
- Realistic Models
- Any CLIP-based workflow
Designed for: **Stable Diffusion · Flux · SDXL · Pony · Anime Models · Realistic Models · Any CLIP-based workflow**
---
# ✨ Features
### ✨ Features
## 🌍 Translation
- Automatic Google Translate integration
- Multi-language support
- Real-time prompt translation
- Offline-safe fallback
| Category | Features |
|----------|----------|
| 🌍 **Translation** | Google Translate integration · Multi-language support · Offline-safe fallback |
| 🧠 **Enhancement** | Quality presets · Style presets · Prompt cleaning · Enhancement system |
| ❌ **Auto Negative** | Anatomy fixes · Blur reduction · Watermark filtering · Artifact reduction · Style-based negatives |
| ⚡ **Performance** | Translation cache · Fast repeated translation · Lightweight · Universal compatibility |
---
## 🧠 Prompt Enhancement
- Quality presets
- Style presets
- Prompt cleaning
- Prompt enhancement system
### 📝 Supported Languages
<div align="center">
| Code | Language | Code | Language |
|------|----------|------|----------|
| `auto` | Auto Detect | `en` | English |
| `id` | Indonesian | `ja` | Japanese |
| `ko` | Korean | `zh-CN` | Chinese (Simplified) |
| `zh-TW` | Chinese (Traditional) | `fr` | French |
| `de` | German | `es` | Spanish |
| `pt` | Portuguese | `ru` | Russian |
| `it` | Italian | `ar` | Arabic |
| `tr` | Turkish | `hi` | Hindi |
| `th` | Thai | `vi` | Vietnamese |
</div>
---
## ❌ Auto Negative
Automatically generates optimized negative prompts.
### 🎨 Presets
Supports:
- anatomy fixes
- blur reduction
- watermark filtering
- artifact reduction
- style-based negatives
<div align="center">
| Quality | Style | Auto Negative |
|---------|-------|---------------|
| `normal` — Standard | `none` — No style | `normal` — Basic |
| `high` — Better details | `anime` · `realistic` · `cinematic` | `strong` — Reduced artifacts |
| `ultra` — Maximum | `photography` · `product` · `fantasy` · `portrait` | `ultra` — Maximum cleanup |
</div>
---
## ⚡ Performance
- Translation cache system
- Fast repeated translation
- Lightweight architecture
- Universal model compatibility
### ⚙️ Workflow Example
---
<details>
<summary><b>See node inputs & outputs</b></summary>
# 📝 Supported Languages
<br>
| Code | Language |
|------|------|
| auto | Auto Detect |
| id | Indonesian |
| en | English |
| ja | Japanese |
| ko | Korean |
| zh-CN | Simplified Chinese |
| zh-TW | Traditional Chinese |
| fr | French |
| de | German |
| es | Spanish |
| pt | Portuguese |
| ru | Russian |
| it | Italian |
| ar | Arabic |
| tr | Turkish |
| hi | Hindi |
| th | Thai |
| vi | Vietnamese |
**Inputs:** `positive_prompt` · `negative_prompt` · `source_language` · `target_language` · `quality_preset` · `style_preset` · `enable_translate` · `enable_enhance` · `enable_clean` · `enable_cache` · `enable_auto_negative` · `auto_negative_strength` · `show_original`
---
# 🎨 Quality Presets
| Preset | Description |
|------|------|
| normal | Standard quality enhancement |
| high | Better details and sharpness |
| ultra | Maximum quality enhancement |
---
# 🎭 Style Presets
| Style | Description |
|------|------|
| none | No style enhancement |
| anime | Anime style optimization |
| realistic | Realistic photography enhancement |
| cinematic | Cinematic lighting and shadows |
| photography | Professional photography style |
| product | Product photography optimization |
| fantasy | Fantasy art enhancement |
| portrait | Portrait optimization |
---
# ❌ Auto Negative Strength
| Strength | Description |
|------|------|
| normal | Basic negative prompt |
| strong | Improved artifact reduction |
| ultra | Maximum cleanup and artifact filtering |
---
---
# ⚙️ Node Inputs
| Input | Description |
|------|------|
| positive_prompt | Positive prompt |
| negative_prompt | Negative prompt |
| source_language | Original language |
| target_language | Translation target language |
| quality_preset | Quality enhancement |
| style_preset | Style enhancement |
| enable_translate | Enable translation |
| enable_enhance | Enable prompt enhancement |
| enable_clean | Clean newline formatting |
| enable_cache | Enable translation cache |
| enable_auto_negative | Enable automatic negative prompts |
| auto_negative_strength | Auto negative strength |
| show_original | Show original + translated text |
---
# 📤 Outputs
| Output | Description |
|------|------|
| positive_conditioning | CLIP positive conditioning |
| negative_conditioning | CLIP negative conditioning |
| positive_prompt_final | Final translated positive prompt |
| negative_prompt_final | Final translated negative prompt |
---
# 🔥 Workflow Example
<img width="570" height="792" alt="image" src="https://github.com/user-attachments/assets/44a6aeb5-1b15-4461-be2c-ec98291ac2e9" />
<img width="1800" height="1121" alt="image" src="https://github.com/user-attachments/assets/36b098b9-4a4e-4787-aaea-8f249674a151" />
---
## Indonesian Input
```text
wanita cantik memakai kimono di malam hari
```
## Final English Prompt
```text
beautiful woman wearing kimono at night, cinematic lighting, high quality, detailed
```
---
# 💡 Tips
- Use `show_original` to debug translations
- Enable `enable_cache` for faster workflows
- Use `ultra` quality for SDXL and Flux
- Use `anime` preset for anime models
- Use `realistic` or `photography` for realism models
---
**Outputs:** `positive_conditioning` · `negative_conditioning` · `positive_prompt_final` · `negative_prompt_final`
</details>
# 📦 Installation
<br>
## Manual Installation
<div align="center">
Go to your ComfyUI custom_nodes folder:
| Indonesian Input | Final English Prompt |
|-----------------|---------------------|
| `wanita cantik memakai kimono di malam hari` | `beautiful woman wearing kimono at night, cinematic lighting, high quality, detailed` |
</div>
---
<div align="center">
<img width="570" alt="Workflow" src="https://github.com/user-attachments/assets/44a6aeb5-1b15-4461-be2c-ec98291ac2e9" />
<br><br>
<img width="900" alt="Workflow detail" src="https://github.com/user-attachments/assets/36b098b9-4a4e-4787-aaea-8f249674a151" />
</div>
---
### 💡 Tips
> 🔹 Use `show_original` to debug translations
> 🔹 Enable `enable_cache` for faster workflows
> 🔹 Use `ultra` quality for SDXL and Flux
> 🔹 Use `anime` preset for anime models
> 🔹 Use `realistic` or `photography` for realism models
</details>
---
<details open>
<summary><b>🖥️ AMD Monitor — Real-Time GPU Overlay</b> — Click to expand</summary>
<br>
> **Zero configuration. Plug & play.** AMD Monitor auto-loads when ComfyUI starts — no node needed.
---
### ✨ Features
<div align="center">
| | Feature | Description |
|:---:|---------|-------------|
| 📊 | **GPU Load** | Real-time utilization % with animated bar |
| 💾 | **VRAM Usage** | Used / Total in GB + utilization bar |
| 🌡️ | **Temperature** | GPU temp in °C/°F *(requires LibreHardwareMonitor)* |
| 💨 | **Fan Speed** | Fan RPM/percentage *(requires LibreHardwareMonitor)* |
| 🏷️ | **GPU Name** | Auto-detects your GPU model *(e.g. RX 7800 XT)* |
| 📈 | **Sparkline Chart** | 30-second GPU Load history graph |
| ⚡ | **Generation Stats** | Duration, RAM Peak, Delta & CPU Peak |
| 🖥️ | **Multi-GPU** | Chip selector for multi-GPU systems |
| ⚠️ | **VRAM Alert** | Toast + inline banner on threshold breach |
| 🖱️ | **Draggable** | Click & drag to reposition |
| ➖ | **Minimize** | Toggle with `−` button |
| ⌨️ | **Hide/Show** | `Ctrl + Shift + M` shortcut |
| ⚙️ | **Settings Panel** | Interval, Alert %, Temp unit, Sparkline toggle |
| 💾 | **Persist Config** | Position & settings saved in localStorage |
</div>
---
### 📸 Preview
<!-- Replace with your own screenshot -->
<!-- <img src="docs/screenshot_overlay.png" width="300" align="right"/> -->
```
┌────────────────────────────┐
│ 🔴 AMD Monitor │
├────────────────────────────┤
│ AMD Radeon RX 7800 XT │
│ ┌───────┬──────────┐ │
│ │GPU │ VRAM │ │
│ │ 1.2% │1.1/4.0 GB│ │
│ │ █░░░░░│███░░░░░░│ │
│ ├───────┼──────────┤ │
│ │ Temp │ Fan │ │
│ │ 45°C │1200 RPM │ │
│ │ ███░░░│ █░░░░░░░│ │
│ └───────┴──────────┘ │
│ ╱ GPU Load History ╲ │
│ ────────────────────── │
│ 🟢 RX 7800 XT │ [ps] │
└────────────────────────────┘
```
---
### ⚙️ Architecture
```
┌──────────────────────────────────────────────┐
│ ComfyUI Server │
│ ┌──────────────────────────────────────┐ │
│ │ on_app_started() │ │
│ │ ├── WS Route → /ws/amd_monitor │ │
│ │ └── Stream loop → every 1s │ │
│ └──────────────┬───────────────────────┘ │
│ │ WebSocket │
│ ┌──────────────▼───────────────────────┐ │
│ │ Backend Auto-Detect │ │
│ │ │ │
│ │ 1. LibreHardwareMonitor ← WMI │ │
│ │ → Temp, Fan, Clock, Power │ │
│ │ │ │
│ │ 2. PowerShell Counters ★ ACTIVE │ │
│ │ → GPU Load, VRAM, GPU Name │ │
│ │ │ │
│ │ 3. ADL (atiadlxx.dll) │ │
│ │ → Temp, Fan (partial) │ │
│ │ │ │
│ │ 4. psutil (system RAM) │ │
│ └──────────────────────────────────────┘ │
│ ┌──────────────────────────────────────┐ │
│ │ Comfy Process Monitor │ │
│ │ Auto-detect generations │ │
│ └──────────────────────────────────────┘ │
└──────────────────┬───────────────────────────┘
│ JSON every 1s
┌──────────────────▼───────────────────────────┐
│ Browser (ComfyUI) │
│ ┌──────────────────────────────────────┐ │
│ │ Floating Overlay (amd_monitor.js) │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```
---
### 🏗️ Backend Priority
<div align="center">
| Priority | Backend | Metrics | Platform |
|:--------:|---------|---------|----------|
| 🥇 | **LibreHardwareMonitor** | Temp, Fan, Clock, Power | Windows |
| 🥈 | **PowerShell Counters** | Load, VRAM, Name | Windows |
| 🥉 | **ADL** (atiadlxx.dll) | Temp, Fan | Windows |
| 4 | **psutil** | System RAM only | All |
</div>
---
### 🌡️ LibreHardwareMonitor Setup
To enable **Temperature, Fan Speed, Clock speeds & Power Draw**:
1. **Auto-download** — LHM portable (8.5MB) is downloaded automatically on first ComfyUI start
```
📁 BangtrixToolkit/monitor/libre_hardware_monitor/LibreHardwareMonitor.exe
```
2. **Run as Administrator** (one-time):
```powershell
.\monitor\libre_hardware_monitor\LibreHardwareMonitor.exe --wmi
```
3. **Restart ComfyUI** — Backend auto-switches to LHM 🎯
> ⚠️ **Without LHM**, core metrics still work via PowerShell Counters: GPU Load, VRAM, GPU Name.
---
### ⌨️ Quick Reference
| Action | Keyboard / Button |
|--------|------------------|
| Toggle overlay | `Ctrl + Shift + M` |
| Minimize | `−` button |
| Settings | `⚙` button |
---
### 📋 Requirements
| Package | Needed | Purpose |
|---------|:------:|---------|
| `aiohttp` | ✅ **Yes** | WebSocket server |
| `psutil` | ✅ Recommended | Process Monitor |
| `requests` | ❌ Optional | Translate node fallback |
</details>
---
## 📦 Installation
<details>
<summary><b>See installation guide</b></summary>
<br>
### 🔧 Manual
```bash
cd "ComfyUI/custom_nodes"
```
Clone repository:
```bash
git clone https://github.com/Anonymzx/BangtrixToolkit.git
```
Install dependencies:
```bash
pip install -r requirements.txt
```
Restart ComfyUI.
> **Restart ComfyUI** after installation.
### 📥 ComfyUI Manager
1. Open **ComfyUI Manager**
2. Go to **Custom Nodes Manager**
3. Search for **BangtrixToolkit**
4. Click **Install**
5. **Restart** ComfyUI
</details>
---
# 🧩 Installation via ComfyUI Manager
## 🚀 Roadmap
1. Open ComfyUI
2. Open ComfyUI Manager
3. Open:
- Custom Nodes Manager
4. Search:
- BangtrixToolkit
5. Click:
- Install
6. Restart ComfyUI
<div align="center">
| Version | Features |
|:-------:|----------|
| **v1** ✅ | Bangtrix Translate Universal |
| **v2** ✅ | AMD Monitor · ROCm Utilities |
| **v3** 🔜 | Smart Cache · Performance Optimizer · Benchmark Tools |
</div>
---
# 🚀 Roadmap
<div align="center">
## v1
- Bangtrix Translate Universal
### ❤️ Created by [Anonymzx](https://github.com/Anonymzx)
## v2
- AMD Monitor
- ROCm Utilities
## v3
- Smart Cache
- Performance Optimizer
- Benchmark Tools
---
# ❤️ Credits
Created by:
- Anonymzx
</div>
+26 -30
View File
@@ -1,24 +1,26 @@
"""
BANGTRIXTOOLKIT - ComfyUI Custom Nodes
======================================
AMD Monitor & Translate Universal
Translate Universal + AMD Monitor Overlay
"""
from .nodes.translate_universal import (
NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS
)
import importlib
from .nodes.amd_monitor import (
NODE_CLASS_MAPPINGS as AMD_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as AMD_NODE_DISPLAY_NAME_MAPPINGS
)
# Dynamic import — hanya load node yang benar-benar ada
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# Gabungkan semua node mappings
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **AMD_NODE_CLASS_MAPPINGS}
NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **AMD_NODE_DISPLAY_NAME_MAPPINGS}
try:
_mod = importlib.import_module(".nodes.translate_universal", package=__package__)
if hasattr(_mod, "NODE_CLASS_MAPPINGS"):
NODE_CLASS_MAPPINGS.update(_mod.NODE_CLASS_MAPPINGS)
if hasattr(_mod, "NODE_DISPLAY_NAME_MAPPINGS"):
NODE_DISPLAY_NAME_MAPPINGS.update(_mod.NODE_DISPLAY_NAME_MAPPINGS)
print("✅ BANGTRIXTOOLKIT: Loaded translate_universal")
except Exception as e:
print(f"⚠️ BANGTRIXTOOLKIT: Skipped translate_universal: {e}")
WEB_DIRECTORY = "./web"
WEB_DIRECTORY = "web"
__all__ = [
"NODE_CLASS_MAPPINGS",
@@ -28,45 +30,39 @@ __all__ = [
# ============================================================
# 🚀 SERVER EXTENSION - WebSocket untuk Real-Time Monitoring
# 🚀 SERVER EXTENSION — AMD Monitor Overlay (WebSocket)
# ============================================================
async def on_app_started(app):
"""Register WebSocket endpoint for AMD Monitor (FIXED: 1 argument only)"""
"""Register WebSocket endpoint for AMD Monitor overlay"""
import asyncio
from aiohttp import web
from .server.amd_server import get_amd_server
amd_server = get_amd_server()
async def websocket_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
await amd_server.handle_client(ws)
return ws
# Register route (safe: won't error if already registered)
try:
app.router.add_get('/ws/amd_monitor', websocket_handler)
print("🔴 BANGTRIXTOOLKIT: AMD Monitor WebSocket at /ws/amd_monitor")
except RuntimeError:
pass # Route already exists
# Start monitoring in background
pass # Route already registered
if not amd_server.running:
amd_server.running = True
asyncio.create_task(amd_server.stream_data())
print("✅ BANGTRIXTOOLKIT: AMD Monitor streaming started")
amd_server.start_streaming(asyncio.get_event_loop())
print("✅ BANGTRIXTOOLKIT: AMD Monitor overlay streaming started")
# Register with ComfyUI (FIXED: lambda to match signature)
# Register with ComfyUI
try:
from server import PromptServer
prompt_server = PromptServer.instance
# Append our async handler (ComfyUI only passes 'app')
prompt_server.app.on_startup.append(on_app_started)
print("✅ BANGTRIXTOOLKIT: Server extension registered")
print("✅ BANGTRIXTOOLKIT: Server extension registered (AMD Monitor overlay)")
except Exception as e:
print(f"⚠️ BANGTRIXTOOLKIT: Server extension skipped: {e}")
+23
View File
@@ -0,0 +1,23 @@
"""
BangtrixToolkit — Hardware Monitoring Package
===============================================
Auto-detects best GPU monitoring backend.
Usage:
from monitor import get_amd_monitor
monitor = get_amd_monitor()
stats = monitor.get_gpu_stats(0)
print(stats.gpu_name, stats.temperature)
"""
from .amd_monitor import get_amd_monitor, AMDMonitor, AMDGPUStats
from .process_monitor import get_process_monitor, ComfyProcessMonitor, GenerationRecord
__all__ = [
"get_amd_monitor",
"AMDMonitor",
"AMDGPUStats",
"get_process_monitor",
"ComfyProcessMonitor",
"GenerationRecord",
]
+115
View File
@@ -0,0 +1,115 @@
"""
AMD Monitor - Unified GPU Monitoring
=====================================
Auto-detects best available backend:
1. LibreHardwareMonitor (Temperature, Fan, Load, Clocks, Power)
2. ADL (AMD Display Library)
3. PowerShell Counters (Utilization, VRAM, GPU name)
4. psutil (System RAM fallback)
Usage:
from monitor import get_amd_monitor
monitor = get_amd_monitor()
stats = monitor.get_gpu_stats(0)
print(stats.gpu_name, stats.utilization_gpu, stats.temperature)
"""
import logging
from dataclasses import dataclass
from typing import Optional, List
from .backends import (
MonitorBackend, AMDGPUStats,
PowerShellBackend,
ADLBackend,
LibreHardwareBackend,
PsutilBackend,
)
logger = logging.getLogger(__name__)
class AMDMonitor:
"""Unified monitor that auto-selects best available backend"""
def __init__(self):
self.available = False
self.method = None
self.gpu_count = 0
self.gpu_names: List[str] = []
self._backend: Optional[MonitorBackend] = None
self._selected_backend_name: Optional[str] = None
self._initialize()
def _initialize(self):
"""Try backends in priority order"""
backends = [
("libre-hardware-monitor", LibreHardwareBackend),
("powershell-counters", PowerShellBackend),
("adl", ADLBackend),
("psutil-fallback", PsutilBackend),
]
for name, cls in backends:
try:
backend = cls()
if backend.initialize():
self._backend = backend
self._selected_backend_name = name
self.available = True
self.gpu_count = backend.gpu_count
self.gpu_names = backend.gpu_names
self.method = name
logger.info(f"AMD Monitor: using {name} — {self.gpu_count} GPU(s)")
return
except Exception as e:
logger.debug(f"AMD Monitor: {name} failed: {e}")
logger.error("AMD Monitor: no backend available")
def get_gpu_stats(self, gpu_id: int = 0) -> AMDGPUStats:
"""Get stats for a specific GPU"""
if not self._backend or not self.available:
return AMDGPUStats(
gpu_id=gpu_id,
is_available=False,
error_message="No backend available"
)
try:
stats = self._backend.get_stats(gpu_id)
stats.gpu_name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else stats.gpu_name
return stats
except Exception as e:
return AMDGPUStats(
gpu_id=gpu_id,
is_available=False,
error_message=str(e)
)
def get_all_gpu_stats(self) -> List[AMDGPUStats]:
"""Get stats for all detected GPUs"""
if not self._backend or self.gpu_count == 0:
return []
return [self.get_gpu_stats(i) for i in range(self.gpu_count)]
def close(self):
"""Cleanup backend resources"""
if self._backend:
self._backend.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# Singleton
_amd_monitor = None
def get_amd_monitor() -> AMDMonitor:
global _amd_monitor
if _amd_monitor is None:
_amd_monitor = AMDMonitor()
return _amd_monitor
+19
View File
@@ -0,0 +1,19 @@
"""
Backend monitoring modules for AMD GPU stats.
Auto-detects best available backend.
"""
from .base import MonitorBackend, AMDGPUStats
from .powershell_backend import PowerShellBackend
from .adl_backend import ADLBackend
from .libre_hardware import LibreHardwareBackend
from .psutil_backend import PsutilBackend
__all__ = [
"MonitorBackend",
"AMDGPUStats",
"PowerShellBackend",
"ADLBackend",
"LibreHardwareBackend",
"PsutilBackend",
]
+113
View File
@@ -0,0 +1,113 @@
"""
ADL (AMD Display Library) Backend
==================================
Direct ctypes binding to atiadlxx.dll on Windows.
"""
import ctypes
import logging
import platform
from ctypes import byref, c_int, c_void_p, c_char, POINTER, Structure
from .base import MonitorBackend, AMDGPUStats
logger = logging.getLogger(__name__)
class ADLTemperature(Structure):
_fields_ = [("iSize", c_int), ("iTemperature", c_int)]
class ADLFanSpeedValue(Structure):
_fields_ = [("iSize", c_int), ("iSpeedType", c_int), ("iFanSpeed", c_int), ("iFlags", c_int)]
class ADLPMActivity(Structure):
_fields_ = [
("iSize", c_int), ("iEngineClock", c_int), ("iMemoryClock", c_int),
("iVddc", c_int), ("iActivityPercent", c_int), ("iCurrentPerformanceLevel", c_int),
("iCurrentBusSpeed", c_int), ("iCurrentBusLanes", c_int), ("iUnknown", c_int * 4),
]
class ADLBackend(MonitorBackend):
name = "adl"
def __init__(self):
super().__init__()
self._dll = None
self._context = c_int(0)
def initialize(self) -> bool:
if platform.system() != "Windows":
return False
try:
self._dll = ctypes.windll.LoadLibrary("atiadlxx.dll")
except:
try:
self._dll = ctypes.windll.LoadLibrary("atiadlxy.dll")
except:
logger.debug("ADL Backend: atiadlxx.dll not found")
return False
# Try to initialize ADL
try:
create = self._dll.ADL_Main_Control_Create
create.restype = c_int
result = create(0) # ADL_Main_Control_Create with default
if result != 0:
logger.debug(f"ADL Backend: init failed: {result}")
return False
self.available = True
self.gpu_count = 1
self.gpu_names = ["AMD GPU (ADL)"]
logger.info("ADL Backend: initialized")
return True
except Exception as e:
logger.debug(f"ADL Backend: init error: {e}")
return False
def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
stats = AMDGPUStats(gpu_id=gpu_id, gpu_name=self.gpu_names[0], is_available=True)
try:
# Temperature
temp = ADLTemperature()
temp.iSize = ctypes.sizeof(ADLTemperature)
get_temp = self._dll.ADL_Overdrive5_Temperature_Get
get_temp.restype = c_int
if get_temp(gpu_id, 0, byref(temp)) == 0:
stats.temperature = temp.iTemperature / 1000.0
# Activity
activity = ADLPMActivity()
activity.iSize = ctypes.sizeof(ADLPMActivity)
get_act = self._dll.ADL_Overdrive5_CurrentActivity_Get
get_act.restype = c_int
if get_act(gpu_id, byref(activity)) == 0:
stats.utilization_gpu = float(activity.iActivityPercent)
stats.core_clock = activity.iEngineClock
stats.memory_clock = activity.iMemoryClock
# Fan speed
fan = ADLFanSpeedValue()
fan.iSize = ctypes.sizeof(ADLFanSpeedValue)
fan.iSpeedType = 0
get_fan = self._dll.ADL_Overdrive5_FanSpeed_Get
get_fan.restype = c_int
if get_fan(gpu_id, 0, byref(fan)) == 0:
stats.fan_speed = fan.iFanSpeed
except Exception as e:
stats.is_available = False
stats.error_message = str(e)
return stats
def close(self):
try:
destroy = self._dll.ADL_Main_Control_Destroy
destroy.restype = c_int
destroy()
except:
pass
+69
View File
@@ -0,0 +1,69 @@
"""
Base classes for monitoring backends.
"""
from dataclasses import dataclass, field
from typing import Optional, List
@dataclass
class AMDGPUStats:
"""Normalized GPU statistics from any backend"""
gpu_id: int = 0
gpu_name: str = ""
utilization_gpu: float = 0.0 # 0-100%
utilization_memory: float = 0.0 # 0-100%
memory_total: int = 0 # Bytes
memory_used: int = 0 # Bytes
memory_free: int = 0 # Bytes
temperature: float = 0.0 # Celsius
fan_speed: int = 0 # 0-100%
core_clock: int = 0 # MHz
memory_clock: int = 0 # MHz
power_draw: float = 0.0 # Watts
is_available: bool = True
error_message: Optional[str] = None
def to_dict(self) -> dict:
return {
'gpu_id': self.gpu_id,
'gpu_name': self.gpu_name,
'utilization_gpu': round(self.utilization_gpu, 1),
'utilization_memory': round(self.utilization_memory, 1),
'memory_total': self.memory_total,
'memory_used': self.memory_used,
'memory_free': self.memory_free,
'temperature': round(self.temperature, 1),
'fan_speed': self.fan_speed,
'core_clock': self.core_clock,
'memory_clock': self.memory_clock,
'power_draw': round(self.power_draw, 1),
'is_available': self.is_available,
}
class MonitorBackend:
"""Base class for all monitoring backends"""
name = "base"
def __init__(self):
self.available = False
self.gpu_count = 0
self.gpu_names: List[str] = []
def initialize(self) -> bool:
"""Detect hardware and prepare backend. Returns True if available."""
raise NotImplementedError
def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
"""Get current stats for a GPU."""
raise NotImplementedError
def get_all_stats(self) -> List[AMDGPUStats]:
"""Get stats for all GPUs."""
return [self.get_stats(i) for i in range(self.gpu_count)]
def close(self):
"""Cleanup resources."""
pass
+175
View File
@@ -0,0 +1,175 @@
"""
LibreHardwareMonitor Backend
============================
Integrates LibreHardwareMonitor (LHM) for full GPU telemetry.
- Auto-detects if LHM is running via WMI
- Auto-downloads LHM portable if not present
- Does NOT auto-run (requires admin) — user runs installer once
- Reads: Temperature, Fan Speed, GPU Load, VRAM, Clocks, Power
"""
import logging
import subprocess
import platform
import os
import json
import urllib.request
import zipfile
from .base import MonitorBackend, AMDGPUStats
logger = logging.getLogger(__name__)
LHM_DOWNLOAD_URL = "https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/releases/download/v0.9.6/LibreHardwareMonitor.NET.10.zip"
class LibreHardwareBackend(MonitorBackend):
name = "libre-hardware-monitor"
def __init__(self):
super().__init__()
self._lhm_exe = None
def initialize(self) -> bool:
if platform.system() != "Windows":
return False
# Check if LHM/OHM sensors are accessible via WMI
if self._detect_wmi_sensors():
logger.info("LHM Backend: detected running instance via WMI")
self.available = True
self.gpu_count = 1
self.gpu_names = ["AMD GPU (LHM)"]
return True
# Download LHM if missing (user just needs to run it)
self._ensure_downloaded()
# Check if exe exists (downloaded but not running)
if self._lhm_exe and os.path.exists(self._lhm_exe):
logger.info(
"LHM Backend: downloaded but not running. "
"Run as Admin: 'monitor\\libre_hardware_monitor\\LibreHardwareMonitor.exe --wmi'"
)
return False
def _get_lhm_dir(self) -> str:
"""LHM storage directory"""
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return os.path.join(base, "libre_hardware_monitor")
def _detect_wmi_sensors(self) -> bool:
"""Check via PowerShell if LHM/OHM WMI sensors exist"""
namespaces = ['root/LibreHardwareMonitor', 'root/OpenHardwareMonitor']
for ns in namespaces:
try:
cmd = [
"powershell", "-NoProfile", "-Command",
f"Get-WmiObject -Namespace '{ns}' -Class Sensor "
f"2>$null | Select-Object -First 1 | ConvertTo-Json"
]
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=3,
creationflags=subprocess.CREATE_NO_WINDOW
)
if result.returncode == 0 and result.stdout.strip() and 'null' not in result.stdout:
return True
except:
pass
return False
def _ensure_downloaded(self):
"""Download LHM portable if not already present"""
lhm_dir = self._get_lhm_dir()
os.makedirs(lhm_dir, exist_ok=True)
# Find existing exe
for root, dirs, files in os.walk(lhm_dir):
for f in files:
if f.lower() == "librehardwaremonitor.exe":
self._lhm_exe = os.path.join(root, f)
return
# Download
zip_path = os.path.join(lhm_dir, "lhm_download.zip")
logger.info("LHM: downloading portable (8.5MB)...")
try:
req = urllib.request.Request(
LHM_DOWNLOAD_URL,
headers={'User-Agent': 'BangtrixToolkit/1.0'}
)
with urllib.request.urlopen(req, timeout=120) as resp:
with open(zip_path, 'wb') as f:
f.write(resp.read())
if os.path.getsize(zip_path) < 500000:
logger.error("LHM: download failed (too small)")
os.remove(zip_path)
return
logger.info("LHM: extracting...")
with zipfile.ZipFile(zip_path, 'r') as zf:
zf.extractall(lhm_dir)
os.remove(zip_path)
# Find exe location after extract
for root, dirs, files in os.walk(lhm_dir):
for f in files:
if f.lower() == "librehardwaremonitor.exe":
self._lhm_exe = os.path.join(root, f)
logger.info(f"LHM: downloaded to {self._lhm_exe}")
return
except Exception as e:
logger.warning(f"LHM download error: {e}")
if os.path.exists(zip_path):
os.remove(zip_path)
def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
"""Get GPU stats from LHM/OHM via WMI"""
name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
stats = AMDGPUStats(gpu_id=gpu_id, gpu_name=name, is_available=True)
# Only works if LHM is running
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"$sensors = Get-WmiObject -Namespace 'root/LibreHardwareMonitor' -Class Sensor "
"2>$null; if (-not $sensors) { $sensors = Get-WmiObject -Namespace "
"'root/OpenHardwareMonitor' -Class Sensor 2>$null }; "
"$sensors | Where-Object { $_.Value -gt 0 } | "
"Select-Object SensorType, Name, Value | ConvertTo-Json"
]
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW
)
if result.returncode == 0 and result.stdout.strip():
data = json.loads(result.stdout.strip())
if isinstance(data, dict):
data = [data]
for item in data:
stype = item.get('SensorType', '')
sname = item.get('Name', '')
svalue = float(item.get('Value', 0))
if stype == 'Temperature' and 'GPU' in sname:
stats.temperature = svalue
elif stype == 'Fan' and 'GPU' in sname:
stats.fan_speed = int(svalue)
elif stype == 'Load' and 'GPU' in sname:
if 'Memory' not in sname:
stats.utilization_gpu = svalue
else:
stats.utilization_memory = svalue
elif stype == 'Clock' and 'GPU' in sname:
if 'Memory' in sname:
stats.memory_clock = int(svalue)
elif 'Core' in sname:
stats.core_clock = int(svalue)
except Exception as e:
logger.warning(f"LHM sensor error: {e}")
return stats
+224
View File
@@ -0,0 +1,224 @@
"""
PowerShell Counter Backend
==========================
Reads GPU stats via Windows Performance Counters + WMI.
No extra dependencies (uses built-in PowerShell).
"""
import logging
import subprocess
import platform
import json
import re
from .base import MonitorBackend, AMDGPUStats
logger = logging.getLogger(__name__)
class PowerShellBackend(MonitorBackend):
name = "powershell-counters"
def initialize(self) -> bool:
if platform.system() != "Windows":
logger.debug("PS Backend: Windows only")
return False
return self._detect_gpus()
def _detect_gpus(self) -> bool:
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject Win32_VideoController | "
"Where-Object { $_.AdapterRAM -gt 0 } | "
"Select-Object Name, AdapterRAM | ConvertTo-Json"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
if result.returncode == 0 and result.stdout.strip():
data = json.loads(result.stdout.strip())
if isinstance(data, dict):
data = [data]
for gpu in data:
name = gpu.get('Name', 'Unknown GPU').strip()
self.gpu_names.append(name)
self.gpu_count = len(self.gpu_names)
self.available = self.gpu_count > 0
if self.available:
logger.info(f"PS Backend: {self.gpu_count} GPU(s): {self.gpu_names}")
return self.available
except Exception as e:
logger.debug(f"PS Backend detect error: {e}")
return False
def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
gpu_util = self._get_gpu_utilization()
temp = self._get_gpu_temperature()
fan = self._get_gpu_fan_speed()
# Get VRAM — try multiple methods
mem_total, mem_used = self._get_vram_info()
# Only fallback to system RAM if absolutely no GPU info
if mem_total == 0:
try:
import psutil
svmem = psutil.virtual_memory()
mem_total = svmem.total
mem_used = svmem.used
except:
pass
mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name=name,
utilization_gpu=gpu_util,
utilization_memory=mem_util,
memory_total=mem_total,
memory_used=mem_used,
memory_free=mem_total - mem_used,
temperature=temp,
fan_speed=fan,
is_available=True,
)
def _get_vram_info(self) -> tuple:
"""
Get VRAM total & used in bytes.
Tries multiple methods due to WMI AdapterRAM 4GB limit bug.
"""
# Method 1: Direct registry query for accurate VRAM
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"$gpu = Get-WmiObject Win32_VideoController | Select-Object -First 1; "
"[PSCustomObject]@{"
" Total = if ($gpu.AdapterRAM -and $gpu.AdapterRAM -gt 4GB) { $gpu.AdapterRAM } else { "
" # Try registry for accurate VRAM > 4GB\n"
" $paths = Get-ChildItem 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\*' "
"-ErrorAction SilentlyContinue | Where-Object { $_.GetValue('HardwareInformation.AdapterString') -match 'Radeon|AMD' }; "
" foreach ($p in $paths) { "
" $vram = $p.GetValue('HardwareInformation.qwMemorySize'); "
" if ($vram -and $vram -gt 0) { $vram; break } "
" } "
" }; "
" Used = 0 "
"} | ConvertTo-Json"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
data = json.loads(result.stdout.strip())
total = int(data.get('Total', 0))
used = int(data.get('Used', 0))
if total > 0:
return total, used
except:
pass
# Method 2: Try reading registry directly via PowerShell
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"$paths = Get-ChildItem 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\"
"{4d36e968-e325-11ce-bfc1-08002be10318}\\*' -ErrorAction SilentlyContinue; "
"foreach ($p in $paths) { "
" $str = $p.GetValue('HardwareInformation.AdapterString'); "
" if ($str -match 'Radeon|AMD|RDNA') { "
" $mem = $p.GetValue('HardwareInformation.qwMemorySize'); "
" if ($mem) { $mem; break } "
" } "
"}"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
total = int(result.stdout.strip())
if total > 0:
return total, 0
except:
pass
# Method 3: AdapterRAM from WMI (may be capped at 4GB for some drivers)
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"(Get-WmiObject Win32_VideoController | Select-Object -First 1).AdapterRAM"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3,
creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
total = int(result.stdout.strip())
if total > 0:
return total, 0
except:
pass
return 0, 0
def _get_gpu_utilization(self) -> float:
try:
# Only query 3D engine type to avoid noise from other engines
ps_cmd = (
"@(Get-Counter '\\GPU Engine(*engtype_3D)\\*' -ErrorAction SilentlyContinue "
"| Select-Object -ExpandProperty CounterSamples "
"| Where-Object { $_.CookedValue -gt 0 }).CookedValue "
"| Measure-Object -Average | Select-Object -ExpandProperty Average"
)
result = subprocess.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW
)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
# Counter returns nanoseconds per sample
# Normalize: typical 60fps = 16.67ms per frame = 16,670,000 ns
# 100% utilization on a single engine ≈ 16,670,000 ns
# Multiple GPUs/engines sum — cap at 100
pct = (val / 16670000.0) * 100.0
return min(100.0, max(0.0, pct))
except:
pass
return 0.0
def _get_gpu_temperature(self) -> float:
for ns in ['root/LibreHardwareMonitor', 'root/OpenHardwareMonitor']:
try:
cmd = [
"powershell", "-NoProfile", "-Command",
f"Get-WmiObject -Namespace '{ns}' -Class Sensor 2>$null | "
f"Where-Object {{ $_.SensorType -eq 'Temperature' -and "
f"$_.Name -match 'GPU' }} | Select-Object -First 1 -ExpandProperty Value"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3,
creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
if val > 0:
return val
except:
pass
return 0.0
def _get_gpu_fan_speed(self) -> int:
for ns in ['root/LibreHardwareMonitor', 'root/OpenHardwareMonitor']:
try:
cmd = [
"powershell", "-NoProfile", "-Command",
f"Get-WmiObject -Namespace '{ns}' -Class Sensor 2>$null | "
f"Where-Object {{ $_.SensorType -eq 'Fan' -and "
f"$_.Name -match 'GPU' }} | Select-Object -First 1 -ExpandProperty Value"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3,
creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
if val > 0:
return int(val)
except:
pass
return 0
+40
View File
@@ -0,0 +1,40 @@
"""
Psutil Fallback Backend
=======================
Uses psutil for basic system memory info (last resort).
"""
import logging
from .base import MonitorBackend, AMDGPUStats
logger = logging.getLogger(__name__)
class PsutilBackend(MonitorBackend):
name = "psutil-fallback"
def initialize(self) -> bool:
try:
import psutil
self.available = True
self.gpu_count = 1
self.gpu_names = ["System RAM (no GPU backend)"]
logger.info("Psutil Backend: fallback active")
return True
except ImportError:
logger.debug("Psutil Backend: psutil not installed")
return False
def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
import psutil
svmem = psutil.virtual_memory()
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name=self.gpu_names[0],
utilization_gpu=0.0,
utilization_memory=(svmem.used / svmem.total * 100) if svmem.total > 0 else 0,
memory_total=svmem.total,
memory_used=svmem.used,
memory_free=svmem.available,
is_available=True,
)
+226
View File
@@ -0,0 +1,226 @@
"""
ComfyUI Process Monitor
=======================
Monitors ComfyUI process during image generation.
Auto-detects generation start/end and records peak memory/CPU.
Usage:
from monitor import get_process_monitor
monitor = get_process_monitor()
monitor.start_monitoring()
# ... generate images ...
print(monitor.get_summary())
"""
import logging
import time
import threading
from dataclasses import dataclass
from typing import Optional, List
logger = logging.getLogger(__name__)
@dataclass
class GenerationRecord:
"""Stats for a single generation run"""
timestamp: float = 0.0
duration: float = 0.0
vram_peak_mb: float = 0.0
vram_start_mb: float = 0.0
vram_end_mb: float = 0.0
vram_delta_mb: float = 0.0
ram_peak_mb: float = 0.0
ram_start_mb: float = 0.0
ram_end_mb: float = 0.0
cpu_peak: float = 0.0
error: Optional[str] = None
@dataclass
class ProcessSnapshot:
"""Point-in-time process metrics"""
timestamp: float
memory_rss_mb: float
memory_vms_mb: float
cpu_percent: float
num_threads: int
class ComfyProcessMonitor:
"""Monitors ComfyUI process for generation metrics"""
def __init__(self, poll_interval: float = 0.5):
self.poll_interval = poll_interval
self._process = None
self._process_pid = None
self._running = False
self._thread = None
self._is_generating = False
self._gen_start_time = 0.0
self._current_record: Optional[GenerationRecord] = None
self._prev_memory_mb = 0.0
self.history: List[GenerationRecord] = []
self.current: Optional[GenerationRecord] = None
self._snapshots: List[ProcessSnapshot] = []
self._lock = threading.Lock()
# Detection thresholds (tuned)
self._MEMORY_START_THRESHOLD_MB = 50
self._CPU_START_THRESHOLD = 15
self._MEMORY_END_THRESHOLD_MB = -30
self._CPU_END_THRESHOLD = 8
def find_comfy_process(self) -> bool:
"""Auto-detect ComfyUI Python process"""
try:
import psutil
pid = None
current = psutil.Process()
cmdline = ' '.join(current.cmdline()).lower()
# Check if WE are the ComfyUI process
if 'comfy' in cmdline or 'main.py' in cmdline:
pid = current.pid
# Search by name
if not pid:
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
name = (proc.info['name'] or '').lower()
cl = ' '.join(proc.info['cmdline'] or []).lower()
if ('python' in name or 'python' in cl) and ('comfy' in cl or 'main.py' in cl):
pid = proc.info['pid']
break
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
if pid:
self._process_pid = pid
self._process = psutil.Process(pid)
logger.info(f"Process Monitor: found PID {pid}")
return True
logger.warning("Process Monitor: ComfyUI not found")
return False
except Exception as e:
logger.warning(f"Process Monitor: find error: {e}")
return False
def start_monitoring(self):
"""Begin background monitoring thread"""
if self._running:
return
if not self.find_comfy_process():
return
self._running = True
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
self._thread.start()
logger.info("Process Monitor: started")
def stop_monitoring(self):
"""Stop background monitoring"""
self._running = False
if self._thread:
self._thread.join(timeout=2)
def _poll_loop(self):
try:
import psutil
except ImportError:
self._running = False
return
while self._running:
try:
if not self._process or not self._process.is_running():
if not self.find_comfy_process():
time.sleep(self.poll_interval * 10)
continue
mem = self._process.memory_info()
cpu = self._process.cpu_percent(interval=0)
rss_mb = mem.rss / (1024 * 1024)
delta = rss_mb - self._prev_memory_mb
snap = ProcessSnapshot(time.time(), rss_mb, mem.vms / (1024*1024), cpu, self._process.num_threads())
with self._lock:
if not self._is_generating:
if delta > self._MEMORY_START_THRESHOLD_MB or cpu > self._CPU_START_THRESHOLD:
self._is_generating = True
self._gen_start_time = time.time()
self._current_record = GenerationRecord(
timestamp=self._gen_start_time, duration=0,
vram_start_mb=rss_mb, vram_peak_mb=rss_mb,
ram_start_mb=rss_mb, ram_peak_mb=rss_mb,
)
self._snapshots = [snap]
logger.info(f"Process Monitor: START (RAM: {rss_mb:.0f}MB)")
else:
if self._current_record:
self._current_record.vram_peak_mb = max(self._current_record.vram_peak_mb, rss_mb)
self._current_record.ram_peak_mb = max(self._current_record.ram_peak_mb, rss_mb)
self._current_record.cpu_peak = max(self._current_record.cpu_peak, cpu)
self._snapshots.append(snap)
if delta < self._MEMORY_END_THRESHOLD_MB and cpu < self._CPU_END_THRESHOLD:
self._finalize(rss_mb)
logger.info(f"Process Monitor: END ({self.current.duration:.1f}s)")
self._prev_memory_mb = rss_mb
except Exception as e:
logger.debug(f"Process Monitor: {e}")
time.sleep(self.poll_interval)
def _finalize(self, end_mb: float):
if not self._current_record:
return
self._current_record.duration = time.time() - self._gen_start_time
self._current_record.vram_end_mb = end_mb
self._current_record.ram_end_mb = end_mb
self._current_record.vram_delta_mb = self._current_record.vram_peak_mb - self._current_record.vram_start_mb
self.current = self._current_record
self.history.append(self._current_record)
if len(self.history) > 50:
self.history = self.history[-50:]
self._is_generating = False
self._current_record = None
self._snapshots = []
@property
def is_generating(self) -> bool:
return self._is_generating
def get_current_generation(self) -> GenerationRecord:
with self._lock:
if self._is_generating and self._current_record:
return self._current_record
return GenerationRecord()
def get_last_generation(self) -> Optional[GenerationRecord]:
if self.history:
return self.history[-1]
return None
def get_summary(self) -> str:
if not self.history:
return "No generations recorded."
lines = ["=== ComfyUI Process Monitor ==="]
total_time = peak_ram = 0
for gen in self.history[-5:]:
total_time += gen.duration
peak_ram = max(peak_ram, gen.ram_peak_mb)
lines.append(f" #{len(lines)}: {gen.duration:.1f}s | RAM: {gen.ram_start_mb:.0f}→{gen.ram_peak_mb:.0f}MB")
lines.append(f" --- Total: {total_time:.1f}s | Peak RAM: {peak_ram:.0f}MB")
return "\n".join(lines)
# Singleton
_process_monitor = None
def get_process_monitor() -> ComfyProcessMonitor:
global _process_monitor
if _process_monitor is None:
_process_monitor = ComfyProcessMonitor()
return _process_monitor
-9
View File
@@ -3,15 +3,6 @@ from .translate_universal import (
NODE_DISPLAY_NAME_MAPPINGS
)
from .amd_monitor import (
NODE_CLASS_MAPPINGS as AMD_CLASS,
NODE_DISPLAY_NAME_MAPPINGS as AMD_DISPLAY
)
# Gabungkan mappings
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **AMD_CLASS}
NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **AMD_DISPLAY}
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS"
-57
View File
@@ -1,57 +0,0 @@
from ..utils.amd_utils import get_amd_monitor
class AMDMonitorNode:
"""
A node to monitor AMD GPU statistics.
Outputs: GPU Usage (%), VRAM Used (MB), VRAM Total (MB), Temperature (C)
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"gpu_index": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1}),
}
}
RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "INT", "INT", "STRING")
RETURN_NAMES = ("gpu_utilization", "vram_usage_pct", "temperature", "vram_used_mb", "vram_total_mb", "status_msg")
FUNCTION = "monitor"
CATEGORY = "BANGTRIXTOOLKIT/System"
def monitor(self, gpu_index):
monitor = get_amd_monitor()
if not monitor.available:
return (0.0, 0.0, 0.0, 0, 0, "Error: AMD Backend not found")
stats = monitor.get_gpu_stats(gpu_index)
if not stats.is_available:
return (0.0, 0.0, 0.0, 0, 0, f"Error: {stats.error_message}")
# Convert bytes to MB for easier reading
vram_used_mb = stats.memory_used / (1024 * 1024)
vram_total_mb = stats.memory_total / (1024 * 1024)
status = "OK"
return (
float(stats.utilization_gpu),
float(stats.utilization_memory),
float(stats.temperature),
int(vram_used_mb),
int(vram_total_mb),
status
)
# Registry
NODE_CLASS_MAPPINGS = {
"BANGTRIX_AMD_Monitor": AMDMonitorNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BANGTRIX_AMD_Monitor": "AMD Monitor (BANGTRIX)"
}
+38 -6
View File
@@ -1,15 +1,47 @@
# HTTP & WebSocket Server (Required for AMD Monitor)
# ============================================
# BANGTRIXTOOLKIT - Dependencies
# ============================================
# Install with: pip install -r requirements.txt
# ============================================
# 📡 WebSocket & HTTP Server (WAJIB)
# ============================================
# Diperlukan untuk AMD Monitor WebSocket + streaming
aiohttp>=3.8.0
async-timeout>=4.0.0
# HTTP Requests (Optional - for translate_universal)
# ============================================
# 🌐 HTTP Client (Opsional - translate node)
# ============================================
# Google Translate API + GitHub API (LHM download)
# NOTE: Kalau tidak diinstall, Python urllib akan dipakai
requests>=2.31.0
# System Monitoring
# ============================================
# 🖥️ System Monitoring (DIREKOMENDASIKAN)
# ============================================
# Diperlukan untuk:
# - Process Monitor (deteksi generasi ComfyUI)
# - VRAM/memory fallback
# - CPU utilization tracking
psutil>=5.9.0
# AMD GPU Monitoring - Linux (ROCm)
# ============================================
# 🐧 AMD GPU - Linux (ROCm)
# ============================================
# Hanya untuk Linux dengan driver ROCm terinstall
pyrsmi>=0.2.0; sys_platform == "linux"
# AMD GPU Monitoring - Windows (ADL)
pyadl>=1.0.0; sys_platform == "win32"
# ============================================
# 🪟 AMD GPU - Windows (Legacy ADL)
# ============================================
# Opsional: untuk GPU temperature/fan via ADL
# NOTE: Sebagian driver AMD terbaru mungkin tidak kompatibel
pyadl>=1.0.0; sys_platform == "win32"
# ============================================
# 🌡️ LibreHardwareMonitor (Windows)
# ============================================
# TIDAK PERLU install! Auto-download dari GitHub saat runtime.
# Baca: Temperature, Fan, Clock, Power, Load via WMI.
# Minimal: Python 3.8+ (built-in: subprocess, wmi, powershell)
+224 -58
View File
@@ -1,13 +1,16 @@
"""
AMD Monitor Server
==================
WebSocket server for streaming AMD GPU stats to frontend
WebSocket server for streaming AMD GPU stats to frontend.
Supports multi-GPU, commands, history, and ComfyUI process monitoring.
Default interval: 5s
"""
import asyncio
import json
import logging
from typing import Set
from collections import deque
from typing import Set, Dict
logger = logging.getLogger(__name__)
@@ -17,89 +20,252 @@ class AMDMonitorServer:
self.monitor = None # Lazy load
self.clients: Set = set()
self.running = False
self.update_interval = 1.0 # seconds
self.update_interval = 1.0 # seconds — real-time
self._stream_task = None
# Process monitor
self._process_monitor = None
# Multi-GPU history
self.history_maxlen = 30
self.gpu_history: Dict[int, deque] = {}
def _get_monitor(self):
"""Lazy load monitor to avoid import issues at startup"""
if self.monitor is None:
from ..utils.amd_utils import get_amd_monitor
self.monitor = get_amd_monitor()
try:
from ..monitor import get_amd_monitor
self.monitor = get_amd_monitor()
except ImportError:
from ..utils.amd_utils import get_amd_monitor as _legacy_monitor
self.monitor = _legacy_monitor()
return self.monitor
def _get_process_monitor(self):
if self._process_monitor is None:
try:
from ..monitor.process_monitor import get_process_monitor
self._process_monitor = get_process_monitor()
self._process_monitor.start_monitoring()
except ImportError:
from ..utils.amd_process_monitor import get_process_monitor as _legacy_pm
self._process_monitor = _legacy_pm()
self._process_monitor.start_monitoring()
except Exception as e:
logger.debug(f"Process monitor init: {e}")
return self._process_monitor
async def broadcast(self, message: str):
"""Send data to all connected clients"""
if self.clients:
await asyncio.gather(
*[client.send_str(message) for client in self.clients],
return_exceptions=True
)
if not self.clients:
return
results = await asyncio.gather(
*[client.send_str(message) for client in self.clients.copy()],
return_exceptions=True
)
for i, result in enumerate(results):
if isinstance(result, Exception):
client_list = list(self.clients)
if i < len(client_list):
logger.debug(f"AMD Monitor: Removing dead client: {result}")
self.clients.discard(client_list[i])
async def handle_client(self, ws):
"""Handle WebSocket client connection"""
self.clients.add(ws)
logger.info(f"AMD Monitor: Client connected. Total clients: {len(self.clients)}")
try:
async for msg in ws:
if msg.type == 1: # Text message
if msg.data == "ping":
await ws.send_str("pong")
if msg.type == 1:
await self._handle_message(ws, msg.data)
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"AMD Monitor: Client error: {e}")
finally:
self.clients.discard(ws)
logger.info(f"AMD Monitor: Client disconnected. Total clients: {len(self.clients)}")
async def stream_data(self):
"""Stream GPU stats periodically"""
monitor = self._get_monitor()
while self.running:
try:
if monitor.available:
stats = monitor.get_gpu_stats(0) # GPU 0
data = {
"type": "amd_stats",
"gpu_id": stats.gpu_id,
"gpu_utilization": stats.utilization_gpu,
"vram_usage_pct": stats.utilization_memory,
"vram_used_mb": stats.memory_used / (1024 * 1024),
"vram_total_mb": stats.memory_total / (1024 * 1024),
"temperature": stats.temperature,
"fan_speed": stats.fan_speed,
"is_available": stats.is_available,
"method": monitor.method,
"gpu_count": monitor.gpu_count
}
else:
data = {
"type": "amd_stats",
"is_available": False,
"error": "AMD backend not available",
"method": None,
"gpu_count": 0
}
await self.broadcast(json.dumps(data))
except Exception as e:
logger.error(f"AMD Monitor: Stream error: {e}")
await asyncio.sleep(self.update_interval)
async def _handle_message(self, ws, raw: str):
try:
cmd = json.loads(raw)
cmd_type = cmd.get("type", "")
def start(self, loop=None):
"""Start the monitoring loop safely"""
if cmd_type == "ping":
await ws.send_str(json.dumps({"type": "pong"}))
elif cmd_type == "set_interval":
interval = float(cmd.get("interval", 5.0))
self.update_interval = max(0.5, min(30.0, interval))
await ws.send_str(json.dumps({
"type": "config_updated",
"update_interval": self.update_interval
}))
elif cmd_type == "get_history":
gpu_id = cmd.get("gpu_id", 0)
history = list(self.gpu_history.get(gpu_id, []))
await ws.send_str(json.dumps({
"type": "history",
"gpu_id": gpu_id,
"values": history
}))
elif cmd_type == "request_stats":
monitor = self._get_monitor()
data = self._build_stats_data(monitor)
process = self._get_process_monitor()
if process:
data["process"] = self._build_process_data(process)
await ws.send_str(json.dumps(data))
else:
await ws.send_str(json.dumps({
"type": "error",
"message": f"Unknown command: {cmd_type}"
}))
except json.JSONDecodeError:
if raw == "ping":
await ws.send_str("pong")
def _build_process_data(self, process):
"""Build process monitoring payload"""
data = {
"is_generating": process.is_generating,
}
if process.is_generating:
gen = process.get_current_generation()
data["generation"] = {
"duration": round(gen.duration, 1),
"vram_peak_mb": round(gen.vram_peak_mb, 0),
"ram_start_mb": round(gen.ram_start_mb, 0),
"ram_peak_mb": round(gen.ram_peak_mb, 0),
"cpu_peak": round(gen.cpu_peak, 1),
}
else:
last = process.get_last_generation()
if last:
data["last_generation"] = {
"duration": round(last.duration, 1),
"vram_peak_mb": round(last.vram_peak_mb, 0),
"vram_delta_mb": round(last.vram_delta_mb, 0),
"ram_start_mb": round(last.ram_start_mb, 0),
"ram_peak_mb": round(last.ram_peak_mb, 0),
"ram_end_mb": round(last.ram_end_mb, 0),
"cpu_peak": round(last.cpu_peak, 1),
}
# Generation count
data["generation_count"] = len(process.history) if process.history else 0
return data
def _build_stats_data(self, monitor, gpu_id: int = 0):
if not monitor.available:
return {
"type": "amd_stats",
"gpu_id": gpu_id,
"gpu_name": "",
"gpu_count": monitor.gpu_count,
"is_available": False,
"error": "AMD backend not available",
"method": monitor.method,
"history": []
}
stats = monitor.get_gpu_stats(gpu_id)
# Update history
if gpu_id not in self.gpu_history:
self.gpu_history[gpu_id] = deque(maxlen=self.history_maxlen)
self.gpu_history[gpu_id].append(stats.utilization_gpu)
data = {
"type": "amd_stats",
"gpu_id": stats.gpu_id,
"gpu_name": stats.gpu_name or f"AMD GPU {stats.gpu_id}",
"gpu_count": monitor.gpu_count,
"method": monitor.method or "unknown",
"is_available": stats.is_available,
"gpu_utilization": round(stats.utilization_gpu, 1),
"vram_usage_pct": round(stats.utilization_memory, 1),
"vram_used_mb": round(stats.memory_used / (1024 * 1024), 0),
"vram_total_mb": round(stats.memory_total / (1024 * 1024), 0),
"temperature": round(stats.temperature, 1),
"fan_speed": stats.fan_speed,
"core_clock_mhz": stats.core_clock,
"memory_clock_mhz": stats.memory_clock,
"power_draw_watts": round(stats.power_draw, 1),
"history": list(self.gpu_history.get(gpu_id, [])),
}
if not stats.is_available and stats.error_message:
data["error"] = stats.error_message
return data
async def stream_data(self):
monitor = self._get_monitor()
process_mon = self._get_process_monitor()
logger.info(f"AMD Monitor: Stream data loop started (interval={self.update_interval}s)")
try:
while self.running:
try:
if monitor.available and monitor.gpu_count > 0:
for gpu_id in range(monitor.gpu_count):
data = self._build_stats_data(monitor, gpu_id)
# Attach process monitoring data
if process_mon:
data["process"] = self._build_process_data(process_mon)
await self.broadcast(json.dumps(data))
else:
data = {
"type": "amd_stats",
"gpu_id": 0,
"gpu_name": "",
"gpu_count": monitor.gpu_count,
"is_available": False,
"error": "AMD backend not available",
"method": monitor.method,
"history": []
}
await self.broadcast(json.dumps(data))
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"AMD Monitor: Stream error: {e}")
await asyncio.sleep(self.update_interval)
finally:
logger.info("AMD Monitor: Stream data loop ended")
def start_streaming(self, loop=None):
if self.running:
return
return self._stream_task
self.running = True
logger.info("AMD Monitor: Starting data stream")
loop = loop or asyncio.get_event_loop()
self._stream_task = asyncio.ensure_future(self.stream_data(), loop=loop)
return self._stream_task
def stop_streaming(self):
self.running = False
if self._stream_task and not self._stream_task.done():
self._stream_task.cancel()
self._stream_task = None
logger.info("AMD Monitor: Stream stopped")
# Singleton instance
_amd_server = None
def get_amd_server() -> AMDMonitorServer:
global _amd_server
if _amd_server is None:
-54
View File
@@ -1,54 +0,0 @@
"""
Quick test script for AMD Monitor backend
Run: python_embeded\python.exe test_amd_monitor.py
"""
import sys
import os
# Add project to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
print("=== BANGTRIXTOOLKIT AMD Monitor Test ===\n")
# Test 1: Import utils
print("1. Testing amd_utils import...")
try:
from utils.amd_utils import get_amd_monitor, AMDGPUStats
print(" ✅ amd_utils imported")
except Exception as e:
print(f" ❌ amd_utils import failed: {e}")
sys.exit(1)
# Test 2: Initialize monitor
print("\n2. Initializing AMD monitor...")
monitor = get_amd_monitor()
print(f" Available: {monitor.available}")
print(f" Method: {monitor.method}")
print(f" GPU Count: {monitor.gpu_count}")
# Test 3: Get stats
if monitor.available:
print("\n3. Fetching GPU stats...")
stats = monitor.get_gpu_stats(0)
print(f" GPU ID: {stats.gpu_id}")
print(f" Utilization: {stats.utilization_gpu}%")
print(f" VRAM: {stats.memory_used / 1024 / 1024:.0f} MB / {stats.memory_total / 1024 / 1024:.0f} MB")
print(f" Temperature: {stats.temperature}°C")
print(f" Fan: {stats.fan_speed}%")
print(f" Status: {'✅ OK' if stats.is_available else '❌ Error: ' + str(stats.error_message)}")
else:
print("\n⚠️ AMD backend not available - check requirements & drivers")
# Test 4: Check dependencies
print("\n4. Dependency check:")
deps = ['aiohttp', 'psutil', 'pyadl', 'pyrsmi']
for dep in deps:
try:
__import__(dep)
ver = getattr(__import__(dep), '__version__', 'unknown')
print(f" ✅ {dep}: {ver}")
except ImportError:
print(f" ⚪ {dep}: not installed")
print("\n=== Test Complete ===")
+1
View File
@@ -0,0 +1 @@
+348
View File
@@ -0,0 +1,348 @@
"""
AMD ADL (Display Library) Direct Binding
=========================================
Low-level ctypes binding to atiadlxx.dll for GPU monitoring on Windows.
No dependencies required (uses ctypes which is built-in Python).
Provides: GPU Utilization, Temperature, Fan Speed, GPU Name, Core/Memory Clocks
"""
import ctypes
import ctypes.util
import logging
import platform
from ctypes import byref, c_int, c_void_p, c_char_p, c_char, POINTER, Structure
logger = logging.getLogger(__name__)
# =============================================
# DLL LOADING
# =============================================
_adl_dll = None
_adl_initialized = False
def _load_adl():
global _adl_dll
if _adl_dll is not None:
return True
system = platform.system()
if system != "Windows":
return False
try:
_adl_dll = ctypes.windll.LoadLibrary("atiadlxx.dll")
logger.debug("ADL: loaded atiadlxx.dll from System32")
return True
except:
try:
_adl_dll = ctypes.windll.LoadLibrary("atiadlxy.dll")
logger.debug("ADL: loaded atiadlxy.dll (fallback)")
return True
except:
logger.debug("ADL: cannot load atiadlxx.dll / atiadlxy.dll")
return False
# =============================================
# ADL TYPES
# =============================================
class ADLAdapterInfo(Structure):
_fields_ = [
("strAdapterName", c_char * 256),
("iAdapterIndex", c_int),
("iBusNumber", c_int),
("iDeviceNumber", c_int),
("iFunctionNumber", c_int),
("iVendorID", c_int),
("strAdapterName_256", c_char * 256),
("isPresent", c_int),
("iDisplayType", c_int * 4),
("iFormFactor", c_int),
("iClusterInfo", c_int),
("strBusString", c_char * 64),
("strBusNumber", c_char * 64),
("iFunction", c_int),
("iRevisionID", c_int),
("iExtendedRevisionID", c_int),
("iMemorySizeMB", c_int),
]
class ADLTemperature(Structure):
_fields_ = [
("iSize", c_int),
("iTemperature", c_int),
]
class ADLFanSpeedValue(Structure):
_fields_ = [
("iSize", c_int),
("iSpeedType", c_int),
("iFanSpeed", c_int),
("iFlags", c_int),
]
class ADLPMActivity(Structure):
_fields_ = [
("iSize", c_int),
("iEngineClock", c_int),
("iMemoryClock", c_int),
("iVddc", c_int),
("iActivityPercent", c_int),
("iCurrentPerformanceLevel", c_int),
("iCurrentBusSpeed", c_int),
("iCurrentBusLanes", c_int),
("iUnknown", c_int * 4),
]
class ADLODParameters(Structure):
_fields_ = [
("iSize", c_int),
("iEngineClock", c_int),
("iMemoryClock", c_int),
("iVddc", c_int),
("iActivityPercent", c_int),
("iCurrentPerformanceLevel", c_int),
("iCurrentMinEngineClock", c_int),
("iCurrentMinMemoryClock", c_int),
("iCurrentMaxEngineClock", c_int),
("iCurrentMaxMemoryClock", c_int),
]
# =============================================
# ADL CONTEXT
# =============================================
class ADLContext:
"""Manages ADL lifecycle and GPU adapter info"""
def __init__(self):
self.available = False
self.num_adapters = 0
self.adapters = []
self.adapter_names = []
self._context = None
def initialize(self) -> bool:
if not _load_adl():
return False
try:
# ADL2_Main_Control_Create(callback, enumConnectedAdapters)
ADL2_Main_Control_Create = _adl_dll.ADL2_Main_Control_Create
ADL2_Main_Control_Create.argtypes = [c_void_p, c_int]
ADL2_Main_Control_Create.restype = c_int
context = c_int(0)
result = ADL2_Main_Control_Create(None, byref(context))
if result != 0:
logger.warning(f"ADL: Main_Control_Create failed: {result}")
return False
self._context = context
# Get adapter count
ADL2_Adapter_NumberOfAdapters_Get = _adl_dll.ADL2_Adapter_NumberOfAdapters_Get
ADL2_Adapter_NumberOfAdapters_Get.argtypes = [c_int, POINTER(c_int)]
ADL2_Adapter_NumberOfAdapters_Get.restype = c_int
num = c_int(0)
result = ADL2_Adapter_NumberOfAdapters_Get(context.value, byref(num))
if result != 0:
logger.warning(f"ADL: NumberOfAdapters_Get failed: {result}")
self._destroy()
return False
self.num_adapters = num.value
# Query adapter info for all adapters
if self.num_adapters > 0:
self._query_adapters()
self.available = True
logger.info(f"ADL: Initialized — {len(self.adapters)} adapter(s)")
return True
except Exception as e:
logger.warning(f"ADL: Init error: {e}")
return False
def _query_adapters(self):
"""Get adapter info for all adapters"""
try:
ADL2_Adapter_AdapterInfo_Get = _adl_dll.ADL2_Adapter_AdapterInfo_Get
ADL2_Adapter_AdapterInfo_Get.argtypes = [c_int, c_void_p, POINTER(c_int)]
ADL2_Adapter_AdapterInfo_Get.restype = c_int
# Allocate memory via ADL
ADL2_Main_Memory_Alloc = _adl_dll.ADL2_Main_Memory_Alloc
ADL2_Main_Memory_Alloc.argtypes = [c_int, c_int, c_void_p]
ADL2_Main_Memory_Alloc.restype = c_int
buf_size = c_int(ctypes.sizeof(ADLAdapterInfo) * self.num_adapters)
buffer = ctypes.create_string_buffer(buf_size.value)
# Use our own buffer, ADL will read present adapters
result = ADL2_Adapter_AdapterInfo_Get(
self._context.value,
buffer,
byref(buf_size)
)
if result == 0:
info_array = (ADLAdapterInfo * self.num_adapters).from_buffer(buffer)
total = min(self.num_adapters, buf_size.value // ctypes.sizeof(ADLAdapterInfo))
for i in range(total):
info = info_array[i]
if info.isPresent or info.iAdapterIndex >= 0:
name = info.strAdapterName.decode('utf-8', errors='replace').strip()
if not name:
name = info.strAdapterName_256.decode('utf-8', errors='replace').strip()
self.adapters.append({
'index': info.iAdapterIndex,
'name': name,
})
self.adapter_names.append(name)
# Fallback: if no adapters found, try ADL2_Adapter_AdapterInfo_Get_All
if len(self.adapters) == 0:
self._query_adapters_fallback()
except Exception as e:
logger.warning(f"ADL: AdapterInfo error: {e}")
self.adapters = [{'index': 0, 'name': 'AMD GPU 0'}]
self.adapter_names = ['AMD GPU 0']
def _query_adapters_fallback(self):
"""Fallback: just create virtual adapters based on count"""
for i in range(self.num_adapters):
self.adapters.append({'index': i, 'name': f'AMD GPU {i}'})
self.adapter_names.append(f'AMD GPU {i}')
def get_temperature(self, adapter_index: int) -> float:
"""Get GPU temperature in Celsius"""
try:
ADL2_Overdrive6_Temperature_Get = _adl_dll.ADL2_Overdrive6_Temperature_Get
ADL2_Overdrive6_Temperature_Get.argtypes = [c_int, c_int, POINTER(ADLTemperature)]
ADL2_Overdrive6_Temperature_Get.restype = c_int
temp = ADLTemperature()
temp.iSize = ctypes.sizeof(ADLTemperature)
result = ADL2_Overdrive6_Temperature_Get(self._context.value, adapter_index, byref(temp))
if result == 0:
return temp.iTemperature / 1000.0
except:
pass
return 0.0
def get_fan_speed(self, adapter_index: int) -> int:
"""Get GPU fan speed in percent"""
try:
ADL2_Overdrive6_FanSpeed_Get = _adl_dll.ADL2_Overdrive6_FanSpeed_Get
ADL2_Overdrive6_FanSpeed_Get.argtypes = [c_int, c_int, POINTER(ADLFanSpeedValue)]
ADL2_Overdrive6_FanSpeed_Get.restype = c_int
fan = ADLFanSpeedValue()
fan.iSize = ctypes.sizeof(ADLFanSpeedValue)
fan.iSpeedType = 0 # ADL_DL_FANCTRL_SPEED_TYPE_PERCENT
result = ADL2_Overdrive6_FanSpeed_Get(self._context.value, adapter_index, byref(fan))
if result == 0:
return fan.iFanSpeed
except:
pass
return 0
def get_activity(self, adapter_index: int):
"""Get GPU activity: utilization, engine clock, memory clock"""
try:
ADL2_Overdrive6_CurrentActivity_Get = _adl_dll.ADL2_Overdrive6_CurrentActivity_Get
ADL2_Overdrive6_CurrentActivity_Get.argtypes = [c_int, c_int, POINTER(ADLPMActivity)]
ADL2_Overdrive6_CurrentActivity_Get.restype = c_int
activity = ADLPMActivity()
activity.iSize = ctypes.sizeof(ADLPMActivity)
result = ADL2_Overdrive6_CurrentActivity_Get(self._context.value, adapter_index, byref(activity))
if result == 0:
return {
'utilization': activity.iActivityPercent,
'engine_clock': activity.iEngineClock,
'memory_clock': activity.iMemoryClock,
}
except:
pass
return {'utilization': 0, 'engine_clock': 0, 'memory_clock': 0}
def get_memory_info(self, adapter_index: int):
"""Get VRAM size in MB"""
# Memory info from adapter info
for adapter in self.adapters:
if adapter['index'] == adapter_index:
# We stored the adapter index, but not memory size directly
pass
return 0
def _destroy(self):
"""Clean up ADL context"""
try:
if self._context is not None:
ADL2_Main_Control_Destroy = _adl_dll.ADL2_Main_Control_Destroy
ADL2_Main_Control_Destroy.argtypes = [c_int]
ADL2_Main_Control_Destroy.restype = c_int
ADL2_Main_Control_Destroy(self._context.value)
self._context = None
except:
pass
def close(self):
self._destroy()
# =============================================
# GLOBAL ADL INSTANCE
# =============================================
_adl_context = None
def get_adl_context() -> ADLContext:
global _adl_context
if _adl_context is None:
ctx = ADLContext()
if ctx.initialize():
_adl_context = ctx
else:
_adl_context = ctx # Return anyway, .available will be False
return _adl_context
# =============================================
# TEST
# =============================================
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
ctx = get_adl_context()
if ctx.available:
print(f"ADL: {len(ctx.adapters)} adapter(s)")
for i, adapter in enumerate(ctx.adapters):
print(f" GPU {i}: {adapter['name']} (index={adapter['index']})")
temp = ctx.get_temperature(adapter['index'])
fan = ctx.get_fan_speed(adapter['index'])
activity = ctx.get_activity(adapter['index'])
print(f" Temp: {temp:.1f}°C")
print(f" Fan: {fan}%")
print(f" Utilization: {activity['utilization']}%")
print(f" Engine Clock: {activity['engine_clock']} MHz")
print(f" Memory Clock: {activity['memory_clock']} MHz")
else:
print("ADL: Not available")
+255
View File
@@ -0,0 +1,255 @@
"""
GPU Performance Counters via PowerShell
========================================
Windows-only: reads GPU utilization, VRAM usage, and temperature
via built-in performance counters and WMI.
No dependencies required beyond built-in Python + PowerShell.
"""
import subprocess
import logging
import platform
import json
import re
logger = logging.getLogger(__name__)
class GPUCounterMonitor:
"""Monitor GPU metrics via PowerShell performance counters and WMI"""
def __init__(self):
self.available = False
self.gpu_count = 0
self.gpu_names = []
self.method = "powershell-counters"
self._detect()
def _detect(self):
"""Detect GPU availability via WMI"""
if platform.system() != "Windows":
return
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject Win32_VideoController | "
"Where-Object { $_.AdapterRAM -gt 0 } | "
"Select-Object Name, AdapterRAM | ConvertTo-Json"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
if result.returncode == 0 and result.stdout.strip():
data = json.loads(result.stdout.strip())
if isinstance(data, dict):
data = [data]
for gpu in data:
name = gpu.get('Name', 'Unknown GPU').strip()
self.gpu_names.append(name)
self.gpu_count = len(self.gpu_names)
if self.gpu_count > 0:
self.available = True
logger.info(f"GPU Counters: {self.gpu_count} GPU(s): {self.gpu_names}")
return
except Exception as e:
logger.debug(f"GPU detection error: {e}")
def get_gpu_utilization(self, gpu_id: int = 0) -> float:
"""Get GPU utilization % via performance counters"""
try:
# Get average 3D engine usage
ps_cmd = (
"Get-Counter '\\GPU Engine(*engtype_3D)\\*' -SampleInterval 0 -MaxSamples 1 "
"2>$null | Select-Object -ExpandProperty CounterSamples | "
"Measure-Object -Property CookedValue -Average | "
"Select-Object -ExpandProperty Average"
)
result = subprocess.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=3
)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
if val > 0:
return val
except Exception as e:
logger.debug(f"GPU utilization error: {e}")
return 0.0
def _detect_hardware_monitors(self):
"""Detect which hardware monitoring tools are installed"""
monitors = []
# LibreHardwareMonitor / OpenHardwareMonitor WMI namespace
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject -Namespace 'root/LibreHardwareMonitor' -Class Sensor 2>$null | "
"Select-Object -First 1 | ConvertTo-Json"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3)
if result.returncode == 0 and result.stdout.strip() and result.stdout.strip() != 'null':
monitors.append("LibreHardwareMonitor")
except:
pass
if 'LibreHardwareMonitor' not in monitors:
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject -Namespace 'root/OpenHardwareMonitor' -Class Sensor 2>$null | "
"Select-Object -First 1 | ConvertTo-Json"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3)
if result.returncode == 0 and result.stdout.strip() and result.stdout.strip() != 'null':
monitors.append("OpenHardwareMonitor")
except:
pass
# MSI Afterburner shared memory
try:
import ctypes
try:
# Try to open the shared memory file
import ctypes.wintypes
handle = ctypes.windll.kernel32.OpenFileMappingW(
0x0001, False, "MSE_Afterburner_SharedMemory"
)
if handle:
monitors.append("MSIAfterburner")
ctypes.windll.kernel32.CloseHandle(handle)
except:
pass
except:
pass
self._hw_monitors = monitors
if monitors:
logger.info(f"GPU Counters: Detected HW monitors: {monitors}")
return monitors
def get_gpu_temperature(self, gpu_id: int = 0) -> float:
"""Get GPU temperature via LibreHardwareMonitor, OpenHardwareMonitor, or other tools"""
# Try LibreHardwareMonitor first (if installed)
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject -Namespace 'root/LibreHardwareMonitor' -Class Sensor "
"2>$null | Where-Object { $_.SensorType -eq 'Temperature' -and "
"$_.Name -match 'GPU' } | Select-Object -First 1 -ExpandProperty Value"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3, creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
if val > 0:
return val
except:
pass
# Try OpenHardwareMonitor
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject -Namespace 'root/OpenHardwareMonitor' -Class Sensor "
"2>$null | Where-Object { $_.SensorType -eq 'Temperature' -and "
"$_.Name -match 'GPU' } | Select-Object -First 1 -ExpandProperty Value"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3, creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
if val > 0:
return val
except:
pass
return 0.0
def get_fan_speed(self, gpu_id: int = 0) -> int:
"""Get fan speed from hardware monitors"""
# LibreHardwareMonitor
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject -Namespace 'root/LibreHardwareMonitor' -Class Sensor "
"2>$null | Where-Object { $_.SensorType -eq 'Fan' -and "
"$_.Name -match 'GPU' } | Select-Object -First 1 -ExpandProperty Value"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3, creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
if val > 0:
return int(val)
except:
pass
# OpenHardwareMonitor
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject -Namespace 'root/OpenHardwareMonitor' -Class Sensor "
"2>$null | Where-Object { $_.SensorType -eq 'Fan' -and "
"$_.Name -match 'GPU' } | Select-Object -First 1 -ExpandProperty Value"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3, creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
if val > 0:
return int(val)
except:
pass
return 0
def get_gpu_vram_mb(self, gpu_id: int = 0) -> dict:
"""Get VRAM info from WMI"""
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"(Get-WmiObject Win32_VideoController | Select-Object -First 1).AdapterRAM"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=3)
if result.returncode == 0 and result.stdout.strip():
total_bytes = int(result.stdout.strip())
total_mb = total_bytes / (1024 * 1024)
# We can't get actual VRAM usage without ADLX/ADL
# Estimate via process memory if needed
return {'total_mb': total_mb, 'used_mb': 0, 'free_mb': total_mb}
except:
pass
return {'total_mb': 0, 'used_mb': 0, 'free_mb': 0}
def get_all_metrics(self, gpu_id: int = 0) -> dict:
"""Get all metrics for a GPU in one call"""
return {
'utilization': self.get_gpu_utilization(gpu_id),
'temperature': self.get_gpu_temperature(gpu_id),
'vram': self.get_gpu_vram_mb(gpu_id),
'fan_speed': self.get_fan_speed(gpu_id),
'gpu_name': self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"GPU {gpu_id}",
}
# Singleton
_gpu_counter = None
def get_gpu_counter() -> GPUCounterMonitor:
global _gpu_counter
if _gpu_counter is None:
_gpu_counter = GPUCounterMonitor()
return _gpu_counter
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
mon = get_gpu_counter()
if mon.available:
print(f"GPU Counters: {mon.gpu_count} GPU(s)")
for i, name in enumerate(mon.gpu_names):
print(f"\n GPU {i}: {name}")
metrics = mon.get_all_metrics(i)
print(f" Utilization: {metrics['utilization']:.1f}%")
print(f" Temperature: {metrics['temperature']:.1f}°C")
print(f" VRAM: {metrics['vram']['total_mb']:.0f} MB total")
print(f" Fan: {metrics['fan_speed']}%")
else:
print("GPU Counters: Not available")
+337
View File
@@ -0,0 +1,337 @@
"""
AMD Process Monitor
===================
Monitors ComfyUI Python process memory/GPU usage during generations.
Auto-detects when generation starts/ends and records peak metrics.
"""
import logging
import time
import threading
from dataclasses import dataclass, field
from typing import Optional, List
logger = logging.getLogger(__name__)
@dataclass
class GenerationRecord:
"""Stats for a single generation run"""
timestamp: float
duration: float # seconds
vram_peak_mb: float = 0
vram_start_mb: float = 0
vram_end_mb: float = 0
vram_delta_mb: float = 0 # peak - start
ram_peak_mb: float = 0
ram_start_mb: float = 0
ram_end_mb: float = 0
cpu_peak: float = 0 # CPU utilization peak during generation
error: Optional[str] = None
@dataclass
class ProcessSnapshot:
"""Snapshot of a process at a point in time"""
timestamp: float
memory_rss_mb: float
memory_vms_mb: float
cpu_percent: float
num_threads: int
class ComfyProcessMonitor:
"""Monitors the ComfyUI main process for generation metrics"""
def __init__(self, poll_interval: float = 0.5):
self.poll_interval = poll_interval
self._process = None
self._process_pid = None
self._running = False
self._thread = None
# Generation detection
self._is_generating = False
self._gen_start_time = 0
self._current_record = None
self._prev_memory_mb = 0
# History
self.history: List[GenerationRecord] = []
self.current: Optional[GenerationRecord] = None
# Snapshots during current generation (for graph)
self._snapshots: List[ProcessSnapshot] = []
# Mutex for thread safety
self._lock = threading.Lock()
# Threshold tuning — lebih sensitif
self._MEMORY_START_THRESHOLD_MB = 50 # sebelumnya 100
self._CPU_START_THRESHOLD = 15 # sebelumnya 20
self._MEMORY_END_THRESHOLD_MB = -30 # sebelumnya -50
self._CPU_END_THRESHOLD = 8 # sebelumnya 10
def find_comfy_process(self) -> bool:
"""Find the ComfyUI main process"""
try:
import psutil
current_pid = None
# Method 1: Current process could be the ComfyUI worker
proc = psutil.Process()
proc_name = proc.name().lower()
# Check if we're running inside ComfyUI
cmdline = proc.cmdline()
cmdline_str = ' '.join(cmdline).lower()
if 'comfyui' in cmdline_str or 'main.py' in cmdline_str or 'comfy' in cmdline_str:
current_pid = proc.pid
# Method 2: Find by name
if not current_pid:
for p in psutil.process_iter(['pid', 'name', 'cmdline']):
try:
name = (p.info['name'] or '').lower()
cl = ' '.join(p.info['cmdline'] or []).lower()
if ('python' in name or 'python' in cl) and ('comfy' in cl or 'main.py' in cl):
current_pid = p.info['pid']
break
# Also catch 'nodes' or 'comfy' in path
if ('python' in name) and ('comfy' in cl):
current_pid = p.info['pid']
break
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
if current_pid:
self._process_pid = current_pid
self._process = psutil.Process(current_pid)
logger.info(f"Process Monitor: Found ComfyUI PID={current_pid}")
return True
logger.warning("Process Monitor: Could not find ComfyUI process")
return False
except ImportError:
logger.warning("Process Monitor: psutil not installed")
return False
except Exception as e:
logger.warning(f"Process Monitor: Find error: {e}")
return False
def start_monitoring(self):
"""Start background polling thread"""
if self._running:
return
if not self.find_comfy_process():
return
self._running = True
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
self._thread.start()
logger.info("Process Monitor: Started monitoring")
def stop_monitoring(self):
"""Stop background polling"""
self._running = False
if self._thread:
self._thread.join(timeout=2)
self._thread = None
def _poll_loop(self):
"""Main polling loop"""
try:
import psutil
except ImportError:
self._running = False
return
while self._running:
try:
if not self._process or not self._process.is_running():
logger.warning("Process Monitor: Process died, restarting...")
if not self.find_comfy_process():
time.sleep(self.poll_interval * 10)
continue
# Get memory info
mem_info = self._process.memory_info()
cpu_percent = self._process.cpu_percent(interval=0)
rss_mb = mem_info.rss / (1024 * 1024)
vms_mb = mem_info.vms / (1024 * 1024)
num_threads = self._process.num_threads()
snapshot = ProcessSnapshot(
timestamp=time.time(),
memory_rss_mb=rss_mb,
memory_vms_mb=vms_mb,
cpu_percent=cpu_percent,
num_threads=num_threads,
)
# Detect generation: significant & sustained memory increase
mem_delta = rss_mb - self._prev_memory_mb
with self._lock:
if not self._is_generating:
# Start condition: tuned thresholds
if mem_delta > self._MEMORY_START_THRESHOLD_MB or cpu_percent > self._CPU_START_THRESHOLD:
self._is_generating = True
self._gen_start_time = time.time()
self._current_record = GenerationRecord(
timestamp=self._gen_start_time,
duration=0,
vram_start_mb=rss_mb,
vram_peak_mb=rss_mb,
ram_start_mb=rss_mb,
ram_peak_mb=rss_mb,
)
self._snapshots = [snapshot]
logger.info(
f"Process Monitor: Generation START "
f"(RAM: {rss_mb:.0f}MB, CPU: {cpu_percent:.1f}%)"
)
else:
# In middle of generation — track peak
if self._current_record:
self._current_record.vram_peak_mb = max(
self._current_record.vram_peak_mb, rss_mb
)
self._current_record.ram_peak_mb = max(
self._current_record.ram_peak_mb, rss_mb
)
self._current_record.cpu_peak = max(
self._current_record.cpu_peak, cpu_percent
)
self._snapshots.append(snapshot)
# End condition: tuned thresholds
if mem_delta < self._MEMORY_END_THRESHOLD_MB and cpu_percent < self._CPU_END_THRESHOLD:
self._finalize_generation(rss_mb)
logger.info(
f"Process Monitor: Generation END "
f"(duration={self.current.duration:.1f}s, "
f"peak RAM={self.current.ram_peak_mb:.0f}MB)"
)
self._prev_memory_mb = rss_mb
except (psutil.NoSuchProcess, psutil.AccessDenied):
logger.warning("Process Monitor: Process access error")
time.sleep(self.poll_interval * 5)
except Exception as e:
logger.debug(f"Process Monitor: Poll error: {e}")
time.sleep(self.poll_interval)
def _finalize_generation(self, end_memory_mb: float):
"""Complete a generation record"""
if not self._current_record:
return
self._current_record.duration = time.time() - self._gen_start_time
self._current_record.vram_end_mb = end_memory_mb
self._current_record.ram_end_mb = end_memory_mb
self._current_record.vram_delta_mb = (
self._current_record.vram_peak_mb - self._current_record.vram_start_mb
)
self.current = self._current_record
self.history.append(self._current_record)
# Keep last 50 records
if len(self.history) > 50:
self.history = self.history[-50:]
# Reset
self._is_generating = False
self._current_record = None
self._snapshots = []
def get_current_generation(self) -> GenerationRecord:
"""Get current in-progress generation record"""
with self._lock:
if self._is_generating and self._current_record:
return self._current_record
return GenerationRecord(timestamp=0, duration=0)
def get_last_generation(self) -> Optional[GenerationRecord]:
"""Get the most recent finished generation"""
if self.history:
return self.history[-1]
return None
def get_summary(self) -> str:
"""Get a text summary of all generations"""
if not self.history:
return "No generations recorded yet."
lines = [f"=== ComfyUI Process Monitor ==="]
total_time = 0
peak_vram = 0
peak_ram = 0
for i, gen in enumerate(self.history[-5:]): # Last 5
total_time += gen.duration
peak_vram = max(peak_vram, gen.vram_peak_mb)
peak_ram = max(peak_ram, gen.ram_peak_mb)
lines.append(
f" #{i + 1}: {gen.duration:.1f}s "
f"| RAM: {gen.ram_start_mb:.0f}→{gen.ram_peak_mb:.0f}→{gen.ram_end_mb:.0f}MB"
)
if gen.vram_delta_mb > 0:
lines[-1] += f" (+{gen.vram_delta_mb:.0f}MB peak)"
lines.append(
f" --- Total: {total_time:.1f}s | "
f"Peak RAM: {peak_ram:.0f}MB | "
f"Peak VRAM: {peak_vram:.0f}MB"
)
return "\n".join(lines)
@property
def is_generating(self) -> bool:
return self._is_generating
# Singleton
_process_monitor = None
def get_process_monitor() -> ComfyProcessMonitor:
global _process_monitor
if _process_monitor is None:
_process_monitor = ComfyProcessMonitor()
return _process_monitor
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
mon = get_process_monitor()
mon.start_monitoring()
print("Monitoring ComfyUI process...")
print("Run a generation in ComfyUI to see stats.")
print("Press Ctrl+C to stop and see summary.\n")
try:
while True:
time.sleep(5)
if mon.is_generating:
gen = mon.get_current_generation()
print(f" ⚡ Generating... {gen.duration:.1f}s | "
f"Peak: {gen.vram_peak_mb:.0f}MB")
else:
last = mon.get_last_generation()
if last:
print(f" Last gen: {last.duration:.1f}s | "
f"Peak: {last.vram_peak_mb:.0f}MB | "
f"Delta: {last.vram_delta_mb:.0f}MB")
except KeyboardInterrupt:
pass
finally:
mon.stop_monitoring()
print("\n" + mon.get_summary())
+495 -118
View File
@@ -1,7 +1,16 @@
"""
AMD Utils - GPU Monitoring Backend
===================================
Multi-platform AMD GPU monitoring.
Supports: Linux (ROCm/pyrsmi), Windows (ADL/ADLX, WMI), Fallback (psutil)
"""
import platform
import logging
import subprocess
import re
from dataclasses import dataclass
from typing import Optional
from typing import Optional, List
logger = logging.getLogger(__name__)
@@ -9,14 +18,18 @@ logger = logging.getLogger(__name__)
@dataclass
class AMDGPUStats:
gpu_id: int
utilization_gpu: float
utilization_memory: float
memory_total: int # Bytes
memory_used: int # Bytes
memory_free: int # Bytes
temperature: float
fan_speed: int
is_available: bool
gpu_name: str = ""
utilization_gpu: float = 0.0
utilization_memory: float = 0.0
memory_total: int = 0 # Bytes
memory_used: int = 0 # Bytes
memory_free: int = 0 # Bytes
temperature: float = 0.0 # Celsius
fan_speed: int = 0 # Percent
core_clock: int = 0 # MHz
memory_clock: int = 0 # MHz
power_draw: float = 0.0 # Watts
is_available: bool = True
error_message: Optional[str] = None
@@ -25,13 +38,14 @@ class AMDMonitor:
self.available = False
self.method = None
self.gpu_count = 0
self.gpu_names: List[str] = []
self._backend = None
self._initialize()
def _initialize(self):
system = platform.system()
# --- LINUX IMPLEMENTATION (ROCm) ---
# --- LINUX: ROCm / pyrsmi ---
if system == "Linux":
try:
import pyrsmi
@@ -40,86 +54,240 @@ class AMDMonitor:
self.method = "pyrsmi"
self.available = True
self.gpu_count = pyrsmi.rocm_smi_get_num_devices()
logger.info(f"AMD Monitor: Linux detected, using pyrsmi. Found {self.gpu_count} GPUs.")
self.gpu_names = [f"AMD GPU {i}" for i in range(self.gpu_count)]
logger.info(f"AMD Monitor: ROCm/pyrsmi — {self.gpu_count} GPU(s)")
return
except ImportError:
logger.warning("Linux detected but 'pyrsmi' not installed.")
logger.debug("Linux: pyrsmi not installed")
except Exception as e:
logger.warning(f"Linux pyrsmi init failed: {e}")
logger.warning(f"Linux pyrsmi: {e}")
# --- WINDOWS IMPLEMENTATION (ADL) ---
# --- WINDOWS ---
elif system == "Windows":
try:
import pyadl
self._backend = pyadl
# Try init without custom allocator (safer)
try:
res = pyadl.ADL2_Main_Control_Create(0) # Use default allocator
except:
res = 0 # Assume success if function doesn't exist
if res == 0:
adapters = pyadl.ADL2_Adapter_AdapterInfo_Get_All()
self.gpu_count = len([a for a in adapters if getattr(a, 'is_present', True)])
self.method = "pyadl"
self.available = True
logger.info(f"AMD Monitor: Windows + pyadl. Found {self.gpu_count} GPUs.")
return
except ImportError:
logger.warning("Windows: pyadl not installed")
except Exception as e:
logger.warning(f"Windows pyadl error: {e}")
# Fallback: Try psutil for basic system info
try:
import psutil
self.method = "psutil-fallback"
self.available = True # Partial availability
self.gpu_count = 1 # Assume 1 GPU for fallback
logger.info("AMD Monitor: Using psutil fallback (limited AMD info)")
# Try ADLX
if self._try_adlx():
return
except ImportError:
pass
# Try pyadl
if self._try_pyadl():
return
# WMI-based detection + monitoring (best fallback on Windows)
if self._try_wmi():
return
# Last resort: psutil
self._try_psutil()
logger.error("No AMD monitoring backend available. Check requirements.")
if self.available:
return
if not self.available:
logger.error(f"AMD Monitor: No backend available on {system}")
# ==============================================
# BACKEND DETECTION
# ==============================================
def _try_adlx(self):
try:
import ctypes
paths = [
"C:/Program Files/AMD/ADLX/lib/ADLX.dll",
"C:/Program Files/AMD/ADLX/bin/ADLX.dll",
]
for path in paths:
try:
self._adlx_dll = ctypes.CDLL(path)
self.method = "adlx"
self.available = True
self._detect_gpu_names_wmi() or self.gpu_names.__setitem__(slice(None), ["AMD GPU 0"])
self.gpu_count = max(len(self.gpu_names), 1)
logger.info(f"AMD Monitor: ADLX at {path}")
return True
except:
continue
except:
pass
return False
def _try_pyadl(self):
try:
import pyadl
self._backend = pyadl
try:
pyadl.ADL2_Main_Control_Create(0)
except:
pass
adapters = pyadl.ADL2_Adapter_AdapterInfo_Get_All()
present = [a for a in adapters if getattr(a, 'is_present', True)]
self.gpu_count = len(present)
self.gpu_names = [getattr(a, 'strAdapterName', f'AMD GPU {i}') for i, a in enumerate(present)]
self.method = "pyadl"
self.available = True
logger.info(f"AMD Monitor: pyadl — {self.gpu_count} GPU(s): {self.gpu_names}")
return True
except ImportError:
pass
except AttributeError as e:
logger.debug(f"pyadl API mismatch: {e}")
except Exception as e:
logger.warning(f"pyadl error: {e}")
return False
def _try_wmi(self):
"""WMI-based detection + monitoring on Windows"""
# Import the GPU counter monitor module
success = self._detect_gpu_names_wmi()
if not success or self.gpu_count == 0:
return False
# Try to use the PowerShell counter backend for real utilization data
try:
from .amd_gpu_counter import get_gpu_counter
counter_mon = get_gpu_counter()
if counter_mon.available:
self._counter_mon = counter_mon
self.method = "powershell-counters"
self.available = True
# Use names from counter monitor (more accurate)
if counter_mon.gpu_names:
self.gpu_names = counter_mon.gpu_names
self.gpu_count = len(self.gpu_names)
logger.info(f"AMD Monitor: PowerShell counters — {self.gpu_count} GPU(s): {self.gpu_names}")
return True
except Exception as e:
logger.debug(f"GPU counter import failed: {e}")
# Fallback: basic WMI with no real-time metrics
self.method = "wmi-basic"
self.available = True
logger.info(f"AMD Monitor: WMI basic — {self.gpu_count} GPU(s): {self.gpu_names}")
return True
def _try_psutil(self):
try:
import psutil
self.method = "psutil-fallback"
self.available = True
self.gpu_count = 1
self.gpu_names = ["System RAM (no GPU backend)"]
logger.info("AMD Monitor: psutil-fallback")
except ImportError:
pass
def _detect_gpu_names_wmi(self) -> bool:
"""Use PowerShell to get AMD GPU names via WMI"""
try:
cmd = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject Win32_VideoController | "
"Where-Object { $_.Name -match 'AMD|Radeon|RDNA' } | "
"Select-Object -ExpandProperty Name"
]
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=5
)
if result.returncode == 0 and result.stdout.strip():
names = [n.strip() for n in result.stdout.strip().split('\n') if n.strip()]
if names:
self.gpu_names = names
self.gpu_count = len(names)
return True
# Fallback: get ALL video controllers
cmd2 = [
"powershell", "-NoProfile", "-Command",
"Get-WmiObject Win32_VideoController | Select-Object -ExpandProperty Name"
]
result2 = subprocess.run(
cmd2, capture_output=True, text=True, timeout=5
)
if result2.returncode == 0 and result2.stdout.strip():
names = [n.strip() for n in result2.stdout.strip().split('\n') if n.strip()]
if names:
self.gpu_names = names
self.gpu_count = len(names)
return True
except Exception as e:
logger.debug(f"WMI detection error: {e}")
return False
# ==============================================
# GET STATS
# ==============================================
def get_gpu_stats(self, gpu_id: int = 0) -> AMDGPUStats:
if not self.available:
return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, "Backend not available")
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name="",
is_available=False,
error_message="Backend not available"
)
try:
if self.method == "pyrsmi":
return self._get_stats_linux(gpu_id)
elif self.method == "adlx":
return self._get_stats_adlx(gpu_id)
elif self.method == "pyadl":
return self._get_stats_windows(gpu_id)
return self._get_stats_pyadl(gpu_id)
elif self.method in ("wmi", "wmi-basic"):
return self._get_stats_wmi(gpu_id)
elif self.method == "powershell-counters":
return self._get_stats_powershell(gpu_id)
elif self.method == "psutil-fallback":
return self._get_stats_windows_fallback(gpu_id)
return self._get_stats_psutil(gpu_id)
except Exception as e:
return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, str(e))
def _get_stats_linux(self, gpu_id: int) -> AMDGPUStats:
rsmi = self._backend
# Utilization
util = rsmi.rocm_smi_get_gpu_utilization(gpu_id)
# Memory (ROCm returns KB, convert to Bytes)
mem_total_kb = rsmi.rocm_smi_get_gpu_memory_total(gpu_id)
mem_used_kb = rsmi.rocm_smi_get_gpu_memory_used(gpu_id)
mem_total = mem_total_kb * 1024
mem_used = mem_used_kb * 1024
mem_free = mem_total - mem_used
mem_util = (mem_used / mem_total) * 100 if mem_total > 0 else 0
# Temp (ROCm returns mC, convert to C)
temp = rsmi.rocm_smi_get_temp(gpu_id, 0) / 1000.0
# Fan
fan = rsmi.rocm_smi_get_fan_speed(gpu_id, 0)
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name=self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else "",
is_available=False,
error_message=str(e)
)
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name=self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else "",
is_available=False,
error_message="No matching backend handler"
)
def get_all_gpu_stats(self) -> List[AMDGPUStats]:
if not self.available or self.gpu_count == 0:
return []
return [self.get_gpu_stats(i) for i in range(self.gpu_count)]
# ==============================================
# BACKEND: Linux / ROCm
# ==============================================
def _get_stats_linux(self, gpu_id: int) -> AMDGPUStats:
rsmi = self._backend
util = float(rsmi.rocm_smi_get_gpu_utilization(gpu_id))
mem_total_kb = float(rsmi.rocm_smi_get_gpu_memory_total(gpu_id))
mem_used_kb = float(rsmi.rocm_smi_get_gpu_memory_used(gpu_id))
mem_total = int(mem_total_kb * 1024)
mem_used = int(mem_used_kb * 1024)
mem_free = mem_total - mem_used
mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
temp = float(rsmi.rocm_smi_get_temp(gpu_id, 0)) / 1000.0
fan = int(rsmi.rocm_smi_get_fan_speed(gpu_id, 0))
core_clk = 0
mem_clk = 0
power = 0.0
try:
core_clk = int(rsmi.rocm_smi_get_gpu_clk_freq(gpu_id, 0))
mem_clk = int(rsmi.rocm_smi_get_gpu_clk_freq(gpu_id, 1))
power = float(rsmi.rocm_smi_get_power_avg(gpu_id)) / 1000.0
except:
pass
name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name=name,
utilization_gpu=util,
utilization_memory=mem_util,
memory_total=mem_total,
@@ -127,84 +295,293 @@ class AMDMonitor:
memory_free=mem_free,
temperature=temp,
fan_speed=fan,
core_clock=core_clk,
memory_clock=mem_clk,
power_draw=power,
is_available=True
)
def _get_stats_windows(self, gpu_id: int) -> AMDGPUStats:
adl = self._backend
adapters = adl.ADL2_Adapter_AdapterInfo_Get_All()
active_adapters = [a for a in adapters if getattr(a, 'is_present', True)]
if gpu_id >= len(active_adapters):
return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, "GPU Index out of range")
adapter = active_adapters[gpu_id]
adapter_index = adapter.adapter_index
# ==============================================
# BACKEND: Windows / WMI
# ==============================================
# Utilization (Overdrive6 is for GCN and newer)
gpu_util = 0
try:
usage = adl.ADL2_Overdrive6_CurrentUsage_Get(adapter_index, 0)
gpu_util = usage.iEngineClock
except:
pass
def _get_stats_wmi(self, gpu_id: int) -> AMDGPUStats:
"""Get GPU stats via PowerShell WMI queries"""
name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
# Memory (VRAM) - ADL doesn't expose this easily
# Try to get GPU utilization via performance counters
gpu_util = 0.0
temp = 0.0
fan = 0
mem_used = 0
mem_total = 0
mem_util = 0
# Temperature
temp = 0
mem_free = 0
mem_util = 0.0
try:
temp_data = adl.ADL2_Overdrive6_Temperature_Get(adapter_index, 0)
temp = temp_data.iTemperature / 1000.0 # ADL returns in milli-Celsius
except:
pass
# Fan Speed
fan = 0
try:
fan_data = adl.ADL2_Overdrive6_FanSpeed_Get(adapter_index, 0)
fan = fan_data.iFanSpeedPercent
except:
pass
# GPU Utilization via PowerShell Performance Counters
ps_cmd = (
"Get-Counter '\\GPU Engine(*engtype_3D)\\*' -SampleInterval 0 -MaxSamples 1 "
"2>$null | Select-Object -ExpandProperty CounterSamples | "
"Measure-Object -Property CookedValue -Average | "
"Select-Object -ExpandProperty Average"
)
result = subprocess.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=3
)
if result.returncode == 0 and result.stdout.strip():
val = float(result.stdout.strip())
if val > 0:
gpu_util = val
# GPU Temperature via WMI (AMD-specific)
temp_cmd = (
"Get-WmiObject -Namespace 'root/OpenHardwareMonitor' -Class Sensor "
"2>$null | Where-Object { $_.SensorType -eq 'Temperature' -and "
"$_.Name -match 'GPU' -and $_.Value -gt 0 } | "
"Select-Object -First 1 -ExpandProperty Value"
)
result_temp = subprocess.run(
["powershell", "-NoProfile", "-Command", temp_cmd],
capture_output=True, text=True, timeout=3
)
if result_temp.returncode == 0 and result_temp.stdout.strip():
temp = float(result_temp.stdout.strip())
# Fallback temperature: read from GPU registry via AMDPowerProfiling
if temp == 0:
reg_cmd = (
"Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\"
"{4d36e968-e325-11ce-bfc1-08002be10318}\\*' -Name 'AdpaterDesc' -ErrorAction SilentlyContinue "
"2>$null | Select-Object -ExpandProperty AdpaterDesc -First 1"
)
# Memory (VRAM) via WMI
vram_cmd = (
"Get-WmiObject Win32_VideoController | "
"Select-Object -First 1 -Property AdapterRAM, CurrentHorizontalResolution"
)
result_vram = subprocess.run(
["powershell", "-NoProfile", "-Command",
"(Get-WmiObject Win32_VideoController | Select-Object -First 1).AdapterRAM"],
capture_output=True, text=True, timeout=3
)
if result_vram.returncode == 0 and result_vram.stdout.strip():
mem_total = int(result_vram.stdout.strip())
# We can't get precise VRAM usage without ADLX, but estimate via process
if mem_total > 0:
mem_util = psutil_vram_estimate()
mem_used = int(mem_total * mem_util / 100)
mem_free = mem_total - mem_used
except Exception as e:
logger.debug(f"WMI stats error: {e}")
# Fallback to system RAM estimation for VRAM
if mem_total == 0:
try:
import psutil
svmem = psutil.virtual_memory()
mem_total = svmem.total
mem_used = svmem.used
mem_free = svmem.available
mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
except:
pass
return AMDGPUStats(
gpu_id=gpu_id,
utilization_gpu=gpu_util,
gpu_name=name,
utilization_gpu=gpu_util,
utilization_memory=mem_util,
memory_total=mem_total,
memory_used=mem_used,
memory_free=0,
memory_free=mem_free,
temperature=temp,
fan_speed=fan,
core_clock=0,
memory_clock=0,
power_draw=0,
is_available=True
)
def _get_stats_windows_fallback(self, gpu_id: int) -> AMDGPUStats:
"""Fallback using psutil when ADL fails"""
import psutil
# Basic system memory (not GPU-specific, but better than nothing)
svmem = psutil.virtual_memory()
# ==============================================
# BACKEND: Windows / pyadl
# ==============================================
def _get_stats_pyadl(self, gpu_id: int) -> AMDGPUStats:
adl = self._backend
name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
try:
adapters = adl.ADL2_Adapter_AdapterInfo_Get_All()
active = [a for a in adapters if getattr(a, 'is_present', True)]
if gpu_id >= len(active):
return AMDGPUStats(gpu_id=gpu_id, gpu_name=name, is_available=False, error_message="GPU out of range")
idx = active[gpu_id].adapter_index
gpu_util = 0.0
try:
usage = adl.ADL2_Overdrive6_CurrentUsage_Get(idx, 0)
gpu_util = float(usage.iEngineClock) / 100.0
except:
pass
temp = 0.0
try:
td = adl.ADL2_Overdrive6_Temperature_Get(idx, 0)
temp = float(td.iTemperature) / 1000.0
except:
pass
fan = 0
try:
fd = adl.ADL2_Overdrive6_FanSpeed_Get(idx, 0)
fan = int(fd.iFanSpeedPercent)
except:
pass
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name=name,
utilization_gpu=gpu_util,
temperature=temp,
fan_speed=fan,
is_available=True
)
except Exception as e:
return AMDGPUStats(gpu_id=gpu_id, gpu_name=name, is_available=False, error_message=str(e))
# ==============================================
# BACKEND: Windows / ADLX (placeholder)
# ==============================================
def _get_stats_adlx(self, gpu_id: int) -> AMDGPUStats:
name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
return AMDGPUStats(
gpu_id=gpu_id,
utilization_gpu=0,
utilization_memory=(svmem.used / svmem.total * 100),
gpu_name=name,
is_available=False,
error_message="ADLX full binding not yet implemented"
)
# ==============================================
# BACKEND: Windows / PowerShell Counters
# ==============================================
def _get_stats_powershell(self, gpu_id: int) -> AMDGPUStats:
"""Get GPU stats via PowerShell performance counters"""
try:
from .amd_gpu_counter import get_gpu_counter
except ImportError:
return self._get_stats_psutil(gpu_id)
try:
counter = get_gpu_counter()
metrics = counter.get_all_metrics(gpu_id)
name = metrics['gpu_name'] or (self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}")
# VRAM
vram = metrics['vram']
mem_total = int(vram['total_mb'] * 1024 * 1024) if vram['total_mb'] > 0 else 0
mem_used = int(vram['used_mb'] * 1024 * 1024) if vram['used_mb'] > 0 else 0
mem_free = int(vram['free_mb'] * 1024 * 1024) if vram['free_mb'] > 0 else mem_total
# If no VRAM used data, estimate from psutil process memory
if mem_used == 0 and mem_total > 0:
mem_util = psutil_vram_estimate()
mem_used = int(mem_total * mem_util / 100)
mem_free = mem_total - mem_used
elif mem_total > 0:
mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
else:
# No VRAM data at all — use system RAM as fallback
try:
import psutil
svmem = psutil.virtual_memory()
mem_total = svmem.total
mem_used = svmem.used
mem_free = svmem.available
mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
if not name:
name = "System RAM (GPU VRAM unavailable)"
except:
mem_util = 0
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name=name,
utilization_gpu=metrics['utilization'],
utilization_memory=mem_util,
memory_total=mem_total,
memory_used=mem_used,
memory_free=mem_free,
temperature=metrics['temperature'],
fan_speed=metrics['fan_speed'],
core_clock=0,
memory_clock=0,
power_draw=0,
is_available=True
)
except Exception as e:
logger.warning(f"PowerShell counter error: {e}")
return self._get_stats_psutil(gpu_id)
# ==============================================
# BACKEND: psutil fallback
# ==============================================
def _get_stats_psutil(self, gpu_id: int) -> AMDGPUStats:
import psutil
svmem = psutil.virtual_memory()
name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else "System RAM"
return AMDGPUStats(
gpu_id=gpu_id,
gpu_name=name,
utilization_gpu=0.0,
utilization_memory=(svmem.used / svmem.total * 100) if svmem.total > 0 else 0,
memory_total=svmem.total,
memory_used=svmem.used,
memory_free=svmem.available,
temperature=0,
temperature=0.0,
fan_speed=0,
is_available=True
)
# ==============================================
# HELPERS
# ==============================================
def psutil_vram_estimate() -> float:
"""Rough VRAM usage estimate from process memory"""
try:
import psutil
total = 0
for proc in psutil.process_iter(['memory_info', 'name']):
try:
info = proc.info
if info.get('memory_info'):
total += info['memory_info'].rss
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
svmem = psutil.virtual_memory()
return (total / svmem.total * 100) if svmem.total > 0 else 0
except:
return 0
# ==============================================
# Singleton
# ==============================================
_amd_monitor = None
def get_amd_monitor():
global _amd_monitor
if _amd_monitor is None:
+463 -424
View File
@@ -1,44 +1,44 @@
/**
* BANGTRIXTOOLKIT - AMD Monitor Floating Widget
* BANGTRIXTOOLKIT - AMD Monitor Overlay v3.2
* Real-time AMD GPU monitoring overlay for ComfyUI
* Cross-platform: Windows (ADL) / Linux (ROCm)
* Features: GPU name, sparkline, generation stats, settings, multi-GPU
*/
(function() {
(function () {
"use strict";
// ===== CONFIGURATION =====
const CONFIG = {
wsUrl: `ws://${window.location.host}/ws/amd_monitor`,
updateInterval: 1000,
var CONFIG = {
storageKey: 'bangtrix_amd_monitor_config',
defaultPosition: { top: '60px', right: '20px' }
};
// ===== STATE =====
let ws = null;
let reconnectTimer = null;
let reconnectAttempts = 0;
const MAX_RECONNECT = 5;
// Widget state
let widget = null;
let isMinimized = false;
let isVisible = true;
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
var ws = null;
var reconnectTimer = null;
var reconnectAttempts = 0;
var MAX_RECONNECT = 15;
// ===== INITIALIZATION =====
var widget = null;
var isMinimized = false;
var isVisible = true;
var isDragging = false;
var dragOffset = { x: 0, y: 0 };
var gpuDataMap = {};
var selectedGpuId = 0;
var gpuCount = 1;
var historyData = [];
var savedConfig = {};
// ===== UTILITY =====
function $(id) { return document.getElementById(id); }
// ===== INIT =====
function init() {
if (document.getElementById('bangtrix-amd-monitor')) {
console.log('AMD Monitor: Already initialized');
return;
}
console.log('AMD Monitor: Initializing floating widget...');
createWidget();
setupEventListeners();
console.log('[AMD Monitor] Initializing v3.2...');
loadConfig();
createWidget();
applyConfigToWidget();
setupEventListeners();
connectWebSocket();
}
@@ -47,235 +47,151 @@
widget = document.createElement('div');
widget.id = 'bangtrix-amd-monitor';
widget.className = 'amd-monitor-widget';
widget.innerHTML = `
<div class="amd-header">
<div class="amd-title">
<span class="amd-icon">🔴</span>
<span class="amd-text">AMD Monitor</span>
</div>
<div class="amd-controls">
<button class="amd-btn amd-btn-minimize" title="Minimize">−</button>
<button class="amd-btn amd-btn-settings" title="Settings">⚙</button>
<button class="amd-btn amd-btn-close" title="Hide">✕</button>
</div>
</div>
<div class="amd-body">
<div class="amd-grid">
<div class="amd-stat">
<span class="amd-label">GPU Load</span>
<span class="amd-value" id="amd-gpu-util">--</span>
<div class="amd-bar"><div class="amd-fill" id="amd-gpu-bar"></div></div>
</div>
<div class="amd-stat">
<span class="amd-label">VRAM</span>
<span class="amd-value" id="amd-vram">-- / -- GB</span>
<div class="amd-bar"><div class="amd-fill" id="amd-vram-bar"></div></div>
</div>
<div class="amd-stat">
<span class="amd-label">Temperature</span>
<span class="amd-value" id="amd-temp">--°C</span>
<div class="amd-bar"><div class="amd-fill amd-temp" id="amd-temp-bar"></div></div>
</div>
<div class="amd-stat">
<span class="amd-label">Fan Speed</span>
<span class="amd-value" id="amd-fan">--%</span>
<div class="amd-bar"><div class="amd-fill" id="amd-fan-bar"></div></div>
</div>
</div>
<div class="amd-footer">
<span class="amd-status" id="amd-status">Initializing...</span>
<span class="amd-method" id="amd-method">--</span>
</div>
</div>
`;
injectStyles();
widget.innerHTML =
'<div class="amd-header">' +
'<div class="amd-title">' +
'<span class="amd-icon">🔴</span>' +
'<span class="amd-text">AMD Monitor</span>' +
'</div>' +
'<div class="amd-controls">' +
'<button class="amd-btn amd-btn-settings" title="Settings">⚙</button>' +
'<button class="amd-btn amd-btn-minimize" title="Minimize">−</button>' +
'<button class="amd-btn amd-btn-close" title="Hide">✕</button>' +
'</div>' +
'</div>' +
'<div class="amd-body">' +
'<div class="amd-gpu-selector" id="amd-gpu-selector" style="display:none;">' +
'<span class="amd-label-small">GPU:</span>' +
'<div class="amd-chip-group" id="amd-gpu-chips"></div>' +
'</div>' +
'<div class="amd-gpu-name" id="amd-gpu-name">AMD GPU</div>' +
'<div class="amd-grid">' +
'<div class="amd-stat"><span class="amd-label">GPU Load</span><span class="amd-value" id="amd-gpu-util">--</span><div class="amd-bar"><div class="amd-fill" id="amd-gpu-bar"></div></div></div>' +
'<div class="amd-stat"><span class="amd-label">VRAM</span><span class="amd-value" id="amd-vram">-- / -- GB</span><div class="amd-bar"><div class="amd-fill" id="amd-vram-bar"></div></div></div>' +
'<div class="amd-stat"><span class="amd-label">Temp</span><span class="amd-value" id="amd-temp">N/A</span><div class="amd-bar"><div class="amd-fill amd-temp" id="amd-temp-bar"></div></div></div>' +
'<div class="amd-stat"><span class="amd-label">Fan</span><span class="amd-value" id="amd-fan">N/A</span><div class="amd-bar"><div class="amd-fill" id="amd-fan-bar"></div></div></div>' +
'</div>' +
'<div class="amd-sparkline-container" id="amd-sparkline-container" style="display:none;">' +
'<div class="amd-label-small">GPU Load History</div>' +
'<canvas class="amd-sparkline" id="amd-sparkline" width="232" height="40"></canvas>' +
'</div>' +
'<div class="amd-process" id="amd-process">' +
'<div class="amd-process-header">' +
'<span class="amd-label-small">Generation Stats</span>' +
'<span class="amd-process-status" id="amd-process-status">● idle</span>' +
'</div>' +
'<div class="amd-process-body">' +
'<div class="amd-stat-row"><span class="amd-label-xs">Duration</span><span class="amd-value-sm" id="amd-gen-duration">--</span></div>' +
'<div class="amd-stat-row"><span class="amd-label-xs">RAM Peak</span><span class="amd-value-sm" id="amd-gen-ram-peak">--</span></div>' +
'<div class="amd-stat-row"><span class="amd-label-xs">RAM Delta</span><span class="amd-value-sm" id="amd-gen-ram-delta">--</span></div>' +
'<div class="amd-stat-row"><span class="amd-label-xs">CPU Peak</span><span class="amd-value-sm" id="amd-gen-cpu-peak">--</span></div>' +
'</div>' +
'</div>' +
'<div class="amd-alert" id="amd-alert" style="display:none;">' +
'<span class="amd-alert-icon">⚠️</span><span class="amd-alert-text" id="amd-alert-text">VRAM Alert</span>' +
'</div>' +
'<div class="amd-settings" id="amd-settings" style="display:none;">' +
'<div class="amd-settings-row"><span class="amd-label-small">Interval</span><select class="amd-select" id="amd-settings-interval"><option value="1">1s</option><option value="5" selected>5s</option><option value="10">10s</option><option value="30">30s</option></select></div>' +
'<div class="amd-settings-row"><span class="amd-label-small">VRAM Alert %</span><input type="number" class="amd-input" id="amd-settings-vram-threshold" value="90" min="50" max="100" /></div>' +
'<div class="amd-settings-row"><span class="amd-label-small">Temp Unit</span><select class="amd-select" id="amd-settings-temp-unit"><option value="celsius" selected>°C</option><option value="fahrenheit">°F</option></select></div>' +
'<div class="amd-settings-row"><span class="amd-label-small">Sparkline</span><label class="amd-toggle"><input type="checkbox" id="amd-settings-sparkline" checked /><span class="amd-toggle-slider"></span></label></div>' +
'</div>' +
'<div class="amd-footer"><span class="amd-status" id="amd-status">Initializing...</span><span class="amd-method" id="amd-method">--</span></div>' +
'</div>';
var style = document.createElement('style');
style.id = 'bangtrix-amd-monitor-styles';
style.textContent =
'#bangtrix-amd-monitor{position:fixed;top:60px;right:20px;width:260px;' +
'background:rgba(18,18,24,0.92);border:1px solid rgba(196,48,43,0.3);border-radius:10px;' +
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:11px;' +
'color:#e0e0e0;z-index:9999;box-shadow:0 8px 32px rgba(0,0,0,0.4);' +
'backdrop-filter:blur(8px);user-select:none;}' +
'#bangtrix-amd-monitor.hidden{display:none}' +
'#bangtrix-amd-monitor.minimized .amd-body{display:none}' +
'.amd-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;' +
'background:linear-gradient(135deg,rgba(196,48,43,0.2)0%,rgba(139,32,29,0.1)100%);' +
'border-radius:10px 10px 0 0;cursor:move;border-bottom:1px solid rgba(255,255,255,0.05)}' +
'.amd-title{display:flex;align-items:center;gap:8px;font-weight:600;font-size:12px;color:#fff}' +
'.amd-icon{font-size:14px;animation:pulse 2s infinite}' +
'@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}}' +
'.amd-controls{display:flex;gap:4px}' +
'.amd-btn{background:rgba(255,255,255,0.1);border:none;color:#ccc;width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:12px}' +
'.amd-btn:hover{background:rgba(255,255,255,0.2);color:#fff}' +
'.amd-btn-close:hover{background:rgba(255,80,80,0.3)}' +
'.amd-body{padding:12px 14px}' +
'.amd-gpu-name{text-align:center;font-size:11px;font-weight:600;color:#ff6b6b;margin-bottom:8px;' +
'padding:4px 8px;border-radius:4px;background:rgba(255,255,255,0.04);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.amd-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}' +
'.amd-stat{display:flex;flex-direction:column;gap:2px}' +
'.amd-label{color:#888;font-size:10px;text-transform:uppercase}' +
'.amd-label-small{color:#888;font-size:10px}' +
'.amd-value{font-weight:600;font-size:13px;color:#00e676}' +
'.amd-value.na{color:#666}' +
'.amd-value.warning{color:#ffaa00}' +
'.amd-value.critical{color:#ff4444}' +
'.amd-bar{height:4px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;margin-top:2px}' +
'.amd-fill{height:100%;border-radius:2px;transition:width 0.3s ease;width:0%;background:linear-gradient(90deg,#00e676,#00c853)}' +
'.amd-fill.amd-temp{background:linear-gradient(90deg,#00e676,#ffaa00,#ff4444)}' +
'.amd-sparkline-container{margin-top:8px}' +
'.amd-sparkline{width:100%;height:40px;border-radius:4px;background:rgba(0,0,0,0.3)}' +
'.amd-process{margin-top:8px;padding:6px 8px;background:rgba(0,0,0,0.2);border-radius:6px;border:1px solid rgba(255,255,255,0.06)}' +
'.amd-process-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}' +
'.amd-process-status{font-size:10px}' +
'.amd-process-status.generating{color:#00e676}' +
'.amd-process-status.idle{color:#888}' +
'.amd-process-body{display:grid;grid-template-columns:1fr 1fr;gap:2px 8px}' +
'.amd-stat-row{display:flex;justify-content:space-between}' +
'.amd-label-xs{color:#666;font-size:9px}' +
'.amd-value-sm{color:#ccc;font-size:10px}' +
'.amd-alert{display:flex;align-items:center;gap:6px;padding:6px 10px;margin-top:8px;' +
'background:rgba(255,80,80,0.15);border:1px solid rgba(255,80,80,0.3);border-radius:6px;font-size:11px;color:#ff6b6b}' +
'.amd-settings{margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05)}' +
'.amd-settings-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}' +
'.amd-select,.amd-input{background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.15);color:#e0e0e0;padding:2px 6px;border-radius:4px;font-size:10px;width:70px}' +
'.amd-input{width:50px}' +
'.amd-toggle{position:relative;width:32px;height:16px;cursor:pointer}' +
'.amd-toggle input{display:none}' +
'.amd-toggle-slider{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.15);border-radius:8px;transition:0.2s}' +
'.amd-toggle-slider:before{content:"";position:absolute;width:12px;height:12px;left:2px;bottom:2px;background:#ccc;border-radius:50%;transition:0.2s}' +
'.amd-toggle input:checked+.amd-toggle-slider{background:rgba(196,48,43,0.6)}' +
'.amd-toggle input:checked+.amd-toggle-slider:before{transform:translateX(16px);background:#fff}' +
'.amd-footer{display:flex;justify-content:space-between;margin-top:10px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05);font-size:10px;color:#666}' +
'.amd-status{transition:color 0.2s}' +
'.amd-status.connected{color:#00e676}' +
'.amd-status.disconnected{color:#ff4444}' +
'.amd-status.warning{color:#ffaa00}' +
'.amd-method{color:#888;font-family:monospace;font-size:9px}';
document.head.appendChild(style);
document.body.appendChild(widget);
setupDrag();
return widget;
}
function injectStyles() {
const style = document.createElement('style');
style.id = 'bangtrix-amd-monitor-styles';
style.textContent = `
#bangtrix-amd-monitor {
position: fixed;
top: 60px;
right: 20px;
width: 240px;
background: rgba(18, 18, 24, 0.92);
border: 1px solid rgba(196, 48, 43, 0.3);
border-radius: 10px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 11px;
color: #e0e0e0;
z-index: 9999;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
user-select: none;
transition: opacity 0.2s, transform 0.2s;
}
#bangtrix-amd-monitor.hidden { display: none; }
#bangtrix-amd-monitor.minimized .amd-body { display: none; }
.amd-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: linear-gradient(135deg, rgba(196, 48, 43, 0.2) 0%, rgba(139, 32, 29, 0.1) 100%);
border-radius: 10px 10px 0 0;
cursor: move;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.amd-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 12px;
color: #fff;
}
.amd-icon { font-size: 14px; animation: pulse 2s infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.amd-controls { display: flex; gap: 4px; }
.amd-btn {
background: rgba(255,255,255,0.1);
border: none;
color: #ccc;
width: 22px;
height: 22px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.amd-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
.amd-btn-close:hover { background: rgba(255, 80, 80, 0.3); color: #fff; }
.amd-body { padding: 12px 14px; }
.amd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; }
.amd-stat {
display: flex;
flex-direction: column;
gap: 4px;
}
.amd-label {
color: #888;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.amd-value {
font-weight: 600;
font-size: 13px;
color: #00e676;
transition: color 0.2s;
}
.amd-value.warning { color: #ffaa00; }
.amd-value.critical { color: #ff4444; }
.amd-bar {
height: 4px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
overflow: hidden;
margin-top: 2px;
}
.amd-fill {
height: 100%;
background: linear-gradient(90deg, #00e676, #00c853);
border-radius: 2px;
transition: width 0.3s ease;
width: 0%;
}
.amd-fill.amd-temp {
background: linear-gradient(90deg, #00e676, #ffaa00, #ff4444);
}
.amd-footer {
display: flex;
justify-content: space-between;
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid rgba(255,255,255,0.05);
font-size: 10px;
color: #666;
}
.amd-status { transition: color 0.2s; }
.amd-status.connected { color: #00e676; }
.amd-status.disconnected { color: #ff4444; }
.amd-status.warning { color: #ffaa00; }
.amd-method { color: #888; font-family: monospace; }
.amd-header:active { cursor: grabbing; }
@media (max-width: 768px) {
#bangtrix-amd-monitor { width: 200px; font-size: 10px; }
.amd-grid { grid-template-columns: 1fr; }
}
`;
document.head.appendChild(style);
}
// ===== DRAG FUNCTIONALITY =====
// ===== DRAG =====
function setupDrag() {
const header = widget.querySelector('.amd-header');
header.addEventListener('mousedown', (e) => {
var header = widget.querySelector('.amd-header');
if (!header) return;
header.addEventListener('mousedown', function(e) {
if (e.target.closest('.amd-btn')) return;
isDragging = true;
const rect = widget.getBoundingClientRect();
var rect = widget.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
widget.style.transition = 'none';
document.body.style.cursor = 'grabbing';
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
});
function onDrag(e) {
if (!isDragging) return;
const x = e.clientX - dragOffset.x;
const y = e.clientY - dragOffset.y;
const maxX = window.innerWidth - widget.offsetWidth + 20;
const maxY = window.innerHeight - widget.offsetHeight + 20;
widget.style.left = Math.max(10, Math.min(x, maxX)) + 'px';
widget.style.top = Math.max(10, Math.min(y, maxY)) + 'px';
var maxX = window.innerWidth - widget.offsetWidth + 20;
var maxY = window.innerHeight - widget.offsetHeight + 20;
widget.style.left = Math.max(10, Math.min(e.clientX - dragOffset.x, maxX)) + 'px';
widget.style.top = Math.max(10, Math.min(e.clientY - dragOffset.y, maxY)) + 'px';
widget.style.right = 'auto';
}
function stopDrag() {
isDragging = false;
widget.style.transition = '';
@@ -288,253 +204,376 @@
// ===== EVENT LISTENERS =====
function setupEventListeners() {
widget.querySelector('.amd-btn-minimize').addEventListener('click', () => {
var minBtn = widget.querySelector('.amd-btn-minimize');
if (minBtn) minBtn.addEventListener('click', function() {
isMinimized = !isMinimized;
widget.classList.toggle('minimized', isMinimized);
widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : '−';
minBtn.textContent = isMinimized ? '+' : '−';
saveConfig();
});
widget.querySelector('.amd-btn-close').addEventListener('click', () => {
isVisible = !isVisible;
widget.classList.toggle('hidden', !isVisible);
var closeBtn = widget.querySelector('.amd-btn-close');
if (closeBtn) closeBtn.addEventListener('click', function() {
isVisible = false;
widget.classList.add('hidden');
saveConfig();
showToast('Hidden. Ctrl+Shift+M to show.');
});
var settingsBtn = widget.querySelector('.amd-btn-settings');
var settingsPanel = $('amd-settings');
if (settingsBtn && settingsPanel) settingsBtn.addEventListener('click', function() {
var isOpen = settingsPanel.style.display !== 'none';
settingsPanel.style.display = isOpen ? 'none' : 'block';
settingsBtn.classList.toggle('active', !isOpen);
});
var intervalSelect = $('amd-settings-interval');
if (intervalSelect) intervalSelect.addEventListener('change', function(e) {
sendCommand({type:'set_interval',interval:parseFloat(e.target.value)});
saveConfig();
});
widget.querySelector('.amd-btn-settings').addEventListener('click', () => {
showToast('Settings coming soon!');
var vramInput = $('amd-settings-vram-threshold');
if (vramInput) vramInput.addEventListener('change', function(e) {
savedConfig.vramThreshold = parseInt(e.target.value) || 90;
saveConfig();
});
var tempSelect = $('amd-settings-temp-unit');
if (tempSelect) tempSelect.addEventListener('change', function(e) {
savedConfig.tempUnit = e.target.value;
saveConfig();
if (gpuDataMap[selectedGpuId]) refreshDisplay(selectedGpuId);
});
var sparkCheck = $('amd-settings-sparkline');
if (sparkCheck) sparkCheck.addEventListener('change', function(e) {
var container = $('amd-sparkline-container');
if (container) container.style.display = e.target.checked ? 'block' : 'none';
saveConfig();
});
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'M') {
e.preventDefault();
isVisible = !isVisible;
widget.classList.toggle('hidden', !isVisible);
if (isVisible) connectWebSocket();
saveConfig();
}
});
}
// ===== CONFIGURATION PERSISTENCE =====
// ===== CONFIG =====
function loadConfig() {
try {
const saved = localStorage.getItem(CONFIG.storageKey);
if (saved) {
const config = JSON.parse(saved);
if (config.position) {
if (config.position.left) {
widget.style.left = config.position.left;
widget.style.right = 'auto';
}
if (config.position.top) {
widget.style.top = config.position.top;
}
}
if (config.minimized !== undefined) {
isMinimized = config.minimized;
widget.classList.toggle('minimized', isMinimized);
widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : '−';
}
if (config.visible !== undefined) {
isVisible = config.visible;
widget.classList.toggle('hidden', !isVisible);
}
}
} catch (e) {
console.warn('AMD Monitor: Failed to load config:', e);
try { var saved = localStorage.getItem(CONFIG.storageKey); if (saved) savedConfig = JSON.parse(saved); } catch(e) {}
}
function applyConfigToWidget() {
if (!widget) return;
var c = savedConfig;
if (c.position) {
if (c.position.left) { widget.style.left = c.position.left; widget.style.right = 'auto'; }
if (c.position.top) widget.style.top = c.position.top;
}
if (c.minimized !== undefined) {
isMinimized = c.minimized;
widget.classList.toggle('minimized', isMinimized);
var btn = widget.querySelector('.amd-btn-minimize');
if (btn) btn.textContent = isMinimized ? '+' : '−';
}
if (c.visible !== undefined) { isVisible = c.visible; widget.classList.toggle('hidden', !isVisible); }
var sel = $('amd-settings-interval'); if (sel) sel.value = String(c.updateInterval || 5);
var inp = $('amd-settings-vram-threshold'); if (inp) inp.value = c.vramThreshold || 90;
var sel2 = $('amd-settings-temp-unit'); if (sel2) sel2.value = c.tempUnit || 'celsius';
var chk = $('amd-settings-sparkline');
if (chk) { chk.checked = c.showSparkline !== false; var cont = $('amd-sparkline-container'); if (cont) cont.style.display = chk.checked ? 'block' : 'none'; }
}
function saveConfig() {
try {
const config = {
position: {
top: widget.style.top,
left: widget.style.left
},
minimized: isMinimized,
visible: isVisible
var config = {
position: { top: widget.style.top, left: widget.style.left },
minimized: isMinimized, visible: isVisible,
updateInterval: parseFloat($('amd-settings-interval')?.value) || 5,
vramThreshold: parseInt($('amd-settings-vram-threshold')?.value) || 90,
tempUnit: $('amd-settings-temp-unit')?.value || 'celsius',
showSparkline: $('amd-settings-sparkline')?.checked !== false
};
localStorage.setItem(CONFIG.storageKey, JSON.stringify(config));
} catch (e) {
console.warn('AMD Monitor: Failed to save config:', e);
}
} catch(e) {}
}
// ===== WEBSOCKET CONNECTION =====
// ===== WEBSOCKET =====
function connectWebSocket() {
if (ws?.readyState === WebSocket.OPEN) return;
console.log('AMD Monitor: Connecting to', CONFIG.wsUrl);
if (ws && ws.readyState === WebSocket.OPEN) return;
if (!isVisible) return;
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = protocol + '//' + window.location.host + '/ws/amd_monitor';
console.log('[AMD Monitor] Connecting...');
updateStatus('Connecting...', 'warning');
ws = new WebSocket(CONFIG.wsUrl);
ws.onopen = () => {
console.log('AMD Monitor: Connected');
ws = new WebSocket(url);
ws.onopen = function() {
console.log('[AMD Monitor] Connected');
updateStatus('Connected', 'connected');
updateMethod('Live');
reconnectAttempts = 0;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'amd_stats') {
updateDisplay(data);
}
} catch (e) {
console.error('AMD Monitor: Parse error:', e);
}
ws.onmessage = function(event) {
try { var data = JSON.parse(event.data); if (data.type === 'amd_stats') handleStatsData(data); } catch(e) {}
};
ws.onclose = () => {
console.log('AMD Monitor: Disconnected');
ws.onclose = function() {
updateStatus('Disconnected', 'disconnected');
updateMethod('--');
if (reconnectAttempts < MAX_RECONNECT) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 8000);
console.log(`AMD Monitor: Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
var delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 10000);
reconnectTimer = setTimeout(connectWebSocket, delay);
}
};
ws.onerror = (error) => {
console.error('AMD Monitor: WebSocket error:', error);
updateStatus('Connection error', 'disconnected');
};
ws.onerror = function() {};
}
// ===== DISPLAY UPDATES =====
function updateDisplay(data) {
function sendCommand(cmd) { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(cmd)); }
// ===== DATA HANDLING =====
function handleStatsData(data) {
gpuDataMap[data.gpu_id] = data;
if (data.gpu_count && gpuCount !== data.gpu_count) { gpuCount = data.gpu_count; updateGpuSelector(); }
if (data.gpu_id === selectedGpuId) refreshDisplay(selectedGpuId);
}
function refreshDisplay(gpuId) {
var data = gpuDataMap[gpuId];
if (!data) return;
var nameEl = $('amd-gpu-name');
if (nameEl) nameEl.textContent = data.gpu_name || ('AMD GPU ' + gpuId);
if (!data.is_available) {
setStat('amd-gpu-util', 'N/A');
setStat('amd-vram', 'N/A');
setStat('amd-temp', 'N/A');
setStat('amd-fan', 'N/A');
setBar('amd-gpu-bar', 0);
setBar('amd-vram-bar', 0);
setBar('amd-temp-bar', 0);
setBar('amd-fan-bar', 0);
updateStatus(data.error || 'AMD backend unavailable', 'warning');
['amd-gpu-util','amd-vram','amd-temp','amd-fan'].forEach(function(id) { setStat(id, 'N/A'); });
['amd-gpu-bar','amd-vram-bar','amd-temp-bar','amd-fan-bar'].forEach(function(id) { setBar(id, 0); });
updateStatus(data.error || 'Unavailable', 'warning');
updateMethod(data.method || '--');
return;
}
const gpuUtil = data.gpu_utilization || 0;
setStat('amd-gpu-util', `${gpuUtil.toFixed(1)}%`, gpuUtil);
// GPU Load
var gpuUtil = data.gpu_utilization !== undefined ? data.gpu_utilization : 0;
setStat('amd-gpu-util', gpuUtil.toFixed(1) + '%', gpuUtil);
setBar('amd-gpu-bar', gpuUtil);
const vramUsedGB = (data.vram_used_mb || 0) / 1024;
const vramTotalGB = (data.vram_total_mb || 0) / 1024;
const vramPct = data.vram_usage_pct || (vramTotalGB > 0 ? (vramUsedGB / vramTotalGB * 100) : 0);
setStat('amd-vram', `${vramUsedGB.toFixed(1)} / ${vramTotalGB.toFixed(1)} GB`, vramPct);
setBar('amd-vram-bar', vramPct);
const temp = data.temperature || 0;
setStat('amd-temp', `${temp.toFixed(1)}°C`, temp);
setBar('amd-temp-bar', Math.min(temp, 100));
const fan = data.fan_speed || 0;
setStat('amd-fan', `${fan}%`, fan);
setBar('amd-fan-bar', fan);
updateStatus(`GPU ${data.gpu_id}`, 'connected');
updateMethod(data.method || 'unknown');
// VRAM
var vramUsedMB = data.vram_used_mb || 0;
var vramTotalMB = data.vram_total_mb || 0;
var vramPct = data.vram_usage_pct || (vramTotalMB > 0 ? (vramUsedMB / vramTotalMB * 100) : 0);
if (vramTotalMB > 0) {
setStat('amd-vram', (vramUsedMB / 1024).toFixed(1) + ' / ' + (vramTotalMB / 1024).toFixed(1) + ' GB', vramPct);
setBar('amd-vram-bar', vramPct);
} else { setStat('amd-vram', 'N/A'); setBar('amd-vram-bar', 0); }
// Temperature
var temp = data.temperature || 0;
if (temp > 0) {
var tUnit = savedConfig.tempUnit || 'celsius';
var tempDisplay = temp;
var tempUnitStr = '°C';
if (tUnit === 'fahrenheit') { tempDisplay = temp * 9/5 + 32; tempUnitStr = '°F'; }
setStat('amd-temp', tempDisplay.toFixed(1) + tempUnitStr, temp);
setBar('amd-temp-bar', Math.min(temp, 100));
} else { setStat('amd-temp', 'N/A'); setBar('amd-temp-bar', 0); }
// Fan
var fan = data.fan_speed || 0;
if (fan > 0) { setStat('amd-fan', fan + '%', fan); setBar('amd-fan-bar', fan); }
else { setStat('amd-fan', 'N/A'); setBar('amd-fan-bar', 0); }
// Status
var methodInfo = data.method || 'unknown';
if (data.core_clock_mhz > 0) methodInfo += ' | ' + data.core_clock_mhz + 'MHz';
if (data.power_draw_watts > 0) methodInfo += ' | ' + data.power_draw_watts + 'W';
updateMethod(methodInfo);
var statusText = data.gpu_name || ('GPU ' + gpuId);
if (gpuCount > 1) statusText += ' (' + (gpuId + 1) + '/' + gpuCount + ')';
updateStatus(statusText, 'connected');
// Sparkline
if (data.history && data.history.length > 0) {
historyData = data.history;
drawSparkline(historyData);
var sparkContainer = $('amd-sparkline-container');
if (sparkContainer) {
var chk = $('amd-settings-sparkline');
sparkContainer.style.display = (chk && chk.checked) ? 'block' : 'none';
}
}
// Process stats
updateProcessDisplay(data);
checkVramAlert(vramPct);
}
function setStat(elementId, value, numericValue = null) {
const el = document.getElementById(elementId);
function updateProcessDisplay(data) {
var process = data.process;
if (!process) return;
var statusEl = $('amd-process-status');
var durEl = $('amd-gen-duration');
var ramPeakEl = $('amd-gen-ram-peak');
var ramDeltaEl = $('amd-gen-ram-delta');
var cpuPeakEl = $('amd-gen-cpu-peak');
if (!statusEl) return;
if (process.is_generating && process.generation) {
var gen = process.generation;
statusEl.textContent = '● generating';
statusEl.className = 'amd-process-status generating';
if (durEl) durEl.textContent = gen.duration + 's';
if (ramPeakEl) ramPeakEl.textContent = gen.ram_peak_mb + ' MB';
if (ramDeltaEl) ramDeltaEl.textContent = '+' + (gen.ram_peak_mb - gen.ram_start_mb) + ' MB';
if (cpuPeakEl) cpuPeakEl.textContent = gen.cpu_peak + '%';
} else if (process.last_generation) {
var last = process.last_generation;
statusEl.textContent = '● idle (' + (process.generation_count || 0) + ' gens)';
statusEl.className = 'amd-process-status idle';
if (durEl) durEl.textContent = last.duration + 's';
if (ramPeakEl) ramPeakEl.textContent = last.ram_peak_mb + ' MB';
if (ramDeltaEl) ramDeltaEl.textContent = '+' + last.vram_delta_mb + ' MB';
if (cpuPeakEl) cpuPeakEl.textContent = last.cpu_peak + '%';
} else {
statusEl.textContent = '● idle';
statusEl.className = 'amd-process-status idle';
if (durEl) durEl.textContent = '--'; if (ramPeakEl) ramPeakEl.textContent = '--';
if (ramDeltaEl) ramDeltaEl.textContent = '--'; if (cpuPeakEl) cpuPeakEl.textContent = '--';
}
}
function updateGpuSelector() {
var selector = $('amd-gpu-selector');
var chips = $('amd-gpu-chips');
if (!selector || !chips) return;
if (gpuCount <= 1) { selector.style.display = 'none'; return; }
selector.style.display = 'block';
chips.innerHTML = '';
for (var i = 0; i < gpuCount; i++) (function(idx) {
var chip = document.createElement('span');
chip.className = 'amd-chip' + (idx === selectedGpuId ? ' active' : '');
chip.textContent = 'GPU ' + idx;
chip.addEventListener('click', function() {
selectedGpuId = idx;
chips.querySelectorAll('.amd-chip').forEach(function(c) { c.classList.remove('active'); });
chip.classList.add('active');
refreshDisplay(selectedGpuId);
});
chips.appendChild(chip);
})(i);
}
// ===== SPARKLINE =====
function drawSparkline(values) {
var canvas = $('amd-sparkline');
if (!canvas) return;
var ctx = canvas.getContext('2d');
var w = canvas.width, h = canvas.height, padding = 2;
ctx.clearRect(0, 0, w, h);
if (values.length < 2 || values.every(function(v) { return v === 0; })) {
ctx.fillStyle = '#555'; ctx.font = '10px monospace'; ctx.textAlign = 'center';
ctx.fillText('awaiting data...', w / 2, h / 2 + 3); return;
}
var max = Math.max.apply(null, values); if (max < 1) max = 1;
var stepX = (w - padding * 2) / (values.length - 1);
ctx.beginPath(); ctx.strokeStyle = '#00e676'; ctx.lineWidth = 1.5; ctx.lineJoin = 'round';
for (var i = 0; i < values.length; i++) {
var x = padding + i * stepX;
var y = h - padding - (values[i] / max) * (h - padding * 2);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.lineTo(padding + (values.length - 1) * stepX, h - padding);
ctx.lineTo(padding, h - padding); ctx.closePath();
ctx.fillStyle = 'rgba(0,230,118,0.1)'; ctx.fill();
var lastVal = values[values.length - 1];
ctx.fillStyle = '#00e676'; ctx.font = 'bold 9px monospace'; ctx.textAlign = 'right';
ctx.fillText(lastVal.toFixed(0) + '%', w - padding, 10);
}
// ===== ALERTS =====
var lastAlertTime = 0;
function checkVramAlert(vramPct) {
var threshold = savedConfig.vramThreshold || 90;
if (vramPct >= threshold && Date.now() - lastAlertTime > 10000) {
lastAlertTime = Date.now();
var alertEl = $('amd-alert');
var alertText = $('amd-alert-text');
if (alertEl && alertText) {
alertText.textContent = 'VRAM ' + vramPct.toFixed(0) + '% >= ' + threshold + '%';
alertEl.style.display = 'flex';
setTimeout(function() { if (alertEl) alertEl.style.display = 'none'; }, 5000);
}
}
}
// ===== UI HELPERS =====
function setStat(elementId, value, numericValue) {
var el = $(elementId);
if (!el) return;
el.textContent = value;
el.classList.remove('warning', 'critical');
if (numericValue !== null) {
if (elementId === 'amd-temp' && numericValue > 80) {
el.classList.add('critical');
} else if (elementId === 'amd-temp' && numericValue > 65) {
el.classList.add('warning');
} else if ((elementId === 'amd-gpu-util' || elementId === 'amd-vram') && numericValue > 90) {
el.classList.add('critical');
} else if ((elementId === 'amd-gpu-util' || elementId === 'amd-vram') && numericValue > 75) {
el.classList.add('warning');
}
el.classList.remove('warning', 'critical', 'na');
if (value === 'N/A') { el.classList.add('na'); }
else if (numericValue !== null && numericValue !== undefined) {
if ((elementId === 'amd-temp') && numericValue > 80) el.classList.add('critical');
else if ((elementId === 'amd-temp') && numericValue > 65) el.classList.add('warning');
else if ((elementId === 'amd-gpu-util' || elementId.indexOf('vram') >= 0) && numericValue > 90) el.classList.add('critical');
else if ((elementId === 'amd-gpu-util' || elementId.indexOf('vram') >= 0) && numericValue > 75) el.classList.add('warning');
}
}
function setBar(elementId, percent) {
const el = document.getElementById(elementId);
if (!el) return;
el.style.width = `${Math.min(100, Math.max(0, percent))}%`;
var el = $(elementId); if (!el) return;
el.style.width = Math.min(100, Math.max(0, percent)) + '%';
}
function updateStatus(message, className) {
const el = document.getElementById('amd-status');
if (!el) return;
el.textContent = message;
el.className = `amd-status ${className}`;
const icon = widget.querySelector('.amd-icon');
if (icon) {
icon.textContent = className === 'connected' ? '🟢' :
className === 'warning' ? '🟡' : '🔴';
}
var el = $('amd-status'); if (!el) return;
el.textContent = message; el.className = 'amd-status ' + className;
var icon = widget.querySelector('.amd-icon');
if (icon) icon.textContent = className === 'connected' ? '🟢' : className === 'warning' ? '🟡' : '🔴';
}
function updateMethod(method) {
const el = document.getElementById('amd-method');
if (!el) return;
el.textContent = method ? `[${method}]` : '';
}
function updateMethod(method) { var el = $('amd-method'); if (el) el.textContent = method || ''; }
// ===== UTILITIES =====
function showToast(message, duration = 2000) {
const toast = document.createElement('div');
function showToast(message, duration) {
duration = duration || 2000;
var old = document.querySelector('.bangtrix-toast'); if (old) old.remove();
var toast = document.createElement('div');
toast.className = 'bangtrix-toast';
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(30, 30, 40, 0.95);
color: #fff;
padding: 10px 16px;
border-radius: 6px;
font-size: 12px;
z-index: 10001;
border: 1px solid rgba(196, 48, 43, 0.5);
animation: slideIn 0.2s ease;
`;
toast.style.cssText = 'position:fixed;bottom:20px;right:20px;background:rgba(30,30,40,0.95);color:#fff;padding:10px 16px;border-radius:6px;font-size:12px;z-index:10001;border:1px solid rgba(196,48,43,0.5);animation:slideIn 0.2s ease;max-width:300px;';
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.2s ease';
setTimeout(() => toast.remove(), 200);
}, duration);
setTimeout(function() { toast.style.animation = 'slideOut 0.2s ease'; setTimeout(function() { toast.remove(); }, 200); }, duration);
}
// ===== ANIMATIONS =====
if (!document.getElementById('amd-monitor-animations')) {
const animStyle = document.createElement('style');
animStyle.id = 'amd-monitor-animations';
animStyle.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(animStyle);
var s = document.createElement('style');
s.id = 'amd-monitor-animations';
s.textContent = '@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}@keyframes slideOut{from{transform:translateX(0);opacity:1}to{transform:translateX(100%);opacity:0}}';
document.head.appendChild(s);
}
// ===== AMD CHIP SELECTOR =====
if (!document.getElementById('amd-chip-styles')) {
var cs = document.createElement('style');
cs.id = 'amd-chip-styles';
cs.textContent = '.amd-chip{padding:2px 8px;border-radius:10px;font-size:10px;background:rgba(255,255,255,0.08);cursor:pointer;transition:all 0.15s;border:1px solid transparent}.amd-chip:hover{background:rgba(255,255,255,0.15)}.amd-chip.active{background:rgba(196,48,43,0.3);border-color:rgba(196,48,43,0.5);color:#fff}.amd-gpu-selector{margin-bottom:6px}.amd-chip-group{display:flex;gap:4px;flex-wrap:wrap}';
document.head.appendChild(cs);
}
// ===== START =====
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.addEventListener('beforeunload', () => {
if (ws) ws.close();
if (reconnectTimer) clearTimeout(reconnectTimer);
});
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
window.addEventListener('beforeunload', function() { if (ws) ws.close(); if (reconnectTimer) clearTimeout(reconnectTimer); });
})();