summaryrefslogtreecommitdiff
path: root/internal/tui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-08 23:18:52 +0300
committerPaul Buetow <paul@buetow.org>2026-05-08 23:18:52 +0300
commit75c483ec6443f731cc6f2149c4738547eb602c6f (patch)
tree4875b619864a5eeeb8faff84f475c21382acb499 /internal/tui
parentf86699a94bdde7d973ba5d6fa3e7ca4ab2f234fb (diff)
swap global filter in place to skip BPF reattach
Changing the global filter used to call stopTrace + beginTraceCmd, which detached and re-attached every tracepoint and re-loaded the BPF object. On heavily loaded I/O systems that took several seconds and showed an 'Attaching tracepoints...' overlay each time. The probe set never depends on the global filter (ShouldIAttachTracepoint only reads CLI regex flags), so the restart was gratuitous. Now the eventloop holds its filter behind atomic.Pointer with SetFilter / Filter accessors, and the trace starter registers el.SetFilter via the runtime bindings as a SetLiveFilterSetter callback. applyGlobalFilter and undoGlobalFilter call runtime.applyLiveFilter first; only if no trace is running do they fall back to the full restart path.
Diffstat (limited to 'internal/tui')
-rw-r--r--internal/tui/tui.go64
1 files changed, 56 insertions, 8 deletions
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 3a866e5..42441a1 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -65,6 +65,11 @@ type RuntimePublisher interface {
SetEventStreamSource(source eventstream.Source)
SetLiveTrie(liveTrie flamegraphtui.LiveTrieSource)
SetProbeManager(manager ProbeManager)
+ // SetLiveFilterSetter registers (or, with nil, unregisters) a callback that
+ // applies a new global filter to the running trace pipeline in place. The
+ // trace starter passes its eventloop's SetFilter; the TUI calls it on every
+ // filter change to avoid restarting the BPF probes.
+ SetLiveFilterSetter(setter func(globalfilter.Filter))
}
// RuntimeState is the read side of the TUI runtime contract.
@@ -89,14 +94,15 @@ type traceFiltersContextKey struct{}
type runtimeBindings struct {
mu sync.RWMutex
- snapshotSource SnapshotSource
- streamSource eventstream.Source
- streamBuffer *eventstream.RingBuffer
- streamSeq *eventstream.Sequencer
- recorder *parquet.Recorder
- liveTrieSource flamegraphtui.LiveTrieSource
- probeManager ProbeManager
- filterEpoch atomic.Uint64
+ snapshotSource SnapshotSource
+ streamSource eventstream.Source
+ streamBuffer *eventstream.RingBuffer
+ streamSeq *eventstream.Sequencer
+ recorder *parquet.Recorder
+ liveTrieSource flamegraphtui.LiveTrieSource
+ probeManager ProbeManager
+ liveFilterSetter func(globalfilter.Filter)
+ filterEpoch atomic.Uint64
}
type traceFilters struct {
@@ -159,6 +165,27 @@ func (r *runtimeBindings) SetProbeManager(manager ProbeManager) {
r.mu.Unlock()
}
+func (r *runtimeBindings) SetLiveFilterSetter(setter func(globalfilter.Filter)) {
+ r.mu.Lock()
+ r.liveFilterSetter = setter
+ r.mu.Unlock()
+}
+
+// applyLiveFilter swaps the active global filter in place via the setter
+// registered by the trace starter, returning true if a setter was available.
+// Returning false tells the caller it must fall back to a full trace restart
+// (typically because no trace is currently running).
+func (r *runtimeBindings) applyLiveFilter(filter globalfilter.Filter) bool {
+ r.mu.RLock()
+ setter := r.liveFilterSetter
+ r.mu.RUnlock()
+ if setter == nil {
+ return false
+ }
+ setter(filter)
+ return true
+}
+
func (r *runtimeBindings) dashboardSnapshotSource() SnapshotSource {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -915,6 +942,20 @@ func (m Model) applyGlobalFilter(filter globalfilter.Filter, action string) (tea
}
m.runtime.advanceFilterEpoch()
+ // Try the in-place swap first: hand the new filter to the running
+ // eventloop via the registered setter and only reset the dashboard
+ // aggregates so the displayed counts reflect the new filter going
+ // forward. The BPF probes stay attached, so the user no longer sees
+ // the multi-second 'Attaching tracepoints' overlay on filter changes.
+ if m.runtime.applyLiveFilter(nextFilter) {
+ m.dashboard.PrepareForTraceRestart()
+ m.lastErr = nil
+ return m, nil
+ }
+
+ // Fallback: no trace currently running (e.g. first invocation), so
+ // restart the pipeline so the new filter takes effect on the next
+ // trace start.
m.stopTrace()
m.dashboard.PrepareForTraceRestart()
m.attaching = true
@@ -937,6 +978,13 @@ func (m Model) undoGlobalFilter() (tea.Model, tea.Cmd) {
}
m.runtime.advanceFilterEpoch()
+ // Same in-place swap path as applyGlobalFilter — see comment there.
+ if m.runtime.applyLiveFilter(prev) {
+ m.dashboard.PrepareForTraceRestart()
+ m.lastErr = nil
+ return m, nil
+ }
+
m.stopTrace()
m.dashboard.PrepareForTraceRestart()
m.attaching = true