diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-14 08:23:35 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-14 08:23:35 +0300 |
| commit | 2eb861796de41e87de071987ed864b79a0007ab2 (patch) | |
| tree | 65b3a612c9a113d094b97407c04748c2713ddd4b | |
| parent | 34d88ba4c82dac2646db27c74e1cafb0c97dbcf2 (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>
| -rw-r--r-- | internal/tui/dashboard/model.go | 76 | ||||
| -rw-r--r-- | internal/tui/dashboard/model_test.go | 188 | ||||
| -rw-r--r-- | internal/tui/dashboard/tabregistry.go | 4 | ||||
| -rw-r--r-- | internal/tui/tui.go | 18 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 2 |
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{}) |
