summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-01 23:45:37 +0200
committerPaul Buetow <paul@buetow.org>2026-03-01 23:45:37 +0200
commit5775246cb9c2ccfb3469addf6f5fe9a8fc198171 (patch)
tree96290d1bede538c9fd352bc3954bac1ce8ab6873 /internal
parent3b4be9171b7ca13d4ff3e51d14c4e569b1a308f7 (diff)
Thread runtime config instead of global flags reads
Diffstat (limited to 'internal')
-rw-r--r--internal/eventfilter.go15
-rw-r--r--internal/eventloop.go7
-rw-r--r--internal/flamegraph/iordata.go8
-rw-r--r--internal/flamegraph/iordatacollector.go23
-rw-r--r--internal/ior.go3
-rw-r--r--internal/tui/dashboard/model.go12
-rw-r--r--internal/tui/dashboard/model_test.go2
-rw-r--r--internal/tui/dashboard/processes.go7
-rw-r--r--internal/tui/dashboard/processes_test.go8
-rw-r--r--internal/tui/tui.go54
-rw-r--r--internal/tui/tui_test.go2
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)