forked from masonhuang/cluster4npu
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>
208 lines
6.6 KiB
Python
208 lines
6.6 KiB
Python
"""
|
||
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: 進度值(0–100)。
|
||
"""
|
||
_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)
|