cluster4npu/ui/dialogs/benchmark_dialog.py
abin 55040733fe feat: implement Phase 1-4 performance visualization and device management
Phase 1 — Performance Benchmarking:
- PerformanceBenchmarker: sequential vs parallel benchmark with injectable runner
- PerformanceHistory: JSON-backed benchmark history with regression support
- PerformanceDashboard: real-time FPS/latency display widget
- BenchmarkDialog: one-click benchmark with 3-phase progress bar

Phase 2 — Device Management:
- DeviceManager: NPU dongle scan, assign/unassign, load balance recommendation
- DeviceManagementPanel: live device status cards with auto-refresh
- BottleneckAlert: dataclass for pipeline bottleneck detection

Phase 3 — Advanced Features:
- OptimizationEngine: 3 optimization rules (rebalance/adjust_queue/add_devices)
- TemplateManager: 3 built-in pipeline templates (YOLOv5, fire detection, dual-model)

Phase 4 — Report Export:
- ReportExporter: PDF (reportlab, optional) and CSV export
- ExportReportDialog: format selection + path picker UI

192 unit tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:32:05 +08:00

208 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
ui/dialogs/benchmark_dialog.py
BenchmarkDialog — 一鍵啟動 Benchmark 的 QDialog。
顯示三階段進度條(熱機/循序/平行)、即時 FPS、完成後加速倍數大字體
以及循序 vs 平行的 FPS 與延遲對比表。
Benchmark 執行透過 QThread 進行,避免 UI 凍結。
若 pipeline_config 為空,顯示提示訊息並禁用開始按鈕。
"""
from typing import Any, List, Optional
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QProgressBar,
QPushButton,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)
class _BenchmarkWorker(QThread):
"""在背景執行緒執行 benchmark避免 UI 凍結。"""
progress_updated = pyqtSignal(str, int)
result_ready = pyqtSignal(object, object, float)
error_occurred = pyqtSignal(str)
def __init__(self, benchmarker: Any) -> None:
super().__init__()
self._benchmarker = benchmarker
def run(self) -> None:
try:
seq_result, par_result, speedup = self._benchmarker.run_full_benchmark(
progress_callback=self._on_progress
)
self.result_ready.emit(seq_result, par_result, speedup)
except Exception as exc:
self.error_occurred.emit(str(exc))
def _on_progress(self, phase: str, value: int) -> None:
self.progress_updated.emit(phase, value)
class BenchmarkDialog(QDialog):
"""Benchmark 觸發與結果顯示對話框。
Args:
parent: 父視窗。
pipeline_config: 目前的 pipeline Stage 設定列表。若為空,禁用開始按鈕。
"""
def __init__(
self,
parent: Optional[QWidget],
pipeline_config: List[Any],
) -> None:
super().__init__(parent)
self._pipeline_config = pipeline_config
self.seq_result: Optional[Any] = None
self.par_result: Optional[Any] = None
self.current_phase: str = ""
self._worker: Optional[_BenchmarkWorker] = None
self.setWindowTitle("Performance Benchmark")
# UI 元件
self.info_label = QLabel("")
self.progress_bar = QProgressBar()
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(100)
self.fps_label = QLabel("FPS: —")
self.phase_label = QLabel("")
self.speedup_label = QLabel("")
self.result_table = QTableWidget(2, 3)
self.result_table.setHorizontalHeaderLabels(["模式", "FPS", "Avg Latency (ms)"])
self.start_button = QPushButton("開始 Benchmark")
self.close_button = QPushButton("關閉")
self._setup_ui()
self._apply_initial_state()
def _setup_ui(self) -> None:
layout = QVBoxLayout()
layout.addWidget(self.info_label)
progress_row = QHBoxLayout()
progress_row.addWidget(self.progress_bar)
progress_row.addWidget(self.phase_label)
layout.addLayout(progress_row)
fps_row = QHBoxLayout()
fps_row.addWidget(QLabel("即時 FPS"))
fps_row.addWidget(self.fps_label)
layout.addLayout(fps_row)
layout.addWidget(self.speedup_label)
layout.addWidget(self.result_table)
btn_row = QHBoxLayout()
btn_row.addWidget(self.start_button)
btn_row.addWidget(self.close_button)
layout.addLayout(btn_row)
self.setLayout(layout)
def _apply_initial_state(self) -> None:
if not self._pipeline_config:
self.info_label.setText("尚未設定 Pipeline請先在 Pipeline Editor 中建立 Stage。")
self.start_button.setEnabled(False)
else:
self.info_label.setText(f"已載入 {len(self._pipeline_config)} 個 Stage可開始 Benchmark。")
self.start_button.setEnabled(True)
def start_benchmark(self, benchmarker: Any) -> None:
"""在 QThread 中執行 benchmark避免 UI 凍結。
Args:
benchmarker: PerformanceBenchmarker 實例。
"""
self._worker = _BenchmarkWorker(benchmarker)
self._worker.progress_updated.connect(self.update_progress)
self._worker.result_ready.connect(self._on_result_ready)
self._worker.error_occurred.connect(self._on_error)
self._worker.finished.connect(self._worker.deleteLater)
self.start_button.setEnabled(False)
self._worker.start()
def update_progress(self, phase: str, value: int) -> None:
"""更新進度條與當前階段。
Args:
phase: 當前階段名稱("warmup" / "sequential" / "parallel")。
value: 進度值0100
"""
_PHASE_LABELS = {
"warmup": "熱機中...",
"sequential": "循序測試...",
"parallel": "平行測試...",
}
self.current_phase = phase
self.progress_bar.setValue(value)
self.phase_label.setText(_PHASE_LABELS.get(phase, phase))
def show_result(
self,
seq_result: Any,
par_result: Any,
speedup: float,
) -> None:
"""顯示 benchmark 結果。
Args:
seq_result: 循序模式的 BenchmarkResult。
par_result: 平行模式的 BenchmarkResult。
speedup: 加速倍數par.fps / seq.fps
"""
self.seq_result = seq_result
self.par_result = par_result
font = self.speedup_label.font()
font.setPointSize(20)
font.setBold(True)
self.speedup_label.setFont(font)
self.speedup_label.setText(f"{speedup:.1f}x FASTER")
self._populate_table(seq_result, par_result)
def _populate_table(self, seq_result: Any, par_result: Any) -> None:
rows = [
("循序", seq_result),
("平行", par_result),
]
for row_idx, (mode_label, result) in enumerate(rows):
self.result_table.setItem(row_idx, 0, QTableWidgetItem(mode_label))
try:
self.result_table.setItem(row_idx, 1, QTableWidgetItem(f"{result.fps:.1f}"))
self.result_table.setItem(
row_idx, 2, QTableWidgetItem(f"{result.avg_latency_ms:.1f}")
)
except (AttributeError, TypeError):
pass
def _on_result_ready(
self,
seq_result: Any,
par_result: Any,
speedup: float,
) -> None:
self.show_result(seq_result, par_result, speedup)
def _on_error(self, message: str) -> None:
self.info_label.setText(f"Benchmark 失敗:{message}")
self.progress_bar.setValue(0)
self._worker = None
self.start_button.setEnabled(True)