summaryrefslogtreecommitdiff
path: root/internal/tui/dashboard
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-09 08:05:26 +0200
committerPaul Buetow <paul@buetow.org>2026-03-09 08:05:26 +0200
commit9f35d16940f35591dd0b0c782d2ec8e57bba84b5 (patch)
tree031bd61cd47299ed66736012370541f4af760ceb /internal/tui/dashboard
parent1277f03a01fafd5ce7931bf9d48dc92f089c6894 (diff)
tui: harden paused flame rendering
Diffstat (limited to 'internal/tui/dashboard')
-rw-r--r--internal/tui/dashboard/model.go14
-rw-r--r--internal/tui/dashboard/model_test.go57
2 files changed, 66 insertions, 5 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index 3a19b74..55661fb 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -752,7 +752,8 @@ func (m *Model) SetLiveTrie(liveTrie flamegraphtui.LiveTrieSource) {
m.liveTrie = liveTrie
m.flamegraphModel.SetLiveTrie(liveTrie)
if m.width > 0 && m.height > 0 {
- m.flamegraphModel.SetViewport(m.width, m.height)
+ flameWidth, flameHeight := flameViewport(m.width, m.height, m.showHelp)
+ m.flamegraphModel.SetViewport(flameWidth, flameHeight)
}
m.flamegraphModel.RefreshFromLiveTrie()
}
@@ -797,15 +798,19 @@ func (m Model) View() tea.View {
width, height := common.EffectiveViewport(m.width, m.height)
_, activeHeight := flameViewport(width, height, m.showHelp)
streamModel := m.streamModel
+ flameModel := m.flamegraphModel
streamModel.SetFooterVisible(m.showHelp)
if m.activeTab == TabStream {
_, activeHeight = streamViewport(width, height)
}
+ if m.activeTab == TabFlame {
+ flameModel.SetViewport(width, activeHeight)
+ }
var b strings.Builder
b.WriteString(renderTabBar(m.activeTab, width))
b.WriteString("\n")
- b.WriteString(m.renderActiveContent(width, activeHeight, &streamModel))
+ b.WriteString(m.renderActiveContent(width, activeHeight, &streamModel, &flameModel))
b.WriteString("\n")
if m.showHelp {
b.WriteString(renderHelpBarWithStatus(m.keys, width, m.filterSummary()))
@@ -823,7 +828,7 @@ func (m Model) filterSummary() string {
return summary + " | stack: " + strings.Join(m.filterStack, " | ")
}
-func (m Model) renderActiveContent(width, activeHeight int, streamModel *eventstream.Model) string {
+func (m Model) renderActiveContent(width, activeHeight int, streamModel *eventstream.Model, flameModel *flamegraphtui.Model) string {
if m.activeTab == TabSyscalls && m.syscallsVizMode == tabVizModeTreemap {
return renderSyscallsTreemap(m.latest, width, activeHeight, m.syscallsChart.Metric(), m.syscallsTreemapSelection, m.isDark)
}
@@ -853,7 +858,7 @@ func (m Model) renderActiveContent(width, activeHeight int, streamModel *eventst
m.activeTab,
m.latest,
streamModel,
- &m.flamegraphModel,
+ flameModel,
width,
activeHeight,
m.pidFilter,
@@ -1091,7 +1096,6 @@ func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstre
if flameModel == nil {
return common.PanelStyle.Render("Flame: waiting for model...")
}
- flameModel.SetViewport(width, height)
return flameModel.View().Content
}
diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go
index dbcde07..dc4ac93 100644
--- a/internal/tui/dashboard/model_test.go
+++ b/internal/tui/dashboard/model_test.go
@@ -16,6 +16,8 @@ import (
tea "charm.land/bubbletea/v2"
)
+var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;]*m`)
+
type fakeSnapshotSource struct {
snapshots int
snap *statsengine.Snapshot
@@ -41,6 +43,19 @@ func (f *fakeResettableSnapshotSource) Snapshot() *statsengine.Snapshot {
return f.snap
}
+func stripANSIEscape(value string) string {
+ return ansiEscapePattern.ReplaceAllString(value, "")
+}
+
+func firstLineContaining(value, needle string) string {
+ for _, line := range strings.Split(value, "\n") {
+ if strings.Contains(line, needle) {
+ return line
+ }
+ }
+ return ""
+}
+
func TestKeySwitchingChangesActiveTab(t *testing.T) {
m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
@@ -425,6 +440,48 @@ func TestFlameTickPausedFreezesAfterInitialSnapshot(t *testing.T) {
}
}
+func TestPausedFlameDashboardViewPreservesZoomedSelectedLine(t *testing.T) {
+ liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count")
+ coreflamegraph.SeedTestFlameData(liveTrie)
+
+ m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m.activeTab = TabFlame
+
+ next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
+ m = next.(Model)
+ m.SetLiveTrie(liveTrie)
+
+ next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyRight})
+ m = next.(Model)
+ next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
+ m = next.(Model)
+ next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeySpace, Text: " "})
+ m = next.(Model)
+
+ if !m.flamegraphModel.Paused() {
+ t.Fatalf("expected flamegraph model to be paused")
+ }
+
+ flameView := stripANSIEscape(m.flamegraphModel.View().Content)
+ selectedLine := firstLineContaining(flameView, "Selected:")
+ if selectedLine == "" {
+ t.Fatalf("expected flame view to include a selected line, got %q", flameView)
+ }
+ if !strings.Contains(selectedLine, "width=") {
+ t.Fatalf("expected selected line to include width details, got %q", selectedLine)
+ }
+
+ dashboardView := stripANSIEscape(m.View().Content)
+ if !strings.Contains(dashboardView, selectedLine) {
+ t.Fatalf("expected dashboard view to preserve paused zoom selected line %q, got %q", selectedLine, dashboardView)
+ }
+
+ dashboardViewAgain := stripANSIEscape(m.View().Content)
+ if !strings.Contains(dashboardViewAgain, selectedLine) {
+ t.Fatalf("expected repeated dashboard view to preserve paused zoom selected line %q, got %q", selectedLine, dashboardViewAgain)
+ }
+}
+
func TestStreamPausedSupportsJKArrowsAndPageKeys(t *testing.T) {
rb := eventstream.NewRingBuffer()
for i := 0; i < 300; i++ {