Files
ecs/internal/analysis/summary.go
2026-04-17 18:53:49 +08:00

1078 lines
31 KiB
Go
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.
package analysis
import (
"encoding/json"
"fmt"
"io"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/imroc/req/v3"
"github.com/oneclickvirt/ecs/internal/params"
)
var (
mbpsRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*mbps`)
msRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*ms`)
cpuModelZhRe = regexp.MustCompile(`(?im)^\s*CPU\s*型号\s*[:]\s*(.+?)\s*$`)
cpuModelEnRe = regexp.MustCompile(`(?im)^\s*CPU\s*Model\s*[:]\s*(.+?)\s*$`)
threadScoreEnRe = regexp.MustCompile(`(?im)^\s*(\d+)\s*Thread\(s\)\s*Test\s*:\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
threadScoreZhRe = regexp.MustCompile(`(?im)^\s*(\d+)\s*线程测试\((?:单核|多核)\)得分\s*[:]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
gbSingleRe = regexp.MustCompile(`(?im)^\s*Single-Core\s*Score\s*[:]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
gbMultiRe = regexp.MustCompile(`(?im)^\s*Multi-Core\s*Score\s*[:]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
alphaNumRe = regexp.MustCompile(`[a-z0-9]+`)
// New patterns for condensed summary
ansiEscapeRe = regexp.MustCompile(`\x1b\[[0-9;]*m`)
streamFuncRe = regexp.MustCompile(`(?im)^\s*(?:Copy|Scale|Add|Triad)\s*:\s*(\d+(?:\.\d+)?)`)
anyGbpsRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*GB/s`)
anyMbpsRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*MB/s`)
)
const (
cpuStatsPrimaryURL = "https://raw.githubusercontent.com/oneclickvirt/ecs/ranks/cpu_statistics.json"
cpuStatsFallbackURL = "https://github.com/oneclickvirt/ecs/raw/refs/heads/ranks/cpu_statistics.json"
cpuCDNProbeTestURL = "https://raw.githubusercontent.com/spiritLHLS/ecs/main/back/test"
cpuStatsCacheTTL = 30 * time.Minute
cpuStatsFailCacheTTL = 5 * time.Minute
cpuStatsRequestTimout = 6 * time.Second
)
var cpuStatsCDNList = []string{
"https://cdn.spiritlhl.net/",
"http://cdn3.spiritlhl.net/",
"http://cdn1.spiritlhl.net/",
"http://cdn2.spiritlhl.net/",
}
type cpuStatsEntry struct {
CPUPrefix string `json:"cpu_prefix"`
CPUModel string `json:"cpu_model"`
SampleCount int `json:"sample_count"`
MaxSingle float64 `json:"max_single_score"`
MaxMulti float64 `json:"max_multi_score"`
AvgSingle float64 `json:"avg_single_score"`
AvgMulti float64 `json:"avg_multi_score"`
Rank int `json:"rank"`
TypicalCores int `json:"typical_cores"`
TypicalThread int `json:"typical_threads"`
}
type cpuStatsPayload struct {
CPUStatistics []cpuStatsEntry `json:"cpu_statistics"`
}
var (
cpuStatsMu sync.Mutex
cachedCPUStats *cpuStatsPayload
cpuStatsExpireAt time.Time
)
func parseFloatsByRegex(content string, re *regexp.Regexp) []float64 {
matches := re.FindAllStringSubmatch(content, -1)
vals := make([]float64, 0, len(matches))
for _, m := range matches {
if len(m) < 2 {
continue
}
v, err := strconv.ParseFloat(m[1], 64)
if err != nil {
continue
}
vals = append(vals, v)
}
return vals
}
func parseFloatString(s string) (float64, bool) {
clean := strings.ReplaceAll(strings.TrimSpace(s), ",", "")
v, err := strconv.ParseFloat(clean, 64)
if err != nil {
return 0, false
}
return v, true
}
func extractCPUModel(output string) string {
for _, re := range []*regexp.Regexp{cpuModelZhRe, cpuModelEnRe} {
m := re.FindStringSubmatch(output)
if len(m) >= 2 {
model := strings.TrimSpace(m[1])
if model != "" {
return model
}
}
}
return ""
}
func extractCPUScores(output string) (single float64, singleOK bool, multi float64, multiOK bool) {
for _, re := range []*regexp.Regexp{threadScoreEnRe, threadScoreZhRe} {
matches := re.FindAllStringSubmatch(output, -1)
for _, m := range matches {
if len(m) < 3 {
continue
}
threads, err := strconv.Atoi(strings.TrimSpace(m[1]))
if err != nil {
continue
}
score, ok := parseFloatString(m[2])
if !ok {
continue
}
if threads == 1 {
single, singleOK = score, true
continue
}
if threads > 1 && (!multiOK || score > multi) {
multi, multiOK = score, true
}
}
}
if !singleOK {
if m := gbSingleRe.FindStringSubmatch(output); len(m) >= 2 {
if v, ok := parseFloatString(m[1]); ok {
single, singleOK = v, true
}
}
}
if !multiOK {
if m := gbMultiRe.FindStringSubmatch(output); len(m) >= 2 {
if v, ok := parseFloatString(m[1]); ok {
multi, multiOK = v, true
}
}
}
return
}
func normalizeCPUString(s string) string {
s = strings.ToLower(s)
b := strings.Builder{}
b.Grow(len(s))
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
}
}
return b.String()
}
func cpuTokens(s string) []string {
lower := strings.ToLower(s)
raw := alphaNumRe.FindAllString(lower, -1)
if len(raw) == 0 {
return nil
}
seen := make(map[string]struct{}, len(raw))
out := make([]string, 0, len(raw))
for _, t := range raw {
if len(t) < 2 {
continue
}
if _, ok := seen[t]; ok {
continue
}
seen[t] = struct{}{}
out = append(out, t)
}
return out
}
func fuzzyScoreCPUModel(model string, entry cpuStatsEntry) float64 {
nm := normalizeCPUString(model)
ne := normalizeCPUString(entry.CPUModel)
np := normalizeCPUString(entry.CPUPrefix)
if nm == "" || (ne == "" && np == "") {
return 0
}
if nm == ne || nm == np {
return 1
}
containsScore := 0.0
for _, candidate := range []string{ne, np} {
if candidate == "" {
continue
}
if strings.Contains(candidate, nm) || strings.Contains(nm, candidate) {
shortLen := len(nm)
if len(candidate) < shortLen {
shortLen = len(candidate)
}
longLen := len(nm)
if len(candidate) > longLen {
longLen = len(candidate)
}
if longLen > 0 {
ratio := float64(shortLen) / float64(longLen)
if ratio > containsScore {
containsScore = ratio
}
}
}
}
modelTokens := cpuTokens(model)
if len(modelTokens) == 0 {
return containsScore
}
entryTokenSet := make(map[string]struct{})
for _, t := range cpuTokens(entry.CPUModel + " " + entry.CPUPrefix) {
entryTokenSet[t] = struct{}{}
}
overlap := 0
for _, t := range modelTokens {
if _, ok := entryTokenSet[t]; ok {
overlap++
}
}
overlapScore := float64(overlap) / float64(len(modelTokens))
if containsScore > overlapScore {
return containsScore
}
return overlapScore
}
func loadCPUStats() *cpuStatsPayload {
cpuStatsMu.Lock()
defer cpuStatsMu.Unlock()
now := time.Now()
if now.Before(cpuStatsExpireAt) {
return cachedCPUStats
}
client := req.C()
client.SetTimeout(cpuStatsRequestTimout)
endpoints := []string{cpuStatsPrimaryURL, cpuStatsFallbackURL}
availableCDN := detectAvailableCPUCDN(client)
for _, endpoint := range endpoints {
urls := []string{}
if availableCDN != "" {
urls = append(urls, availableCDN+endpoint)
}
urls = append(urls, endpoint)
for _, u := range urls {
payload := tryDecodeCPUStatsFromURL(client, u)
if payload == nil {
continue
}
cachedCPUStats = payload
cpuStatsExpireAt = now.Add(cpuStatsCacheTTL)
return cachedCPUStats
}
}
cachedCPUStats = nil
cpuStatsExpireAt = now.Add(cpuStatsFailCacheTTL)
return nil
}
func detectAvailableCPUCDN(client *req.Client) string {
for _, baseURL := range cpuStatsCDNList {
if checkCPUCDN(client, baseURL) {
return baseURL
}
time.Sleep(500 * time.Millisecond)
}
return ""
}
func checkCPUCDN(client *req.Client, baseURL string) bool {
resp, err := client.R().SetHeader("User-Agent", "goecs-summary/1.0").Get(baseURL + cpuCDNProbeTestURL)
if err != nil {
return false
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return false
}
b, err := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
if err != nil {
return false
}
return strings.Contains(string(b), "success")
}
func tryDecodeCPUStatsFromURL(client *req.Client, u string) *cpuStatsPayload {
resp, err := client.R().SetHeader("User-Agent", "goecs-summary/1.0").Get(u)
if err != nil {
return nil
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil
}
var payload cpuStatsPayload
dec := json.NewDecoder(io.LimitReader(resp.Body, 8<<20))
if err := dec.Decode(&payload); err != nil {
return nil
}
if len(payload.CPUStatistics) == 0 {
return nil
}
return &payload
}
func matchCPUStatsEntry(model string, payload *cpuStatsPayload) *cpuStatsEntry {
if payload == nil || model == "" || len(payload.CPUStatistics) == 0 {
return nil
}
trimModel := strings.TrimSpace(model)
normModel := normalizeCPUString(trimModel)
for i := range payload.CPUStatistics {
entry := &payload.CPUStatistics[i]
if strings.EqualFold(strings.TrimSpace(entry.CPUModel), trimModel) {
return entry
}
}
for i := range payload.CPUStatistics {
entry := &payload.CPUStatistics[i]
if normModel == normalizeCPUString(entry.CPUModel) || normModel == normalizeCPUString(entry.CPUPrefix) {
return entry
}
}
bestIdx := -1
bestScore := 0.0
for i := range payload.CPUStatistics {
score := fuzzyScoreCPUModel(trimModel, payload.CPUStatistics[i])
if score > bestScore {
bestScore = score
bestIdx = i
continue
}
if score == bestScore && bestIdx >= 0 && payload.CPUStatistics[i].SampleCount > payload.CPUStatistics[bestIdx].SampleCount {
bestIdx = i
}
}
if bestIdx >= 0 && bestScore >= 0.45 {
return &payload.CPUStatistics[bestIdx]
}
return nil
}
func cpuTierText(score float64, lang string) string {
if lang == "zh" {
switch {
case score >= 5000:
return "按 README_NEW_USER 的 Sysbench 口径,单核 >5000 可视为高性能第一梯队。"
case score < 500:
return "按 README_NEW_USER 的 Sysbench 口径,单核 <500 属于偏弱性能。"
default:
return "按 README_NEW_USER 的 Sysbench 口径,可按每约 1000 分视作一个性能档位。"
}
}
switch {
case score >= 5000:
return "Per README_NEW_USER Sysbench guidance, single-core > 5000 is considered first-tier high performance."
case score < 500:
return "Per README_NEW_USER Sysbench guidance, single-core < 500 is considered weak performance."
default:
return "Per README_NEW_USER Sysbench guidance, roughly every 1000 points is about one performance tier."
}
}
func summarizeCPUWithRanking(finalOutput, lang string) []string {
model := extractCPUModel(finalOutput)
single, singleOK, multi, multiOK := extractCPUScores(finalOutput)
if !singleOK && !multiOK {
return nil
}
stats := loadCPUStats()
entry := matchCPUStatsEntry(model, stats)
var score float64
var avg float64
var max float64
kind := "single"
if singleOK && entry != nil && entry.AvgSingle > 0 && entry.MaxSingle > 0 {
score, avg, max = single, entry.AvgSingle, entry.MaxSingle
} else if multiOK && entry != nil && entry.AvgMulti > 0 && entry.MaxMulti > 0 {
score, avg, max = multi, entry.AvgMulti, entry.MaxMulti
kind = "multi"
} else if singleOK {
score = single
} else {
score = multi
kind = "multi"
}
lines := make([]string, 0, 4)
if lang == "zh" {
if kind == "single" {
lines = append(lines, fmt.Sprintf("CPU: 检测到单核得分 %.2f。", score))
} else {
lines = append(lines, fmt.Sprintf("CPU: 检测到多核得分 %.2f。", score))
}
} else {
if kind == "single" {
lines = append(lines, fmt.Sprintf("CPU: detected single-core score %.2f.", score))
} else {
lines = append(lines, fmt.Sprintf("CPU: detected multi-core score %.2f.", score))
}
}
if kind == "single" {
lines = append(lines, cpuTierText(score, lang))
}
if entry == nil || avg <= 0 || max <= 0 {
if lang == "zh" {
if model != "" {
lines = append(lines, fmt.Sprintf("CPU 对标: 未在在线榜单中稳定匹配到型号 \"%s\",已仅给出本机分数解读。", model))
} else {
lines = append(lines, "CPU 对标: 未提取到 CPU 型号,已仅给出本机分数解读。")
}
} else {
if model != "" {
lines = append(lines, fmt.Sprintf("CPU ranking: no reliable online match found for model \"%s\"; local score interpretation only.", model))
} else {
lines = append(lines, "CPU ranking: CPU model not found in output; local score interpretation only.")
}
}
return lines
}
reachAvg := score >= avg
gapToMax := max - score
fullBlood := false
if max > 0 {
ratioDiff := (score - max) / max
if ratioDiff < 0 {
ratioDiff = -ratioDiff
}
fullBlood = ratioDiff <= 0.05
}
pctOfAvg := score / avg * 100
pctOfMax := score / max * 100
if lang == "zh" {
lines = append(lines,
fmt.Sprintf("CPU 对标: 匹配 \"%s\"(样本 %d排名 #%d。", entry.CPUModel, entry.SampleCount, entry.Rank),
fmt.Sprintf("平均分达标: %s本机 %.2f,均值 %.2f,达成率 %.2f%%)。", map[bool]string{true: "是", false: "否"}[reachAvg], score, avg, pctOfAvg),
fmt.Sprintf("满血对比: 满血分 %.2f,本机为 %.2f%%,差值 %.2f。", max, pctOfMax, gapToMax),
fmt.Sprintf("满血判定(±5%%波动): %s。", map[bool]string{true: "是", false: "否"}[fullBlood]),
)
} else {
lines = append(lines,
fmt.Sprintf("CPU ranking: matched \"%s\" (samples %d, rank #%d).", entry.CPUModel, entry.SampleCount, entry.Rank),
fmt.Sprintf("Average-level check: %s (local %.2f vs avg %.2f, %.2f%% of avg).", map[bool]string{true: "pass", false: "below avg"}[reachAvg], score, avg, pctOfAvg),
fmt.Sprintf("Full-blood comparison: max %.2f, local is %.2f%% of max, gap %.2f.", max, pctOfMax, gapToMax),
fmt.Sprintf("Full-blood status (within ±5%%): %s.", map[bool]string{true: "yes", false: "no"}[fullBlood]),
)
}
return lines
}
func summarizeBandwidth(vals []float64, lang string) string {
if len(vals) == 0 {
if lang == "zh" {
return "测速: 未检测到有效 Mbps 数据。"
}
return "Speed: no valid Mbps values found."
}
sort.Float64s(vals)
maxV := vals[len(vals)-1]
if lang == "zh" {
switch {
case maxV >= 2000:
return fmt.Sprintf("测速: 峰值约 %.2f Mbps属于高带宽网络。", maxV)
case maxV >= 800:
return fmt.Sprintf("测速: 峰值约 %.2f Mbps带宽表现较好。", maxV)
case maxV >= 200:
return fmt.Sprintf("测速: 峰值约 %.2f Mbps带宽中等可用。", maxV)
default:
return fmt.Sprintf("测速: 峰值约 %.2f Mbps带宽偏低建议关注线路与机型。", maxV)
}
}
switch {
case maxV >= 2000:
return fmt.Sprintf("Speed: peak around %.2f Mbps, high-bandwidth profile.", maxV)
case maxV >= 800:
return fmt.Sprintf("Speed: peak around %.2f Mbps, strong bandwidth performance.", maxV)
case maxV >= 200:
return fmt.Sprintf("Speed: peak around %.2f Mbps, moderate and usable bandwidth.", maxV)
default:
return fmt.Sprintf("Speed: peak around %.2f Mbps, relatively limited bandwidth.", maxV)
}
}
func summarizeLatency(vals []float64, lang string) string {
if len(vals) == 0 {
if lang == "zh" {
return "延迟: 未检测到有效 ms 数据。"
}
return "Latency: no valid ms values found."
}
sort.Float64s(vals)
minV := vals[0]
if lang == "zh" {
switch {
case minV <= 15:
return fmt.Sprintf("延迟: 最优约 %.2f ms实时交互体验优秀。", minV)
case minV <= 45:
return fmt.Sprintf("延迟: 最优约 %.2f ms整体交互体验良好。", minV)
case minV <= 90:
return fmt.Sprintf("延迟: 最优约 %.2f ms可用但有一定时延。", minV)
default:
return fmt.Sprintf("延迟: 最优约 %.2f ms时延偏高建议优化线路。", minV)
}
}
switch {
case minV <= 15:
return fmt.Sprintf("Latency: best around %.2f ms, excellent for interactive workloads.", minV)
case minV <= 45:
return fmt.Sprintf("Latency: best around %.2f ms, generally responsive.", minV)
case minV <= 90:
return fmt.Sprintf("Latency: best around %.2f ms, usable with moderate delay.", minV)
default:
return fmt.Sprintf("Latency: best around %.2f ms, relatively high and may impact responsiveness.", minV)
}
}
func testedScopes(config *params.Config) []string {
scopes := make([]string, 0, 8)
if config.BasicStatus {
scopes = append(scopes, "basic")
}
if config.CpuTestStatus {
scopes = append(scopes, "cpu")
}
if config.MemoryTestStatus {
scopes = append(scopes, "memory")
}
if config.DiskTestStatus {
scopes = append(scopes, "disk")
}
if config.UtTestStatus {
scopes = append(scopes, "unlock")
}
if config.SecurityTestStatus {
scopes = append(scopes, "security")
}
if config.Nt3Status || config.BacktraceStatus || config.PingTestStatus || config.TgdcTestStatus || config.WebTestStatus {
scopes = append(scopes, "network")
}
if config.SpeedTestStatus {
scopes = append(scopes, "speed")
}
return scopes
}
func scopesText(scopes []string, lang string) string {
if len(scopes) == 0 {
if lang == "zh" {
return "无"
}
return "none"
}
labelsZh := map[string]string{
"basic": "系统基础", "cpu": "CPU", "memory": "内存", "disk": "磁盘", "unlock": "解锁", "security": "IP质量", "network": "网络路由", "speed": "带宽测速",
}
labelsEn := map[string]string{
"basic": "system basics", "cpu": "CPU", "memory": "memory", "disk": "disk", "unlock": "unlock", "security": "IP quality", "network": "network route", "speed": "bandwidth",
}
out := make([]string, 0, len(scopes))
for _, s := range scopes {
if lang == "zh" {
out = append(out, labelsZh[s])
} else {
out = append(out, labelsEn[s])
}
}
return strings.Join(out, ", ")
}
// stripAnsiCodes removes ANSI escape codes from a string.
func stripAnsiCodes(s string) string {
return ansiEscapeRe.ReplaceAllString(s, "")
}
// extractAllSectionContent returns the concatenated text of every section whose
// header contains markerZh or markerEn (case-sensitive). Section headers are
// lines that start with three or more dashes (produced by PrintCenteredTitle).
func extractAllSectionContent(output, markerZh, markerEn string) string {
lines := strings.Split(output, "\n")
var sb strings.Builder
inSection := false
for _, line := range lines {
stripped := strings.TrimSpace(line)
if strings.HasPrefix(stripped, "---") {
if strings.Contains(stripped, markerZh) || (markerEn != "" && strings.Contains(stripped, markerEn)) {
inSection = true
continue
}
// Any other section header ends the current one
inSection = false
continue
}
if inSection {
sb.WriteString(line)
sb.WriteString("\n")
}
}
return sb.String()
}
// extractMaxMemoryBandwidth returns the highest bandwidth value (in MB/s) found
// inside the memory-test section(s) of the captured output.
func extractMaxMemoryBandwidth(output string) float64 {
content := extractAllSectionContent(output, "内存测试", "Memory-Test")
if content == "" {
return 0
}
maxMbps := 0.0
// STREAM format: "Copy: 12345.6 ..." the first number is Best Rate MB/s
for _, m := range streamFuncRe.FindAllStringSubmatch(content, -1) {
if v, err := strconv.ParseFloat(m[1], 64); err == nil && v > maxMbps {
maxMbps = v
}
}
// Values reported as GB/s (some dd / mbw outputs)
for _, m := range anyGbpsRe.FindAllStringSubmatch(content, -1) {
if v, err := strconv.ParseFloat(m[1], 64); err == nil {
if mbps := v * 1024; mbps > maxMbps {
maxMbps = mbps
}
}
}
// Values reported as MB/s (mbw, sysbench, dd, winsat …)
for _, m := range anyMbpsRe.FindAllStringSubmatch(content, -1) {
if v, err := strconv.ParseFloat(m[1], 64); err == nil && v > maxMbps {
maxMbps = v
}
}
return maxMbps
}
// inferMemoryDDRAndChannels converts a memory bandwidth (MB/s) to a human-readable
// DDR type + channel string using the thresholds from README_NEW_USER.
//
// DDR3 single: 1024017408 MB/s
// DDR4 single: 1740834816 MB/s
// DDR4 dual: 3481651200 MB/s
// DDR5 single: 5120077824 MB/s
// DDR5 dual: ≥77824 MB/s
func inferMemoryDDRAndChannels(mbps float64, lang string) string {
type tier struct {
minMbps float64
ddr, chZh, chEn string
}
tiers := []tier{
{77824, "DDR5", "双通道", "Dual-Channel"},
{51200, "DDR5", "单通道", "Single-Channel"},
{34816, "DDR4", "双通道", "Dual-Channel"},
{17408, "DDR4", "单通道", "Single-Channel"},
{0, "DDR3", "单通道", "Single-Channel"},
}
for _, t := range tiers {
if mbps >= t.minMbps {
if lang == "zh" {
return t.ddr + " " + t.chZh
}
return t.ddr + " " + t.chEn
}
}
if lang == "zh" {
return "DDR3 单通道"
}
return "DDR3 Single-Channel"
}
// extractDiskTypeAndCount scans all disk-test section(s) for fio 4K rows and
// returns the 4K read speed (MB/s) and the number of unique test paths found.
// Falls back to any MB/s / GB/s value in the disk section when no 4K rows exist.
func extractDiskTypeAndCount(output string) (readMbps float64, pathCount int) {
content := extractAllSectionContent(output, "硬盘测试", "Disk-Test")
if content == "" {
return 0, 0
}
pathSet := make(map[string]struct{})
for _, line := range strings.Split(content, "\n") {
fields := strings.Fields(strings.TrimSpace(line))
// fio output row: <path> <blocksize> <readSpeed> <readUnit(iops)> ...
if len(fields) < 4 {
continue
}
if !strings.EqualFold(fields[1], "4k") {
continue
}
pathSet[fields[0]] = struct{}{}
val, err := strconv.ParseFloat(fields[2], 64)
if err != nil {
continue
}
unit := strings.ToUpper(fields[3])
var mbps float64
if strings.HasPrefix(unit, "GB") {
mbps = val * 1024
} else {
mbps = val
}
if mbps > readMbps {
readMbps = mbps
}
}
pathCount = len(pathSet)
// Fallback: no 4K rows found use any speed value present (dd / winsat)
if readMbps == 0 {
for _, m := range anyGbpsRe.FindAllStringSubmatch(content, -1) {
if v, err := strconv.ParseFloat(m[1], 64); err == nil {
if mbps := v * 1024; mbps > readMbps {
readMbps = mbps
}
}
}
for _, m := range anyMbpsRe.FindAllStringSubmatch(content, -1) {
if v, err := strconv.ParseFloat(m[1], 64); err == nil && v > readMbps {
readMbps = v
}
}
}
if pathCount == 0 && readMbps > 0 {
pathCount = 1
}
return readMbps, pathCount
}
// inferDiskType classifies a disk by its 4K (or sequential fallback) read speed.
//
// NVMe SSD : ≥200 MB/s
// SATA SSD : 50200 MB/s
// HDD : 1050 MB/s
func inferDiskType(readMbps float64, lang string) string {
switch {
case readMbps >= 200:
return "NVMe SSD"
case readMbps >= 50:
if lang == "zh" {
return "SATA SSD"
}
return "SATA SSD"
case readMbps >= 10:
return "HDD"
default:
if lang == "zh" {
return "低性能磁盘"
}
return "Low-Perf Disk"
}
}
// extractISPRanking parses the backtrace section and returns a ranking string
// like "电信 > 联通 > 移动" based on the best route quality detected per ISP.
// Quality tiers: [精品线路]=3, [优质线路]=2, [普通线路]=1.
func extractISPRanking(output, lang string) string {
content := extractAllSectionContent(output, "上游及回程线路检测", "Upstream")
if content == "" {
return ""
}
scores := map[string]int{"电信": 0, "联通": 0, "移动": 0}
for _, raw := range strings.Split(content, "\n") {
line := stripAnsiCodes(raw)
var q int
switch {
case strings.Contains(line, "[精品线路]"):
q = 3
case strings.Contains(line, "[优质线路]"):
q = 2
case strings.Contains(line, "[普通线路]"):
q = 1
default:
continue
}
for isp := range scores {
if strings.Contains(line, isp) && q > scores[isp] {
scores[isp] = q
}
}
}
if scores["电信"] == 0 && scores["联通"] == 0 && scores["移动"] == 0 {
return ""
}
isps := []string{"电信", "联通", "移动"}
order := map[string]int{"电信": 0, "联通": 1, "移动": 2}
sort.Slice(isps, func(i, j int) bool {
si, sj := scores[isps[i]], scores[isps[j]]
if si != sj {
return si > sj
}
return order[isps[i]] < order[isps[j]]
})
return strings.Join(isps, " > ")
}
// extractCPURankCondensed returns "CPU排名 #N 为满血性能的XX.XX%" (zh) or the
// English equivalent, using the same CPU stats lookup as summarizeCPUWithRanking.
func extractCPURankCondensed(finalOutput, lang string) string {
model := extractCPUModel(finalOutput)
single, singleOK, multi, multiOK := extractCPUScores(finalOutput)
if !singleOK && !multiOK {
return ""
}
stats := loadCPUStats()
entry := matchCPUStatsEntry(model, stats)
if entry == nil || entry.Rank <= 0 {
return ""
}
var score, maxScore float64
if singleOK && entry.MaxSingle > 0 {
score, maxScore = single, entry.MaxSingle
} else if multiOK && entry.MaxMulti > 0 {
score, maxScore = multi, entry.MaxMulti
} else {
return ""
}
pct := score / maxScore * 100
if lang == "zh" {
return fmt.Sprintf("CPU排名 #%d 为满血性能的%.2f%%", entry.Rank, pct)
}
return fmt.Sprintf("CPU rank #%d is %.2f%% of full performance", entry.Rank, pct)
}
// extractBandwidthCondensed returns the peak bandwidth as a human-readable
// "网络峰值带宽大于 X.XXGbps" / "> X.XXGbps" string.
func extractBandwidthCondensed(vals []float64, lang string) string {
if len(vals) == 0 {
return ""
}
sort.Float64s(vals)
maxV := vals[len(vals)-1]
if lang == "zh" {
if maxV >= 1000 {
return fmt.Sprintf("网络峰值带宽大于 %.2fGbps", maxV/1000)
}
return fmt.Sprintf("网络峰值带宽大于 %.2fMbps", maxV)
}
if maxV >= 1000 {
return fmt.Sprintf("Peak bandwidth > %.2fGbps", maxV/1000)
}
return fmt.Sprintf("Peak bandwidth > %.2fMbps", maxV)
}
// sectionExists reports whether the output contains a section header matching
// the given Chinese or English marker.
func sectionExists(output, markerZh, markerEn string) bool {
for _, line := range strings.Split(output, "\n") {
stripped := strings.TrimSpace(line)
if strings.HasPrefix(stripped, "---") {
if strings.Contains(stripped, markerZh) || (markerEn != "" && strings.Contains(stripped, markerEn)) {
return true
}
}
}
return false
}
// summaryRow holds a localised label/value pair for the markdown summary table.
type summaryRow struct{ label, value string }
// GenerateSummary creates a Markdown table post-test summary from final output.
func GenerateSummary(config *params.Config, finalOutput string) string {
lang := config.Language
rows := make([]summaryRow, 0, 5)
// helper: localised N/A string
na := func() string {
if lang == "zh" {
return "无有效数据"
}
return "N/A"
}
// 1. CPU rank and full-blood percentage
if config.CpuTestStatus {
model := extractCPUModel(finalOutput)
single, singleOK, multi, multiOK := extractCPUScores(finalOutput)
if singleOK || multiOK {
stats := loadCPUStats()
entry := matchCPUStatsEntry(model, stats)
if entry != nil && entry.Rank > 0 {
var score, maxScore float64
if singleOK && entry.MaxSingle > 0 {
score, maxScore = single, entry.MaxSingle
} else if multiOK && entry.MaxMulti > 0 {
score, maxScore = multi, entry.MaxMulti
}
if maxScore > 0 {
pct := score / maxScore * 100
if lang == "zh" {
rows = append(rows, summaryRow{"CPU排名", fmt.Sprintf("#%d 满血性能 %.2f%%", entry.Rank, pct)})
} else {
rows = append(rows, summaryRow{"CPU Rank", fmt.Sprintf("#%d (%.2f%% of max)", entry.Rank, pct)})
}
} else if lang == "zh" {
rows = append(rows, summaryRow{"CPU排名", na()})
} else {
rows = append(rows, summaryRow{"CPU Rank", na()})
}
} else if sectionExists(finalOutput, "CPU测试", "CPU-Test") {
if lang == "zh" {
rows = append(rows, summaryRow{"CPU排名", na()})
} else {
rows = append(rows, summaryRow{"CPU Rank", na()})
}
}
} else if sectionExists(finalOutput, "CPU测试", "CPU-Test") {
if lang == "zh" {
rows = append(rows, summaryRow{"CPU排名", na()})
} else {
rows = append(rows, summaryRow{"CPU Rank", na()})
}
}
// If section not present at all, the test simply wasn't run omit silently.
}
// 2. Memory DDR type and channels, with average-level check
if config.MemoryTestStatus {
bw := extractMaxMemoryBandwidth(finalOutput)
if bw > 0 {
mem := inferMemoryDDRAndChannels(bw, lang)
// README_NEW_USER threshold: < 10240 MB/s (≈10 GB/s) indicates overselling risk
const memAvgThreshMbps = 10240.0
if lang == "zh" {
if bw >= memAvgThreshMbps {
rows = append(rows, summaryRow{"内存", mem + " (达标)"})
} else {
rows = append(rows, summaryRow{"内存", mem + " (未达标)"})
}
} else {
if bw >= memAvgThreshMbps {
rows = append(rows, summaryRow{"Memory", mem + " (pass)"})
} else {
rows = append(rows, summaryRow{"Memory", mem + " (below avg)"})
}
}
} else if sectionExists(finalOutput, "内存测试", "Memory-Test") {
if lang == "zh" {
rows = append(rows, summaryRow{"内存", na()})
} else {
rows = append(rows, summaryRow{"Memory", na()})
}
}
}
// 3. Disk type and path count, with average-level check
if config.DiskTestStatus {
readMbps, pathCount := extractDiskTypeAndCount(finalOutput)
if readMbps > 0 || pathCount > 0 {
if pathCount <= 0 {
pathCount = 1
}
dtype := inferDiskType(readMbps, lang)
// README_NEW_USER: < 10 MB/s = poor performance / severe overselling
diskOK := readMbps >= 10
var qual string
if lang == "zh" {
if diskOK {
qual = " (达标)"
} else {
qual = " (未达标)"
}
rows = append(rows, summaryRow{"硬盘IO", fmt.Sprintf("%s %d路%s", dtype, pathCount, qual)})
} else {
if diskOK {
qual = " (pass)"
} else {
qual = " (below avg)"
}
rows = append(rows, summaryRow{"Disk IO", fmt.Sprintf("%s %d path(s)%s", dtype, pathCount, qual)})
}
} else if sectionExists(finalOutput, "硬盘测试", "Disk-Test") {
if lang == "zh" {
rows = append(rows, summaryRow{"硬盘IO", na()})
} else {
rows = append(rows, summaryRow{"Disk IO", na()})
}
}
}
// 4. Network peak bandwidth
if config.SpeedTestStatus {
bwVals := parseFloatsByRegex(finalOutput, mbpsRe)
var bwVal string
if len(bwVals) > 0 {
sort.Float64s(bwVals)
maxV := bwVals[len(bwVals)-1]
if maxV >= 1000 {
bwVal = fmt.Sprintf("> %.2fGbps", maxV/1000)
} else {
bwVal = fmt.Sprintf("> %.2fMbps", maxV)
}
}
if bwVal != "" {
if lang == "zh" {
rows = append(rows, summaryRow{"网络峰值带宽", bwVal})
} else {
rows = append(rows, summaryRow{"Peak Bandwidth", bwVal})
}
} else if sectionExists(finalOutput, "测速", "Speed-Test") {
if lang == "zh" {
rows = append(rows, summaryRow{"网络峰值带宽", na()})
} else {
rows = append(rows, summaryRow{"Peak Bandwidth", na()})
}
}
}
// 5. Domestic ISP ranking — only meaningful in Chinese mode (backtrace targets CN ISPs)
if lang == "zh" && config.BacktraceStatus {
if ranking := extractISPRanking(finalOutput, lang); ranking != "" {
rows = append(rows, summaryRow{"国内三大运营商推荐排名", ranking})
} else if sectionExists(finalOutput, "上游及回程线路检测", "") {
rows = append(rows, summaryRow{"国内三大运营商推荐排名", na()})
}
}
if len(rows) == 0 {
if lang == "zh" {
return "无足够数据生成摘要。"
}
return "Insufficient data for summary."
}
// Render as Markdown table
var sb strings.Builder
if lang == "zh" {
sb.WriteString("| 测试项目 | 结果 |\n")
} else {
sb.WriteString("| Test Item | Result |\n")
}
sb.WriteString("|:---------|:-------|\n")
for _, r := range rows {
sb.WriteString("| ")
sb.WriteString(r.label)
sb.WriteString(" | ")
sb.WriteString(r.value)
sb.WriteString(" |\n")
}
return sb.String()
}