""" Benchmark 结果可视化模块 """ import os from typing import List, Dict, Any from datetime import datetime import numpy as np from .results import TestResult from .utils import ensure_dir, setup_logging logger = setup_logging() def generate_visualizations(results: List[TestResult], output_dir: str) -> List[str]: """生成所有可视化图表""" try: import matplotlib.pyplot as plt import matplotlib matplotlib.use('Agg') # 非交互式后端 plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False except ImportError: logger.warning("matplotlib 未安装,跳过可视化") return [] ensure_dir(output_dir) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") generated_files = [] # 1. GPU 利用率 vs 摄像头数量 fig_path = _plot_gpu_vs_cameras(results, output_dir, timestamp) if fig_path: generated_files.append(fig_path) # 2. 吞吐量 vs Batch Size fig_path = _plot_throughput_vs_batch(results, output_dir, timestamp) if fig_path: generated_files.append(fig_path) # 3. 延迟分布 fig_path = _plot_latency_distribution(results, output_dir, timestamp) if fig_path: generated_files.append(fig_path) # 4. 实时性热力图 fig_path = _plot_realtime_heatmap(results, output_dir, timestamp) if fig_path: generated_files.append(fig_path) # 5. GPU 饱和点分析 fig_path = _plot_saturation_analysis(results, output_dir, timestamp) if fig_path: generated_files.append(fig_path) logger.info(f"生成 {len(generated_files)} 个可视化图表") return generated_files def _plot_gpu_vs_cameras(results: List[TestResult], output_dir: str, timestamp: str) -> str: """GPU 利用率 vs 摄像头数量""" import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 6)) # 按 batch size 分组 batch_sizes = sorted(set(r.batch_size for r in results)) camera_counts = sorted(set(r.num_cameras for r in results)) for bs in batch_sizes: bs_results = [r for r in results if r.batch_size == bs] cameras = [] gpu_utils = [] for cam in camera_counts: cam_results = [r for r in bs_results if r.num_cameras == cam] if cam_results: cameras.append(cam) gpu_utils.append(np.mean([r.gpu_utilization_avg for r in cam_results])) if cameras: ax.plot(cameras, gpu_utils, marker='o', label=f'Batch={bs}') ax.axhline(y=85, color='r', linestyle='--', label='饱和阈值 (85%)') ax.set_xlabel('摄像头数量') ax.set_ylabel('GPU 利用率 (%)') ax.set_title('GPU 利用率 vs 摄像头数量') ax.legend() ax.grid(True, alpha=0.3) ax.set_ylim(0, 100) fig_path = os.path.join(output_dir, f'gpu_vs_cameras_{timestamp}.png') plt.savefig(fig_path, dpi=150, bbox_inches='tight') plt.close() return fig_path def _plot_throughput_vs_batch(results: List[TestResult], output_dir: str, timestamp: str) -> str: """吞吐量 vs Batch Size""" import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 6)) batch_sizes = sorted(set(r.batch_size for r in results)) camera_counts = sorted(set(r.num_cameras for r in results)) for cam in camera_counts: cam_results = [r for r in results if r.num_cameras == cam] batches = [] throughputs = [] for bs in batch_sizes: bs_results = [r for r in cam_results if r.batch_size == bs] if bs_results: batches.append(bs) throughputs.append(np.mean([r.total_throughput_fps for r in bs_results])) if batches: ax.plot(batches, throughputs, marker='s', label=f'{cam} 路摄像头') ax.set_xlabel('Batch Size') ax.set_ylabel('总吞吐量 (FPS)') ax.set_title('吞吐量 vs Batch Size') ax.legend() ax.grid(True, alpha=0.3) ax.set_xticks(batch_sizes) fig_path = os.path.join(output_dir, f'throughput_vs_batch_{timestamp}.png') plt.savefig(fig_path, dpi=150, bbox_inches='tight') plt.close() return fig_path def _plot_latency_distribution(results: List[TestResult], output_dir: str, timestamp: str) -> str: """延迟分布对比""" import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 2, figsize=(14, 5)) batch_sizes = sorted(set(r.batch_size for r in results)) # 左图: 平均延迟 ax1 = axes[0] x = np.arange(len(batch_sizes)) width = 0.35 avg_latencies = [np.mean([r.avg_latency_ms for r in results if r.batch_size == bs]) for bs in batch_sizes] p95_latencies = [np.mean([r.p95_latency_ms for r in results if r.batch_size == bs]) for bs in batch_sizes] ax1.bar(x - width/2, avg_latencies, width, label='平均延迟') ax1.bar(x + width/2, p95_latencies, width, label='P95 延迟') ax1.set_xlabel('Batch Size') ax1.set_ylabel('延迟 (ms)') ax1.set_title('延迟 vs Batch Size') ax1.set_xticks(x) ax1.set_xticklabels(batch_sizes) ax1.legend() ax1.grid(True, alpha=0.3, axis='y') # 右图: 按摄像头数量 ax2 = axes[1] camera_counts = sorted(set(r.num_cameras for r in results)) avg_by_cam = [np.mean([r.avg_latency_ms for r in results if r.num_cameras == cam]) for cam in camera_counts] p95_by_cam = [np.mean([r.p95_latency_ms for r in results if r.num_cameras == cam]) for cam in camera_counts] x2 = np.arange(len(camera_counts)) ax2.bar(x2 - width/2, avg_by_cam, width, label='平均延迟') ax2.bar(x2 + width/2, p95_by_cam, width, label='P95 延迟') ax2.set_xlabel('摄像头数量') ax2.set_ylabel('延迟 (ms)') ax2.set_title('延迟 vs 摄像头数量') ax2.set_xticks(x2) ax2.set_xticklabels(camera_counts) ax2.legend() ax2.grid(True, alpha=0.3, axis='y') plt.tight_layout() fig_path = os.path.join(output_dir, f'latency_distribution_{timestamp}.png') plt.savefig(fig_path, dpi=150, bbox_inches='tight') plt.close() return fig_path def _plot_realtime_heatmap(results: List[TestResult], output_dir: str, timestamp: str) -> str: """实时性热力图""" import matplotlib.pyplot as plt batch_sizes = sorted(set(r.batch_size for r in results)) camera_counts = sorted(set(r.num_cameras for r in results)) # 创建矩阵 matrix = np.zeros((len(camera_counts), len(batch_sizes))) for i, cam in enumerate(camera_counts): for j, bs in enumerate(batch_sizes): matching = [r for r in results if r.num_cameras == cam and r.batch_size == bs] if matching: # 实时性比例 realtime_ratio = sum(1 for r in matching if r.is_realtime_capable) / len(matching) matrix[i, j] = realtime_ratio * 100 fig, ax = plt.subplots(figsize=(10, 8)) im = ax.imshow(matrix, cmap='RdYlGn', aspect='auto', vmin=0, vmax=100) ax.set_xticks(np.arange(len(batch_sizes))) ax.set_yticks(np.arange(len(camera_counts))) ax.set_xticklabels(batch_sizes) ax.set_yticklabels(camera_counts) ax.set_xlabel('Batch Size') ax.set_ylabel('摄像头数量') ax.set_title('实时性热力图 (绿色=可实时, 红色=不可实时)') # 添加数值标注 for i in range(len(camera_counts)): for j in range(len(batch_sizes)): text = ax.text(j, i, f'{matrix[i, j]:.0f}%', ha='center', va='center', color='black', fontsize=9) plt.colorbar(im, ax=ax, label='实时性比例 (%)') fig_path = os.path.join(output_dir, f'realtime_heatmap_{timestamp}.png') plt.savefig(fig_path, dpi=150, bbox_inches='tight') plt.close() return fig_path def _plot_saturation_analysis(results: List[TestResult], output_dir: str, timestamp: str) -> str: """GPU 饱和点分析""" import matplotlib.pyplot as plt fig, axes = plt.subplots(2, 2, figsize=(14, 10)) camera_counts = sorted(set(r.num_cameras for r in results)) batch_sizes = sorted(set(r.batch_size for r in results)) # 1. GPU 利用率 vs 吞吐量 ax1 = axes[0, 0] for bs in batch_sizes: bs_results = [r for r in results if r.batch_size == bs] throughputs = [r.total_throughput_fps for r in bs_results] gpu_utils = [r.gpu_utilization_avg for r in bs_results] ax1.scatter(throughputs, gpu_utils, label=f'Batch={bs}', alpha=0.7) ax1.axhline(y=85, color='r', linestyle='--', alpha=0.5) ax1.set_xlabel('吞吐量 (FPS)') ax1.set_ylabel('GPU 利用率 (%)') ax1.set_title('GPU 利用率 vs 吞吐量') ax1.legend() ax1.grid(True, alpha=0.3) # 2. 显存使用 ax2 = axes[0, 1] for bs in batch_sizes: bs_results = [r for r in results if r.batch_size == bs] cameras = [] mem_used = [] for cam in camera_counts: cam_results = [r for r in bs_results if r.num_cameras == cam] if cam_results: cameras.append(cam) mem_used.append(np.mean([r.memory_used_mb for r in cam_results])) if cameras: ax2.plot(cameras, mem_used, marker='o', label=f'Batch={bs}') ax2.set_xlabel('摄像头数量') ax2.set_ylabel('显存使用 (MB)') ax2.set_title('显存使用 vs 摄像头数量') ax2.legend() ax2.grid(True, alpha=0.3) # 3. 丢帧率 ax3 = axes[1, 0] for bs in batch_sizes: bs_results = [r for r in results if r.batch_size == bs] cameras = [] drop_rates = [] for cam in camera_counts: cam_results = [r for r in bs_results if r.num_cameras == cam] if cam_results: cameras.append(cam) drop_rates.append(np.mean([r.frame_drop_rate for r in cam_results])) if cameras: ax3.plot(cameras, drop_rates, marker='s', label=f'Batch={bs}') ax3.axhline(y=5, color='r', linestyle='--', label='阈值 (5%)') ax3.set_xlabel('摄像头数量') ax3.set_ylabel('丢帧率 (%)') ax3.set_title('丢帧率 vs 摄像头数量') ax3.legend() ax3.grid(True, alpha=0.3) # 4. 饱和状态统计 ax4 = axes[1, 1] saturated_count = sum(1 for r in results if r.is_gpu_saturated) not_saturated_count = len(results) - saturated_count ax4.pie([not_saturated_count, saturated_count], labels=['未饱和', '已饱和'], colors=['#2ecc71', '#e74c3c'], autopct='%1.1f%%', startangle=90) ax4.set_title(f'GPU 饱和状态分布\n(共 {len(results)} 个测试)') plt.tight_layout() fig_path = os.path.join(output_dir, f'saturation_analysis_{timestamp}.png') plt.savefig(fig_path, dpi=150, bbox_inches='tight') plt.close() return fig_path