ADD AMD MONITOR BETA
This commit is contained in:
+14
-5
@@ -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
|
||||
@@ -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**
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
> *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
@@ -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}")
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 ===")
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
@@ -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
@@ -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); });
|
||||
})();
|
||||
Reference in New Issue
Block a user