280 lines
12 KiB
Python
280 lines
12 KiB
Python
"""生成 HTML 可视化报告"""
|
|
import json
|
|
import logging
|
|
import os
|
|
|
|
from . import config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def generate_report(report: dict) -> str:
|
|
"""从报告数据生成 HTML 报告文件,返回输出路径"""
|
|
html = build_html(report)
|
|
|
|
os.makedirs(config.REPORT_DIR, exist_ok=True)
|
|
output_path = os.path.join(config.REPORT_DIR, "report.html")
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
f.write(html)
|
|
|
|
logger.info("报告已生成: %s", output_path)
|
|
return output_path
|
|
|
|
|
|
def _diff_color(difficulty: str) -> str:
|
|
"""难度对应的颜色"""
|
|
return {
|
|
"简单": "#22c55e",
|
|
"中等": "#f59e0b",
|
|
"耗时": "#ef4444",
|
|
"压力": "#8b5cf6",
|
|
}.get(difficulty, "#6b7280")
|
|
|
|
|
|
def _stats_rows(results: list) -> str:
|
|
"""构建详细数据表格行 HTML"""
|
|
rows = []
|
|
for r in results:
|
|
stats = r.get("statistics", {})
|
|
if not stats:
|
|
continue
|
|
rows.append(
|
|
f'<tr>'
|
|
f'<td><strong>{r["case_name"]}</strong></td>'
|
|
f'<td><span class="diff-badge" style="background:{_diff_color(r["difficulty"])}">{r["difficulty"]}</span></td>'
|
|
f'<td class="status-{r["status"]}" style="font-weight:bold">{r["status"]}</td>'
|
|
f'<td class="mono">{stats.get("mean_ms", "N/A")}</td>'
|
|
f'<td class="mono">{stats.get("std_ms", "N/A")}</td>'
|
|
f'<td class="mono">{stats.get("min_ms", "N/A")}</td>'
|
|
f'<td class="mono">{stats.get("max_ms", "N/A")}</td>'
|
|
f'<td class="mono">{stats.get("p95_ms", "N/A")}</td>'
|
|
f'</tr>'
|
|
)
|
|
return "\n".join(rows)
|
|
|
|
|
|
def _chart_data(results: list) -> tuple:
|
|
"""提取图表数据:(labels, means, p95s)"""
|
|
labels, means, p95s = [], [], []
|
|
for r in results:
|
|
stats = r.get("statistics", {})
|
|
if stats:
|
|
labels.append(r["case_name"])
|
|
means.append(stats.get("mean_ms", 0))
|
|
p95s.append(stats.get("p95_ms", 0))
|
|
return labels, means, p95s
|
|
|
|
|
|
def _performance_analysis(results: list) -> str:
|
|
"""性能分析摘要"""
|
|
_, means, _ = _chart_data(results)
|
|
if len(means) < 2:
|
|
return ""
|
|
|
|
best_idx = means.index(min(means))
|
|
worst_idx = means.index(max(means))
|
|
ratio = (means[worst_idx] / means[best_idx]) if means[best_idx] > 0 else "N/A"
|
|
avg = sum(means) / len(means)
|
|
|
|
return (
|
|
f'<div class="analysis-box">'
|
|
f'<h3>性能分析</h3>'
|
|
f'<ul>'
|
|
f'<li><strong>最快用例:</strong> {results[best_idx]["case_name"]} ({means[best_idx]:.2f}ms)</li>'
|
|
f'<li><strong>最慢用例:</strong> {results[worst_idx]["case_name"]} ({means[worst_idx]:.2f}ms)</li>'
|
|
f'<li><strong>最快/最慢比:</strong> {ratio}x</li>'
|
|
f'<li><strong>平均延迟:</strong> {avg:.2f}ms</li>'
|
|
f'</ul></div>'
|
|
)
|
|
|
|
|
|
def _concurrency_summary(results: list) -> str:
|
|
"""提取并发压力测试摘要(case_08),无则返回空串"""
|
|
for r in results:
|
|
raw = r.get("raw_data", {}) or {}
|
|
if r.get("case_id") == "case_08_concurrency" and raw:
|
|
def g(key, suffix=""):
|
|
val = raw.get(key)
|
|
return "N/A" if val is None else f"{val}{suffix}"
|
|
|
|
keep = raw.get("tps_keep_rate_pct", 0)
|
|
keep_color = "#22c55e" if keep >= 60 else ("#f59e0b" if keep > 0 else "#ef4444")
|
|
err = raw.get("error_rate_pct", 0)
|
|
err_color = "#22c55e" if err == 0 else "#ef4444"
|
|
|
|
return (
|
|
f'<div class="section">'
|
|
f'<h2>⚡ 并发压力测试摘要</h2>'
|
|
f'<div class="dashboard">'
|
|
f'<div class="stat-card"><div class="label">并发数</div><div class="value" style="color:#8b5cf6">{g("concurrency")}</div></div>'
|
|
f'<div class="stat-card"><div class="label">基准 TPS</div><div class="value" style="color:#6366f1">{g("baseline_tps")}</div></div>'
|
|
f'<div class="stat-card"><div class="label">并发 TPS</div><div class="value" style="color:#6366f1">{g("concurrent_tps")}</div></div>'
|
|
f'<div class="stat-card"><div class="label">TPS 保持率</div><div class="value" style="color:{keep_color}">{g("tps_keep_rate_pct", "%")}</div></div>'
|
|
f'<div class="stat-card"><div class="label">P95 延迟</div><div class="value" style="color:#ef4444">{g("e2e_p95_ms", "ms")}</div></div>'
|
|
f'<div class="stat-card"><div class="label">P99 延迟</div><div class="value" style="color:#ef4444">{g("e2e_p99_ms", "ms")}</div></div>'
|
|
f'<div class="stat-card"><div class="label">错误率</div><div class="value" style="color:{err_color}">{g("error_rate_pct", "%")}</div></div>'
|
|
f'</div></div>'
|
|
)
|
|
return ""
|
|
|
|
|
|
def build_html(report: dict) -> str:
|
|
"""构建 HTML 报告"""
|
|
summary = report.get("summary", {})
|
|
results = report.get("results", [])
|
|
env = report.get("environment", {})
|
|
config_ = report.get("config", {})
|
|
timestamp = report.get("timestamp", "")
|
|
run_id = report.get("run_id", "")
|
|
|
|
case_names, case_means, case_p95s = _chart_data(results)
|
|
|
|
return f"""<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>大模型速度测试报告 - {run_id}</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
<style>
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
background: #f8fafc; color: #1e293b; line-height: 1.6;
|
|
}}
|
|
.container {{ max-width: 1400px; margin: 0 auto; padding: 20px; }}
|
|
.header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 12px; margin-bottom: 20px; }}
|
|
.header h1 {{ font-size: 28px; margin-bottom: 10px; }}
|
|
.header .meta {{ font-size: 14px; opacity: 0.9; }}
|
|
.dashboard {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; }}
|
|
.stat-card {{ background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
|
|
.stat-card .label {{ font-size: 12px; color: #64748b; text-transform: uppercase; }}
|
|
.stat-card .value {{ font-size: 28px; font-weight: bold; margin-top: 5px; }}
|
|
.section {{ background: white; border-radius: 10px; padding: 25px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }}
|
|
.section h2 {{ font-size: 20px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #e2e8f0; }}
|
|
table {{ width: 100%; border-collapse: collapse; }}
|
|
th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #e2e8f0; }}
|
|
th {{ background: #f1f5f9; font-weight: 600; font-size: 13px; text-transform: uppercase; }}
|
|
tr:hover {{ background: #f8fafc; }}
|
|
.mono {{ font-family: 'SF Mono', 'Fira Code', monospace; }}
|
|
.diff-badge {{ padding: 4px 10px; border-radius: 12px; color: white; font-size: 12px; font-weight: 600; }}
|
|
.status-passed {{ color: #22c55e; }}
|
|
.status-failed {{ color: #ef4444; }}
|
|
.chart-container {{ position: relative; height: 400px; margin: 20px 0; }}
|
|
.analysis-box {{ background: #f0fdf4; border-left: 4px solid #22c55e; padding: 15px; border-radius: 6px; }}
|
|
.analysis-box ul {{ padding-left: 20px; margin-top: 10px; }}
|
|
.analysis-box li {{ margin: 5px 0; }}
|
|
footer {{ text-align: center; padding: 20px; color: #64748b; font-size: 13px; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>大模型速度测试报告</h1>
|
|
<div class="meta">
|
|
Run ID: {run_id} | 时间: {timestamp} | 用例: {config_.get('total_cases', 0)} | 重复: {config_.get('repeats', 0)}次
|
|
<br>模型: {env.get('model', 'N/A')} | 平台: {env.get('platform', 'N/A')}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard">
|
|
<div class="stat-card"><div class="label">总用例数</div><div class="value">{summary.get('total_cases', 0)}</div></div>
|
|
<div class="stat-card"><div class="label">通过</div><div class="value" style="color:#22c55e">{summary.get('passed', 0)}</div></div>
|
|
<div class="stat-card"><div class="label">失败</div><div class="value" style="color:#ef4444">{summary.get('failed', 0)}</div></div>
|
|
<div class="stat-card"><div class="label">总耗时</div><div class="value">{summary.get('total_elapsed_seconds', 0):.2f}s</div></div>
|
|
</div>
|
|
|
|
{_concurrency_summary(results)}
|
|
|
|
{_performance_analysis(results)}
|
|
|
|
<div class="section">
|
|
<h2>性能对比柱状图</h2>
|
|
<div class="chart-container"><canvas id="barChart"></canvas></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>P95延迟对比</h2>
|
|
<div class="chart-container"><canvas id="p95Chart"></canvas></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>详细数据</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>用例名称</th><th>难度</th><th>状态</th><th>平均延迟 (ms)</th>
|
|
<th>标准差</th><th>最小</th><th>最大</th><th>P95</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{_stats_rows(results)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>环境信息</h2>
|
|
<table>
|
|
<tr><td>模型</td><td class="mono">{env.get('model', 'N/A')}</td></tr>
|
|
<tr><td>运行平台</td><td class="mono">{env.get('platform', 'N/A')}</td></tr>
|
|
<tr><td>运行ID</td><td class="mono">{run_id}</td></tr>
|
|
<tr><td>测试时间</td><td class="mono">{timestamp}</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<footer>大模型速度测试助手 | 报告由 report.py 自动生成</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const barCtx = document.getElementById('barChart').getContext('2d');
|
|
new Chart(barCtx, {{
|
|
type: 'bar',
|
|
data: {{
|
|
labels: {json.dumps(case_names, ensure_ascii=False)},
|
|
datasets: [{{
|
|
label: '平均延迟 (ms)',
|
|
data: {json.dumps(case_means)},
|
|
backgroundColor: 'rgba(102, 126, 234, 0.7)',
|
|
borderColor: 'rgba(102, 126, 234, 1)',
|
|
borderWidth: 1
|
|
}}]
|
|
}},
|
|
options: {{
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {{
|
|
legend: {{ display: false }},
|
|
title: {{ display: true, text: '各用例平均延迟对比 (ms)', font: {{ size: 16 }} }}
|
|
}},
|
|
scales: {{ y: {{ beginAtZero: true, title: {{ display: true, text: '延迟 (ms)' }} }} }}
|
|
}}
|
|
}});
|
|
|
|
const p95Ctx = document.getElementById('p95Chart').getContext('2d');
|
|
new Chart(p95Ctx, {{
|
|
type: 'bar',
|
|
data: {{
|
|
labels: {json.dumps(case_names, ensure_ascii=False)},
|
|
datasets: [{{
|
|
label: 'P95延迟 (ms)',
|
|
data: {json.dumps(case_p95s)},
|
|
backgroundColor: 'rgba(239, 68, 68, 0.7)',
|
|
borderColor: 'rgba(239, 68, 68, 1)',
|
|
borderWidth: 1
|
|
}}]
|
|
}},
|
|
options: {{
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {{
|
|
legend: {{ display: false }},
|
|
title: {{ display: true, text: '各用例P95延迟 (ms)', font: {{ size: 16 }} }}
|
|
}},
|
|
scales: {{ y: {{ beginAtZero: true, title: {{ display: true, text: '延迟 (ms)' }} }} }}
|
|
}}
|
|
}});
|
|
</script>
|
|
</body>
|
|
</html>"""
|