summaryrefslogtreecommitdiff
path: root/internal/tui/flamegraph/model.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/flamegraph/model.go')
-rw-r--r--internal/tui/flamegraph/model.go66
1 files changed, 53 insertions, 13 deletions
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go
index 11475b8..c1693b1 100644
--- a/internal/tui/flamegraph/model.go
+++ b/internal/tui/flamegraph/model.go
@@ -7,6 +7,7 @@ import (
coreflamegraph "ior/internal/flamegraph"
common "ior/internal/tui/common"
"sort"
+ "time"
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/textinput"
@@ -20,6 +21,10 @@ type snapshotNode struct {
Children []*snapshotNode `json:"c,omitempty"`
}
+type animTickMsg struct{}
+
+const animFrameDuration = 33 * time.Millisecond
+
type zoomState struct {
path string
previousSelectedIdx int
@@ -74,10 +79,11 @@ type Model struct {
fieldPresets [][]string
fieldIndex int
- springs []frameSpring
- paused bool
- isDark bool
- keys flameKeyMap
+ animation AnimationState
+ animating bool
+ paused bool
+ isDark bool
+ keys flameKeyMap
}
// tuiFrame stores one terminal flamegraph frame cell.
@@ -112,8 +118,9 @@ func NewModel(liveTrie *coreflamegraph.LiveTrie) Model {
{"tracepoint", "comm", "path"},
{"pid", "path", "tracepoint"},
},
- isDark: true,
- keys: defaultFlameKeyMap(),
+ isDark: true,
+ keys: defaultFlameKeyMap(),
+ animation: NewAnimationState(30, 6.0, 1.0),
}
}
@@ -125,6 +132,18 @@ func (m Model) Init() tea.Cmd {
// Update handles incoming messages.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
+ case animTickMsg:
+ if !m.animating {
+ return m, nil
+ }
+ m.animating = m.animation.Tick(0)
+ m.frames = m.animation.CurrentFrames()
+ m.clampSelection()
+ m.subtreeSet = computeSubtreeSet(m.frames, m.selectedIdx)
+ if m.animating {
+ return m, animTickCmd()
+ }
+ return m, nil
case tea.KeyPressMsg:
if m.searchActive {
switch msg.String() {
@@ -209,6 +228,8 @@ func (m *Model) SetLiveTrie(liveTrie *coreflamegraph.LiveTrie) {
m.zoomRoot = nil
m.zoomPath = ""
m.subtreeSet = make(map[int]bool)
+ m.animation = NewAnimationState(30, 6.0, 1.0)
+ m.animating = false
}
// RefreshFromLiveTrie loads a new snapshot when the source version changes.
@@ -235,7 +256,7 @@ func (m *Model) RefreshFromLiveTrie() bool {
} else {
m.zoomRoot = nil
}
- m.rebuildFrames()
+ m.rebuildFrames(true)
m.lastVersion = version
return true
}
@@ -245,6 +266,14 @@ func (m Model) LastVersion() uint64 {
return m.lastVersion
}
+// AnimationCmd returns a frame animation tick command when animation is active.
+func (m Model) AnimationCmd() tea.Cmd {
+ if !m.animating {
+ return nil
+ }
+ return animTickCmd()
+}
+
// Paused reports whether live refresh is paused.
func (m Model) Paused() bool {
return m.paused
@@ -254,7 +283,7 @@ func (m Model) Paused() bool {
func (m *Model) SetViewport(width, height int) {
m.width = width
m.height = height
- m.rebuildFrames()
+ m.rebuildFrames(false)
}
// SetDarkMode sets the active color theme mode.
@@ -263,7 +292,7 @@ func (m *Model) SetDarkMode(isDark bool) {
m.searchInput.SetStyles(textinput.DefaultStyles(isDark))
}
-func (m *Model) rebuildFrames() {
+func (m *Model) rebuildFrames(animate bool) {
var root *snapshotNode
rootPath := ""
if m.zoomRoot != nil {
@@ -273,7 +302,14 @@ func (m *Model) rebuildFrames() {
root = m.snapshot
}
m.targetFrames = buildTerminalLayoutWithPath(root, m.width, m.height, rootPath)
- m.frames = append(m.frames[:0], m.targetFrames...)
+ m.animation.SetTargets(m.targetFrames)
+ if animate && len(m.frames) > 0 && !m.animation.Settled() {
+ m.animating = true
+ m.frames = m.animation.CurrentFrames()
+ } else {
+ m.animating = false
+ m.frames = append(m.frames[:0], m.targetFrames...)
+ }
m.clampSelection()
m.subtreeSet = computeSubtreeSet(m.frames, m.selectedIdx)
}
@@ -295,7 +331,7 @@ func (m *Model) zoomIn() {
m.zoomRoot = target
m.zoomPath = selectedPath
m.selectedIdx = 0
- m.rebuildFrames()
+ m.rebuildFrames(false)
}
func (m *Model) zoomUndo() {
@@ -311,7 +347,7 @@ func (m *Model) zoomUndo() {
m.zoomRoot = findNodeByPath(m.snapshot, m.zoomPath)
}
m.selectedIdx = last.previousSelectedIdx
- m.rebuildFrames()
+ m.rebuildFrames(false)
}
func (m *Model) zoomReset() {
@@ -321,7 +357,7 @@ func (m *Model) zoomReset() {
m.zoomRoot = nil
m.zoomPath = ""
m.zoomStack = nil
- m.rebuildFrames()
+ m.rebuildFrames(false)
}
func (m *Model) moveVertical(delta int) {
@@ -415,3 +451,7 @@ func abs(v int) int {
}
return v
}
+
+func animTickCmd() tea.Cmd {
+ return tea.Tick(animFrameDuration, func(time.Time) tea.Msg { return animTickMsg{} })
+}