summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/flags/flags.go4
-rw-r--r--internal/flags/flags_test.go2
-rw-r--r--internal/flamegraph/livehtml.go7
-rw-r--r--internal/flamegraph/livehtml_browser_test.go2
-rw-r--r--internal/flamegraph/livehtml_interaction_test.go2
-rw-r--r--internal/flamegraph/livetrie_test.go33
-rw-r--r--internal/tui/flamegraph/controls.go4
-rw-r--r--internal/tui/flamegraph/model.go40
-rw-r--r--internal/tui/flamegraph/model_test.go42
9 files changed, 112 insertions, 24 deletions
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index 0df1d2d..503aefb 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -83,7 +83,7 @@ func NewFlags() Flags {
LiveInterval: 200 * time.Millisecond,
FlamegraphName: "default",
TUIExportEnable: true,
- CollapsedFields: []string{"comm", "path", "tracepoint"},
+ CollapsedFields: []string{"comm", "tracepoint", "path"},
CountField: "count",
}
}
@@ -221,7 +221,7 @@ func parse() error {
// If future kernels regress, add targeted exclusions here.
if *fields == "" {
- cfg.CollapsedFields = []string{"comm", "path", "tracepoint"}
+ cfg.CollapsedFields = []string{"comm", "tracepoint", "path"}
} else {
cfg.CollapsedFields = strings.Split(*fields, ",")
}
diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go
index 63b668c..7323438 100644
--- a/internal/flags/flags_test.go
+++ b/internal/flags/flags_test.go
@@ -153,7 +153,7 @@ func TestParseDefaultCollapsedFieldsOrder(t *testing.T) {
t.Fatalf("parse returned error: %v", err)
}
- want := []string{"comm", "path", "tracepoint"}
+ want := []string{"comm", "tracepoint", "path"}
if len(cfg.CollapsedFields) != len(want) {
t.Fatalf("default collapsed fields len = %d, want %d", len(cfg.CollapsedFields), len(want))
}
diff --git a/internal/flamegraph/livehtml.go b/internal/flamegraph/livehtml.go
index 90a6d3d..71b955e 100644
--- a/internal/flamegraph/livehtml.go
+++ b/internal/flamegraph/livehtml.go
@@ -116,7 +116,7 @@ const liveHTML = `<!doctype html>
<button id="btn-undo-zoom" type="button">Undo Zoom</button>
<button id="btn-reset-zoom" type="button">Reset Zoom</button>
<button id="btn-reset-baseline" type="button">Reset Baseline</button>
- <button id="btn-toggle-order" class="order-toggle" type="button">Order: comm > path > tracepoint</button>
+ <button id="btn-toggle-order" class="order-toggle" type="button">Order: comm > tracepoint > path</button>
<span id="status">LIVE</span>
</div>
@@ -146,10 +146,11 @@ const liveHTML = `<!doctype html>
resetBaselineBtn: document.getElementById('btn-reset-baseline'),
toggleOrderBtn: document.getElementById('btn-toggle-order'),
orderPresets: [
- 'comm,path,tracepoint',
+ 'comm,tracepoint,path',
'path,tracepoint,comm',
'tracepoint,comm,path',
- 'pid,path,tracepoint'
+ 'pid,tracepoint,path',
+ 'comm,path,tracepoint'
],
orderIndex: 0,
cfg: {
diff --git a/internal/flamegraph/livehtml_browser_test.go b/internal/flamegraph/livehtml_browser_test.go
index c7a16c7..10252a9 100644
--- a/internal/flamegraph/livehtml_browser_test.go
+++ b/internal/flamegraph/livehtml_browser_test.go
@@ -166,7 +166,7 @@ global.prompt = function(){ return ""; };
global.fetch = function() {
return Promise.resolve({
ok: true,
- json: function() { return Promise.resolve({ fields: ["comm", "path", "tracepoint"], snapshot: { n: "", v: 0, t: 0 } }); },
+ json: function() { return Promise.resolve({ fields: ["comm", "tracepoint", "path"], snapshot: { n: "", v: 0, t: 0 } }); },
text: function() { return Promise.resolve("{\"n\":\"\",\"v\":0,\"t\":0}"); }
});
};
diff --git a/internal/flamegraph/livehtml_interaction_test.go b/internal/flamegraph/livehtml_interaction_test.go
index 0de1466..4c947f5 100644
--- a/internal/flamegraph/livehtml_interaction_test.go
+++ b/internal/flamegraph/livehtml_interaction_test.go
@@ -574,7 +574,7 @@ global.prompt = function(){ return ""; };
global.fetch = function() {
return Promise.resolve({
ok: true,
- json: function() { return Promise.resolve({ fields: ["comm", "path", "tracepoint"], snapshot: { n: "", v: 0, t: 0 } }); },
+ json: function() { return Promise.resolve({ fields: ["comm", "tracepoint", "path"], snapshot: { n: "", v: 0, t: 0 } }); },
text: function() { return Promise.resolve("{\"n\":\"\",\"v\":0,\"t\":0}"); }
});
};
diff --git a/internal/flamegraph/livetrie_test.go b/internal/flamegraph/livetrie_test.go
index c5ed32c..71f645c 100644
--- a/internal/flamegraph/livetrie_test.go
+++ b/internal/flamegraph/livetrie_test.go
@@ -48,6 +48,39 @@ func TestLiveTrieIngestIsAdditive(t *testing.T) {
}
}
+func TestLiveTrieCommTracepointPathAggregatesSameSyscallAcrossPaths(t *testing.T) {
+ lt := NewLiveTrie([]string{"comm", "tracepoint", "path"}, "count")
+ lt.AddRecord(IterRecord{
+ Path: "/srv/a",
+ TraceID: types.SYS_ENTER_READ,
+ Comm: "svc",
+ Pid: 1001,
+ Tid: 1001,
+ Cnt: Counter{Count: 1},
+ })
+ lt.AddRecord(IterRecord{
+ Path: "/srv/b",
+ TraceID: types.SYS_ENTER_READ,
+ Comm: "svc",
+ Pid: 1002,
+ Tid: 1002,
+ Cnt: Counter{Count: 1},
+ })
+
+ snap := decodeLiveSnapshot(t, lt)
+ commNode := findSnapshotPath(t, &snap, "svc")
+ if len(commNode.Children) != 1 {
+ t.Fatalf("expected one syscall child under comm node, got %d", len(commNode.Children))
+ }
+ syscallNode := commNode.Children[0]
+ if got, want := syscallNode.Name, "enter_read"; got != want {
+ t.Fatalf("syscall child name = %q, want %q", got, want)
+ }
+ if got, want := syscallNode.Total, uint64(2); got != want {
+ t.Fatalf("syscall aggregate total = %d, want %d", got, want)
+ }
+}
+
func TestLiveTrieVersionIncrementsPerIngest(t *testing.T) {
lt := NewLiveTrie([]string{"comm"}, "count")
if got := lt.Version(); got != 0 {
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: "?"})