summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-14 08:23:35 +0300
committerPaul Buetow <paul@buetow.org>2026-05-14 08:23:35 +0300
commit2eb861796de41e87de071987ed864b79a0007ab2 (patch)
tree65b3a612c9a113d094b97407c04748c2713ddd4b /internal
parent34d88ba4c82dac2646db27c74e1cafb0c97dbcf2 (diff)
wire TUIFastRefreshInterval into dashboard model and update tests
Add fastRefreshMs parameter to NewModelWithConfig so callers can supply the high-frequency tick cadence for stream and flame tabs. Convert the streamTickCmd/flameTickCmd package-level functions to model methods that honour fastRefreshEvery (falling back to the 200 ms constants when zero for backward-compatibility). Add SetFastRefreshInterval setter so RunWithTraceStarterConfig can apply cfg.TUIFastRefreshInterval after construction. Update all 68 test call sites to pass fastRefreshMs=200 and add three new tests covering zero-fallback, stored value, and setter behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/tui/dashboard/model.go76
-rw-r--r--internal/tui/dashboard/model_test.go188
-rw-r--r--internal/tui/dashboard/tabregistry.go4
-rw-r--r--internal/tui/tui.go18
-rw-r--r--internal/tui/tui_test.go2
5 files changed, 204 insertions, 84 deletions
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index 004350a..2104cb6 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -77,6 +77,11 @@ type Model struct {
height int
refreshEvery time.Duration
+ // fastRefreshEvery is the high-frequency tick cadence for the stream and
+ // flame tabs. When zero it falls back to the streamRefreshMs / flameRefreshMs
+ // package-level constants so the model is backwards-compatible with callers
+ // that do not supply a fast-refresh interval.
+ fastRefreshEvery time.Duration
// autoResetEvery is the cadence for the periodic auto-reset of
// aggregate state (live trie + stats engine). Zero disables it.
autoResetEvery time.Duration
@@ -123,11 +128,15 @@ type Model struct {
// NewModel creates a dashboard model with default refresh cadence.
func NewModel(engine SnapshotSource, streamSource eventstream.Source) Model {
- return NewModelWithConfig(engine, streamSource, defaultRefreshMs, common.Keys)
+ return NewModelWithConfig(engine, streamSource, defaultRefreshMs, 0, common.Keys)
}
// NewModelWithConfig creates a dashboard model with explicit refresh and keys.
-func NewModelWithConfig(engine SnapshotSource, streamSource eventstream.Source, refreshMs int, keys common.KeyMap) Model {
+// fastRefreshMs controls the high-frequency tick cadence for the stream and
+// flame tabs (e.g. 200 ms). A value of 0 uses the package-level constants
+// streamRefreshMs / flameRefreshMs (200 ms) so existing call sites are
+// backwards-compatible.
+func NewModelWithConfig(engine SnapshotSource, streamSource eventstream.Source, refreshMs int, fastRefreshMs int, keys common.KeyMap) Model {
if refreshMs <= 0 {
refreshMs = defaultRefreshMs
}
@@ -135,6 +144,7 @@ func NewModelWithConfig(engine SnapshotSource, streamSource eventstream.Source,
activeTab: TabFlame,
engine: engine,
refreshEvery: time.Duration(refreshMs) * time.Millisecond,
+ fastRefreshEvery: time.Duration(fastRefreshMs) * time.Millisecond,
keys: keys,
pidFilter: -1,
syscallsVizMode: tabVizModeTable,
@@ -157,7 +167,8 @@ func NewModelWithConfig(engine SnapshotSource, streamSource eventstream.Source,
// Init starts periodic refresh ticks. The tab registry's InitCmd field is
// consulted to start any additional high-frequency tick the active tab needs
-// (e.g. stream and flame use 200 ms cadence ticks beyond the base 1 s tick).
+// (e.g. stream and flame use a fast cadence controlled by fastRefreshEvery,
+// defaulting to streamRefreshMs / flameRefreshMs when not explicitly set).
func (m Model) Init() tea.Cmd {
cmds := []tea.Cmd{tickCmd(m.refreshEvery)}
d := lookupTab(m.activeTab)
@@ -234,18 +245,19 @@ func (m Model) handleStreamTick() (tea.Model, tea.Cmd) {
return m, nil
}
m.streamModel.Refresh()
- return m, streamTickCmd()
+ // Re-arm with the configurable fast-refresh cadence (fastRefreshEvery).
+ return m, m.streamTickCmd()
}
func (m Model) handleFlameTick() (tea.Model, tea.Cmd) {
if !m.focused || m.activeTab != TabFlame {
return m, nil
}
- // Always re-arm the 200 ms tick. The snapshot refresh itself runs on a
+ // Always re-arm the fast tick. The snapshot refresh itself runs on a
// background goroutine via RefreshFromLiveTrieCmd, so even when a previous
// refresh is still in flight (the cmd returns nil and skips), the tick
- // channel stays alive.
- cmds := []tea.Cmd{flameTickCmd()}
+ // channel stays alive. The cadence is controlled by fastRefreshEvery.
+ cmds := []tea.Cmd{m.flameTickCmd()}
if m.liveTrie != nil {
if refreshCmd := m.flamegraphModel.RefreshFromLiveTrieCmd(); refreshCmd != nil {
cmds = append(cmds, refreshCmd)
@@ -325,7 +337,9 @@ func (m Model) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
prevActiveTab := m.activeTab
handled, cmd := m.handleScrollKey(msg)
if handled && isStreamResumeKey(msg) && m.activeTab == TabStream && !m.streamModel.Paused() {
- cmd = streamTickCmd()
+ // Re-arm the stream tick with the configurable fast-refresh cadence after
+ // the user unpauses the stream with a scroll/space key.
+ cmd = m.streamTickCmd()
}
if !handled {
handled, cmd = m.handleEnterKey(msg)
@@ -983,6 +997,18 @@ func (m Model) AutoResetInterval() time.Duration {
return m.autoResetEvery
}
+// SetFastRefreshInterval overrides the high-frequency tick cadence used by the
+// stream and flame tabs. A zero or negative value resets the behaviour to the
+// package-level constants (streamRefreshMs / flameRefreshMs). Callers such as
+// RunWithTraceStarterConfig use this to wire in cfg.TUIFastRefreshInterval
+// after construction without changing the NewModelWithConfig call chain.
+func (m *Model) SetFastRefreshInterval(d time.Duration) {
+ if d < 0 {
+ d = 0
+ }
+ m.fastRefreshEvery = d
+}
+
// LatestSnapshot returns the most recently received snapshot.
func (m Model) LatestSnapshot() *statsengine.Snapshot {
return m.latest
@@ -1480,11 +1506,41 @@ func renderActiveTabContent(m *Model, tab Tab, snap *statsengine.Snapshot, strea
return d.Render(m, snap, streamModel, flameModel, width, height)
}
-func streamTickCmd() tea.Cmd {
+// streamTickCmd schedules the next high-frequency stream tab refresh tick.
+// It uses m.fastRefreshEvery when set; otherwise it falls back to the
+// streamRefreshMs constant so the behaviour is unchanged for callers that
+// did not supply a fast-refresh interval.
+func (m Model) streamTickCmd() tea.Cmd {
+ d := m.fastRefreshEvery
+ if d <= 0 {
+ d = streamRefreshMs * time.Millisecond
+ }
+ return tea.Tick(d, func(time.Time) tea.Msg { return streamTickMsg{} })
+}
+
+// flameTickCmd schedules the next high-frequency flame tab refresh tick.
+// It uses m.fastRefreshEvery when set; otherwise it falls back to the
+// flameRefreshMs constant so the behaviour is unchanged for callers that
+// did not supply a fast-refresh interval.
+func (m Model) flameTickCmd() tea.Cmd {
+ d := m.fastRefreshEvery
+ if d <= 0 {
+ d = flameRefreshMs * time.Millisecond
+ }
+ return tea.Tick(d, func(time.Time) tea.Msg { return flameTickMsg{} })
+}
+
+// streamTickCmdFn is a package-level adapter used by the tab registry's InitCmd
+// field. It uses the constant cadence and is replaced on subsequent ticks by
+// the model-method version that respects fastRefreshEvery.
+func streamTickCmdFn() tea.Cmd {
return tea.Tick(streamRefreshMs*time.Millisecond, func(time.Time) tea.Msg { return streamTickMsg{} })
}
-func flameTickCmd() tea.Cmd {
+// flameTickCmdFn is a package-level adapter used by the tab registry's InitCmd
+// field. It uses the constant cadence and is replaced on subsequent ticks by
+// the model-method version that respects fastRefreshEvery.
+func flameTickCmdFn() tea.Cmd {
return tea.Tick(flameRefreshMs*time.Millisecond, func(time.Time) tea.Msg { return flameTickMsg{} })
}
diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go
index 87eee52..f758597 100644
--- a/internal/tui/dashboard/model_test.go
+++ b/internal/tui/dashboard/model_test.go
@@ -77,7 +77,7 @@ func TestFlameViewportClampsHeightWithExpandedHelp(t *testing.T) {
}
func TestSnapshotOrZeroReturnsZeroSnapshotWhenLatestMissing(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
snap := m.snapshotOrZero()
if snap.SyscallsCount() != 0 || snap.FilesCount() != 0 || snap.ProcessesCount() != 0 {
@@ -86,7 +86,7 @@ func TestSnapshotOrZeroReturnsZeroSnapshotWhenLatestMissing(t *testing.T) {
}
func TestKeySwitchingChangesActiveTab(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'2'}[0], Text: string([]rune{'2'})})
model := next.(Model)
@@ -120,7 +120,7 @@ func TestKeySwitchingChangesActiveTab(t *testing.T) {
}
func TestArrowAndViKeysDoNotCycleTabs(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabOverview
next, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyRight})
@@ -149,7 +149,7 @@ func TestArrowAndViKeysDoNotCycleTabs(t *testing.T) {
}
func TestSyscallsTabScrollsWithJK(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{{Name: "read", Count: 1}, {Name: "write", Count: 1}}, nil, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
m.latest = &snap
@@ -168,7 +168,7 @@ func TestSyscallsTabScrollsWithJK(t *testing.T) {
}
func TestProcessesTabScrollsWithJK(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{{PID: 1}, {PID: 2}}, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
m.latest = &snap
@@ -187,7 +187,7 @@ func TestProcessesTabScrollsWithJK(t *testing.T) {
}
func TestSyscallsTabSupportsHorizontalColumnNavigation(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{{Name: "read", Count: 1}}, nil, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
m.latest = &snap
@@ -206,7 +206,7 @@ func TestSyscallsTabSupportsHorizontalColumnNavigation(t *testing.T) {
}
func TestFilesTabSupportsHorizontalColumnNavigation(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{{Path: "/a"}}, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
m.latest = &snap
@@ -225,7 +225,7 @@ func TestFilesTabSupportsHorizontalColumnNavigation(t *testing.T) {
}
func TestProcessesTabSupportsHorizontalColumnNavigation(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{{PID: 1, Comm: "alpha"}}, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
m.latest = &snap
@@ -244,7 +244,7 @@ func TestProcessesTabSupportsHorizontalColumnNavigation(t *testing.T) {
}
func TestProcessesTabEnterEmitsGlobalFilterRequest(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{
{PID: 111, Comm: "alpha", Syscalls: 9},
@@ -272,7 +272,7 @@ func TestProcessesTabEnterEmitsGlobalFilterRequest(t *testing.T) {
}
func TestProcessesTabEnterCommColumnEmitsCommFilterRequest(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{
{PID: 111, Comm: "alpha", Syscalls: 9},
@@ -301,7 +301,7 @@ func TestProcessesTabEnterCommColumnEmitsCommFilterRequest(t *testing.T) {
}
func TestProcessesSortKeyTogglesOnSelectedColumn(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{
{PID: 200, Comm: "worker", Syscalls: 9},
@@ -324,7 +324,7 @@ func TestProcessesSortKeyTogglesOnSelectedColumn(t *testing.T) {
}
func TestProcessesReverseSortKeyTogglesOnSelectedColumn(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{
{PID: 200, Comm: "worker", Syscalls: 9},
@@ -347,7 +347,7 @@ func TestProcessesReverseSortKeyTogglesOnSelectedColumn(t *testing.T) {
}
func TestProcessesSortEnterUsesSortedVisibleRow(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{
{PID: 200, Comm: "worker", Syscalls: 9},
@@ -375,7 +375,7 @@ func TestProcessesSortEnterUsesSortedVisibleRow(t *testing.T) {
}
func TestProcessesSortIgnoredOutsideTableMode(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
m.processesVizMode = tabVizModeTreemap
snap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{
@@ -391,7 +391,7 @@ func TestProcessesSortIgnoredOutsideTableMode(t *testing.T) {
}
func TestStatsTickReanchorsSortedProcessSelectionByPID(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
m.processesSort = tableSortState[processSortKey]{active: true, key: processSortKeyComm}
oldSnap := statsengine.NewSnapshot(nil, nil, nil, nil, nil, []statsengine.ProcessSnapshot{
@@ -419,7 +419,7 @@ func TestStatsTickReanchorsSortedProcessSelectionByPID(t *testing.T) {
}
func TestFilesTabScrollsWithJK(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{{Path: "/a"}, {Path: "/b"}}, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
m.latest = &snap
@@ -438,7 +438,7 @@ func TestFilesTabScrollsWithJK(t *testing.T) {
}
func TestSyscallsTabEnterEmitsGlobalFilterRequest(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
{Name: "read", Count: 9},
@@ -466,7 +466,7 @@ func TestSyscallsTabEnterEmitsGlobalFilterRequest(t *testing.T) {
}
func TestSyscallsSortKeyTogglesOnSelectedColumn(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
{Name: "write", Count: 9},
@@ -488,7 +488,7 @@ func TestSyscallsSortKeyTogglesOnSelectedColumn(t *testing.T) {
}
func TestSyscallsReverseSortKeyTogglesOnSelectedColumn(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
{Name: "write", Count: 9},
@@ -510,7 +510,7 @@ func TestSyscallsReverseSortKeyTogglesOnSelectedColumn(t *testing.T) {
}
func TestSyscallsSortReanchorsSelectedSyscall(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
{Name: "write", Count: 9},
@@ -530,7 +530,7 @@ func TestSyscallsSortReanchorsSelectedSyscall(t *testing.T) {
}
func TestSyscallsSortEnterUsesSortedVisibleRow(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
{Name: "write", Count: 9},
@@ -557,7 +557,7 @@ func TestSyscallsSortEnterUsesSortedVisibleRow(t *testing.T) {
}
func TestSyscallsSortIgnoredOutsideTableMode(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
m.syscallsVizMode = tabVizModeTreemap
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
@@ -573,7 +573,7 @@ func TestSyscallsSortIgnoredOutsideTableMode(t *testing.T) {
}
func TestSyscallsP95SortSurvivesWidthExpansion(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
m.width = 120
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
@@ -597,7 +597,7 @@ func TestSyscallsP95SortSurvivesWidthExpansion(t *testing.T) {
}
func TestStatsTickReanchorsSortedSyscallSelectionByName(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
m.syscallsSort = tableSortState[syscallSortKey]{active: true, key: syscallSortKeyName}
oldSnap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
@@ -624,7 +624,7 @@ func TestStatsTickReanchorsSortedSyscallSelectionByName(t *testing.T) {
}
func TestFilesTabGroupedScrollUsesDirectoryOffset(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
m.filesDirGrouped = true
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
@@ -645,7 +645,7 @@ func TestFilesTabGroupedScrollUsesDirectoryOffset(t *testing.T) {
}
func TestFilesTabEnterEmitsGlobalFilterRequest(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
{Path: "/tmp/a"},
@@ -673,7 +673,7 @@ func TestFilesTabEnterEmitsGlobalFilterRequest(t *testing.T) {
}
func TestFilesSortKeyTogglesFlatMode(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
{Path: "/tmp/z.log", Accesses: 9},
@@ -696,7 +696,7 @@ func TestFilesSortKeyTogglesFlatMode(t *testing.T) {
}
func TestFilesReverseSortKeyTogglesFlatMode(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
{Path: "/tmp/z.log", Accesses: 9},
@@ -719,7 +719,7 @@ func TestFilesReverseSortKeyTogglesFlatMode(t *testing.T) {
}
func TestFilesDirReverseSortKeyTogglesGroupedMode(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
m.filesDirGrouped = true
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
@@ -743,7 +743,7 @@ func TestFilesDirReverseSortKeyTogglesGroupedMode(t *testing.T) {
}
func TestFilesSortEnterUsesSortedVisibleRow(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
{Path: "/tmp/z.log", Accesses: 9},
@@ -771,7 +771,7 @@ func TestFilesSortEnterUsesSortedVisibleRow(t *testing.T) {
}
func TestFilesDirSortEnterUsesSortedVisibleRow(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
m.filesDirGrouped = true
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
@@ -800,7 +800,7 @@ func TestFilesDirSortEnterUsesSortedVisibleRow(t *testing.T) {
}
func TestFilesSortStatesPersistAcrossDirToggle(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
{Path: "/var/log/z.log", Accesses: 9},
@@ -833,7 +833,7 @@ func TestFilesSortStatesPersistAcrossDirToggle(t *testing.T) {
func TestStreamSpaceUnpauseSchedulesStreamTick(t *testing.T) {
rb := eventstream.NewRingBuffer()
- m := NewModelWithConfig(nil, rb, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, rb, 250, 200, common.DefaultKeyMap())
m.activeTab = TabStream
m.streamModel.HandleKey("space") // pause
@@ -848,7 +848,7 @@ func TestFlameTickRefreshesFlamegraphModel(t *testing.T) {
liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path"}, "count")
liveTrie.Reset()
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.SetLiveTrie(liveTrie)
m.activeTab = TabFlame
@@ -865,7 +865,7 @@ func TestFlameTickRefreshesFlamegraphModel(t *testing.T) {
func TestSetLiveTriePreloadsInitialSnapshotWithoutVersionChange(t *testing.T) {
liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path"}, "count")
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.SetLiveTrie(liveTrie)
m.activeTab = TabFlame
if !m.flamegraphModel.HasSnapshot() {
@@ -881,7 +881,7 @@ func TestSetLiveTriePreloadsInitialSnapshotWithoutVersionChange(t *testing.T) {
func TestFlameTickPausedFreezesAfterInitialSnapshot(t *testing.T) {
liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path"}, "count")
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.SetLiveTrie(liveTrie)
m.activeTab = TabFlame
@@ -908,7 +908,7 @@ func TestPausedFlameDashboardViewPreservesZoomedSelectedLine(t *testing.T) {
liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count")
coreflamegraph.SeedTestFlameData(liveTrie)
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFlame
next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
@@ -961,7 +961,7 @@ func newPausedStreamModel(t *testing.T) Model {
FileName: fmt.Sprintf("/tmp/file-%03d", i),
})
}
- m := NewModelWithConfig(nil, rb, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, rb, 250, 200, common.DefaultKeyMap())
m.activeTab = TabStream
m.showHelp = true
next, _ := m.Update(tea.WindowSizeMsg{Width: 120, Height: 30})
@@ -1020,7 +1020,7 @@ func rowFromStreamView(t *testing.T, view string) int {
}
func TestDirGroupKeyTogglesOnlyOnFilesTab(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'d'}[0], Text: string([]rune{'d'})})
@@ -1042,7 +1042,7 @@ func TestVisualizationCycleForSyscallsTab(t *testing.T) {
{Name: "read", Count: 9, Bytes: 512},
{Name: "write", Count: 3, Bytes: 1024},
}, nil, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
m.latest = &snap
@@ -1069,7 +1069,7 @@ func TestBubbleMetricToggleForSyscallsTab(t *testing.T) {
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{
{Name: "read", Count: 9, Bytes: 512},
}, nil, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
m.latest = &snap
@@ -1084,7 +1084,7 @@ func TestMetricToggleAppliesInFilesTreemapMode(t *testing.T) {
snap := statsengine.NewSnapshot(nil, nil, nil, nil, []statsengine.FileSnapshot{
{Path: "/var/log/a", Accesses: 5, BytesRead: 120, BytesWritten: 40},
}, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
m.latest = &snap
m.filesDirGrouped = true
@@ -1108,7 +1108,7 @@ func TestFilesVisualizationRequiresDirectoryMode(t *testing.T) {
{Path: "/tmp/a", Accesses: 3},
{Path: "/tmp/b", Accesses: 1},
}, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
m.latest = &snap
@@ -1158,7 +1158,7 @@ func TestBubbleModeUsesJKForSelection(t *testing.T) {
{Name: "read", Count: 9, Bytes: 512},
{Name: "write", Count: 3, Bytes: 1024},
}, nil, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
m.latest = &snap
m.syscallsVizMode = tabVizModeBubbles
@@ -1179,7 +1179,7 @@ func TestTreemapModeUsesJKForSelection(t *testing.T) {
{Name: "read", Count: 9, Bytes: 512},
{Name: "write", Count: 3, Bytes: 1024},
}, nil, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
m.latest = &snap
m.syscallsVizMode = tabVizModeTreemap
@@ -1196,7 +1196,7 @@ func TestFilesIcicleModeSelectionUsesIcicleTileCount(t *testing.T) {
{Path: "/a/b/c/file1", Accesses: 9},
{Path: "/a/d/e/file2", Accesses: 7},
}, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
m.latest = &snap
m.filesDirGrouped = true
@@ -1223,7 +1223,7 @@ func TestTreemapModeRendersTreemapHeader(t *testing.T) {
{Name: "read", Count: 9, Bytes: 512},
{Name: "write", Count: 3, Bytes: 1024},
}, nil, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
m.latest = &snap
m.syscallsVizMode = tabVizModeTreemap
@@ -1241,7 +1241,7 @@ func TestTreemapModeRendersFilesHeader(t *testing.T) {
{Path: "/srv/log/a", Accesses: 9, BytesRead: 400, BytesWritten: 200},
{Path: "/srv/log/b", Accesses: 4, BytesRead: 100, BytesWritten: 40},
}, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
m.latest = &snap
m.filesDirGrouped = true
@@ -1260,7 +1260,7 @@ func TestIcicleModeRendersFilesHeader(t *testing.T) {
{Path: "/srv/log/a", Accesses: 9, BytesRead: 400, BytesWritten: 200},
{Path: "/srv/log/b", Accesses: 4, BytesRead: 100, BytesWritten: 40},
}, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFiles
m.latest = &snap
m.filesDirGrouped = true
@@ -1279,7 +1279,7 @@ func TestTreemapModeRendersProcessesHeader(t *testing.T) {
{PID: 10, Comm: "worker", Syscalls: 12, Bytes: 500},
{PID: 11, Comm: "agent", Syscalls: 4, Bytes: 120},
}, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabProcesses
m.latest = &snap
m.processesVizMode = tabVizModeTreemap
@@ -1293,7 +1293,7 @@ func TestTreemapModeRendersProcessesHeader(t *testing.T) {
}
func TestScrollOffsetDoesNotGrowUnbounded(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabSyscalls
snap := statsengine.NewSnapshot(nil, nil, nil, []statsengine.SyscallSnapshot{{Name: "read", Count: 1}, {Name: "write", Count: 1}}, nil, nil, statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{})
m.latest = &snap
@@ -1310,7 +1310,7 @@ func TestScrollOffsetDoesNotGrowUnbounded(t *testing.T) {
func TestRefreshKeyEmitsRefreshTick(t *testing.T) {
snap := &statsengine.Snapshot{TotalSyscalls: 13}
engine := &fakeSnapshotSource{snap: snap}
- m := NewModelWithConfig(engine, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(engine, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabOverview
next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'r'}[0], Text: string([]rune{'r'})})
_ = next
@@ -1330,7 +1330,7 @@ func TestRefreshKeyEmitsRefreshTick(t *testing.T) {
func TestRefreshKeyResetsBaselineWhenSourceSupportsReset(t *testing.T) {
snap := &statsengine.Snapshot{TotalSyscalls: 5}
engine := &fakeResettableSnapshotSource{snap: snap}
- m := NewModelWithConfig(engine, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(engine, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabOverview
next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'r'}[0], Text: string([]rune{'r'})})
@@ -1353,7 +1353,7 @@ func TestRefreshKeyResetsBaselineWhenSourceSupportsReset(t *testing.T) {
func TestRefreshKeyResetsLiveTrieOutsideFlameTab(t *testing.T) {
liveTrie := coreflamegraph.NewLiveTrie([]string{"comm", "path"}, "count")
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.SetLiveTrie(liveTrie)
m.activeTab = TabSyscalls
before := liveTrie.Version()
@@ -1369,7 +1369,7 @@ func TestRefreshKeyResetsLiveTrieOutsideFlameTab(t *testing.T) {
}
func TestFlameTabReceivesSlashKey(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFlame
m.width = 120
m.height = 30
@@ -1385,7 +1385,7 @@ func TestFlameTabReceivesSlashKey(t *testing.T) {
}
func TestFlameTabReceivesResetAndPauseKeys(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFlame
m.width = 120
m.height = 30
@@ -1407,7 +1407,7 @@ func TestFlameTabReceivesResetAndPauseKeys(t *testing.T) {
}
func TestFlameSearchConsumesNumericTabKeys(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.activeTab = TabFlame
m.width = 120
m.height = 30
@@ -1428,7 +1428,7 @@ func TestFlameSearchConsumesNumericTabKeys(t *testing.T) {
func TestRefreshTickEmitsStatsTickMsg(t *testing.T) {
snap := &statsengine.Snapshot{TotalSyscalls: 9}
engine := &fakeSnapshotSource{snap: snap}
- m := NewModelWithConfig(engine, nil, 100, common.DefaultKeyMap())
+ m := NewModelWithConfig(engine, nil, 100, 200, common.DefaultKeyMap())
next, cmd := m.Update(refreshTickMsg{})
if cmd == nil {
@@ -1491,7 +1491,7 @@ func TestStatsTickClampsGroupedFilesOffset(t *testing.T) {
}
func TestViewRendersTabBarAndHelp(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 1000, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 1000, 200, common.DefaultKeyMap())
out := m.View().Content
if !strings.Contains(out, "Flame") {
t.Fatalf("expected flame tab label in view")
@@ -1505,7 +1505,7 @@ func TestViewRendersTabBarAndHelp(t *testing.T) {
}
func TestFlameTabRendersWaitingForDataPlaceholder(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 1000, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 1000, 200, common.DefaultKeyMap())
m.activeTab = TabFlame
// Dimensions must flow through Update so that sub-model viewports are
// kept in sync. Direct field assignment bypasses the sync logic in
@@ -1542,7 +1542,7 @@ func TestStreamTabViewKeepsTabAndHelpChromeVisible(t *testing.T) {
rb.Push(eventstream.StreamEvent{Syscall: "read"})
}
- m := NewModelWithConfig(nil, rb, 1000, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, rb, 1000, 200, common.DefaultKeyMap())
m.activeTab = TabStream
m.width = 120
m.height = 30
@@ -1559,7 +1559,7 @@ func TestStreamTabViewKeepsTabAndHelpChromeVisible(t *testing.T) {
}
func TestHelpToggleWithH(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 1000, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 1000, 200, common.DefaultKeyMap())
out := m.View().Content
if !strings.Contains(out, "press H for help") {
t.Fatalf("expected default help hint")
@@ -1610,7 +1610,7 @@ func TestTranslateFlamegraphMsgLeavesNonMouseUnchanged(t *testing.T) {
// SetFocused will arm a fresh one when focus returns.
func TestAutoResetTickIgnoredWhileBlurred(t *testing.T) {
engine := &fakeResettableSnapshotSource{}
- m := NewModelWithConfig(engine, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(engine, nil, 250, 200, common.DefaultKeyMap())
if cmd := m.SetAutoResetInterval(50 * time.Millisecond); cmd == nil {
t.Fatalf("SetAutoResetInterval should return a tick command for a positive interval")
}
@@ -1655,7 +1655,7 @@ func TestAutoResetTickIgnoredWhileBlurred(t *testing.T) {
// payload tea.Tick would deliver) rather than waiting on real time.
func TestAutoResetTickResumesOnFocusRegain(t *testing.T) {
engine := &fakeResettableSnapshotSource{}
- m := NewModelWithConfig(engine, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(engine, nil, 250, 200, common.DefaultKeyMap())
m.SetAutoResetInterval(50 * time.Millisecond)
m.SetFocused(false)
@@ -1688,7 +1688,7 @@ func TestAutoResetTickResumesOnFocusRegain(t *testing.T) {
// change (e.g. a duplicate FocusMsg). Bumping the generation in that
// case would silently invalidate a healthy in-flight tick.
func TestSetFocusedNoOpWhenStateUnchanged(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.SetAutoResetInterval(50 * time.Millisecond)
gen := m.autoResetGen
@@ -1704,7 +1704,7 @@ func TestSetFocusedNoOpWhenStateUnchanged(t *testing.T) {
// focus returns but the user has the auto-reset timer turned off. No
// tick should be armed (it would never fire anyway).
func TestSetFocusedReturnsNilWhenTimerDisabled(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
// Timer disabled by default.
m.SetFocused(false)
if cmd := m.SetFocused(true); cmd != nil {
@@ -1722,7 +1722,7 @@ func TestSetFocusedReturnsNilWhenTimerDisabled(t *testing.T) {
// and the status read, so we accept "30s/30s" or "29s/30s" rather than
// pinning an exact remaining string.
func TestAutoResetStatusAddsPausedSuffixWhenBlurred(t *testing.T) {
- m := NewModelWithConfig(nil, nil, 250, common.DefaultKeyMap())
+ m := NewModelWithConfig(nil, nil, 250, 200, common.DefaultKeyMap())
m.SetAutoResetInterval(30 * time.Second)
got := m.autoResetStatus()
if got != "auto-reset: 30s/30s" && got != "auto-reset: 29s/30s" {
@@ -1745,6 +1745,60 @@ func TestAutoResetStatusAddsPausedSuffixWhenBlurred(t *testing.T) {
}
}
+// TestNewModelWithConfigZeroFastRefreshUsesDefault verifies that passing 0 for
+// fastRefreshMs results in the model using the package-level constant cadence
+// (streamRefreshMs / flameRefreshMs) rather than a zero-duration tick, keeping
+// backward-compatibility for callers that do not supply a fast refresh interval.
+func TestNewModelWithConfigZeroFastRefreshUsesDefault(t *testing.T) {
+ m := NewModelWithConfig(nil, nil, 250, 0, common.DefaultKeyMap())
+ if m.fastRefreshEvery != 0 {
+ t.Fatalf("expected fastRefreshEvery=0 (use constant default), got %v", m.fastRefreshEvery)
+ }
+ // streamTickCmd and flameTickCmd should return non-nil commands even when
+ // fastRefreshEvery is zero, falling back to the constant cadence.
+ if cmd := m.streamTickCmd(); cmd == nil {
+ t.Fatalf("streamTickCmd() returned nil with zero fastRefreshEvery")
+ }
+ if cmd := m.flameTickCmd(); cmd == nil {
+ t.Fatalf("flameTickCmd() returned nil with zero fastRefreshEvery")
+ }
+}
+
+// TestNewModelWithConfigFastRefreshStored verifies that a positive fastRefreshMs
+// value is stored on the model and that the tick commands return non-nil commands.
+func TestNewModelWithConfigFastRefreshStored(t *testing.T) {
+ const fastMs = 150
+ m := NewModelWithConfig(nil, nil, 1000, fastMs, common.DefaultKeyMap())
+ want := time.Duration(fastMs) * time.Millisecond
+ if m.fastRefreshEvery != want {
+ t.Fatalf("expected fastRefreshEvery=%v, got %v", want, m.fastRefreshEvery)
+ }
+ if cmd := m.streamTickCmd(); cmd == nil {
+ t.Fatalf("streamTickCmd() returned nil with fastRefreshEvery=%v", want)
+ }
+ if cmd := m.flameTickCmd(); cmd == nil {
+ t.Fatalf("flameTickCmd() returned nil with fastRefreshEvery=%v", want)
+ }
+}
+
+// TestSetFastRefreshIntervalUpdatesModel verifies that SetFastRefreshInterval
+// overwrites fastRefreshEvery and that negative values are clamped to zero
+// (which restores the constant fallback).
+func TestSetFastRefreshIntervalUpdatesModel(t *testing.T) {
+ m := NewModelWithConfig(nil, nil, 1000, 200, common.DefaultKeyMap())
+
+ m.SetFastRefreshInterval(500 * time.Millisecond)
+ if m.fastRefreshEvery != 500*time.Millisecond {
+ t.Fatalf("expected fastRefreshEvery=500ms after Set, got %v", m.fastRefreshEvery)
+ }
+
+ // Negative value should be clamped to zero (constant fallback).
+ m.SetFastRefreshInterval(-1 * time.Millisecond)
+ if m.fastRefreshEvery != 0 {
+ t.Fatalf("expected fastRefreshEvery=0 after negative Set, got %v", m.fastRefreshEvery)
+ }
+}
+
// TestFormatAutoResetRemainingFormats exercises the duration formatter
// used by the chrome countdown: sub-minute durations stay in seconds,
// whole minutes drop the trailing "0s", and mixed values use "MmSs".
diff --git a/internal/tui/dashboard/tabregistry.go b/internal/tui/dashboard/tabregistry.go
index 2a5c7ff..801ecab 100644
--- a/internal/tui/dashboard/tabregistry.go
+++ b/internal/tui/dashboard/tabregistry.go
@@ -64,7 +64,7 @@ var tabDescriptors = map[Tab]tabDescriptor{
ShortName: "Flm",
Position: 10,
AllowedVizModes: []tabVizMode{tabVizModeTable},
- InitCmd: flameTickCmd,
+ InitCmd: flameTickCmdFn,
Render: tabRenderFlame,
HandleScroll: nil,
ShortcutKey: func(k common.KeyMap) key.Binding { return k.One },
@@ -119,7 +119,7 @@ var tabDescriptors = map[Tab]tabDescriptor{
ShortName: "Str",
Position: 70,
AllowedVizModes: []tabVizMode{tabVizModeTable},
- InitCmd: streamTickCmd,
+ InitCmd: streamTickCmdFn,
Render: tabRenderStream,
HandleScroll: tabScrollStream,
ShortcutKey: func(k common.KeyMap) key.Binding { return k.Seven },
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index b73fbf8..03502bb 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -282,6 +282,9 @@ func TraceFiltersFromContext(ctx context.Context) (globalfilter.Filter, bool) {
func RunWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error {
model := newModelWithRuntimeConfig(cfg.PidFilter, filterFromConfig(cfg), cfg.PidFilter, cfg.TidFilter, cfg.TUIExportEnable, starter)
model.dashboard.SetAutoResetInterval(cfg.ResetTimer)
+ // Apply the configurable fast-refresh cadence from the CLI flag so the
+ // stream and flame tabs honour the -tui-fast-refresh value.
+ model.dashboard.SetFastRefreshInterval(cfg.TUIFastRefreshInterval)
program := tea.NewProgram(model)
_, err := program.Run()
return err
@@ -291,6 +294,8 @@ func RunWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error {
func RunTestFlamesWithTraceStarterConfig(cfg flags.Config, starter TraceStarter) error {
model := newModelWithRuntimeConfig(1, filterFromConfig(cfg), 1, -1, cfg.TUIExportEnable, starter)
model.dashboard.SetAutoResetInterval(cfg.ResetTimer)
+ // Apply the configurable fast-refresh cadence from the CLI flag.
+ model.dashboard.SetFastRefreshInterval(cfg.TUIFastRefreshInterval)
program := tea.NewProgram(model)
_, err := program.Run()
return err
@@ -390,7 +395,10 @@ func newModelWithRuntimeConfig(initialPID int, startupFilter globalfilter.Filter
rt := newRuntimeBindings()
pidFilter, tidFilter := resolveStartupPIDFilters(initialPID, startupPidFilter, startupTidFilter)
- dashboard := newDashboardWithRuntime(rt, pidFilter, keys)
+ // Pass 0 for fastRefreshMs so the dashboard uses the package-level default
+ // (200 ms). Callers that hold a flags.Config can override this via
+ // SetFastRefreshInterval after construction.
+ dashboard := newDashboardWithRuntime(rt, pidFilter, keys, 0)
spin := spinner.New()
spin.Spinner = spinner.MiniDot
@@ -437,9 +445,11 @@ func resolveStartupPIDFilters(initialPID, startupPidFilter, startupTidFilter int
}
// newDashboardWithRuntime creates a dark-mode dashboard bound to the given
-// runtime and pre-configured with the initial PID filter.
-func newDashboardWithRuntime(rt *runtimeBindings, pidFilter int, keys KeyMap) dashboardui.Model {
- dashboard := dashboardui.NewModelWithConfig(lateBoundDashboardSource{runtime: rt}, rt.eventStreamSource(), 1000, keys)
+// runtime and pre-configured with the initial PID filter. fastRefreshMs
+// controls the high-frequency tick cadence for stream and flame tabs; pass 0
+// to use the package-level default (200 ms).
+func newDashboardWithRuntime(rt *runtimeBindings, pidFilter int, keys KeyMap, fastRefreshMs int) dashboardui.Model {
+ dashboard := dashboardui.NewModelWithConfig(lateBoundDashboardSource{runtime: rt}, rt.eventStreamSource(), 1000, fastRefreshMs, keys)
dashboard.SetDarkMode(true)
dashboard.SetPidFilter(pidFilter)
return dashboard
diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go
index dae45f7..9e62c55 100644
--- a/internal/tui/tui_test.go
+++ b/internal/tui/tui_test.go
@@ -2091,7 +2091,7 @@ func TestBlurPausesDashboardRefreshAndFocusResumesIt(t *testing.T) {
m := NewModel(-1, func(context.Context) error { return nil })
m.screen = ScreenDashboard
m.attaching = false
- m.dashboard = dashboardui.NewModelWithConfig(nil, nil, 1, m.keys)
+ m.dashboard = dashboardui.NewModelWithConfig(nil, nil, 1, 200, m.keys)
m.focused = true
next, _ := m.Update(tea.BlurMsg{})