summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/flamegraph/livetrie.go39
-rw-r--r--internal/flamegraph/livetrie_test.go48
-rw-r--r--internal/tui/flamegraph/controls.go69
-rw-r--r--internal/tui/flamegraph/model.go31
-rw-r--r--internal/tui/flamegraph/model_test.go54
-rw-r--r--internal/tui/flamegraph/renderer.go15
-rw-r--r--internal/tui/flamegraph/renderer_test.go18
-rw-r--r--internal/tui/tui.go2
-rw-r--r--internal/tui/tui_test.go14
9 files changed, 245 insertions, 45 deletions
diff --git a/internal/flamegraph/livetrie.go b/internal/flamegraph/livetrie.go
index 13d7de9..9f1fd91 100644
--- a/internal/flamegraph/livetrie.go
+++ b/internal/flamegraph/livetrie.go
@@ -42,6 +42,9 @@ type LiveTrie struct {
// NewLiveTrie constructs an empty live trie with the configured frame/count fields.
func NewLiveTrie(fields []string, countField string) *LiveTrie {
+ if !isLiveTrieCountField(countField) {
+ countField = "count"
+ }
return &LiveTrie{
root: &trieNode{
childMap: make(map[string]*trieNode),
@@ -123,6 +126,33 @@ func (lt *LiveTrie) Fields() []string {
return out
}
+// CountField returns the active metric used to aggregate node values.
+func (lt *LiveTrie) CountField() string {
+ lt.mu.RLock()
+ field := lt.countField
+ lt.mu.RUnlock()
+ return field
+}
+
+// SetCountField changes the active aggregation metric and starts a new baseline.
+func (lt *LiveTrie) SetCountField(countField string) error {
+ field := strings.TrimSpace(countField)
+ if !isLiveTrieCountField(field) {
+ return fmt.Errorf("invalid count field %q", countField)
+ }
+
+ lt.mu.Lock()
+ if lt.countField == field {
+ lt.mu.Unlock()
+ return nil
+ }
+ lt.countField = field
+ lt.resetLocked()
+ lt.mu.Unlock()
+ lt.invalidateCache()
+ return nil
+}
+
// Reconfigure changes frame fields and clears accumulated data for a new baseline.
func (lt *LiveTrie) Reconfigure(fields []string) error {
normalized, err := normalizeLiveTrieFields(fields)
@@ -239,6 +269,15 @@ func isLiveTrieField(field string) bool {
}
}
+func isLiveTrieCountField(field string) bool {
+ switch field {
+ case "count", "duration", "durationToPrev", "bytes":
+ return true
+ default:
+ return false
+ }
+}
+
func subtreeTotal(node *trieNode) uint64 {
total := node.value
for _, child := range node.children {
diff --git a/internal/flamegraph/livetrie_test.go b/internal/flamegraph/livetrie_test.go
index 71f645c..53bdf1f 100644
--- a/internal/flamegraph/livetrie_test.go
+++ b/internal/flamegraph/livetrie_test.go
@@ -223,6 +223,54 @@ func TestLiveTrieReconfigureRejectsInvalidFields(t *testing.T) {
}
}
+func TestLiveTrieSetCountFieldSwitchesMetricAndResetsBaseline(t *testing.T) {
+ lt := NewLiveTrie([]string{"comm"}, "count")
+ lt.Ingest(newTestPair("svc", 42, 1001, "/tmp/a", 10, 1, 64))
+
+ initial := decodeLiveSnapshot(t, lt)
+ if got, want := initial.Total, uint64(1); got != want {
+ t.Fatalf("count snapshot total = %d, want %d", got, want)
+ }
+
+ if err := lt.SetCountField("bytes"); err != nil {
+ t.Fatalf("set count field: %v", err)
+ }
+ if got, want := lt.CountField(), "bytes"; got != want {
+ t.Fatalf("count field = %q, want %q", got, want)
+ }
+
+ empty := decodeLiveSnapshot(t, lt)
+ if got := empty.Total; got != 0 {
+ t.Fatalf("expected reset baseline after metric switch, total=%d", got)
+ }
+
+ lt.Ingest(newTestPair("svc", 42, 1002, "/tmp/b", 10, 1, 64))
+ bytesSnap := decodeLiveSnapshot(t, lt)
+ if got, want := bytesSnap.Total, uint64(64); got != want {
+ t.Fatalf("bytes snapshot total = %d, want %d", got, want)
+ }
+ leaf := findSnapshotPath(t, &bytesSnap, "svc")
+ if got, want := leaf.Total, uint64(64); got != want {
+ t.Fatalf("bytes leaf total = %d, want %d", got, want)
+ }
+}
+
+func TestLiveTrieSetCountFieldRejectsInvalidValue(t *testing.T) {
+ lt := NewLiveTrie([]string{"comm"}, "count")
+ lt.Ingest(newTestPair("svc", 42, 1001, "/tmp/a", 1, 1, 1))
+ beforeVersion := lt.Version()
+
+ if err := lt.SetCountField("bogus"); err == nil {
+ t.Fatalf("expected invalid count field error")
+ }
+ if got, want := lt.CountField(), "count"; got != want {
+ t.Fatalf("count field changed unexpectedly: got %q want %q", got, want)
+ }
+ if got := lt.Version(); got != beforeVersion {
+ t.Fatalf("version changed on invalid count field: got %d want %d", got, beforeVersion)
+ }
+}
+
func TestLiveTrieSnapshotJSONCaching(t *testing.T) {
lt := NewLiveTrie([]string{"comm"}, "count")
lt.Ingest(newTestPair("svc", 42, 1001, "/tmp/a", 1, 1, 1))
diff --git a/internal/tui/flamegraph/controls.go b/internal/tui/flamegraph/controls.go
index cd74df5..06e6d0d 100644
--- a/internal/tui/flamegraph/controls.go
+++ b/internal/tui/flamegraph/controls.go
@@ -13,10 +13,7 @@ func (m *Model) togglePause() {
m.paused = !m.paused
}
-func (m *Model) resetBaseline() {
- if m.liveTrie != nil {
- m.liveTrie.Reset()
- }
+func (m *Model) clearSnapshotState(clearSearch bool) {
m.zoomRoot = nil
m.zoomPath = ""
m.zoomStack = nil
@@ -25,11 +22,20 @@ func (m *Model) resetBaseline() {
m.globalTotal = 0
m.frames = nil
m.targetFrames = nil
- m.searchQuery = ""
m.matchIndices = make(map[int]bool)
m.filterVisible = make(map[int]bool)
m.subtreeSet = make(map[int]bool)
m.hasNavigableSnapshot = false
+ if clearSearch {
+ m.searchQuery = ""
+ }
+}
+
+func (m *Model) resetBaseline() {
+ if m.liveTrie != nil {
+ m.liveTrie.Reset()
+ }
+ m.clearSnapshotState(true)
m.statusMessage = "Baseline reset"
}
@@ -45,21 +51,26 @@ func (m *Model) cycleFieldOrder() {
return
}
}
- m.zoomRoot = nil
- m.zoomPath = ""
- m.zoomStack = nil
- m.selectedIdx = 0
- m.snapshot = nil
- m.globalTotal = 0
- m.frames = nil
- m.targetFrames = nil
- m.matchIndices = make(map[int]bool)
- m.filterVisible = make(map[int]bool)
- m.subtreeSet = make(map[int]bool)
- m.hasNavigableSnapshot = false
+ m.clearSnapshotState(false)
m.statusMessage = "Order: " + strings.Join(nextPreset, "/")
}
+func (m *Model) toggleCountField() {
+ next := "bytes"
+ if m.countField == "bytes" {
+ next = "count"
+ }
+ if m.liveTrie != nil {
+ if err := m.liveTrie.SetCountField(next); err != nil {
+ m.statusMessage = "Metric toggle error: " + err.Error()
+ return
+ }
+ }
+ m.countField = next
+ m.clearSnapshotState(false)
+ m.statusMessage = "Metric: " + m.countFieldLabel() + " (new baseline)"
+}
+
func (m *Model) toggleHelp() {
m.showHelp = !m.showHelp
}
@@ -70,7 +81,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/esc:undo | r:reset | space/p:pause", state, compactFramePath(m.currentRootPath()), order)
+ line := fmt.Sprintf("%s | view:%s | o:order(%s) | b:metric(%s) | /:search | enter:zoom | u/esc:undo | r:reset | space/p:pause", state, compactFramePath(m.currentRootPath()), order, m.countFieldLabel())
if m.searchQuery != "" {
line += " | filter:" + m.searchQuery
}
@@ -92,7 +103,7 @@ func (m Model) helpOverlay() string {
if width <= 0 {
width = 80
}
- help := "Flame help: j/k depth h/l sibling pgup top pgdn root enter zoom u/backspace/esc undo / search n/N matches space/p pause r reset baseline o order ? help"
+ help := "Flame help: j/k depth h/l sibling pgup top pgdn root enter zoom u/backspace/esc undo / search n/N matches space/p pause r reset baseline o order b metric ? help"
return common.HelpBarStyle.Width(width).Render(padOrTrim(help, width))
}
@@ -118,17 +129,18 @@ func (m Model) selectionStatusLine() string {
if m.globalTotal > 0 {
systemShare = percentOfTotal(frame.Total, m.globalTotal)
}
- shareLabel := fmt.Sprintf("%.2f%% system", systemShare)
+ metric := m.countFieldLabel()
+ shareLabel := fmt.Sprintf("%.2f%% of total %s", systemShare, metric)
if strings.TrimSpace(m.searchQuery) != "" && len(m.matchIndices) > 0 {
filterTotal, _ := filterCoverageTotals(m.frames, m.matchIndices, m.globalTotal)
if filterTotal > 0 {
selectedFilterTotal := filterCoverageTotalForPath(m.frames, m.matchIndices, frame.Path)
filterShare := percentOfTotal(selectedFilterTotal, filterTotal)
- shareLabel = fmt.Sprintf("%.2f%% filter", filterShare)
+ shareLabel = fmt.Sprintf("%.2f%% of filtered %s", filterShare, metric)
}
}
- line := fmt.Sprintf("[%s] sel:%d/%d %s | path:%s | depth:%d | total:%d | %s",
- mode, selIdx+1, len(m.frames), frame.Name, compactFramePath(frame.Path), frame.Depth, frame.Total, shareLabel)
+ line := fmt.Sprintf("[%s] sel:%d/%d %s | path:%s | depth:%d | total(%s):%d | %s",
+ mode, selIdx+1, len(m.frames), frame.Name, compactFramePath(frame.Path), frame.Depth, m.countFieldLabel(), frame.Total, shareLabel)
if m.searchQuery != "" {
line += " | filter:" + m.searchQuery
}
@@ -148,3 +160,14 @@ func (m Model) currentFieldPresetLabel() string {
}
return strings.Join(m.fieldPresets[idx], "/")
}
+
+func (m Model) countFieldLabel() string {
+ switch m.countField {
+ case "count":
+ return "events"
+ case "bytes":
+ return "bytes"
+ default:
+ return m.countField
+ }
+}
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go
index 2b974fe..c4973fb 100644
--- a/internal/tui/flamegraph/model.go
+++ b/internal/tui/flamegraph/model.go
@@ -88,6 +88,7 @@ type Model struct {
fieldPresets [][]string
fieldIndex int
+ countField string
animation AnimationState
animating bool
@@ -132,11 +133,13 @@ func NewModel(liveTrie *coreflamegraph.LiveTrie) Model {
{"pid", "tracepoint", "path"},
{"comm", "path", "tracepoint"},
},
- isDark: true,
- keys: defaultFlameKeyMap(),
- animation: NewAnimationState(30, 6.0, 1.0),
+ isDark: true,
+ keys: defaultFlameKeyMap(),
+ animation: NewAnimationState(30, 6.0, 1.0),
+ countField: "count",
}
m.syncFieldPresetToTrie()
+ m.syncCountFieldToTrie()
return m
}
@@ -213,6 +216,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case isCycleOrderKey(msg):
handled = true
m.cycleFieldOrder()
+ case isCycleMetricKey(msg):
+ handled = true
+ m.toggleCountField()
case isHelpToggleKey(msg):
handled = true
m.toggleHelp()
@@ -265,6 +271,7 @@ func (m Model) ConsumesKey(msg tea.KeyPressMsg) bool {
isPauseKey(msg),
isResetBaselineKey(msg),
isCycleOrderKey(msg),
+ isCycleMetricKey(msg),
isHelpToggleKey(msg):
return true
case isZoomInKey(msg, m.keys),
@@ -293,7 +300,7 @@ func (m Model) View() tea.View {
renderHeight = 3
}
- content := RenderTerminalView(m.frames, m.width, renderHeight, m.selectedIdx, m.subtreeSet, m.matchIndices, m.filterVisible, m.globalTotal, m.isDark, m.searchActive, m.searchQuery)
+ content := RenderTerminalView(m.frames, m.width, renderHeight, m.selectedIdx, m.subtreeSet, m.matchIndices, m.filterVisible, m.globalTotal, m.countFieldLabel(), m.isDark, m.searchActive, m.searchQuery)
content = replaceHeaderLine(content, m.toolbarLine())
if m.searchActive {
content = replaceFooterLine(content, m.searchFooter())
@@ -312,6 +319,7 @@ func (m Model) View() tea.View {
func (m *Model) SetLiveTrie(liveTrie *coreflamegraph.LiveTrie) {
m.liveTrie = liveTrie
m.syncFieldPresetToTrie()
+ m.syncCountFieldToTrie()
m.lastVersion = 0
m.snapshot = nil
m.globalTotal = 0
@@ -349,6 +357,18 @@ func (m *Model) syncFieldPresetToTrie() {
m.fieldIndex = 0
}
+func (m *Model) syncCountFieldToTrie() {
+ if m.liveTrie == nil {
+ m.countField = "count"
+ return
+ }
+ field := strings.TrimSpace(m.liveTrie.CountField())
+ if field == "" {
+ field = "count"
+ }
+ m.countField = field
+}
+
// RefreshFromLiveTrie loads a new snapshot when the source version changes.
func (m *Model) RefreshFromLiveTrie() bool {
if m.liveTrie == nil {
@@ -881,6 +901,9 @@ func isResetBaselineKey(msg tea.KeyPressMsg) bool {
return keyString(msg) == "r"
}
func isCycleOrderKey(msg tea.KeyPressMsg) bool { return keyString(msg) == "o" }
+func isCycleMetricKey(msg tea.KeyPressMsg) bool {
+ return keyString(msg) == "b"
+}
func isHelpToggleKey(msg tea.KeyPressMsg) bool { return keyString(msg) == "?" }
func isZoomInKey(msg tea.KeyPressMsg, keys flameKeyMap) bool {
diff --git a/internal/tui/flamegraph/model_test.go b/internal/tui/flamegraph/model_test.go
index 355facc..74ce8d9 100644
--- a/internal/tui/flamegraph/model_test.go
+++ b/internal/tui/flamegraph/model_test.go
@@ -661,11 +661,29 @@ func TestViewIncludesSelectionStatusBar(t *testing.T) {
if !strings.Contains(view, "[LIVE] sel:2/2 child") {
t.Fatalf("expected selection status bar to include selected frame info, got %q", view)
}
- if !strings.Contains(view, "40.00% system") {
+ if !strings.Contains(view, "40.00% of total events") {
t.Fatalf("expected selection status bar to include selected share, got %q", view)
}
}
+func TestViewSelectionStatusUsesBytesLabelInBytesMode(t *testing.T) {
+ m := NewModel(nil)
+ m.width = 120
+ m.height = 20
+ m.countField = "bytes"
+ m.frames = []tuiFrame{
+ {Name: "root", Depth: 0, Col: 0, Row: 0, Width: 120, Total: 200, Percent: 100, Path: "root"},
+ {Name: "child", Depth: 1, Col: 0, Row: 1, Width: 60, Total: 80, Percent: 40, Path: "root" + pathSeparator + "child"},
+ }
+ m.selectedIdx = 1
+ m.globalTotal = 200
+
+ view := m.View().Content
+ if !strings.Contains(view, "40.00% of total bytes") {
+ t.Fatalf("expected bytes-based selection share label, got %q", view)
+ }
+}
+
func TestViewFitsViewportHeightAndKeepsSearchFooterVisible(t *testing.T) {
m := NewModel(nil)
m.width = 100
@@ -723,7 +741,7 @@ func TestViewFilterSelectionStatusUsesFilteredTotalAndKeepsContextVisible(t *tes
m.recomputeFilterState()
view := m.View().Content
- if !strings.Contains(view, "100.00% filter") {
+ if !strings.Contains(view, "100.00% of filtered events") {
t.Fatalf("expected filtered selection share in status line, got %q", view)
}
if !strings.Contains(view, "drop") || !strings.Contains(view, "noise") {
@@ -750,6 +768,30 @@ func TestControlCycleFieldOrderReconfiguresLiveTrie(t *testing.T) {
}
}
+func TestControlMetricToggleReconfiguresLiveTrieCountField(t *testing.T) {
+ liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count")
+ m := NewModel(liveTrie)
+
+ m = pressFlameKey(t, m, tea.KeyPressMsg{Code: []rune{'b'}[0], Text: "b"})
+ if got, want := m.countField, "bytes"; got != want {
+ t.Fatalf("expected model count field %q, got %q", want, got)
+ }
+ if got, want := liveTrie.CountField(), "bytes"; got != want {
+ t.Fatalf("expected live trie count field %q, got %q", want, got)
+ }
+ if got, want := m.statusMessage, "Metric: bytes (new baseline)"; got != want {
+ t.Fatalf("expected metric toggle status %q, got %q", want, got)
+ }
+
+ m = pressFlameKey(t, m, tea.KeyPressMsg{Code: []rune{'b'}[0], Text: "b"})
+ if got, want := m.countField, "count"; got != want {
+ t.Fatalf("expected model count field %q after second toggle, got %q", want, got)
+ }
+ if got, want := liveTrie.CountField(), "count"; got != want {
+ t.Fatalf("expected live trie count field %q after second toggle, got %q", want, got)
+ }
+}
+
func TestNewModelAlignsPresetIndexToLiveTrieFields(t *testing.T) {
liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count")
m := NewModel(liveTrie)
@@ -758,6 +800,14 @@ func TestNewModelAlignsPresetIndexToLiveTrieFields(t *testing.T) {
}
}
+func TestNewModelAlignsCountFieldToLiveTrie(t *testing.T) {
+ liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "bytes")
+ m := NewModel(liveTrie)
+ if got, want := m.countField, "bytes"; got != want {
+ t.Fatalf("expected model count field to align with trie field, got %q want %q", got, want)
+ }
+}
+
func TestControlHelpToggle(t *testing.T) {
m := NewModel(nil)
m = pressFlameKey(t, m, tea.KeyPressMsg{Code: []rune{'?'}[0], Text: "?"})
diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go
index 3ae9a11..e4c4043 100644
--- a/internal/tui/flamegraph/renderer.go
+++ b/internal/tui/flamegraph/renderer.go
@@ -192,7 +192,7 @@ func semanticFrameColor(name string) (color.Color, bool) {
}
// RenderTerminalView renders a terminal flamegraph viewport from laid out frames.
-func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtreeSet, matchSet, filterSet map[int]bool, globalTotal uint64, isDark, searchActive bool, searchQuery string) string {
+func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtreeSet, matchSet, filterSet map[int]bool, globalTotal uint64, metricLabel string, isDark, searchActive bool, searchQuery string) string {
if width < minFlameWidth {
return common.PanelStyle.Render("Flame: terminal too narrow (need >= 60 columns)")
}
@@ -202,6 +202,9 @@ func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtr
if len(frames) == 0 {
return common.PanelStyle.Render("Flame: waiting for data...")
}
+ if strings.TrimSpace(metricLabel) == "" {
+ metricLabel = "events"
+ }
filterActive := strings.TrimSpace(searchQuery) != ""
if filterActive {
@@ -267,13 +270,13 @@ func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtr
if len(frames) > 0 {
frameCoverage = 100 * float64(visibleFrames) / float64(len(frames))
}
- status := fmt.Sprintf("Filter %q: %.1f%% system (%d/%d matches, %.1f%% frames shown) | Selected: %s total=%d depth=%d %.2f%% filter",
- searchQuery, filterSystemShare, pos, len(matches), frameCoverage,
- selected.Name, selected.Total, selected.Depth, selectedFilterShare)
+ status := fmt.Sprintf("Filter %q: %.1f%% %s (%d/%d matches, %.1f%% frames shown) | Selected: %s total(%s)=%d depth=%d %.2f%% filtered %s",
+ searchQuery, filterSystemShare, metricLabel, pos, len(matches), frameCoverage,
+ selected.Name, metricLabel, selected.Total, selected.Depth, selectedFilterShare, metricLabel)
return renderViewRows(toolbar, status, rowsForRender(frames, width, rowOffset, maxRow, barHeight, availableRows, selected.Path, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterActive), width)
} else {
- status := fmt.Sprintf("Selected: %s [%s] total=%d depth=%d col=%d width=%d share=%.2f%%",
- selected.Name, compactFramePath(selected.Path), selected.Total, selected.Depth, selected.Col, selected.Width, selectedSystemShare)
+ status := fmt.Sprintf("Selected: %s [%s] total(%s)=%d depth=%d col=%d width=%d share=%.2f%% %s",
+ selected.Name, compactFramePath(selected.Path), metricLabel, selected.Total, selected.Depth, selected.Col, selected.Width, selectedSystemShare, metricLabel)
return renderViewRows(toolbar, status, rowsForRender(frames, width, rowOffset, maxRow, barHeight, availableRows, selected.Path, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterActive), width)
}
}
diff --git a/internal/tui/flamegraph/renderer_test.go b/internal/tui/flamegraph/renderer_test.go
index 091aeec..c546200 100644
--- a/internal/tui/flamegraph/renderer_test.go
+++ b/internal/tui/flamegraph/renderer_test.go
@@ -152,7 +152,7 @@ func TestTerminalFrameColorSemanticPalette(t *testing.T) {
}
func TestRenderTerminalViewShowsNarrowMessage(t *testing.T) {
- out := RenderTerminalView(nil, 50, 10, 0, nil, nil, nil, 0, true, false, "")
+ out := RenderTerminalView(nil, 50, 10, 0, nil, nil, nil, 0, "events", true, false, "")
if !strings.Contains(out, "terminal too narrow") {
t.Fatalf("expected narrow terminal warning, got %q", out)
}
@@ -177,7 +177,7 @@ func TestRenderTerminalViewIncludesToolbarAndStatus(t *testing.T) {
}
frames := BuildTerminalLayout(snapshot, 80, 6)
- out := RenderTerminalView(frames, 80, 6, 1, nil, nil, nil, 0, true, false, "")
+ out := RenderTerminalView(frames, 80, 6, 1, nil, nil, nil, 0, "events", true, false, "")
if !strings.Contains(out, "Flame | view:root | frames:2") {
t.Fatalf("expected toolbar to include frame count, got %q", out)
}
@@ -196,7 +196,7 @@ func TestRenderTerminalViewFillsAvailableHeightForShallowTree(t *testing.T) {
}
frames := BuildTerminalLayout(snapshot, 100, 20)
- out := RenderTerminalView(frames, 100, 20, 1, nil, nil, nil, 0, true, false, "")
+ out := RenderTerminalView(frames, 100, 20, 1, nil, nil, nil, 0, "events", true, false, "")
lines := strings.Split(out, "\n")
if got, want := len(lines), 20; got != want {
t.Fatalf("expected render to fill viewport height (%d lines), got %d", want, got)
@@ -223,7 +223,7 @@ func TestRenderTerminalViewShowsPersistentFilterContext(t *testing.T) {
frames := BuildTerminalLayout(snapshot, 80, 6)
matchSet := map[int]bool{1: true}
- out := RenderTerminalView(frames, 140, 6, 1, nil, matchSet, nil, 0, true, false, "child")
+ out := RenderTerminalView(frames, 140, 6, 1, nil, matchSet, nil, 0, "events", true, false, "child")
if !strings.Contains(out, `Filter "child"`) {
t.Fatalf("expected filter context in status line, got %q", out)
}
@@ -257,9 +257,9 @@ func TestRenderTerminalViewFilterKeepsNonMatchingBranchesVisible(t *testing.T) {
}
matchSet := map[int]bool{needleIdx: true}
- out := RenderTerminalView(frames, 180, 8, needleIdx, nil, matchSet, nil, 100, true, false, "needle")
- if !strings.Contains(out, `Filter "needle": 60.0% system`) {
- t.Fatalf("expected filter status to report 60.0%% system share, got %q", out)
+ out := RenderTerminalView(frames, 180, 8, needleIdx, nil, matchSet, nil, 100, "bytes", true, false, "needle")
+ if !strings.Contains(out, `Filter "needle": 60.0% bytes`) {
+ t.Fatalf("expected filter status to report 60.0%% bytes share, got %q", out)
}
if !strings.Contains(out, "keep") || !strings.Contains(out, "needle") {
t.Fatalf("expected matching branch to remain visible, got %q", out)
@@ -267,7 +267,7 @@ func TestRenderTerminalViewFilterKeepsNonMatchingBranchesVisible(t *testing.T) {
if !strings.Contains(out, "drop") || !strings.Contains(out, "noise") {
t.Fatalf("expected non-matching branch to remain visible (greyed), got %q", out)
}
- if !strings.Contains(out, "100.00% filter") {
+ if !strings.Contains(out, "100.00% filtered bytes") {
t.Fatalf("expected selected match share to be computed against filtered total, got %q", out)
}
}
@@ -315,7 +315,7 @@ func TestRenderTerminalViewShowsDeepLevelTruncationHint(t *testing.T) {
},
}
frames := BuildTerminalLayout(snapshot, 80, 10)
- out := RenderTerminalView(frames, 80, 4, 0, nil, nil, nil, 0, true, false, "")
+ out := RenderTerminalView(frames, 80, 4, 0, nil, nil, nil, 0, "events", true, false, "")
if !strings.Contains(out, "showing deepest levels") {
t.Fatalf("expected truncation hint in toolbar, got %q", out)
}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 4006d84..12c904d 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -890,7 +890,7 @@ func (m Model) helpSections() []helpSection {
"arrows/hjkl navigate pgup top pgdn root",
"enter zoom u/backspace/esc undo",
"/ filter n/N match next/prev",
- "space/p pause o order r reset baseline",
+ "space/p pause o order b metric r reset baseline",
},
},
{
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index 87ad340..cd9e6cd 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -505,6 +505,20 @@ func TestFlameOrderKeyDoesNotOpenProbeModal(t *testing.T) {
}
}
+func TestFlameMetricKeyDoesNotOpenProbeModal(t *testing.T) {
+ m := NewModel(-1, func(context.Context) error { return nil })
+ m.screen = ScreenDashboard
+ m.attaching = false
+ m.width = 120
+ m.height = 30
+
+ next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'b'}[0], Text: string([]rune{'b'})})
+ updated := next.(Model)
+ if updated.probeModal.Visible() {
+ t.Fatalf("expected flame metric key to stay in flame tab, not open probes modal")
+ }
+}
+
func TestSelectPIDKeyReturnsToFreshPickerAndStopsTrace(t *testing.T) {
m := NewModel(-1, func(context.Context) error { return nil })
m.screen = ScreenDashboard