diff options
Diffstat (limited to 'internal/tui/flamegraph')
| -rw-r--r-- | internal/tui/flamegraph/controls.go | 4 | ||||
| -rw-r--r-- | internal/tui/flamegraph/model.go | 40 | ||||
| -rw-r--r-- | internal/tui/flamegraph/model_test.go | 42 |
3 files changed, 70 insertions, 16 deletions
diff --git a/internal/tui/flamegraph/controls.go b/internal/tui/flamegraph/controls.go index f411a13..b307717 100644 --- a/internal/tui/flamegraph/controls.go +++ b/internal/tui/flamegraph/controls.go @@ -70,7 +70,7 @@ func (m Model) toolbarLine() string { state = lipgloss.NewStyle().Foreground(common.ColorDanger).Bold(true).Render("[PAUSED]") } order := m.currentFieldPresetLabel() - line := fmt.Sprintf("%s | view:%s | o:order(%s) | /:search | enter:zoom | u:undo | r:reset | space/p:pause", state, compactFramePath(m.currentRootPath()), order) + line := fmt.Sprintf("%s | view:%s | o:order(%s) | /:search | enter:zoom | u/esc:undo | r:reset | space/p:pause", state, compactFramePath(m.currentRootPath()), order) if m.searchQuery != "" { line += " | filter:" + m.searchQuery } @@ -92,7 +92,7 @@ func (m Model) helpOverlay() string { if width <= 0 { width = 80 } - help := "Flame help: j/k depth h/l sibling enter zoom u/backspace undo esc reset / search n/N matches space/p pause r reset baseline o order ? help" + help := "Flame help: j/k depth h/l sibling enter zoom u/backspace/esc undo / search n/N matches space/p pause r reset baseline o order ? help" return common.HelpBarStyle.Width(width).Render(padOrTrim(help, width)) } diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go index 66fefc9..07bae5d 100644 --- a/internal/tui/flamegraph/model.go +++ b/internal/tui/flamegraph/model.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "image/color" + "slices" "sort" "strings" "time" @@ -49,8 +50,8 @@ func defaultFlameKeyMap() flameKeyMap { PrevSibling: key.NewBinding(key.WithKeys("h", "left")), NextSibling: key.NewBinding(key.WithKeys("l", "right")), ZoomIn: key.NewBinding(key.WithKeys("enter")), - ZoomUndo: key.NewBinding(key.WithKeys("backspace", "u")), - ZoomReset: key.NewBinding(key.WithKeys("esc")), + ZoomUndo: key.NewBinding(key.WithKeys("backspace", "u", "esc")), + ZoomReset: key.NewBinding(), } } @@ -114,22 +115,25 @@ func NewModel(liveTrie *coreflamegraph.LiveTrie) Model { searchInput.SetWidth(32) searchInput.SetStyles(textinput.DefaultStyles(true)) - return Model{ + m := Model{ liveTrie: liveTrie, matchIndices: make(map[int]bool), filterVisible: make(map[int]bool), subtreeSet: make(map[int]bool), searchInput: searchInput, fieldPresets: [][]string{ - {"comm", "path", "tracepoint"}, + {"comm", "tracepoint", "path"}, {"path", "tracepoint", "comm"}, {"tracepoint", "comm", "path"}, - {"pid", "path", "tracepoint"}, + {"pid", "tracepoint", "path"}, + {"comm", "path", "tracepoint"}, }, isDark: true, keys: defaultFlameKeyMap(), animation: NewAnimationState(30, 6.0, 1.0), } + m.syncFieldPresetToTrie() + return m } // Init starts the flamegraph model. @@ -286,6 +290,7 @@ func (m Model) View() tea.View { // SetLiveTrie updates the data source used by the flamegraph model. func (m *Model) SetLiveTrie(liveTrie *coreflamegraph.LiveTrie) { m.liveTrie = liveTrie + m.syncFieldPresetToTrie() m.lastVersion = 0 m.snapshot = nil m.globalTotal = 0 @@ -302,6 +307,27 @@ func (m *Model) SetLiveTrie(liveTrie *coreflamegraph.LiveTrie) { m.hasNavigableSnapshot = false } +func (m *Model) syncFieldPresetToTrie() { + if m.liveTrie == nil { + m.fieldIndex = 0 + return + } + fields := m.liveTrie.Fields() + if len(fields) == 0 { + m.fieldIndex = 0 + return + } + for idx, preset := range m.fieldPresets { + if slices.Equal(preset, fields) { + m.fieldIndex = idx + return + } + } + custom := slices.Clone(fields) + m.fieldPresets = append([][]string{custom}, m.fieldPresets...) + m.fieldIndex = 0 +} + // RefreshFromLiveTrie loads a new snapshot when the source version changes. func (m *Model) RefreshFromLiveTrie() bool { if m.liveTrie == nil { @@ -760,11 +786,11 @@ func isZoomInKey(msg tea.KeyPressMsg, keys flameKeyMap) bool { } func isZoomUndoKey(msg tea.KeyPressMsg, keys flameKeyMap) bool { - return key.Matches(msg, keys.ZoomUndo) || msg.Code == tea.KeyBackspace + return key.Matches(msg, keys.ZoomUndo) || msg.Code == tea.KeyBackspace || msg.Code == tea.KeyEsc } func isZoomResetKey(msg tea.KeyPressMsg, keys flameKeyMap) bool { - return key.Matches(msg, keys.ZoomReset) || msg.Code == tea.KeyEsc + return key.Matches(msg, keys.ZoomReset) } func isMoveShallowerKey(msg tea.KeyPressMsg, keys flameKeyMap) bool { diff --git a/internal/tui/flamegraph/model_test.go b/internal/tui/flamegraph/model_test.go index e98d936..e253c76 100644 --- a/internal/tui/flamegraph/model_test.go +++ b/internal/tui/flamegraph/model_test.go @@ -21,6 +21,9 @@ func TestNewModelDefaults(t *testing.T) { if len(m.fieldPresets) == 0 { t.Fatalf("expected default field presets to be initialized") } + if got, want := m.fieldPresets[0], []string{"comm", "tracepoint", "path"}; !reflect.DeepEqual(got, want) { + t.Fatalf("default field preset[0] = %v, want %v", got, want) + } if !m.isDark { t.Fatalf("expected dark mode enabled by default") } @@ -399,7 +402,7 @@ func TestFilteredNavigationSkipsHiddenBranches(t *testing.T) { } } -func TestZoomInUndoResetAndNestedZoom(t *testing.T) { +func TestZoomInUndoSingleLevelAndNestedEsc(t *testing.T) { m := newZoomModel() m.selectedIdx = mustFrameIndex(t, m.frames, "root"+pathSeparator+"A") @@ -423,17 +426,33 @@ func TestZoomInUndoResetAndNestedZoom(t *testing.T) { t.Fatalf("expected nested zoom stack to preserve parent path, got %#v", m.zoomStack) } - m = pressFlameKey(t, m, tea.KeyPressMsg{Code: tea.KeyBackspace}) + m = pressFlameKey(t, m, tea.KeyPressMsg{Code: tea.KeyEsc}) if got, want := m.zoomPath, "root"+pathSeparator+"A"; got != want { - t.Fatalf("expected zoomPath after undo %q, got %q", want, got) + t.Fatalf("expected zoomPath after esc undo %q, got %q", want, got) } if len(m.zoomStack) != 1 { - t.Fatalf("expected one stack entry after undo, got %d", len(m.zoomStack)) + t.Fatalf("expected one stack entry after esc undo, got %d", len(m.zoomStack)) } m = pressFlameKey(t, m, tea.KeyPressMsg{Code: tea.KeyEsc}) if m.zoomPath != "" || m.zoomRoot != nil || len(m.zoomStack) != 0 { - t.Fatalf("expected zoom reset to root state, got path=%q root=%+v stack=%d", m.zoomPath, m.zoomRoot, len(m.zoomStack)) + t.Fatalf("expected second esc undo to return to root state, got path=%q root=%+v stack=%d", m.zoomPath, m.zoomRoot, len(m.zoomStack)) + } +} + +func TestZoomResetToRoot(t *testing.T) { + m := newZoomModel() + m.selectedIdx = mustFrameIndex(t, m.frames, "root"+pathSeparator+"A") + m = pressFlameKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) + m.selectedIdx = mustFrameIndex(t, m.frames, "root"+pathSeparator+"A"+pathSeparator+"A1") + m = pressFlameKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnter}) + if m.zoomPath == "" || len(m.zoomStack) == 0 { + t.Fatalf("expected nested zoom before reset") + } + + m.zoomReset() + if m.zoomPath != "" || m.zoomRoot != nil || len(m.zoomStack) != 0 { + t.Fatalf("expected explicit zoom reset to clear zoom stack, got path=%q root=%+v stack=%d", m.zoomPath, m.zoomRoot, len(m.zoomStack)) } } @@ -620,10 +639,11 @@ func TestControlCycleFieldOrderReconfiguresLiveTrie(t *testing.T) { liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count") m := NewModel(liveTrie) initial := append([]string(nil), m.fieldPresets[m.fieldIndex]...) + expectedNextIdx := (m.fieldIndex + 1) % len(m.fieldPresets) m = pressFlameKey(t, m, tea.KeyPressMsg{Code: []rune{'o'}[0], Text: "o"}) - if m.fieldIndex != 1 { - t.Fatalf("expected field index to advance to 1, got %d", m.fieldIndex) + if m.fieldIndex != expectedNextIdx { + t.Fatalf("expected field index to advance to %d, got %d", expectedNextIdx, m.fieldIndex) } next := m.fieldPresets[m.fieldIndex] if reflect.DeepEqual(initial, next) { @@ -634,6 +654,14 @@ func TestControlCycleFieldOrderReconfiguresLiveTrie(t *testing.T) { } } +func TestNewModelAlignsPresetIndexToLiveTrieFields(t *testing.T) { + liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count") + m := NewModel(liveTrie) + if got, want := m.fieldPresets[m.fieldIndex], []string{"comm", "path", "tracepoint"}; !reflect.DeepEqual(got, want) { + t.Fatalf("expected model field preset to align with trie fields, got %v want %v", got, want) + } +} + func TestControlHelpToggle(t *testing.T) { m := NewModel(nil) m = pressFlameKey(t, m, tea.KeyPressMsg{Code: []rune{'?'}[0], Text: "?"}) |
