@@ -142,6 +142,15 @@ def __init__(
142142 self .mmap_regions = {}
143143 self .cmdline_cache = {} # pid -> cmdline, populated on first successful read
144144
145+ # Bounded-cache maintenance. The path resolver and cmdline caches are
146+ # only evicted on PROCESS_EXEC, so over a long-running trace they would
147+ # otherwise grow without bound. Maintenance runs from the perf-callback
148+ # (polling) thread — the only thread that mutates these caches — so it
149+ # never races with event processing.
150+ self ._event_count = 0
151+ self ._maintenance_interval = 50000 # events between cache sweeps
152+ self ._cmdline_cache_max = 100000 # hard cap on cmdline cache entries
153+
145154 if cache_sample_rate > 1 :
146155 self .writer .set_cache_sampling (cache_sample_rate )
147156
@@ -191,9 +200,11 @@ def _print_event(self, cpu, data, size):
191200 data: Raw event data pointer
192201 size: Size of the event data
193202 """
203+ self ._tick_maintenance ()
204+
194205 event = self .b ["events" ].event (data )
195206 op_name = self .flag_mapper .op_fs_types .get (event .op , "[unknown]" )
196-
207+
197208 try :
198209 filename = event .filename .decode ()
199210 if self .anonymous :
@@ -517,6 +528,46 @@ def _read_cmdline_cached(self, pid: int) -> str:
517528 self .cmdline_cache [pid ] = result
518529 return result
519530
531+ def _tick_maintenance (self ) -> None :
532+ """
533+ Advance the event counter and run cache maintenance when it is due.
534+
535+ Called from every perf callback that runs on the polling thread and
536+ touches the long-lived caches (``_print_event``, ``_print_event_dual``,
537+ ``_print_event_io_uring``) so a workload dominated by any single event
538+ family — e.g. rename/link/symlink (dual) or async io_uring I/O — still
539+ triggers eviction instead of growing the caches unbounded.
540+ """
541+ self ._event_count += 1
542+ if self ._event_count % self ._maintenance_interval == 0 :
543+ self ._run_cache_maintenance ()
544+
545+ def _run_cache_maintenance (self ) -> None :
546+ """
547+ Bound the long-lived caches so an indefinite trace does not leak memory.
548+
549+ Runs from the perf-callback (polling) thread, which is the only thread
550+ that mutates ``cmdline_cache`` and the path resolver caches, so no
551+ locking is required.
552+
553+ - Delegates to ``PathResolver.cleanup_old_cache`` (otherwise never
554+ called), which prunes stale per-PID entries and caps ``inode_to_path``.
555+ - Caps ``cmdline_cache`` at ``_cmdline_cache_max`` entries. Entries are
556+ deliberately retained past process exit so that CLOSE/EXIT events
557+ buffered after the process is gone can still resolve a cmdline; when
558+ the cap is exceeded we drop the oldest half (dicts preserve insertion
559+ order), keeping the most recently seen PIDs.
560+ """
561+ try :
562+ self .path_resolver .cleanup_old_cache ()
563+ except Exception as e :
564+ if self .verbose :
565+ logger ("warning" , f"Path resolver cache cleanup failed: { e } " )
566+
567+ if len (self .cmdline_cache ) > self ._cmdline_cache_max :
568+ items = list (self .cmdline_cache .items ())
569+ self .cmdline_cache = dict (items [len (items ) // 2 :])
570+
520571 def _handle_process_exec (self , pid : int ) -> None :
521572 """
522573 Clear the mmap_regions and cmdline caches for a PID on exec.
@@ -551,6 +602,8 @@ def _print_event_dual(self, cpu, data, size):
551602 data: Raw event data pointer
552603 size: Size of the event data
553604 """
605+ self ._tick_maintenance ()
606+
554607 event = self .b ["events_dual" ].event (data )
555608 op_name = self .flag_mapper .op_fs_types .get (event .op , "[unknown]" )
556609
@@ -752,6 +805,8 @@ def _print_event_io_uring(self, cpu, data, size):
752805 data: Raw event data pointer
753806 size: Size of the event data
754807 """
808+ self ._tick_maintenance ()
809+
755810 e = self .b ["io_uring_events" ].event (data )
756811 ts = datetime .today ()
757812
@@ -1096,7 +1151,10 @@ def trace(self):
10961151 run_with_spinner ("Compressing trace output" , self .writer .force_flush )
10971152
10981153 if self .automatic_upload :
1099- run_with_spinner ("Uploading traces" , lambda : self .upload_manager .stop_worker (False ))
1154+ # Drain the upload queue before stopping the workers — passing
1155+ # False here set the stop event immediately and abandoned any
1156+ # traces still queued for upload.
1157+ run_with_spinner ("Uploading traces" , lambda : self .upload_manager .stop_worker (True , timeout = 30 ))
11001158 try :
11011159 os .removedirs (self .writer .output_dir )
11021160 except OSError :
0 commit comments