diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-18 09:04:17 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-18 09:04:17 +0200 |
| commit | 1731c3723ced92a5dc8e54fb0caf4e33b2c7ba70 (patch) | |
| tree | b384a235ca927177413bb198e0fd421f2bfbaa2b /internal/eventloop_state.go | |
| parent | 6ab80599c8f8ba688a0415ecbeb03e494ef31f04 (diff) | |
refactor: extract pairTracker and extend fdTracker to reduce eventLoop responsibilities (task 428)
The eventLoop struct held 20+ fields across 5+ responsibilities (SRP violation).
Extract two cohesive sub-structs:
- pairTracker: enter/exit pair matching, age-based LRU pruning, and
DurationToPrev tracking. Replaces enterEvs/enterEvAges/prevPairTimes/
maxPendingEnterEvs/cacheAge fields with a single embedded value.
- fdTracker (extended): absorbs procFdCache/procFdAges/maxProcFdCacheSize,
moving all procfs-resolution cache logic (resolve, cache, prune, delete)
off eventLoop and onto the tracker that already owns the fd table.
eventLoop drops from 20 fields to 12. All methods that previously reached
into eventLoop fields now live on the struct that owns the data.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'internal/eventloop_state.go')
| -rw-r--r-- | internal/eventloop_state.go | 217 |
1 files changed, 125 insertions, 92 deletions
diff --git a/internal/eventloop_state.go b/internal/eventloop_state.go index cd6e428..9622fd1 100644 --- a/internal/eventloop_state.go +++ b/internal/eventloop_state.go @@ -1,21 +1,32 @@ package internal import ( - "sort" + "cmp" + "slices" "ior/internal/event" "ior/internal/file" ) +// fdTracker holds the process's open file-descriptor table and a procfs +// resolution cache for fds that were opened before tracing started. type fdTracker struct { - files map[int32]file.File + files map[int32]file.File + procFdCache map[uint64]*file.FdFile // procfs-resolved metadata for unknown FDs + procFdAges map[uint64]uint64 // access age per cache entry, for LRU eviction + maxCacheSize int // max entries before eviction; 0 = defaultMaxProcFdCacheSize + age uint64 // monotonic counter for LRU ordering } func newFDTracker(files map[int32]file.File) *fdTracker { if files == nil { files = make(map[int32]file.File) } - return &fdTracker{files: files} + return &fdTracker{ + files: files, + procFdCache: make(map[uint64]*file.FdFile), + procFdAges: make(map[uint64]uint64), + } } func (t *fdTracker) get(fd int32) (file.File, bool) { @@ -39,111 +50,168 @@ func (t *fdTracker) closeRangeFrom(first int32) { } } -func (e *eventLoop) resolveFdFile(fd int32, pid uint32) file.File { - if fdFile, ok := e.fdState().get(fd); ok { +// resolve returns the file.File for fd, checking the fd table first, then the +// procfs cache, and finally resolving via procfs and caching the result. +func (t *fdTracker) resolve(fd int32, pid uint32) file.File { + if fdFile, ok := t.get(fd); ok { return fdFile } if fd < 0 { return file.NewFd(fd, "", -1) } - - if cached, ok := e.cachedProcFdFile(fd, pid); ok { + if cached, ok := t.cachedProcFdFile(fd, pid); ok { return cached } - // Cache first procfs resolution to avoid repeated /proc lookups for hot unknown FDs. discovered := file.NewFdWithPid(fd, pid) - e.setProcFdCache(fd, pid, discovered) + t.setProcFdCache(fd, pid, discovered) return discovered } -func (e *eventLoop) cachedProcFdFile(fd int32, pid uint32) (*file.FdFile, bool) { +func (t *fdTracker) cachedProcFdFile(fd int32, pid uint32) (*file.FdFile, bool) { + if t.procFdCache == nil { + return nil, false + } key := procFdCacheKey(pid, fd) - cache, ok := e.procFdCacheState()[key] + cache, ok := t.procFdCache[key] if ok { - e.procFdCacheAgeState()[key] = e.nextCacheAge() + t.age++ + t.procFdAges[key] = t.age } return cache, ok } -func (e *eventLoop) setProcFdCache(fd int32, pid uint32, resolved *file.FdFile) { +func (t *fdTracker) setProcFdCache(fd int32, pid uint32, resolved *file.FdFile) { + if t.procFdCache == nil { + t.procFdCache = make(map[uint64]*file.FdFile) + t.procFdAges = make(map[uint64]uint64) + } key := procFdCacheKey(pid, fd) - e.procFdCacheState()[key] = resolved - e.procFdCacheAgeState()[key] = e.nextCacheAge() - e.pruneProcFdCache() + t.age++ + t.procFdCache[key] = resolved + t.procFdAges[key] = t.age + t.pruneCache() } -func (e *eventLoop) deleteProcFdCache(fd int32, pid uint32) { - e.deleteProcFdCacheKey(procFdCacheKey(pid, fd)) +func (t *fdTracker) deleteProcFdCache(fd int32, pid uint32) { + t.deleteCacheKey(procFdCacheKey(pid, fd)) } -func (e *eventLoop) deleteProcFdCacheFrom(first int32, pid uint32) { - cache := e.procFdCacheState() - for key := range cache { +func (t *fdTracker) deleteProcFdCacheFrom(first int32, pid uint32) { + if t.procFdCache == nil { + return + } + for key := range t.procFdCache { cachePid := uint32(key >> 32) cacheFd := int32(uint32(key)) if cachePid == pid && cacheFd >= first { - e.deleteProcFdCacheKey(key) + t.deleteCacheKey(key) } } } -func (e *eventLoop) procFdCacheState() map[uint64]*file.FdFile { - if e.procFdCache == nil { - e.procFdCache = make(map[uint64]*file.FdFile) +func (t *fdTracker) pruneCache() { + if t.procFdCache == nil { + return + } + limit := t.cacheLimit() + if len(t.procFdCache) <= limit { + return } - return e.procFdCache + trimOldestProcFdEntries(t.procFdCache, t.procFdAges, trimTarget(limit)) } -func (e *eventLoop) procFdCacheAgeState() map[uint64]uint64 { - if e.procFdCacheAges == nil { - e.procFdCacheAges = make(map[uint64]uint64) +func (t *fdTracker) cacheLimit() int { + if t.maxCacheSize > 0 { + return t.maxCacheSize } - return e.procFdCacheAges + return defaultMaxProcFdCacheSize } -func (e *eventLoop) enterEventAgeState() map[uint32]uint64 { - if e.enterEvAges == nil { - e.enterEvAges = make(map[uint32]uint64) - } - return e.enterEvAges +// deleteCacheKey removes a cache entry by its composite key. +// delete on a nil map is a no-op in Go, so this is safe even before any cache entries are set. +func (t *fdTracker) deleteCacheKey(key uint64) { + delete(t.procFdCache, key) + delete(t.procFdAges, key) } -func (e *eventLoop) enterEventState() map[uint32]*event.Pair { - if e.enterEvs == nil { - e.enterEvs = make(map[uint32]*event.Pair) +// pairTracker holds the state for matching sys_enter events to their sys_exit +// counterparts and computing inter-syscall durations per TID. +type pairTracker struct { + enters map[uint32]*event.Pair // pending enter events, keyed by TID + enterAges map[uint32]uint64 // insertion order per TID, for LRU eviction + prevTimes map[uint32]uint64 // previous pair's exit time per TID, for DurationToPrev + maxSize int // max pending enter events before pruning; 0 = default + age uint64 // monotonic counter for LRU ordering +} + +func newPairTracker() pairTracker { + return pairTracker{ + enters: make(map[uint32]*event.Pair), + enterAges: make(map[uint32]uint64), + prevTimes: make(map[uint32]uint64), } - return e.enterEvs } -func (e *eventLoop) setEnterEvent(enterEv event.Event) { +// set stores enterEv as a pending enter event for its TID, recycling any +// prior unmatched enter for the same TID, then prunes if over the limit. +// Maps are initialized lazily on first write; consume is safe on a nil map because +// Go map reads on nil return the zero value. +func (p *pairTracker) set(enterEv event.Event) { + if p.enters == nil { + p.enters = make(map[uint32]*event.Pair) + p.enterAges = make(map[uint32]uint64) + p.prevTimes = make(map[uint32]uint64) + } tid := enterEv.GetTid() pair := event.NewPair(enterEv) - if prev, ok := e.enterEventState()[tid]; ok && prev != nil { + if prev, ok := p.enters[tid]; ok && prev != nil { prev.Recycle() } - e.enterEventState()[tid] = pair - e.enterEventAgeState()[tid] = e.nextCacheAge() - e.prunePendingEnterEvents() + p.age++ + p.enters[tid] = pair + p.enterAges[tid] = p.age + p.prune() } -func (e *eventLoop) consumeEnterEvent(tid uint32) (*event.Pair, bool) { - pair, ok := e.enterEventState()[tid] +// consume removes and returns the pending enter pair for tid. +// Reading a nil map returns the zero value in Go, so this is safe before any set call. +func (p *pairTracker) consume(tid uint32) (*event.Pair, bool) { + pair, ok := p.enters[tid] if !ok { return nil, false } - delete(e.enterEventState(), tid) - delete(e.enterEventAgeState(), tid) + delete(p.enters, tid) + delete(p.enterAges, tid) return pair, true } -func (e *eventLoop) prunePendingEnterEvents() { - state := e.enterEventState() - limit := e.pendingEnterLimit() - if len(state) <= limit { +// prevTime returns the exit time of the previous pair for tid, used to compute DurationToPrev. +func (p *pairTracker) prevTime(tid uint32) uint64 { + return p.prevTimes[tid] +} + +// setPrevTime records the exit time of the most recent completed pair for tid. +func (p *pairTracker) setPrevTime(tid uint32, t uint64) { + if p.prevTimes == nil { + p.prevTimes = make(map[uint32]uint64) + } + p.prevTimes[tid] = t +} + +func (p *pairTracker) prune() { + limit := p.limit() + if len(p.enters) <= limit { return } - trimOldestPendingPairs(state, e.enterEventAgeState(), trimTarget(limit)) + trimOldestPendingPairs(p.enters, p.enterAges, trimTarget(limit)) +} + +func (p *pairTracker) limit() int { + if p.maxSize > 0 { + return p.maxSize + } + return defaultMaxPendingEnterEvs } func trimOldestPendingPairs(state map[uint32]*event.Pair, ages map[uint32]uint64, targetSize int) { @@ -157,10 +225,9 @@ func trimOldestPendingPairs(state map[uint32]*event.Pair, ages map[uint32]uint64 } oldest := make([]pendingPairAge, 0, len(state)) for tid := range state { - age := ages[tid] - oldest = append(oldest, pendingPairAge{tid: tid, age: age}) + oldest = append(oldest, pendingPairAge{tid: tid, age: ages[tid]}) } - sort.Slice(oldest, func(i, j int) bool { return oldest[i].age < oldest[j].age }) + slices.SortFunc(oldest, func(a, b pendingPairAge) int { return cmp.Compare(a.age, b.age) }) for _, entry := range oldest[:excess] { if pair, ok := state[entry.tid]; ok && pair != nil { pair.Recycle() @@ -170,15 +237,6 @@ func trimOldestPendingPairs(state map[uint32]*event.Pair, ages map[uint32]uint64 } } -func (e *eventLoop) pruneProcFdCache() { - state := e.procFdCacheState() - limit := e.procFdCacheLimit() - if len(state) <= limit { - return - } - trimOldestProcFdEntries(state, e.procFdCacheAgeState(), trimTarget(limit)) -} - func trimOldestProcFdEntries(state map[uint64]*file.FdFile, ages map[uint64]uint64, targetSize int) { excess := len(state) - targetSize if excess <= 0 { @@ -190,40 +248,15 @@ func trimOldestProcFdEntries(state map[uint64]*file.FdFile, ages map[uint64]uint } oldest := make([]procFdAge, 0, len(state)) for key := range state { - age := ages[key] - oldest = append(oldest, procFdAge{key: key, age: age}) + oldest = append(oldest, procFdAge{key: key, age: ages[key]}) } - sort.Slice(oldest, func(i, j int) bool { return oldest[i].age < oldest[j].age }) + slices.SortFunc(oldest, func(a, b procFdAge) int { return cmp.Compare(a.age, b.age) }) for _, entry := range oldest[:excess] { delete(state, entry.key) delete(ages, entry.key) } } -func (e *eventLoop) deleteProcFdCacheKey(key uint64) { - delete(e.procFdCacheState(), key) - delete(e.procFdCacheAgeState(), key) -} - -func (e *eventLoop) nextCacheAge() uint64 { - e.cacheAge++ - return e.cacheAge -} - -func (e *eventLoop) pendingEnterLimit() int { - if e.maxPendingEnterEvs > 0 { - return e.maxPendingEnterEvs - } - return defaultMaxPendingEnterEvs -} - -func (e *eventLoop) procFdCacheLimit() int { - if e.maxProcFdCacheSize > 0 { - return e.maxProcFdCacheSize - } - return defaultMaxProcFdCacheSize -} - func trimTarget(limit int) int { target := limit - limit/cacheTrimDivisor if target < 1 { |
