diff options
| -rw-r--r-- | internal/eventfilter.go | 15 | ||||
| -rw-r--r-- | internal/eventloop.go | 7 | ||||
| -rw-r--r-- | internal/flamegraph/iordata.go | 8 | ||||
| -rw-r--r-- | internal/flamegraph/iordatacollector.go | 23 | ||||
| -rw-r--r-- | internal/ior.go | 3 | ||||
| -rw-r--r-- | internal/tui/dashboard/model.go | 12 | ||||
| -rw-r--r-- | internal/tui/dashboard/model_test.go | 2 | ||||
| -rw-r--r-- | internal/tui/dashboard/processes.go | 7 | ||||
| -rw-r--r-- | internal/tui/dashboard/processes_test.go | 8 | ||||
| -rw-r--r-- | internal/tui/tui.go | 54 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 2 |
11 files changed, 88 insertions, 53 deletions
diff --git a/internal/eventfilter.go b/internal/eventfilter.go index 0b3f121..4ff0385 100644 --- a/internal/eventfilter.go +++ b/internal/eventfilter.go @@ -3,7 +3,6 @@ package internal import ( "fmt" "ior/internal/event" - "ior/internal/flags" "ior/internal/types" "strings" ) @@ -15,23 +14,23 @@ type eventFilter struct { pathFilter string } -func newEventFilter() *eventFilter { +func newEventFilter(commFilter, pathFilter string) *eventFilter { var ef eventFilter - if flags.Get().CommFilter != "" { - if len(flags.Get().CommFilter) > types.MAX_FILENAME_LENGTH { + if commFilter != "" { + if len(commFilter) > types.MAX_FILENAME_LENGTH { panic(fmt.Sprintf("Comm filter's max size is %d", types.MAX_PROGNAME_LENGTH)) } ef.commFilterEnable = true - ef.commFilter = flags.Get().CommFilter + ef.commFilter = commFilter } - if flags.Get().PathFilter != "" { - if len(flags.Get().PathFilter) > types.MAX_FILENAME_LENGTH { + if pathFilter != "" { + if len(pathFilter) > types.MAX_FILENAME_LENGTH { panic(fmt.Sprintf("Path filter's max size is %d", types.MAX_FILENAME_LENGTH)) } ef.pathFilterEnable = true - ef.pathFilter = flags.Get().PathFilter + ef.pathFilter = pathFilter } return &ef diff --git a/internal/eventloop.go b/internal/eventloop.go index 62ab148..6975df3 100644 --- a/internal/eventloop.go +++ b/internal/eventloop.go @@ -22,10 +22,13 @@ const sysEnterNameToHandleAtName = "name_to_handle_at" type eventLoopConfig struct { pidFilter int + commFilter string + pathFilter string liveFlamegraph bool liveInterval time.Duration collapsedFields []string countField string + flamegraphName string flamegraphEnable bool pprofEnable bool plainMode bool @@ -61,7 +64,7 @@ type eventLoop struct { func newEventLoop(cfg eventLoopConfig) *eventLoop { el := &eventLoop{ - filter: newEventFilter(), + filter: newEventFilter(cfg.commFilter, cfg.pathFilter), enterEvs: make(map[uint32]*event.Pair), pendingHandles: make(map[uint32]string), files: make(map[int32]file.File), @@ -70,7 +73,7 @@ func newEventLoop(cfg eventLoopConfig) *eventLoop { prevPairTimes: make(map[uint32]uint64), rawHandlers: make(map[EventType]rawEventHandler), printCb: func(ep *event.Pair) { fmt.Println(ep); ep.Recycle() }, - flamegraph: flamegraph.New(), + flamegraph: flamegraph.New(cfg.flamegraphName), cfg: cfg, done: make(chan struct{}), } diff --git a/internal/flamegraph/iordata.go b/internal/flamegraph/iordata.go index db5bad6..61a65a9 100644 --- a/internal/flamegraph/iordata.go +++ b/internal/flamegraph/iordata.go @@ -7,7 +7,6 @@ import ( "io" "ior/internal/event" "ior/internal/file" - "ior/internal/flags" "ior/internal/types" "iter" "os" @@ -98,13 +97,16 @@ func (iod iorData) merge(other iorData) iorData { return iod } -func (iod iorData) serializeToFile() error { +func (iod iorData) serializeToFile(flamegraphName string) error { hostname, err := os.Hostname() if err != nil { panic(err) } + if flamegraphName == "" { + flamegraphName = "default" + } - filename := fmt.Sprintf("%s-%s-%s.ior.zst", hostname, flags.Get().FlamegraphName, + filename := fmt.Sprintf("%s-%s-%s.ior.zst", hostname, flamegraphName, time.Now().Format("2006-01-02_15:04:05")) fmt.Println("Writing", filename) tmpFilename := fmt.Sprintf("%s.tmp", filename) diff --git a/internal/flamegraph/iordatacollector.go b/internal/flamegraph/iordatacollector.go index 948af97..9e92b63 100644 --- a/internal/flamegraph/iordatacollector.go +++ b/internal/flamegraph/iordatacollector.go @@ -4,22 +4,27 @@ import ( "context" "fmt" "ior/internal/event" - "ior/internal/flags" "runtime" "sync" ) type IorDataCollector struct { - flags flags.Flags - Ch chan *event.Pair - Done chan error - workers []worker + flamegraphName string + Ch chan *event.Pair + Done chan error + workers []worker } -func New() IorDataCollector { +func New(flamegraphName ...string) IorDataCollector { + name := "default" + if len(flamegraphName) > 0 && flamegraphName[0] != "" { + name = flamegraphName[0] + } + f := IorDataCollector{ - Ch: make(chan *event.Pair, 4096), - Done: make(chan error, 1), + flamegraphName: name, + Ch: make(chan *event.Pair, 4096), + Done: make(chan error, 1), } numWorkers := runtime.NumCPU() / 4 if numWorkers == 0 { @@ -50,7 +55,7 @@ func (f IorDataCollector) Start(ctx context.Context) { fmt.Println("Worker", i+1, "merged") } } - if err := iod.serializeToFile(); err != nil { + if err := iod.serializeToFile(f.flamegraphName); err != nil { f.Done <- err return } diff --git a/internal/ior.go b/internal/ior.go index 51373c4..0d824cd 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -223,10 +223,13 @@ func newEventLoopConfig(cfg flags.Flags) eventLoopConfig { copy(fields, cfg.CollapsedFields) return eventLoopConfig{ pidFilter: cfg.PidFilter, + commFilter: cfg.CommFilter, + pathFilter: cfg.PathFilter, liveFlamegraph: cfg.LiveFlamegraph, liveInterval: cfg.LiveInterval, collapsedFields: fields, countField: cfg.CountField, + flamegraphName: cfg.FlamegraphName, flamegraphEnable: cfg.FlamegraphEnable, pprofEnable: cfg.PprofEnable, plainMode: cfg.PlainMode, diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go index c9c96c3..fc9caf6 100644 --- a/internal/tui/dashboard/model.go +++ b/internal/tui/dashboard/model.go @@ -39,6 +39,7 @@ type Model struct { refreshEvery time.Duration keys common.KeyMap + pidFilter int syscallsOffset int filesOffset int filesDirGrouped bool @@ -63,6 +64,7 @@ func NewModelWithConfig(engine SnapshotSource, streamSource *eventstream.RingBuf engine: engine, refreshEvery: time.Duration(refreshMs) * time.Millisecond, keys: keys, + pidFilter: -1, streamModel: eventstream.NewModel(streamSource), } } @@ -280,6 +282,11 @@ func (m *Model) SetStreamSource(source *eventstream.RingBuffer) { m.streamModel.SetSource(source) } +// SetPidFilter updates the active PID filter used by tab render hints. +func (m *Model) SetPidFilter(pid int) { + m.pidFilter = pid +} + // View renders the tab bar, active tab scaffold, and help bar. func (m Model) View() string { width, height := common.EffectiveViewport(m.width, m.height) @@ -299,6 +306,7 @@ func (m Model) View() string { &streamModel, width, activeHeight, + m.pidFilter, m.syscallsOffset, m.filesOffset, m.filesDirGrouped, @@ -318,7 +326,7 @@ func tickCmd(d time.Duration) tea.Cmd { return tea.Tick(d, func(time.Time) tea.Msg { return refreshTickMsg{} }) } -func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstream.Model, width, height, syscallsOffset, filesOffset int, filesDirGrouped bool, filesDirOffset, processesOffset int) string { +func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstream.Model, width, height, pidFilter, syscallsOffset, filesOffset int, filesDirGrouped bool, filesDirOffset, processesOffset int) string { if tab == TabStream { if streamModel == nil { return common.PanelStyle.Render("Stream: waiting for source...") @@ -341,7 +349,7 @@ func renderActiveTab(tab Tab, snap *statsengine.Snapshot, streamModel *eventstre } return renderFilesWithOffset(snap, width, height, filesOffset) case TabProcesses: - return renderProcessesWithOffset(snap, width, height, processesOffset) + return renderProcessesWithOffset(snap, width, height, processesOffset, pidFilter) case TabLatency: return renderLatencyGapsTab(snap, width, height) default: diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go index 0b269b1..87b60e3 100644 --- a/internal/tui/dashboard/model_test.go +++ b/internal/tui/dashboard/model_test.go @@ -386,7 +386,7 @@ func TestRenderActiveTabUsesDirectoryFilesViewWhenGrouped(t *testing.T) { statsengine.HistogramSnapshot{}, statsengine.HistogramSnapshot{}, ) - out := renderActiveTab(TabFiles, &snap, nil, 120, 30, 0, 0, true, 0, 0) + out := renderActiveTab(TabFiles, &snap, nil, 120, 30, -1, 0, 0, true, 0, 0) if !strings.Contains(out, "Directory") { t.Fatalf("expected grouped directory files view header, got %q", out) } diff --git a/internal/tui/dashboard/processes.go b/internal/tui/dashboard/processes.go index 03a38f1..281a86a 100644 --- a/internal/tui/dashboard/processes.go +++ b/internal/tui/dashboard/processes.go @@ -2,7 +2,6 @@ package dashboard import ( "fmt" - "ior/internal/flags" "ior/internal/statsengine" "strconv" "strings" @@ -11,10 +10,10 @@ import ( ) func renderProcesses(snap *statsengine.Snapshot, width, height int) string { - return renderProcessesWithOffset(snap, width, height, 0) + return renderProcessesWithOffset(snap, width, height, 0, -1) } -func renderProcessesWithOffset(snap *statsengine.Snapshot, width, height, offset int) string { +func renderProcessesWithOffset(snap *statsengine.Snapshot, width, height, offset, pidFilter int) string { if snap == nil { return "Processes: waiting for stats..." } @@ -44,7 +43,7 @@ func renderProcessesWithOffset(snap *statsengine.Snapshot, width, height, offset tbl.SetCursor(cursor) out := tbl.View() + fmt.Sprintf("\nRow %d/%d", cursor+1, len(rows)) - if flags.Get().PidFilter > 0 { + if pidFilter > 0 { out += "\n" + "Note: this tab is most useful with All PIDs." } return out diff --git a/internal/tui/dashboard/processes_test.go b/internal/tui/dashboard/processes_test.go index 4db490d..24c1c1b 100644 --- a/internal/tui/dashboard/processes_test.go +++ b/internal/tui/dashboard/processes_test.go @@ -4,13 +4,10 @@ import ( "strings" "testing" - "ior/internal/flags" "ior/internal/statsengine" ) func TestRenderProcessesIncludesHeaders(t *testing.T) { - flags.SetPidFilter(-1) - snap := statsengine.NewSnapshot( nil, nil, nil, nil, nil, @@ -34,9 +31,6 @@ func TestRenderProcessesIncludesHeaders(t *testing.T) { } func TestRenderProcessesShowsSinglePIDNote(t *testing.T) { - flags.SetPidFilter(77) - t.Cleanup(func() { flags.SetPidFilter(-1) }) - snap := statsengine.NewSnapshot( nil, nil, nil, nil, nil, @@ -47,7 +41,7 @@ func TestRenderProcessesShowsSinglePIDNote(t *testing.T) { statsengine.HistogramSnapshot{}, ) - out := renderProcesses(&snap, 100, 20) + out := renderProcessesWithOffset(&snap, 100, 20, 0, 77) if !strings.Contains(out, "most useful with All PIDs") { t.Fatalf("expected single-pid guidance note") } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index d585a0b..10e7988 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -127,7 +127,8 @@ func Run() error { // RunWithTraceStarter starts the TUI program with a custom trace starter. func RunWithTraceStarter(starter TraceStarter) error { - model := NewModel(flags.Get().PidFilter, starter) + cfg := flags.Get() + model := newModelWithRuntimeConfig(cfg.PidFilter, cfg.PidFilter, cfg.TUIExportEnable, starter) program := tea.NewProgram(model, tea.WithAltScreen()) _, err := program.Run() return err @@ -153,29 +154,46 @@ type Model struct { startTrace TraceStarter traceStop context.CancelFunc + + pidFilter int + exportEnabled bool } // NewModel creates the top-level TUI model. func NewModel(initialPID int, startTrace TraceStarter) Model { + cfg := flags.Get() + return newModelWithRuntimeConfig(initialPID, cfg.PidFilter, cfg.TUIExportEnable, startTrace) +} + +func newModelWithRuntimeConfig(initialPID, startupPidFilter int, exportEnabled bool, startTrace TraceStarter) Model { spin := spinner.New() spin.Spinner = spinner.MiniDot if startTrace == nil { startTrace = defaultTraceStarter } keys := Keys - if !flags.Get().TUIExportEnable { + if !exportEnabled { keys.Export = key.NewBinding() } + dashboard := dashboardui.NewModelWithConfig(lateBoundDashboardSource{}, getEventStreamSource(), 1000, keys) + pidFilter := selectedPIDFilter(startupPidFilter) + if initialPID > 0 { + pidFilter = selectedPIDFilter(initialPID) + } + dashboard.SetPidFilter(pidFilter) + model := Model{ - screen: ScreenPIDPicker, - pidPicker: pidpicker.New(), - dashboard: dashboardui.NewModelWithConfig(lateBoundDashboardSource{}, getEventStreamSource(), 1000, keys), - exporter: tuiexport.NewModel(), - probeModal: probes.NewModel(getProbeManager()), - keys: keys, - spin: spin, - startTrace: startTrace, + screen: ScreenPIDPicker, + pidPicker: pidpicker.New(), + dashboard: dashboard, + exporter: tuiexport.NewModel(), + probeModal: probes.NewModel(getProbeManager()), + keys: keys, + spin: spin, + startTrace: startTrace, + pidFilter: pidFilter, + exportEnabled: exportEnabled, } if initialPID > 0 { @@ -216,7 +234,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.stopTrace() return m, tea.Quit } - if flags.Get().TUIExportEnable && m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Export) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() { + if m.exportEnabled && m.screen == ScreenDashboard && !m.attaching && m.lastErr == nil && key.Matches(msg, m.keys.Export) && !m.exporter.Visible() && !m.probeModal.Visible() && !m.dashboard.BlocksGlobalShortcuts() { m.exporter = m.exporter.Open() return m, nil } @@ -231,7 +249,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.reselectTID() } case tuiexport.RequestMsg: - return m, runExportCmd(msg.Option, m.dashboard.LatestSnapshot()) + return m, runExportCmd(m.exportEnabled, msg.Option, m.dashboard.LatestSnapshot()) case tuiexport.CompletedMsg: var cmd tea.Cmd m.exporter, cmd = m.exporter.Update(msg) @@ -316,6 +334,8 @@ func (m Model) handlePidSelected(msg PidSelectedMsg) (tea.Model, tea.Cmd) { m.stopTrace() flags.SetPidFilter(pid) flags.SetTidFilter(-1) + m.pidFilter = pid + m.dashboard.SetPidFilter(pid) m.screen = ScreenDashboard m.attaching = true m.lastErr = nil @@ -324,13 +344,15 @@ func (m Model) handlePidSelected(msg PidSelectedMsg) (tea.Model, tea.Cmd) { func (m Model) handleTidSelected(msg TidSelectedMsg) (tea.Model, tea.Cmd) { tid := selectedPIDFilter(msg.Tid) - pid := flags.Get().PidFilter + pid := m.pidFilter if msg.Pid > 0 { pid = msg.Pid } m.stopTrace() flags.SetPidFilter(pid) flags.SetTidFilter(tid) + m.pidFilter = pid + m.dashboard.SetPidFilter(pid) m.screen = ScreenDashboard m.attaching = true m.lastErr = nil @@ -357,7 +379,7 @@ func (m Model) reselectPID() (tea.Model, tea.Cmd) { } func (m Model) reselectTID() (tea.Model, tea.Cmd) { - pid := flags.Get().PidFilter + pid := m.pidFilter m.stopTrace() m.screen = ScreenPIDPicker @@ -451,9 +473,9 @@ func (m Model) View() string { } } -func runExportCmd(option tuiexport.Option, snap *statsengine.Snapshot) tea.Cmd { +func runExportCmd(exportEnabled bool, option tuiexport.Option, snap *statsengine.Snapshot) tea.Cmd { return func() tea.Msg { - if !flags.Get().TUIExportEnable { + if !exportEnabled { return tuiexport.FailedMsg{Err: errors.New("tui export is disabled by -tuiExport=false")} } switch option { diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index b0e1861..ed361a6 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -467,7 +467,7 @@ func TestRunExportCmdCSVWritesFile(t *testing.T) { t.Cleanup(func() { _ = os.Chdir(prev) }) snap := &statsengine.Snapshot{TotalSyscalls: 1} - msg := runExportCmd(tuiexport.OptionCSV, snap)() + msg := runExportCmd(true, tuiexport.OptionCSV, snap)() done, ok := msg.(tuiexport.CompletedMsg) if !ok { t.Fatalf("expected CompletedMsg, got %T", msg) |
