296 lines
8.2 KiB
Go
296 lines
8.2 KiB
Go
package stress
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// metricSpec 单条曲线规格
|
||
type metricSpec struct {
|
||
Key string `json:"key"`
|
||
Label string `json:"label"`
|
||
Color string `json:"color"`
|
||
Unit string `json:"unit"`
|
||
}
|
||
|
||
// chartSpec 图表规格(左轴 / 右轴)
|
||
type chartSpec struct {
|
||
Left []metricSpec
|
||
Right []metricSpec
|
||
LeftPct bool
|
||
LeftTitle string
|
||
RightTitle string
|
||
}
|
||
|
||
// kvPair 指标明细
|
||
type kvPair struct {
|
||
Label string `json:"label"`
|
||
Value string `json:"value"`
|
||
}
|
||
|
||
// metricSeries 单条曲线序列
|
||
type metricSeries struct {
|
||
Label string `json:"label"`
|
||
Color string `json:"color"`
|
||
Data []float64 `json:"data"`
|
||
Unit string `json:"unit"`
|
||
}
|
||
|
||
// chartPayload 图表数据
|
||
type chartPayload struct {
|
||
Labels []string `json:"labels"`
|
||
LeftSets []metricSeries `json:"left_sets"`
|
||
RightSets []metricSeries `json:"right_sets"`
|
||
LeftTitle string `json:"left_title"`
|
||
RightTitle string `json:"right_title"`
|
||
LeftPct bool `json:"left_pct"`
|
||
}
|
||
|
||
// testCard 单个测试卡片数据
|
||
type testCard struct {
|
||
Name string `json:"name"`
|
||
Label string `json:"label"`
|
||
Status string `json:"status"`
|
||
Duration string `json:"duration"`
|
||
Score float64 `json:"score"`
|
||
ScoreUnit string `json:"score_unit"`
|
||
ScoreHint string `json:"score_hint"`
|
||
Metrics []kvPair `json:"metrics"`
|
||
Benchmarks []BenchItem `json:"benchmarks,omitempty"` // 性能/温度基准评级
|
||
OverallLevel Level `json:"overall_level,omitempty"` // 综合评级(各项最差)
|
||
OverallText string `json:"overall_text,omitempty"`
|
||
OverallColor string `json:"overall_color,omitempty"`
|
||
Output string `json:"output"`
|
||
Chart *chartPayload `json:"chart,omitempty"`
|
||
}
|
||
|
||
// reportHTMLData 报告整体数据
|
||
type reportHTMLData struct {
|
||
Title string `json:"title"`
|
||
IP string `json:"ip"`
|
||
Time string `json:"time"`
|
||
Duration string `json:"duration"`
|
||
Overall string `json:"overall"`
|
||
Summary string `json:"summary"`
|
||
SysInfo map[string]string `json:"sys_info"`
|
||
Cards []testCard `json:"cards"`
|
||
}
|
||
|
||
// WriteReportHTML 生成综合 HTML 报告(跑分卡片 + 多曲线图),返回文件路径
|
||
// 默认文件名:report-<ip>.html
|
||
func WriteReportHTML(ip string, report *Report, reportDir string) (string, error) {
|
||
filename := fmt.Sprintf("report-%s.html", SanitizeFilename(ip))
|
||
return WriteReportHTMLTo(ip, report, reportDir, filename)
|
||
}
|
||
|
||
// WriteReportHTMLTo 生成综合 HTML 报告到指定文件名,返回完整文件路径
|
||
func WriteReportHTMLTo(ip string, report *Report, reportDir, filename string) (string, error) {
|
||
data := buildReportData(ip, report)
|
||
dataJSON, err := json.Marshal(data)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
html := strings.Replace(reportHTMLTemplate, "__DATA__", string(dataJSON), 1)
|
||
|
||
if err := os.MkdirAll(reportDir, 0755); err != nil {
|
||
return "", err
|
||
}
|
||
path := filepath.Join(reportDir, filename)
|
||
if err := os.WriteFile(path, []byte(html), 0644); err != nil {
|
||
return "", err
|
||
}
|
||
return path, nil
|
||
}
|
||
|
||
func buildReportData(ip string, report *Report) reportHTMLData {
|
||
data := reportHTMLData{
|
||
Title: fmt.Sprintf("%s · 硬件压力测试报告", ip),
|
||
IP: ip,
|
||
Time: report.StartTime.Format("2006-01-02 15:04:05"),
|
||
Duration: report.Duration.Round(time.Millisecond).String(),
|
||
Overall: Status(report),
|
||
Summary: report.Summary(),
|
||
SysInfo: report.SysInfo,
|
||
}
|
||
// 硬件型号:从 sysInfo["cpu"] 读取,用于按硬件库匹配基准
|
||
cpuModel := ""
|
||
if report.SysInfo != nil {
|
||
cpuModel = report.SysInfo["cpu"]
|
||
}
|
||
for _, res := range report.Results {
|
||
data.Cards = append(data.Cards, buildCard(cpuModel, res))
|
||
}
|
||
return data
|
||
}
|
||
|
||
func buildCard(cpuModel string, res Result) testCard {
|
||
card := testCard{
|
||
Name: string(res.Type),
|
||
Label: testLabel(res.Type),
|
||
Status: res.Status,
|
||
Duration: res.Duration.Round(time.Millisecond).String(),
|
||
Score: res.Score,
|
||
ScoreUnit: ScoreUnit(res.Type),
|
||
ScoreHint: scoreHint(res.Type),
|
||
Output: res.Output,
|
||
}
|
||
for _, name := range sortedMetricNames(res.Metrics) {
|
||
card.Metrics = append(card.Metrics, kvPair{
|
||
Label: MetricLabel(name),
|
||
Value: fmt.Sprintf("%.1f%s", res.Metrics[name], MetricUnit(name)),
|
||
})
|
||
}
|
||
// 性能/温度基准评级(仅在有数据时填充;按硬件库匹配专属阈值)
|
||
if items := EvaluateResult(res, cpuModel); len(items) > 0 {
|
||
card.Benchmarks = items
|
||
if lvl := OverallLevel(items); lvl != "" {
|
||
d := LevelMeta(lvl)
|
||
card.OverallLevel = lvl
|
||
card.OverallText = d.Text
|
||
card.OverallColor = d.Color
|
||
}
|
||
}
|
||
if len(res.Samples) > 0 {
|
||
if payload, ok := buildChart(res); ok {
|
||
card.Chart = &payload
|
||
}
|
||
}
|
||
return card
|
||
}
|
||
|
||
func buildChart(res Result) (chartPayload, bool) {
|
||
spec := chartSpecFor(res.Type)
|
||
if len(spec.Left) == 0 && len(spec.Right) == 0 {
|
||
return chartPayload{}, false
|
||
}
|
||
|
||
payload := chartPayload{
|
||
LeftTitle: spec.LeftTitle,
|
||
RightTitle: spec.RightTitle,
|
||
LeftPct: spec.LeftPct,
|
||
}
|
||
for _, s := range res.Samples {
|
||
payload.Labels = append(payload.Labels, s.Time)
|
||
}
|
||
|
||
hasData := func(key string) bool {
|
||
for _, s := range res.Samples {
|
||
if s.HasMetric(key) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
buildSeries := func(specs []metricSpec) []metricSeries {
|
||
var out []metricSeries
|
||
for _, m := range specs {
|
||
if !hasData(m.Key) {
|
||
continue
|
||
}
|
||
series := metricSeries{Label: m.Label, Color: m.Color, Unit: m.Unit}
|
||
for _, s := range res.Samples {
|
||
series.Data = append(series.Data, s.Values[m.Key])
|
||
}
|
||
out = append(out, series)
|
||
}
|
||
return out
|
||
}
|
||
|
||
payload.LeftSets = buildSeries(spec.Left)
|
||
payload.RightSets = buildSeries(spec.Right)
|
||
return payload, true
|
||
}
|
||
|
||
// testLabel 测试项中文名
|
||
func testLabel(t TestType) string {
|
||
switch t {
|
||
case TestCPU:
|
||
return "CPU 压力测试"
|
||
case TestMemory:
|
||
return "内存压力测试"
|
||
case TestDiskIO:
|
||
return "磁盘 IO 测试"
|
||
case TestMemNative:
|
||
return "内存精压测试"
|
||
case TestFull:
|
||
return "综合工况测试"
|
||
case MonitorTemp:
|
||
return "温度监控"
|
||
case MonitorDmesg:
|
||
return "内核日志检查"
|
||
default:
|
||
return string(t)
|
||
}
|
||
}
|
||
|
||
// scoreHint 跑分含义说明
|
||
func scoreHint(t TestType) string {
|
||
switch t {
|
||
case TestCPU:
|
||
return "每秒操作数(bogo ops/s)"
|
||
case TestMemory:
|
||
return "每秒操作数(bogo ops/s)"
|
||
case TestDiskIO:
|
||
return "读写带宽合计(MB/s)"
|
||
case TestFull:
|
||
return "综合每秒操作数(bogo ops/s)"
|
||
case TestMemNative:
|
||
return "内存带宽(MB/s)"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// chartSpecFor 返回某类测试的曲线规格
|
||
func chartSpecFor(t TestType) chartSpec {
|
||
switch t {
|
||
case TestCPU:
|
||
return chartSpec{
|
||
Left: []metricSpec{{Key: "cpu", Label: "CPU 利用率", Color: "#f97316", Unit: "%"}},
|
||
Right: []metricSpec{{Key: "temp", Label: "温度", Color: "#ef4444", Unit: "°C"}},
|
||
LeftPct: true,
|
||
LeftTitle: "利用率 (%)",
|
||
RightTitle: "温度 (°C)",
|
||
}
|
||
case TestMemory:
|
||
return chartSpec{
|
||
Left: []metricSpec{{Key: "mem_used_pct", Label: "内存使用率", Color: "#8b5cf6", Unit: "%"}},
|
||
Right: []metricSpec{{Key: "temp", Label: "温度", Color: "#ef4444", Unit: "°C"}},
|
||
LeftPct: true,
|
||
LeftTitle: "使用率 (%)",
|
||
RightTitle: "温度 (°C)",
|
||
}
|
||
case TestDiskIO:
|
||
return chartSpec{
|
||
Left: []metricSpec{{Key: "iops_total", Label: "IOPS", Color: "#22c55e", Unit: ""}},
|
||
Right: []metricSpec{{Key: "bw_total", Label: "吞吐", Color: "#3b82f6", Unit: "MB/s"}},
|
||
LeftPct: false,
|
||
LeftTitle: "IOPS",
|
||
RightTitle: "吞吐 (MB/s)",
|
||
}
|
||
case TestFull:
|
||
return chartSpec{
|
||
Left: []metricSpec{
|
||
{Key: "cpu", Label: "CPU 利用率", Color: "#f97316", Unit: "%"},
|
||
{Key: "mem_used_pct", Label: "内存使用率", Color: "#8b5cf6", Unit: "%"},
|
||
},
|
||
Right: []metricSpec{{Key: "temp", Label: "温度", Color: "#ef4444", Unit: "°C"}},
|
||
LeftPct: true,
|
||
LeftTitle: "利用率 (%)",
|
||
RightTitle: "温度 (°C)",
|
||
}
|
||
default:
|
||
return chartSpec{}
|
||
}
|
||
}
|
||
|
||
// SanitizeFilename 清理 IP 中的特殊字符,用于文件名
|
||
func SanitizeFilename(ip string) string {
|
||
return strings.ReplaceAll(ip, ":", "_")
|
||
}
|