From 9f35d16940f35591dd0b0c782d2ec8e57bba84b5 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 9 Mar 2026 08:05:26 +0200 Subject: tui: harden paused flame rendering --- internal/tui/dashboard/model.go | 14 +++++---- internal/tui/dashboard/model_test.go | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) (limited to 'internal/tui/dashboard') 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++ { -- cgit v1.2.3