package dashboard
import (
"fmt"
"slices"
"strings"
"time"
"ior/internal/globalfilter"
"ior/internal/globalfilter/presenter"
"ior/internal/statsengine"
common "ior/internal/tui/common"
"ior/internal/tui/eventstream"
flamegraphtui "ior/internal/tui/flamegraph"
"ior/internal/tui/messages"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
)
const defaultRefreshMs = 1000
const streamRefreshMs = 200
const flameRefreshMs = 200
const bubbleRefreshMs = 33
const streamChromeRows = 4
const dashboardHelpHintRows = 1
const dashboardExpandedHelpRows = 2
const dashboardTabBarRows = 1
// SnapshotSource is the dashboard data source. Snapshot returns nil, nil when
// the engine is nil. A non-nil error indicates that snapshot construction
// failed and the caller should discard the result.
type SnapshotSource interface {
Snapshot() (*statsengine.Snapshot, error)
}
// resettableSnapshotSource extends SnapshotSource with a Reset method that
// clears accumulated state and restarts the series baselines.
type resettableSnapshotSource interface {
Reset()
Snapshot() (*statsengine.Snapshot, error)
}
type refreshTickMsg struct{}
type streamTickMsg struct{}
type flameTickMsg struct{}
type bubbleTickMsg struct{}
// autoResetTickMsg fires when the auto-reset timer elapses. It carries the
// generation it was scheduled for so that stale ticks (from a previous
// interval setting) are ignored rather than triggering a wrong-cadence reset.
type autoResetTickMsg struct {
generation uint64
}
type streamEditorDoneMsg struct {
err error
}
type tabVizMode uint8
const (
tabVizModeTable tabVizMode = iota
tabVizModeBubbles
tabVizModeTreemap
tabVizModeIcicle
)
// Model is the dashboard tab framework model.
type Model struct {
activeTab Tab
engine SnapshotSource
latest *statsengine.Snapshot
liveTrie flamegraphtui.LiveTrieSource
width int
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
// autoResetGen is incremented every time autoResetEvery changes so
// in-flight ticks scheduled under the previous cadence can be ignored.
autoResetGen uint64
// autoResetArmedAt is the wall-clock instant the current tick was
// scheduled. The next reset is expected at autoResetArmedAt +
// autoResetEvery; autoResetStatus uses this to render the live
// countdown ("12s/30s") in the chrome. Updated on every arm
// (SetAutoResetInterval, focus regain, tick re-arm).
autoResetArmedAt time.Time
keys common.KeyMap
globalFilter globalfilter.Filter
filterStack []string
recordingStatus string
pidFilter int
syscallsOffset int
syscallsCol int
syscallsSort tableSortState[syscallSortKey]
syscallsTreemapSelection int
nonIOOffset int
nonIOCol int
filesOffset int
filesCol int
filesSort tableSortState[fileSortKey]
filesDirGrouped bool
filesDirOffset int
filesDirCol int
filesDirSort tableSortState[fileDirSortKey]
processesOffset int
processesCol int
processesSort tableSortState[processSortKey]
syscallsVizMode tabVizMode
filesVizMode tabVizMode
processesVizMode tabVizMode
streamModel eventstream.Model
flamegraphModel flamegraphtui.Model
syscallsChart bubbleChart
filesChart bubbleChart
processesChart bubbleChart
showHelp bool
isDark bool
focused bool
}
// NewModel creates a dashboard model with default refresh cadence.
func NewModel(engine SnapshotSource, streamSource eventstream.Source) Model {
return NewModelWithConfig(engine, streamSource, defaultRefreshMs, 0, common.Keys)
}
// NewModelWithConfig creates a dashboard model with explicit refresh and keys.
// 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
}
m := Model{
activeTab: TabFlame,
engine: engine,
refreshEvery: time.Duration(refreshMs) * time.Millisecond,
fastRefreshEvery: time.Duration(fastRefreshMs) * time.Millisecond,
keys: keys,
pidFilter: -1,
syscallsVizMode: tabVizModeTable,
filesVizMode: tabVizModeTable,
processesVizMode: tabVizModeTable,
streamModel: eventstream.NewModel(streamSource),
flamegraphModel: flamegraphtui.NewModel(nil),
syscallsChart: newBubbleChart(),
filesChart: newBubbleChart(),
processesChart: newBubbleChart(),
isDark: true,
focused: true,
}
// showHelp starts false; align the stream footer visibility so it matches
// from the first render without relying on View() to fix up the mismatch.
m.streamModel.SetFooterVisible(false)
m.SetDarkMode(true)
return m
}
// 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 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)
if d.InitCmd != nil {
// Pass the model so the closure can read fastRefreshEvery and use
// the configured cadence rather than falling back to a constant.
cmds = append(cmds, d.InitCmd(&m))
} else if m.bubbleEnabledForTab(m.activeTab) {
cmds = append(cmds, bubbleTickCmdFn())
}
if cmd := m.autoResetTickCmd(); cmd != nil {
cmds = append(cmds, cmd)
}
if len(cmds) == 1 {
return cmds[0]
}
return
|