feat(tui): GPU Platform Stress Test — live nvtop chart during test
Apply the same pattern as NVIDIA SAT: launch nvtop via tea.ExecProcess so it occupies the full terminal as a live GPU chart (temp, power, fan, utilisation lines) while the stress test runs in the background. - Add screenGPUStressRunning screen + dedicated running/render handlers - startGPUStressTest: tea.Batch(stress goroutine, tea.ExecProcess(nvtop)) - [o] reopen nvtop at any time; [a] abort (cancels context) - Graceful degradation: test still runs if nvtop is not on PATH - gpuStressDoneMsg routes result to screenOutput on completion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bee/audit/internal/platform"
|
"bee/audit/internal/platform"
|
||||||
@@ -140,20 +139,7 @@ func (m model) updateConfirm(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
pollSATProgress("gpu-amd", since),
|
pollSATProgress("gpu-amd", since),
|
||||||
)
|
)
|
||||||
case actionRunFanStress:
|
case actionRunFanStress:
|
||||||
m.busyTitle = "GPU Platform Stress Test"
|
return m.startGPUStressTest()
|
||||||
m.progressPrefix = "fan-stress"
|
|
||||||
m.progressSince = time.Now()
|
|
||||||
m.progressLines = nil
|
|
||||||
since := m.progressSince
|
|
||||||
opts := hcFanStressOpts(m.hcMode, m.app)
|
|
||||||
return m, tea.Batch(
|
|
||||||
func() tea.Msg {
|
|
||||||
ctx := context.Background()
|
|
||||||
result, err := m.app.RunFanStressTestResult(ctx, opts)
|
|
||||||
return resultMsg{title: result.Title, body: result.Body, err: err, back: screenHealthCheck}
|
|
||||||
},
|
|
||||||
pollSATProgress("fan-stress", since),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
|
|||||||
@@ -44,3 +44,9 @@ type nvidiaSATDoneMsg struct {
|
|||||||
body string
|
body string
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gpuStressDoneMsg struct {
|
||||||
|
title string
|
||||||
|
body string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -155,6 +156,64 @@ func (m model) hcRunFanStress() (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startGPUStressTest launches the GPU Platform Stress Test and nvtop concurrently.
|
||||||
|
// nvtop occupies the full terminal as a live chart; the stress test runs in background.
|
||||||
|
func (m model) startGPUStressTest() (tea.Model, tea.Cmd) {
|
||||||
|
opts := hcFanStressOpts(m.hcMode, m.app)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
m.gpuStressCancel = cancel
|
||||||
|
m.gpuStressAborted = false
|
||||||
|
m.screen = screenGPUStressRunning
|
||||||
|
m.nvidiaSATCursor = 0
|
||||||
|
|
||||||
|
stressCmd := func() tea.Msg {
|
||||||
|
result, err := m.app.RunFanStressTestResult(ctx, opts)
|
||||||
|
return gpuStressDoneMsg{title: result.Title, body: result.Body, err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
nvtopPath, lookErr := exec.LookPath("nvtop")
|
||||||
|
if lookErr != nil {
|
||||||
|
return m, stressCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, tea.Batch(
|
||||||
|
stressCmd,
|
||||||
|
tea.ExecProcess(exec.Command(nvtopPath), func(_ error) tea.Msg {
|
||||||
|
return nvtopClosedMsg{}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateGPUStressRunning handles keys on the GPU stress running screen.
|
||||||
|
func (m model) updateGPUStressRunning(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg.String() {
|
||||||
|
case "o", "O":
|
||||||
|
nvtopPath, err := exec.LookPath("nvtop")
|
||||||
|
if err != nil {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, tea.ExecProcess(exec.Command(nvtopPath), func(_ error) tea.Msg {
|
||||||
|
return nvtopClosedMsg{}
|
||||||
|
})
|
||||||
|
case "a", "A":
|
||||||
|
if m.gpuStressCancel != nil {
|
||||||
|
m.gpuStressCancel()
|
||||||
|
m.gpuStressCancel = nil
|
||||||
|
}
|
||||||
|
m.gpuStressAborted = true
|
||||||
|
m.screen = screenHealthCheck
|
||||||
|
m.cursor = 0
|
||||||
|
case "ctrl+c":
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderGPUStressRunning() string {
|
||||||
|
return "GPU PLATFORM STRESS TEST\n\nTest is running...\n\n[o] Open nvtop [a] Abort test [ctrl+c] quit\n"
|
||||||
|
}
|
||||||
|
|
||||||
func (m model) hcRunAll() (tea.Model, tea.Cmd) {
|
func (m model) hcRunAll() (tea.Model, tea.Cmd) {
|
||||||
for _, sel := range m.hcSel {
|
for _, sel := range m.hcSel {
|
||||||
if sel {
|
if sel {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const (
|
|||||||
screenConfirm screen = "confirm"
|
screenConfirm screen = "confirm"
|
||||||
screenNvidiaSATSetup screen = "nvidia_sat_setup"
|
screenNvidiaSATSetup screen = "nvidia_sat_setup"
|
||||||
screenNvidiaSATRunning screen = "nvidia_sat_running"
|
screenNvidiaSATRunning screen = "nvidia_sat_running"
|
||||||
|
screenGPUStressRunning screen = "gpu_stress_running"
|
||||||
)
|
)
|
||||||
|
|
||||||
type actionKind string
|
type actionKind string
|
||||||
@@ -93,6 +94,10 @@ type model struct {
|
|||||||
nvidiaSATCancel func()
|
nvidiaSATCancel func()
|
||||||
nvidiaSATAborted bool
|
nvidiaSATAborted bool
|
||||||
|
|
||||||
|
// GPU Platform Stress Test running
|
||||||
|
gpuStressCancel func()
|
||||||
|
gpuStressAborted bool
|
||||||
|
|
||||||
// SAT verbose progress (CPU / Memory / Storage / AMD GPU)
|
// SAT verbose progress (CPU / Memory / Storage / AMD GPU)
|
||||||
progressLines []string
|
progressLines []string
|
||||||
progressPrefix string
|
progressPrefix string
|
||||||
|
|||||||
@@ -108,6 +108,28 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m.handleNvidiaGPUsMsg(msg)
|
return m.handleNvidiaGPUsMsg(msg)
|
||||||
case nvtopClosedMsg:
|
case nvtopClosedMsg:
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case gpuStressDoneMsg:
|
||||||
|
if m.gpuStressAborted {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if m.gpuStressCancel != nil {
|
||||||
|
m.gpuStressCancel()
|
||||||
|
m.gpuStressCancel = nil
|
||||||
|
}
|
||||||
|
m.prevScreen = screenHealthCheck
|
||||||
|
m.screen = screenOutput
|
||||||
|
m.title = msg.title
|
||||||
|
if msg.err != nil {
|
||||||
|
body := strings.TrimSpace(msg.body)
|
||||||
|
if body == "" {
|
||||||
|
m.body = fmt.Sprintf("ERROR: %v", msg.err)
|
||||||
|
} else {
|
||||||
|
m.body = fmt.Sprintf("%s\n\nERROR: %v", body, msg.err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.body = msg.body
|
||||||
|
}
|
||||||
|
return m, m.refreshSnapshotCmd()
|
||||||
case nvidiaSATDoneMsg:
|
case nvidiaSATDoneMsg:
|
||||||
if m.nvidiaSATAborted {
|
if m.nvidiaSATAborted {
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -152,6 +174,8 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m.updateNvidiaSATSetup(msg)
|
return m.updateNvidiaSATSetup(msg)
|
||||||
case screenNvidiaSATRunning:
|
case screenNvidiaSATRunning:
|
||||||
return m.updateNvidiaSATRunning(msg)
|
return m.updateNvidiaSATRunning(msg)
|
||||||
|
case screenGPUStressRunning:
|
||||||
|
return m.updateGPUStressRunning(msg)
|
||||||
case screenExportTargets:
|
case screenExportTargets:
|
||||||
return m.updateMenu(msg, len(m.targets), m.handleExportTargetsMenu)
|
return m.updateMenu(msg, len(m.targets), m.handleExportTargetsMenu)
|
||||||
case screenInterfacePick:
|
case screenInterfacePick:
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ func (m model) View() string {
|
|||||||
body = renderNvidiaSATSetup(m)
|
body = renderNvidiaSATSetup(m)
|
||||||
case screenNvidiaSATRunning:
|
case screenNvidiaSATRunning:
|
||||||
body = renderNvidiaSATRunning()
|
body = renderNvidiaSATRunning()
|
||||||
|
case screenGPUStressRunning:
|
||||||
|
body = renderGPUStressRunning()
|
||||||
case screenOutput:
|
case screenOutput:
|
||||||
body = fmt.Sprintf("%s\n\n%s\n\n[enter/esc] back [ctrl+c] quit\n", m.title, strings.TrimSpace(m.body))
|
body = fmt.Sprintf("%s\n\n%s\n\n[enter/esc] back [ctrl+c] quit\n", m.title, strings.TrimSpace(m.body))
|
||||||
default:
|
default:
|
||||||
|
|||||||
Reference in New Issue
Block a user