diff --git a/audit/internal/platform/sat.go b/audit/internal/platform/sat.go
index 41ebb89..2be426b 100644
--- a/audit/internal/platform/sat.go
+++ b/audit/internal/platform/sat.go
@@ -552,9 +552,13 @@ func (s *System) RunMemoryAcceptancePack(ctx context.Context, baseDir string, si
if passes <= 0 {
passes = 1
}
+ // Bound memtester with a hard wall-clock timeout: ~2.5 min per 100 MB per
+ // pass, plus a fixed 2-minute buffer. Without this, a stuck memory
+ // controller can cause memtester to spin forever on a single subtest.
+ timeoutSec := sizeMB*passes*150/100 + 120
return runAcceptancePackCtx(ctx, baseDir, "memory", []satJob{
{name: "01-free-before.log", cmd: []string{"free", "-h"}},
- {name: "02-memtester.log", cmd: []string{"memtester", fmt.Sprintf("%dM", sizeMB), fmt.Sprintf("%d", passes)}},
+ {name: "02-memtester.log", cmd: []string{"timeout", fmt.Sprintf("%d", timeoutSec), "memtester", fmt.Sprintf("%dM", sizeMB), fmt.Sprintf("%d", passes)}},
{name: "03-free-after.log", cmd: []string{"free", "-h"}},
}, logFunc)
}
diff --git a/audit/internal/webui/api.go b/audit/internal/webui/api.go
index 3ea975e..e756a46 100644
--- a/audit/internal/webui/api.go
+++ b/audit/internal/webui/api.go
@@ -1529,6 +1529,11 @@ func (h *handler) handleAPINetworkRollback(w http.ResponseWriter, _ *http.Reques
writeJSON(w, map[string]string{"status": "rolled back"})
}
+func (h *handler) handleAPIBenchmarkResults(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprint(w, renderBenchmarkResultsCard(h.opts.ExportDir))
+}
+
func (h *handler) rollbackPendingNetworkChange() error {
h.pendingNetMu.Lock()
pnc := h.pendingNet
diff --git a/audit/internal/webui/pages.go b/audit/internal/webui/pages.go
index b6bc7dd..173796c 100644
--- a/audit/internal/webui/pages.go
+++ b/audit/internal/webui/pages.go
@@ -2002,7 +2002,7 @@ func renderBenchmark(opts HandlerOptions) string {
-` + renderBenchmarkResultsCard(opts.ExportDir) + `
+`+`
`+renderBenchmarkResultsCard(opts.ExportDir)+`
`+`
Benchmark Output
@@ -2188,7 +2188,9 @@ function runNvidiaBenchmark(kind) {
if (e.data) failures += 1;
term.textContent += (e.data ? '\nERROR: ' + e.data : '\nCompleted.') + '\n';
term.scrollTop = term.scrollHeight;
+ const isLast = (idx + 1 >= taskIds.length);
streamNext(idx + 1, failures);
+ if (isLast) { benchmarkRefreshResults(); }
});
benchmarkES.onerror = function() {
if (benchmarkES) {
@@ -2208,18 +2210,30 @@ function runNvidiaBenchmark(kind) {
}
benchmarkLoadGPUs();
+
+function benchmarkRefreshResults() {
+ fetch('/api/benchmark/results')
+ .then(function(r) { return r.text(); })
+ .then(function(html) {
+ const el = document.getElementById('benchmark-results-section');
+ if (el) el.innerHTML = html;
+ })
+ .catch(function() {});
+}
`
}
func renderBenchmarkResultsCard(exportDir string) string {
maxIdx, runs := loadBenchmarkHistory(exportDir)
- return renderBenchmarkResultsCardFromRuns(
- "Perf Results",
+ perf := renderBenchmarkResultsCardFromRuns(
+ "Performance Results",
"Composite score by saved benchmark run and GPU.",
- "No saved benchmark runs yet.",
+ "No saved performance benchmark runs yet.",
maxIdx,
runs,
)
+ power := renderPowerBenchmarkResultsCard(exportDir)
+ return perf + "\n" + power
}
func renderBenchmarkResultsCardFromRuns(title, description, emptyMessage string, maxGPUIndex int, runs []benchmarkHistoryRun) string {
@@ -2299,6 +2313,126 @@ func loadBenchmarkHistoryFromPaths(paths []string) (int, []benchmarkHistoryRun)
return maxGPUIndex, runs
}
+func renderPowerBenchmarkResultsCard(exportDir string) string {
+ baseDir := app.DefaultBeeBenchPowerDir
+ if strings.TrimSpace(exportDir) != "" {
+ baseDir = filepath.Join(exportDir, "bee-bench", "power")
+ }
+ paths, err := filepath.Glob(filepath.Join(baseDir, "power-*", "result.json"))
+ if err != nil || len(paths) == 0 {
+ return `
Power / Thermal Fit Results
No saved power benchmark runs yet.
`
+ }
+ sort.Strings(paths)
+
+ type powerRun struct {
+ generatedAt time.Time
+ displayTime string
+ result platform.NvidiaPowerBenchResult
+ }
+ var runs []powerRun
+ for _, path := range paths {
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ continue
+ }
+ var r platform.NvidiaPowerBenchResult
+ if err := json.Unmarshal(raw, &r); err != nil {
+ continue
+ }
+ runs = append(runs, powerRun{
+ generatedAt: r.GeneratedAt,
+ displayTime: r.GeneratedAt.Local().Format("2006-01-02 15:04:05"),
+ result: r,
+ })
+ }
+ sort.Slice(runs, func(i, j int) bool {
+ return runs[i].generatedAt.After(runs[j].generatedAt)
+ })
+
+ // Show only the most recent run's GPU slot table, plus a run history summary.
+ var b strings.Builder
+ b.WriteString(`
Power / Thermal Fit Results
`)
+
+ latest := runs[0].result
+ b.WriteString(`
Latest run: ` + html.EscapeString(runs[0].displayTime))
+ if latest.Hostname != "" {
+ b.WriteString(` — ` + html.EscapeString(latest.Hostname))
+ }
+ if latest.OverallStatus != "" {
+ statusColor := "var(--ok)"
+ if latest.OverallStatus != "OK" {
+ statusColor = "var(--warn)"
+ }
+ b.WriteString(` — ` + html.EscapeString(latest.OverallStatus) + ``)
+ }
+ b.WriteString(`
`)
+
+ if len(latest.GPUs) > 0 {
+ b.WriteString(`
`)
+ b.WriteString(`| GPU | Model | Nominal W | Achieved W | P95 Observed W | Status | `)
+ b.WriteString(`
`)
+ for _, gpu := range latest.GPUs {
+ derated := gpu.Derated || (gpu.DefaultPowerLimitW > 0 && gpu.AppliedPowerLimitW < gpu.DefaultPowerLimitW-1)
+ rowStyle := ""
+ achievedStyle := ""
+ if derated {
+ rowStyle = ` style="background:rgba(255,180,0,0.08)"`
+ achievedStyle = ` style="color:#e6a000;font-weight:600"`
+ }
+ statusLabel := gpu.Status
+ if statusLabel == "" {
+ statusLabel = "OK"
+ }
+ statusColor := "var(--ok)"
+ if statusLabel != "OK" {
+ statusColor = "var(--warn)"
+ }
+ nominalStr := "-"
+ if gpu.DefaultPowerLimitW > 0 {
+ nominalStr = fmt.Sprintf("%.0f", gpu.DefaultPowerLimitW)
+ }
+ achievedStr := "-"
+ if gpu.AppliedPowerLimitW > 0 {
+ achievedStr = fmt.Sprintf("%.0f", gpu.AppliedPowerLimitW)
+ }
+ p95Str := "-"
+ if gpu.MaxObservedPowerW > 0 {
+ p95Str = fmt.Sprintf("%.0f", gpu.MaxObservedPowerW)
+ }
+ b.WriteString(``)
+ b.WriteString(`| ` + strconv.Itoa(gpu.Index) + ` | `)
+ b.WriteString(`` + html.EscapeString(gpu.Name) + ` | `)
+ b.WriteString(`` + nominalStr + ` | `)
+ b.WriteString(`` + achievedStr + ` | `)
+ b.WriteString(`` + p95Str + ` | `)
+ b.WriteString(`` + html.EscapeString(statusLabel) + ` | `)
+ b.WriteString(`
`)
+ }
+ b.WriteString(`
`)
+ }
+
+ if len(runs) > 1 {
+ b.WriteString(`
` + strconv.Itoa(len(runs)) + ` runs total
`)
+ b.WriteString(`| # | Time | GPUs | Status |
`)
+ for i, run := range runs {
+ statusColor := "var(--ok)"
+ if run.result.OverallStatus != "OK" {
+ statusColor = "var(--warn)"
+ }
+ b.WriteString(``)
+ b.WriteString(`| #` + strconv.Itoa(i+1) + ` | `)
+ b.WriteString(`` + html.EscapeString(run.displayTime) + ` | `)
+ b.WriteString(`` + strconv.Itoa(len(run.result.GPUs)) + ` | `)
+ b.WriteString(`` + html.EscapeString(run.result.OverallStatus) + ` | `)
+ b.WriteString(`
`)
+ }
+ b.WriteString(`
`)
+ }
+
+ b.WriteString(`
`)
+ return b.String()
+}
+
// ── Burn ──────────────────────────────────────────────────────────────────────
func renderBurn() string {
diff --git a/audit/internal/webui/server.go b/audit/internal/webui/server.go
index 47670ac..595c233 100644
--- a/audit/internal/webui/server.go
+++ b/audit/internal/webui/server.go
@@ -263,6 +263,7 @@ func NewHandler(opts HandlerOptions) http.Handler {
mux.HandleFunc("POST /api/sat/abort", h.handleAPISATAbort)
mux.HandleFunc("POST /api/bee-bench/nvidia/perf/run", h.handleAPIBenchmarkNvidiaRunKind("nvidia-bench-perf"))
mux.HandleFunc("POST /api/bee-bench/nvidia/power/run", h.handleAPIBenchmarkNvidiaRunKind("nvidia-bench-power"))
+ mux.HandleFunc("GET /api/benchmark/results", h.handleAPIBenchmarkResults)
// Tasks
mux.HandleFunc("GET /api/tasks", h.handleAPITasksList)