| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: GPL-2.0 | |
| """Measures futex contention.""" | |
| from __future__ import annotations | |
| import argparse | |
| from collections import defaultdict | |
| from typing import Dict, Tuple | |
| import perf | |
| class LockStats: | |
| """Aggregate lock contention information.""" | |
| def __init__(self) -> None: | |
| self.count = 0 | |
| self.total_time = 0 | |
| self.min_time = 0 | |
| self.max_time = 0 | |
| def add(self, duration: int) -> None: | |
| """Add a new duration measurement.""" | |
| self.count += 1 | |
| self.total_time += duration | |
| if self.count == 1: | |
| self.min_time = duration | |
| self.max_time = duration | |
| else: | |
| self.min_time = min(self.min_time, duration) | |
| self.max_time = max(self.max_time, duration) | |
| def avg(self) -> float: | |
| """Return average duration.""" | |
| return self.total_time / self.count if self.count > 0 else 0.0 | |
| process_names: Dict[int, str] = {} | |
| start_times: Dict[int, Tuple[int, int]] = {} | |
| session = None | |
| durations: Dict[Tuple[int, int], LockStats] = defaultdict(LockStats) | |
| FUTEX_WAIT = 0 | |
| FUTEX_PRIVATE_FLAG = 128 | |
| FUTEX_CLOCK_REALTIME = 256 | |
| # Mask out FUTEX_PRIVATE_FLAG and FUTEX_CLOCK_REALTIME so variants such as | |
| # FUTEX_WAIT_PRIVATE (0 | 128) match the base FUTEX_WAIT command. | |
| FUTEX_CMD_MASK = ~(FUTEX_PRIVATE_FLAG | FUTEX_CLOCK_REALTIME) | |
| def handle_start(pid: int, tid: int, uaddr: int, op: int, start_time: int) -> None: | |
| """Handle a futex sys_enter event.""" | |
| if (op & FUTEX_CMD_MASK) != FUTEX_WAIT: | |
| return | |
| try: | |
| if session: | |
| process = session.find_thread(pid, tid) | |
| if process: | |
| process_names[tid] = process.comm() or "unknown" | |
| except (TypeError, AttributeError): | |
| pass | |
| if tid not in process_names: | |
| process_names[tid] = "unknown" | |
| start_times[tid] = (uaddr, start_time) | |
| def handle_end(tid: int, end_time: int) -> None: | |
| """Handle a futex sys_exit event.""" | |
| if tid not in start_times: | |
| return | |
| (uaddr, start_time) = start_times[tid] | |
| del start_times[tid] | |
| durations[(tid, uaddr)].add(end_time - start_time) | |
| def process_event(sample: perf.sample_event) -> None: | |
| """Process a single sample event.""" | |
| event_name = str(sample.evsel) | |
| if event_name.startswith("evsel(") and event_name.endswith(")"): | |
| event_name = event_name[6:-1] | |
| if event_name.startswith("syscalls:sys_enter_futex"): | |
| uaddr = getattr(sample, "uaddr", None) | |
| op = getattr(sample, "op", None) | |
| if uaddr is None or op is None: | |
| return # Tracepoint fields missing, skip silent attribution to 0 | |
| handle_start(getattr(sample, "sample_pid", sample.sample_tid), | |
| sample.sample_tid, uaddr, op, sample.sample_time) | |
| elif event_name.startswith("syscalls:sys_exit_futex"): | |
| handle_end(sample.sample_tid, sample.sample_time) | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser(description="Measure futex contention") | |
| ap.add_argument("-i", "--input", default="perf.data", help="Input file name") | |
| args = ap.parse_args() | |
| try: | |
| session = perf.session(perf.data(args.input), sample=process_event) | |
| try: | |
| session.process_events() | |
| except KeyboardInterrupt: | |
| pass | |
| for ((t, u), stats) in sorted(durations.items()): | |
| avg_ns = stats.avg() | |
| print(f"{process_names.get(t, 'unknown')}[{t}] lock {u:x} contended " | |
| f"{stats.count} times, {avg_ns:.0f} avg ns " | |
| f"[max: {stats.max_time} ns, min {stats.min_time} ns]") | |
| finally: | |
| # Break the reference cycle between the global session and the | |
| # process_event callback so the C perf.session object is freed. | |
| session = None |