diff --git a/audit/internal/webui/charts_svg.go b/audit/internal/webui/charts_svg.go
index ee896e3..6cf357a 100644
--- a/audit/internal/webui/charts_svg.go
+++ b/audit/internal/webui/charts_svg.go
@@ -6,6 +6,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
"time"
"bee/audit/internal/platform"
@@ -52,6 +53,12 @@ var metricChartPalette = []string{
"#ffbe5c",
}
+var gpuLabelCache struct {
+ mu sync.Mutex
+ loadedAt time.Time
+ byIndex map[int]string
+}
+
func renderMetricChartSVG(title string, labels []string, times []time.Time, datasets [][]float64, names []string, yMin, yMax *float64, canvasHeight int, timeline []chartTimelineSegment) ([]byte, error) {
pointCount := len(labels)
if len(times) > pointCount {
@@ -76,15 +83,7 @@ func renderMetricChartSVG(title string, labels []string, times []time.Time, data
}
}
- mn, avg, mx := globalStats(datasets)
- if mx > 0 {
- title = fmt.Sprintf("%s ↓%s ~%s ↑%s",
- title,
- chartLegendNumber(mn),
- chartLegendNumber(avg),
- chartLegendNumber(mx),
- )
- }
+ statsLabel := chartStatsLabel(datasets)
legendItems := []metricChartSeries{}
for i, name := range names {
@@ -106,7 +105,7 @@ func renderMetricChartSVG(title string, labels []string, times []time.Time, data
var b strings.Builder
writeSVGOpen(&b, layout.Width, layout.Height)
- writeChartFrame(&b, title, layout.Width, layout.Height)
+ writeChartFrame(&b, title, statsLabel, layout.Width, layout.Height)
writeTimelineIdleSpans(&b, layout, start, end, timeline)
writeVerticalGrid(&b, layout, times, pointCount, 8)
writeHorizontalGrid(&b, layout, scale)
@@ -133,7 +132,7 @@ func renderGPUOverviewChartSVG(idx int, samples []platform.LiveMetricSample, tim
labels := sampleTimeLabels(samples)
times := sampleTimes(samples)
svg, err := drawGPUOverviewChartSVG(
- fmt.Sprintf("GPU %d Overview", idx),
+ gpuDisplayLabel(idx)+" Overview",
labels,
times,
[]metricChartSeries{
@@ -214,7 +213,7 @@ func drawGPUOverviewChartSVG(title string, labels []string, times []time.Time, s
var b strings.Builder
writeSVGOpen(&b, width, height)
- writeChartFrame(&b, title, width, height)
+ writeChartFrame(&b, title, "", width, height)
writeTimelineIdleSpans(&b, layout, start, end, timeline)
writeVerticalGrid(&b, layout, times, pointCount, 8)
writeHorizontalGrid(&b, layout, scales[0])
@@ -457,10 +456,14 @@ func writeSVGClose(b *strings.Builder) {
b.WriteString("\n")
}
-func writeChartFrame(b *strings.Builder, title string, width, height int) {
+func writeChartFrame(b *strings.Builder, title, subtitle string, width, height int) {
fmt.Fprintf(b, ``+"\n", width, height)
fmt.Fprintf(b, `%s`+"\n",
width/2, sanitizeChartText(title))
+ if strings.TrimSpace(subtitle) != "" {
+ fmt.Fprintf(b, `%s`+"\n",
+ width/2, sanitizeChartText(subtitle))
+ }
}
func writePlotBorder(b *strings.Builder, layout chartLayout) {
@@ -545,7 +548,21 @@ func writeSeriesPolyline(b *strings.Builder, layout chartLayout, times []time.Ti
x := chartXForTime(chartPointTime(times, 0), start, end, layout.PlotLeft, layout.PlotRight)
y := chartYForValue(values[0], scale, layout.PlotTop, layout.PlotBottom)
fmt.Fprintf(b, ``+"\n", x, y, color)
+ return
}
+ peakIdx := 0
+ peakValue := values[0]
+ for idx, value := range values[1:] {
+ if value >= peakValue {
+ peakIdx = idx + 1
+ peakValue = value
+ }
+ }
+ x := chartXForTime(chartPointTime(times, peakIdx), start, end, layout.PlotLeft, layout.PlotRight)
+ y := chartYForValue(peakValue, scale, layout.PlotTop, layout.PlotBottom)
+ fmt.Fprintf(b, ``+"\n", x, y, color)
+ fmt.Fprintf(b, ``+"\n",
+ x, y-10, x-5, y-18, x+5, y-18, color)
}
func writeLegend(b *strings.Builder, layout chartLayout, series []metricChartSeries) {
@@ -711,3 +728,49 @@ func valueClamp(value float64, scale chartScale) float64 {
}
return value
}
+
+func chartStatsLabel(datasets [][]float64) string {
+ mn, avg, mx := globalStats(datasets)
+ if mx <= 0 && avg <= 0 && mn <= 0 {
+ return ""
+ }
+ return fmt.Sprintf("min %s avg %s max %s",
+ chartLegendNumber(mn),
+ chartLegendNumber(avg),
+ chartLegendNumber(mx),
+ )
+}
+
+func gpuDisplayLabel(idx int) string {
+ if name := gpuModelNameByIndex(idx); name != "" {
+ return fmt.Sprintf("GPU %d — %s", idx, name)
+ }
+ return fmt.Sprintf("GPU %d", idx)
+}
+
+func gpuModelNameByIndex(idx int) string {
+ now := time.Now()
+ gpuLabelCache.mu.Lock()
+ if now.Sub(gpuLabelCache.loadedAt) > 30*time.Second || gpuLabelCache.byIndex == nil {
+ gpuLabelCache.loadedAt = now
+ gpuLabelCache.byIndex = loadGPUModelNames()
+ }
+ name := strings.TrimSpace(gpuLabelCache.byIndex[idx])
+ gpuLabelCache.mu.Unlock()
+ return name
+}
+
+func loadGPUModelNames() map[int]string {
+ out := map[int]string{}
+ gpus, err := platform.New().ListNvidiaGPUs()
+ if err != nil {
+ return out
+ }
+ for _, gpu := range gpus {
+ name := strings.TrimSpace(gpu.Name)
+ if name != "" {
+ out[gpu.Index] = name
+ }
+ }
+ return out
+}
diff --git a/audit/internal/webui/pages.go b/audit/internal/webui/pages.go
index b9cdae1..77ce0be 100644
--- a/audit/internal/webui/pages.go
+++ b/audit/internal/webui/pages.go
@@ -860,6 +860,35 @@ func renderMetrics() string {