| #!/usr/bin/python3 | |
| # | |
| # Copyright (C) 2013-2026 Frederic Weisbecker | |
| # Copyright (C) 2024-2026 Marco Crivellari | |
| # SPDX-License-Identifier: GPL-3.0-or-later | |
| import re | |
| import sys | |
| import statistics | |
| import os | |
| import argparse | |
| from collections import defaultdict | |
| from cpunoise_lib import parse_cpulist, get_nohz_full_cpus | |
| from check_configs import check_configs | |
| from log_output import Log | |
| class Noise: | |
| def __init__(self, ts): | |
| self.start = ts | |
| self.end = None | |
| def complete(self, end): | |
| self.end = end | |
| def duration(self): | |
| if self.end is None: | |
| return None | |
| else: | |
| return self.end - self.start | |
| def match_exit(self, ne): | |
| return False | |
| def histable(self): | |
| return True | |
| class SchedLatNoise(Noise): | |
| def __init__(self, next_task, ts): | |
| super().__init__(ts) | |
| self.next_task = next_task | |
| def create_hist(self): | |
| return SchedLatHist(self) | |
| def histable(self): | |
| return not isinstance(self.next_task, IdleTask) | |
| class IrqWorkNoise(Noise): | |
| def create_hist(self): | |
| return IrqWorkHist(self) | |
| class LocalTimerNoise(Noise): | |
| def create_hist(self): | |
| return LocalTimerHist(self) | |
| class PageFaultNoise(Noise): | |
| def create_hist(self): | |
| return PageFaultHist(self) | |
| class ReschedVecNoise(Noise): | |
| def create_hist(self): | |
| return ReschedVecHist(self) | |
| class SchedNoise(Noise): | |
| def __init__(self, next_task, ts): | |
| super().__init__(ts) | |
| self.next_task = next_task | |
| def create_hist(self): | |
| return SchedHist(self) | |
| def histable(self): | |
| return not isinstance(self.next_task, IdleTask) | |
| class SchedPreemptNoise(Noise): | |
| def __init__(self, next_task, ts): | |
| super().__init__(ts) | |
| self.next_task = next_task | |
| def create_hist(self): | |
| return SchedPreemptHist(self) | |
| def match_exit(self, n): | |
| return isinstance(self, n) | |
| def histable(self): | |
| return not isinstance(self.next_task, IdleTask) | |
| class TickDepNoise(Noise): | |
| def __init__(self, ts, dep): | |
| super().__init__(ts) | |
| self.dep = dep | |
| def create_hist(self): | |
| return TickDepHist(self, self.dep) | |
| class SoftirqNoise(Noise): | |
| def __init__(self, ts, vec): | |
| super().__init__(ts) | |
| self.vec = vec | |
| def create_hist(self): | |
| return SoftirqHist(self, self.vec) | |
| def match_exit(self, ne): | |
| return self.vec == ne.vec | |
| class CallbackNoise(Noise): | |
| def __init__(self, ts, func): | |
| super().__init__(ts) | |
| self.func = func | |
| def create_hist(self): | |
| return CallbackHist(self, self.func, self.name()) | |
| def match_exit(self, ne): | |
| return type(self) is type(ne) and self.func == ne.func | |
| class CsdNoise(CallbackNoise): | |
| def name(self): | |
| return "CSD (IPI)" | |
| class TimerNoise(CallbackNoise): | |
| def name(self): | |
| return "Timer" | |
| class WorkqueueNoise(CallbackNoise): | |
| def name(self): | |
| return "Workqueue" | |
| class SyscallNoise(CallbackNoise): | |
| def name(self): | |
| return "Syscall" | |
| class UnknownNoise(Noise): | |
| def create_hist(self): | |
| return UnknownHist(self) | |
| def __repr__(self): | |
| return "Unknown" | |
| class Hist: | |
| def __init__(self, n): | |
| self.nr_noise = 0 | |
| self.duration_list = [] | |
| self.add(n) | |
| def add(self, n): | |
| self.nr_noise += 1 | |
| duration = n.duration() | |
| if duration is not None: | |
| self.duration_list.append(duration) | |
| class TickDepHist(Hist): | |
| def __init__(self, n, dep): | |
| super().__init__(n) | |
| self.dep = dep | |
| def noise_match(self, n): | |
| return isinstance(n, TickDepNoise) | |
| def __repr__(self): | |
| return f"Tick dep[{self.dep}]" | |
| class IrqWorkHist(Hist): | |
| def noise_match(self, n): | |
| return isinstance(n, IrqWorkNoise) | |
| def __repr__(self): | |
| return "Irq work" | |
| class LocalTimerHist(Hist): | |
| def noise_match(self, n): | |
| return isinstance(n, LocalTimerNoise) | |
| def __repr__(self): | |
| return "Local timer" | |
| class PageFaultHist(Hist): | |
| def noise_match(self, n): | |
| return isinstance(n, PageFaultNoise) | |
| def __repr__(self): | |
| return "Page fault" | |
| class ReschedVecHist(Hist): | |
| def noise_match(self, n): | |
| return isinstance(n, ReschedVecNoise) | |
| def __repr__(self): | |
| return "Reschedule IPI" | |
| class SchedLatHist(Hist): | |
| def __init__(self, n): | |
| super().__init__(n) | |
| self.next_task = n.next_task | |
| def noise_match(self, n): | |
| return isinstance(n, SchedLatNoise) and n.next_task is self.next_task | |
| def __repr__(self): | |
| return "Wakeup Latency" | |
| class SchedPreemptHist(Hist): | |
| def __init__(self, n): | |
| super().__init__(n) | |
| self.next_task = n.next_task | |
| def noise_match(self, n): | |
| return isinstance(n, SchedPreemptNoise) and n.next_task is self.next_task | |
| def __repr__(self): | |
| return "Preempt. noise" | |
| class SchedHist(Hist): | |
| def __init__(self, n): | |
| super().__init__(n) | |
| self.next_task = n.next_task | |
| def noise_match(self, n): | |
| return isinstance(n, SchedNoise) and n.next_task is self.next_task | |
| def __repr__(self): | |
| return f"Sched switch: {self.next_task.comm}:{self.next_task.pid}" | |
| class SoftirqHist(Hist): | |
| def __init__(self, n, vec): | |
| super().__init__(n) | |
| self.vec = vec | |
| def noise_match(self, n): | |
| return isinstance(n, SoftirqNoise) and self.vec == n.vec | |
| def __repr__(self): | |
| return f"Softirq[{self.vec}]" | |
| class CallbackHist(Hist): | |
| def __init__(self, n, func, name): | |
| super().__init__(n) | |
| self.func = func | |
| self.noise_class = n.__class__ | |
| self.name = name | |
| def noise_match(self, n): | |
| return isinstance(n, self.noise_class) and self.func == n.func | |
| def __repr__(self): | |
| return f"{self.name} (func = {self.func})" | |
| class UnknownHist(Hist): | |
| def noise_match(self, n): | |
| return isinstance(n, UnknownNoise) | |
| def __repr__(self): | |
| return "Unknown" | |
| class Task: | |
| current = {} | |
| CTX_UNKNOWN = -1 | |
| CTX_KERNEL = 0 | |
| CTX_USER = 1 | |
| def __init__(self, pid, comm, cpu, state=None): | |
| self.pid = pid | |
| self.comm = comm | |
| self.cpus = set([cpu]) | |
| self.curr_cpu = cpu | |
| self.curr_state = state | |
| self.noise_stack = [] | |
| self.hists = [] | |
| self.nr_noise = 0 | |
| self.seq_nr_noise = 0 | |
| self.nr_hists = 0 | |
| self.noise_duration = 0 | |
| self.ctx = Task.CTX_UNKNOWN | |
| self.kernel_entry_ts = -1 | |
| def sched_out(self, next_task, ts, state): | |
| self.curr_state = state | |
| if self.is_preempted(): | |
| n = SchedPreemptNoise(self, ts) | |
| self.push_noise(n) | |
| n = SchedNoise(next_task, ts) | |
| self.push_noise(n) | |
| next_task.sched_in(ts, self.curr_cpu) | |
| def sched_in(self, ts, cpu): | |
| self.curr_cpu = cpu | |
| self.cpus.add(cpu) | |
| Task.current[cpu] = self | |
| self.pop_noise_match_class(ts, SchedNoise) | |
| self.pop_noise_match_exit(ts, SchedPreemptNoise) | |
| def add_noise(self, n): | |
| self.nr_noise += 1 | |
| self.add_hist(n) | |
| def push_noise(self, n): | |
| self.seq_nr_noise += 1 | |
| self.noise_stack.append(n) | |
| def __pop_noise(self, ts): | |
| if not self.noise_stack: | |
| return | |
| n = self.noise_stack.pop() | |
| if ts is not None: | |
| n.complete(ts) | |
| self.add_noise(n) | |
| def pop_noise_match_class(self, ts, noise_class): | |
| if not self.noise_stack: | |
| return | |
| if not isinstance(self.noise_stack[-1], noise_class): | |
| print(f"Expected last event was sched_switch on pid {self.pid} ts: {ts}") | |
| return | |
| self.__pop_noise(ts) | |
| def pop_noise_match_exit(self, ts, ne): | |
| for n in self.noise_stack[::-1]: | |
| if n.match_exit(ne): | |
| self.noise_stack.remove(n) | |
| n.complete(ts) | |
| self.add_noise(n) | |
| return | |
| def add_hist(self, n): | |
| if not n.histable(): | |
| return | |
| self.nr_hists += 1 | |
| for h in self.hists: | |
| if h.noise_match(n): | |
| h.add(n) | |
| return | |
| self.hists.append(n.create_hist()) | |
| def flush_noise_stack(self, ts): | |
| if self.seq_nr_noise > 1: | |
| ts = None | |
| while self.noise_stack: | |
| self.__pop_noise(ts) | |
| self.seq_nr_noise = 0 | |
| def kernel_exit(self, ts): | |
| if self.ctx == Task.CTX_KERNEL: | |
| self.noise_duration += ts - self.kernel_entry_ts | |
| if self.seq_nr_noise == 0: | |
| n = UnknownNoise(self.kernel_entry_ts) | |
| n.complete(ts) | |
| self.add_hist(n) | |
| elif self.ctx != Task.CTX_UNKNOWN: | |
| print(f"Unexpected kernel exit at {ts} (lost traces?)") | |
| self.ctx = Task.CTX_USER | |
| self.flush_noise_stack(ts) | |
| def kernel_enter(self, ts): | |
| if self.ctx not in (Task.CTX_USER, Task.CTX_UNKNOWN): | |
| print(f"Unexpected kernel entry at {ts} (lost traces?)") | |
| self.kernel_entry_ts = ts | |
| self.ctx = Task.CTX_KERNEL | |
| def is_awakened(self): | |
| return bool(self.noise_stack) and isinstance(self.noise_stack[-1], SchedLatNoise) | |
| def is_preempted(self): | |
| return self.curr_state == 'R' and not isinstance(self, IdleTask) | |
| def __repr__(self): | |
| ret = f"Task: {self.comm} Pid: {self.pid} cpus: {str(self.cpus).strip()}" | |
| if len(self.cpus) > 1: | |
| ret += " (Migrated!!!)" | |
| return ret | |
| class IdleTask(Task): | |
| pass | |
| taskdict = {} | |
| idle_taskdict = {} | |
| def get_task(pid, comm, cpu, state=None): | |
| if pid == 0: | |
| if cpu in idle_taskdict: | |
| return (False, idle_taskdict[cpu]) | |
| t = IdleTask(pid, comm, cpu, state) | |
| idle_taskdict[cpu] = t | |
| else: | |
| if pid in taskdict: | |
| return (False, taskdict[pid]) | |
| t = Task(pid, comm, cpu, state) | |
| taskdict[pid] = t | |
| return (True, t) | |
| # --- Pre-compiled Regular Expressions for Log Parsing --- | |
| RE_TICK_STOP = re.compile(r"tick_stop: .+?dependency=(\w+)", re.S) | |
| RE_RCU_DYNTICK = re.compile(r"rcu_dyntick: (\S+)", re.S) | |
| RE_SCHED_WAKEUP_1 = re.compile(r"sched_wakeup:\s+comm=(.+?)\s+pid=([0-9]+)") | |
| RE_SCHED_WAKEUP_2 = re.compile(r"(?:sched_wakeup:\s+)?(.+?):([0-9]+)\s+\[([0-9]+)\]\s+CPU:([0-9]+)") | |
| RE_SCHED_SWITCH_1 = re.compile(r"sched_switch:\s+prev_comm=.+? prev_pid=[0-9]+.+?prev_state=(\S+)\s+==> next_comm=(.+?) next_pid=([0-9]+)", re.S) | |
| RE_SCHED_SWITCH_2 = re.compile(r"sched_switch:\s+.+?:[0-9]+.+?\[.+?\]\s+(\S+)\s+==> (.+?):([0-9]+)", re.S) | |
| RE_SOFTIRQ_ENTRY = re.compile(r"softirq_entry:\s+vec=[0-9] \[action=(.+?)\]", re.S) | |
| RE_SOFTIRQ_EXIT = re.compile(r"softirq_exit:\s+vec=[0-9] \[action=(.+?)\]", re.S) | |
| RE_IRQ_WORK_ENTRY = re.compile(r"irq_work_entry:.+", re.S) | |
| RE_PAGE_FAULT_USER = re.compile(r"page_fault_user:.+", re.S) | |
| RE_RESCHEDULE_ENTRY = re.compile(r"reschedule_entry:.+", re.S) | |
| RE_PAGE_FAULT_KERNEL = re.compile(r"page_fault_kernel:.+", re.S) | |
| RE_LOCAL_TIMER_ENTRY = re.compile(r"local_timer_entry:.+", re.S) | |
| RE_CSD_FUNC_ENTRY = re.compile(r"csd_function_entry:\s+func=([^,]+),", re.S) | |
| RE_CSD_FUNC_EXIT = re.compile(r"csd_function_exit:\s+func=([^,]+),", re.S) | |
| RE_TIMER_EXPIRE_ENTRY = re.compile(r"timer_expire_entry: timer=\S+ function=(\S+)", re.S) | |
| RE_HRTIMER_EXPIRE_ENTRY = re.compile(r"hrtimer_expire_entry: hrtimer=\S+ .*?function=(\S+)", re.S) | |
| RE_WQ_EXECUTE_START = re.compile(r"workqueue_execute_start: work struct [^:]+: function (\S+)", re.S) | |
| RE_WQ_EXECUTE_END = re.compile(r"workqueue_execute_end: work struct [^:]+: function (\S+)", re.S) | |
| RE_SYSCALL_ENTER = re.compile(r"(sys_[^(]+)[(]", re.S) | |
| RE_SYSCALL_EXIT = re.compile(r"(sys_[^(]+) ->", re.S) | |
| RE_UNHANDLED = re.compile(r"([^: (]+).+", re.S) | |
| TRACE_REG = re.compile(r"^\s+(.+?)-([0-9]+)\s+\[([0-9]{3})\].+?([0-9]+.[0-9]{6}):\s+(.+?)$", re.S) | |
| def tick_stop(line, task, cpu, ts): | |
| s = RE_TICK_STOP.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| dep = s.group(1) | |
| if dep != "NONE": | |
| n = TickDepNoise(ts, dep) | |
| task.push_noise(n) | |
| return True | |
| def rcu_dyntick(line, task, cpu, ts): | |
| s = RE_RCU_DYNTICK.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| what = s.group(1) | |
| if what in ("Start", "StartIrq"): | |
| task.kernel_exit(ts) | |
| elif what in ("End", "EndIrq"): | |
| task.kernel_enter(ts) | |
| return True | |
| def sched_wakeup(line, task, cpu, ts): | |
| s = RE_SCHED_WAKEUP_1.search(line) | |
| if s is None: | |
| s = RE_SCHED_WAKEUP_2.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| wake_comm, wake_pid = s.group(1), int(s.group(2)) | |
| _, wake_task = get_task(wake_pid, wake_comm, cpu) | |
| n = SchedLatNoise(wake_task, ts) | |
| wake_task.push_noise(n) | |
| return True | |
| def sched_switch(line, task, cpu, ts): | |
| s = RE_SCHED_SWITCH_1.search(line) | |
| if s is None: | |
| s = RE_SCHED_SWITCH_2.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| state, next_comm, next_pid = s.group(1), s.group(2), int(s.group(3)) | |
| _, next_task = get_task(next_pid, next_comm, cpu, state) | |
| if next_task.is_awakened(): | |
| next_task.pop_noise_match_class(ts, SchedLatNoise) | |
| task.sched_out(next_task, ts, state) | |
| return True | |
| def softirq_entry(line, task, cpu, ts): | |
| s = RE_SOFTIRQ_ENTRY.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| n = SoftirqNoise(ts, s.group(1)) | |
| task.push_noise(n) | |
| return True | |
| def softirq_exit(line, task, cpu, ts): | |
| s = RE_SOFTIRQ_EXIT.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| n = SoftirqNoise(ts, s.group(1)) | |
| task.pop_noise_match_exit(ts, n) | |
| return True | |
| def irq_work_entry(line, task, cpu, ts): | |
| if not RE_IRQ_WORK_ENTRY.search(line): | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(IrqWorkNoise(ts)) | |
| return True | |
| def page_fault_user(line, task, cpu, ts): | |
| if not RE_PAGE_FAULT_USER.search(line): | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(PageFaultNoise(ts)) | |
| return True | |
| def reschedule_entry(line, task, cpu, ts): | |
| if not RE_RESCHEDULE_ENTRY.search(line): | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(ReschedVecNoise(ts)) | |
| return True | |
| def page_fault_kernel(line, task, cpu, ts): | |
| if not RE_PAGE_FAULT_KERNEL.search(line): | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(PageFaultNoise(ts)) | |
| return True | |
| def local_timer_entry(line, task, cpu, ts): | |
| if not RE_LOCAL_TIMER_ENTRY.search(line): | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(LocalTimerNoise(ts)) | |
| return True | |
| def csd_function_entry(line, task, cpu, ts): | |
| s = RE_CSD_FUNC_ENTRY.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(CsdNoise(ts, s.group(1))) | |
| return True | |
| def csd_function_exit(line, task, cpu, ts): | |
| s = RE_CSD_FUNC_EXIT.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| task.pop_noise_match_exit(ts, CsdNoise(ts, s.group(1))) | |
| return True | |
| def timer_expire_entry(line, task, cpu, ts): | |
| s = RE_TIMER_EXPIRE_ENTRY.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(TimerNoise(ts, s.group(1))) | |
| return True | |
| def hrtimer_expire_entry(line, task, cpu, ts): | |
| s = RE_HRTIMER_EXPIRE_ENTRY.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(TimerNoise(ts, s.group(1))) | |
| return True | |
| def workqueue_execute_start(line, task, cpu, ts): | |
| s = RE_WQ_EXECUTE_START.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| task.push_noise(WorkqueueNoise(ts, s.group(1))) | |
| return True | |
| def workqueue_execute_end(line, task, cpu, ts): | |
| s = RE_WQ_EXECUTE_END.search(line) | |
| if s is None: | |
| print(line) | |
| sys.exit(-1) | |
| task.pop_noise_match_exit(ts, WorkqueueNoise(ts, s.group(1))) | |
| return True | |
| def syscall_enter(line, task, cpu, ts): | |
| s = RE_SYSCALL_ENTER.search(line) | |
| if s is None: | |
| return False | |
| task.push_noise(SyscallNoise(ts, s.group(1))) | |
| return True | |
| def syscall_exit(line, task, cpu, ts): | |
| s = RE_SYSCALL_EXIT.search(line) | |
| if s is None: | |
| return False | |
| task.pop_noise_match_exit(ts, SyscallNoise(ts, s.group(1))) | |
| return True | |
| def syscall(line, task, cpu, ts): | |
| return syscall_enter(line, task, cpu, ts) or syscall_exit(line, task, cpu, ts) | |
| unhandled_events = set() | |
| def unhandled_event(line, task, cpu, ts): | |
| s = RE_UNHANDLED.match(line) | |
| if s is None: | |
| print(f"Unhandled event parsing issue: {line}") | |
| sys.exit(-1) | |
| unhandled_events.add(s.group(1)) | |
| return True | |
| # --- CMD-LINE PARSER --- | |
| def parse_arguments(): | |
| parser = argparse.ArgumentParser(description="Monitor sources of interference on isolated CPUs") | |
| parser.add_argument("-t", "--task", dest="exact_tasks", action="append", default=[], | |
| help="Tasks pattern to filter (can be specified multiple times). Matches any tasks that starts with the specified pattern") | |
| parser.add_argument("-p", "--pid-task", dest="pid_tasks", action="append", default=[], | |
| help="Match PID of the task") | |
| parser.add_argument("-c", "--cpulist", dest="cpulist_raw", type=str, | |
| help="List of nohz_full CPUs (ex: 2-11,13-18)") | |
| parser.add_argument("-o", "--check-config-only", dest="only_configs", action="store_true", default=False, | |
| help="Run a configuration check only. If a trace file is present, it will be ignored") | |
| parser.add_argument("-s", "--skip-configs", dest="skip_config", action="store_true", default=False, | |
| help="Skip the configuration check. Useful if noise_parser is only used to parse captured data of another machine") | |
| parser.add_argument("-C", "--no-color", dest="no_color", action="store_true", default=False, | |
| help="Disable colored output, even on a terminal. Output is already plain text when redirected to a file or pipe") | |
| parser.add_argument("trace_file", nargs='?', help="Trace file to parse") | |
| return parser.parse_args() | |
| def task_matches_filters(task_name, task_pid, exact_tasks=None, pid_tasks=None): | |
| exact_tasks = exact_tasks or [] | |
| pid_tasks = pid_tasks or [] | |
| if not exact_tasks and not pid_tasks: | |
| return True | |
| if task_name in exact_tasks: | |
| return True | |
| if str(task_pid) in pid_tasks: | |
| return True | |
| return False | |
| def get_hist_category(h): | |
| if isinstance(h, CallbackHist): | |
| return h.name | |
| category_map = { | |
| TickDepHist: "Tick dependency", | |
| SchedHist: "Sched switch", | |
| SoftirqHist: "Softirq" | |
| } | |
| return category_map.get(type(h), str(h)) | |
| def task_duration_stats(t): | |
| durations = [sum(h.duration_list) for h in t.hists if h.duration_list] | |
| return sum(durations), bool(durations) | |
| def print_table(title, rows, total_row, name_width=60, nr_width=10, dur_width=15): | |
| sep = "=" * (name_width + nr_width + dur_width + 2) | |
| print("\n" + sep) | |
| Log.header(title) | |
| print(sep) | |
| print(f"{'Name':<{name_width}} {'Nr':>{nr_width}} {'Duration (us)':>{dur_width}}") | |
| for name, nr, dur_str in rows: | |
| print(f"{name:<{name_width}} {nr:>{nr_width},} {dur_str:>{dur_width}}") | |
| print("-" * (name_width + nr_width + dur_width + 2)) | |
| total_name, total_nr, total_dur = total_row | |
| print(f"{total_name:<{name_width}} {total_nr:>{nr_width},} {total_dur:>{dur_width},}\n") | |
| events = [ | |
| ("tick_stop:", tick_stop), | |
| ("rcu_dyntick:", rcu_dyntick), | |
| ("sched_wakeup", sched_wakeup), | |
| ("sched_switch", sched_switch), | |
| ("softirq_entry", softirq_entry), | |
| ("softirq_exit", softirq_exit), | |
| ("irq_work_entry", irq_work_entry), | |
| ("page_fault_user", page_fault_user), | |
| ("reschedule_entry", reschedule_entry), | |
| ("local_timer_entry", local_timer_entry), | |
| ("page_fault_kernel", page_fault_kernel), | |
| ("csd_function_exit", csd_function_exit), | |
| ("csd_function_entry", csd_function_entry), | |
| ("timer_expire_entry", timer_expire_entry), | |
| ("hrtimer_expire_entry", hrtimer_expire_entry), | |
| ("workqueue_execute_start", workqueue_execute_start), | |
| ("workqueue_execute_end", workqueue_execute_end), | |
| ("sys_", syscall), | |
| ("", unhandled_event), # must be last! | |
| ] | |
| if __name__ == "__main__": | |
| args = parse_arguments() | |
| if args.no_color: | |
| Log.enable_color = False | |
| cpulist = parse_cpulist(args.cpulist_raw) if args.cpulist_raw else None | |
| if cpulist is None: | |
| print("INFO: --cpulist param. not set: reading nohz_full from sysfs") | |
| cpulist = get_nohz_full_cpus() | |
| if not args.skip_config: | |
| check_configs(cpulist) | |
| if args.only_configs: | |
| sys.exit(0) | |
| if not args.trace_file or not os.path.exists(args.trace_file): | |
| print(f"File '{args.trace_file}' not found.\n") | |
| sys.exit(0) | |
| Log.header("Monitoring sources of interference on isolated cpus") | |
| fsize = os.path.getsize(args.trace_file) | |
| cursor = 0 | |
| nr = 0 | |
| with open(args.trace_file, 'r') as f: | |
| for line in f: | |
| cursor += len(line) | |
| if nr % 1000 == 0: # Throttle stderr writes for performance | |
| sys.stderr.write(f"Processing {cursor / fsize * 100:.2f}%\r") | |
| nr += 1 | |
| s = TRACE_REG.match(line) | |
| if s is not None: | |
| comm = s.group(1) | |
| pid = int(s.group(2)) | |
| cpu = int(s.group(3)) | |
| raw_ts = s.group(4).split(".") | |
| ts = (int(raw_ts[0]) * (10**6)) + int(raw_ts[1]) | |
| rest = s.group(5) | |
| if cpulist is not None and cpu not in cpulist: | |
| continue | |
| is_new, task = get_task(pid, comm, cpu) | |
| if is_new: | |
| task.sched_in(0, cpu) | |
| for prefix, handler in events: | |
| if rest.startswith(prefix): | |
| if handler(rest, task, cpu, ts): | |
| break | |
| tasklist = list(taskdict.values()) | |
| for t in tasklist: | |
| t.flush_noise_stack(None) | |
| print(f"\nUnhandled events: {','.join(unhandled_events)}\n") | |
| tasklist_sorted = sorted(tasklist, key=lambda t: t.noise_duration, reverse=True) | |
| category_summary = defaultdict(lambda: {'nr': 0, 'duration': 0, 'has_known_duration': False}) | |
| total_summary_nr = 0 | |
| total_summary_duration = 0 | |
| valid_tasks = [] | |
| for t in tasklist_sorted: | |
| if t.nr_hists == 0: | |
| continue | |
| if not task_matches_filters(t.comm, t.pid, args.exact_tasks, args.pid_tasks): | |
| continue | |
| valid_tasks.append(t) | |
| t.hists = sorted(t.hists, key=lambda h: sum(h.duration_list), reverse=True) | |
| print(f"{str(t):<65} {'Nr':>10} {'Duration (us)':>17}") | |
| for h in t.hists: | |
| duration_str = "Unknown" if not h.duration_list else str(sum(h.duration_list)) | |
| print(f" {str(h):<61} {h.nr_noise:>10} {duration_str:>17}") | |
| cat = get_hist_category(h) | |
| category_summary[cat]['nr'] += h.nr_noise | |
| total_summary_nr += h.nr_noise | |
| if h.duration_list: | |
| dur = sum(h.duration_list) | |
| category_summary[cat]['duration'] += dur | |
| category_summary[cat]['has_known_duration'] = True | |
| total_summary_duration += dur | |
| print("\n" + " " * 66 + "-" * 28) | |
| print(f" {'Total':<61} {t.nr_hists:>10} {t.noise_duration:>17}\n") | |
| # --- Print Summaries --- | |
| task_rows = [] | |
| for t in valid_tasks: | |
| task_total_dur, has_known = task_duration_stats(t) | |
| migrated_str = " (Migrated!!!)" if len(t.cpus) > 1 else "" | |
| task_str = f"{t.comm} (pid {t.pid}, cpus {str(t.cpus).strip()}){migrated_str}" | |
| dur_str = f"{task_total_dur:,}" if has_known else "Unknown" | |
| task_rows.append((task_str, t.nr_hists, dur_str, task_total_dur)) | |
| task_rows.sort(key=lambda row: row[3], reverse=True) | |
| task_rows = [row[:3] for row in task_rows] | |
| print_table("=== Summary: Top Tasks by Noise Duration ===", task_rows, | |
| ("Total", total_summary_nr, total_summary_duration)) | |
| sorted_categories = sorted(category_summary.items(), key=lambda item: item[1]['duration'], reverse=True) | |
| category_rows = [] | |
| for cat, stats in sorted_categories: | |
| dur_str = f"{stats['duration']:,}" if stats['has_known_duration'] else "Unknown" | |
| category_rows.append((cat, stats['nr'], dur_str)) | |
| print_table("=== Summary: Noise by Category (all isolated CPUs) ===", category_rows, | |
| ("Total", total_summary_nr, total_summary_duration)) |