#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 """ Print Intel PT Events including Power Events and PTWRITE. Ported from tools/perf/scripts/python/intel-pt-events.py """ from __future__ import annotations import argparse from typing import Dict, List, Tuple import contextlib from ctypes import addressof, create_string_buffer import io import os import struct import sys from typing import Any, Optional import typing import perf try: from libxed import LibXED as _LibXED LibXED: Optional[type[_LibXED]] = _LibXED except ImportError: LibXED = None def sample_flags_to_name(flags: int) -> str: """Implement perf's sample_flags_to_name.""" if not isinstance(flags, int) or flags == 0: return "".ljust(21) # PERF_IP_FLAG_* bit positions (from tools/perf/util/event.h): # bit 0: PERF_IP_FLAG_BRANCH # bit 1: PERF_IP_FLAG_CALL # bit 2: PERF_IP_FLAG_RETURN # bit 3: PERF_IP_FLAG_CONDITIONAL # bit 4: PERF_IP_FLAG_SYSCALLRET # bit 5: PERF_IP_FLAG_ASYNC # bit 6: PERF_IP_FLAG_INTERRUPT # bit 7: PERF_IP_FLAG_TX_ABORT # bit 8: PERF_IP_FLAG_TRACE_BEGIN # bit 9: PERF_IP_FLAG_TRACE_END # bit 10: PERF_IP_FLAG_IN_TX # bit 11: PERF_IP_FLAG_VMENTRY # bit 12: PERF_IP_FLAG_VMEXIT # bit 13: PERF_IP_FLAG_INTR_DISABLE # bit 14: PERF_IP_FLAG_INTR_TOGGLE # bit 15: PERF_IP_FLAG_BRANCH_MISS # bit 16: PERF_IP_FLAG_NOT_TAKEN sample_flags = [ ((1 << 0) | (1 << 1), "call"), ((1 << 0) | (1 << 2), "return"), ((1 << 0) | (1 << 3), "jcc"), ((1 << 0), "jmp"), ((1 << 0) | (1 << 1) | (1 << 6), "int"), ((1 << 0) | (1 << 2) | (1 << 6), "iret"), ((1 << 0) | (1 << 1) | (1 << 4), "syscall"), ((1 << 0) | (1 << 2) | (1 << 4), "sysret"), ((1 << 0) | (1 << 5), "async"), ((1 << 0) | (1 << 1) | (1 << 5) | (1 << 6), "hw int"), ((1 << 0) | (1 << 7), "tx abrt"), ((1 << 0) | (1 << 8), "tr strt"), ((1 << 0) | (1 << 9), "tr end"), ((1 << 0) | (1 << 1) | (1 << 11), "vmentry"), ((1 << 0) | (1 << 1) | (1 << 12), "vmexit"), ] additional_mask = (1 << 10) | (1 << 13) | (1 << 14) branch_event_mask = (1 << 15) | (1 << 16) xf = flags & additional_mask rem_flags = flags & ~additional_mask if rem_flags & (1 << 8): prefix = "tr strt " elif rem_flags & (1 << 9): prefix = "tr end " else: prefix = "" rem_flags &= ~((1 << 8) | (1 << 9)) types = rem_flags & ~branch_event_mask type_name = "" for f_mask, f_name in sample_flags: if f_mask == types: type_name = f_name break s = prefix + type_name ev_parts = [] if rem_flags & (1 << 15): ev_parts.append("miss") if rem_flags & (1 << 16): ev_parts.append("not_taken") if ev_parts: s += "/" + ",".join(ev_parts) + "/" if xf: xs = "(" if xf & (1 << 10): xs += "x" if xf & (1 << 13): xs += "D" if xf & (1 << 14): xs += "t" xs += ")" if len(s) + len(xs) < 21: s = s + xs.rjust(21 - len(s)) else: s = s + " " + xs return s.ljust(21) class IntelPTAnalyzer: """Analyzes Intel PT events and prints details.""" def __init__(self, cfg: argparse.Namespace): self.args = cfg self.session: Optional[perf.session] = None self.insn = False self.src = False self.source_file_name: Optional[str] = None self.line_number: int = 0 self.dso: Optional[str] = None self.stash_dict: Dict[int, List[str]] = {} self.output: Any = None self.output_pos: int = 0 self.cpu: int = -1 self.time: int = 0 self.switch_str: Dict[int, str] = {} if cfg.insn_trace: print("Intel PT Instruction Trace") self.insn = True elif cfg.src_trace: print("Intel PT Source Trace") self.insn = True self.src = True else: print("Intel PT Branch Trace, Power Events, Event Trace and PTWRITE") self.disassembler: Any = None self.inst: Any = None if self.insn and LibXED is not None: try: self.disassembler = LibXED() self.inst = self.disassembler.instruction() except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError, ImportError) as e: print(f"Failed to initialize LibXED: {e}") self.disassembler = None self.inst = None def print_ptwrite(self, raw_buf: bytes) -> None: """Print PTWRITE data.""" if not raw_buf: return try: data = struct.unpack_from(" None: """Print CBR data.""" if len(raw_buf) < 12: return try: data = struct.unpack_from(" None: """Print MWAIT data.""" try: data = struct.unpack_from("> 32) & 0x3 print(f"hints: {hints:#x} extensions: {extensions:#x}", end=' ') def print_pwre(self, raw_buf: bytes) -> None: """Print PWRE data.""" try: data = struct.unpack_from("> 7) & 1 cstate = (payload >> 12) & 0xf subcstate = (payload >> 8) & 0xf print(f"hw: {hw} cstate: {cstate} sub-cstate: {subcstate}", end=' ') def print_exstop(self, raw_buf: bytes) -> None: """Print EXSTOP data.""" try: data = struct.unpack_from(" None: """Print PWRX data.""" try: data = struct.unpack_from("> 4) & 0xf wake_reason = (payload >> 8) & 0xf print(f"deepest cstate: {deepest_cstate} last cstate: {last_cstate} " f"wake reason: {wake_reason:#x}", end=' ') def print_psb(self, raw_buf: bytes) -> None: """Print PSB data.""" try: data = struct.unpack_from(" None: """Print EVT data.""" glb_cfe = ["", "INTR", "IRET", "SMI", "RSM", "SIPI", "INIT", "VMENTRY", "VMEXIT", "VMEXIT_INTR", "SHUTDOWN", "", "UINT", "UIRET"] + [""] * 18 glb_evd = ["", "PFA", "VMXQ", "VMXR"] + [""] * 60 try: data = struct.unpack_from("> 7 vector = data[1] evd_cnt = data[2] s = glb_cfe[typ] if s: print(f" cfe: {s} IP: {ip_flag} vector: {vector}", end=' ') else: print(f" cfe: {typ} IP: {ip_flag} vector: {vector}", end=' ') pos = 4 for _ in range(evd_cnt): if len(raw_buf) < pos + 16: break try: data = struct.unpack_from(" None: """Print IFLAG data.""" try: data = struct.unpack_from("{iflag} {s} branch", end=' ') def common_start_str(self, comm: str, sample: perf.sample_event) -> str: """Return common start string for display.""" ts = sample.sample_time cpu = sample.sample_cpu pid = sample.sample_pid tid = sample.sample_tid machine_pid = getattr(sample, "machine_pid", 0) if machine_pid: vcpu = getattr(sample, "vcpu", -1) return (f"VM:{machine_pid:5d} VCPU:{vcpu:03d} {comm:>16s} {pid:5d}/{tid:<5d} " f"[{cpu:03d}] {ts // 1000000000:9d}.{ts % 1000000000:09d} ") return (f"{comm:>16s} {pid:5d}/{tid:<5d} [{cpu:03d}] " f"{ts // 1000000000:9d}.{ts % 1000000000:09d} ") def print_common_start(self, comm: str, sample: perf.sample_event, name: str) -> None: """Print common start info.""" flags_disp = sample_flags_to_name(getattr(sample, "flags", 0)) print(self.common_start_str(comm, sample) + f"{name:>8s} {flags_disp:>21s}", end=' ') def print_instructions_start(self, comm: str, sample: perf.sample_event) -> None: """Print instructions start info.""" raw_flags = getattr(sample, "flags", 0) if isinstance(raw_flags, int) and (raw_flags & (1 << 10)): print(self.common_start_str(comm, sample) + "x", end=' ') else: print(self.common_start_str(comm, sample), end=' ') def disassem(self, insn: bytes, ip: int) -> Tuple[int, str]: """Disassemble instruction using LibXED.""" inst = self.inst if self.inst is not None else self.disassembler.instruction() is_64_bit = getattr(self.session, "is_64_bit", True) if self.session else True self.disassembler.set_mode(inst, 0 if is_64_bit else 1) buf = create_string_buffer(insn) return self.disassembler.disassemble_one(inst, addressof(buf), len(insn), ip) def print_common_ip(self, sample: perf.sample_event, symbol: str, dso: str) -> None: """Print IP and symbol info.""" ip = sample.sample_ip offs = f"+{sample.sym_offset:#x}" if getattr(sample, "sym_offset", None) is not None else "" cyc_cnt = getattr(sample, "sample_cyc_count", 0) if cyc_cnt: insn_cnt = getattr(sample, "sample_insn_count", 0) ipc_str = f" IPC: {insn_cnt / cyc_cnt:#.2f} ({insn_cnt}/{cyc_cnt})" else: ipc_str = "" if self.insn and self.disassembler is not None: try: insn = sample.insn() except AttributeError: insn = None if insn: cnt, text = self.disassem(insn, ip) byte_str = (f"{ip:x}").rjust(16) for k in range(cnt): byte_str += f" {insn[k]:02x}" print(f"{byte_str:<40s} {text:<30s}", end=' ') else: print(f"{ip:16x}", end=' ') print(f"{symbol}{offs} ({dso})", end=' ') else: print(f"{ip:16x} {symbol}{offs} ({dso})", end=' ') addr = getattr(sample, "sample_addr", getattr(sample, "addr", 0)) addr_correlates_sym = ( bool(addr) or getattr(sample, "addr_symbol", None) is not None or getattr(sample, "addr_dso", None) is not None ) if addr_correlates_sym: addr_dso = (sample.addr_dso or '[unknown]') addr_symbol = (sample.addr_symbol or '[unknown]') addr_offs = (f"+{sample.addr_sym_offset:#x}" if getattr(sample, "addr_sym_offset", None) is not None else "") print(f"=> {addr:x} {addr_symbol}{addr_offs} ({addr_dso}){ipc_str}") else: print(ipc_str) def print_srccode(self, comm: str, sample: perf.sample_event, symbol: str, dso: str, with_insn: bool) -> None: """Print source code info.""" ip = sample.sample_ip if symbol == "[unknown]": start_str = self.common_start_str(comm, sample) + (f"{ip:x}").rjust(16).ljust(40) else: offs = (f"+{sample.sym_offset:#x}" if getattr(sample, "sym_offset", None) is not None else "") start_str = self.common_start_str(comm, sample) + (symbol + offs).ljust(40) if with_insn and self.insn and self.disassembler is not None: try: insn = sample.insn() except AttributeError: insn = None if insn: _, text = self.disassem(insn, ip) start_str += text.ljust(30) source_file_name: typing.Any = None line_number: typing.Any = 0 source_line: typing.Any = None try: res_srcc = sample.srccode() if res_srcc: source_file_name, line_number, source_line = res_srcc except (AttributeError, ValueError, TypeError): pass if source_file_name: if self.line_number == line_number and self.source_file_name == source_file_name: src_str = "" else: if len(source_file_name) > 40: src_file = ("..." + source_file_name[-37:]) + " " else: src_file = source_file_name.ljust(41) if source_line is None: src_str = src_file + str(line_number).rjust(4) + " " else: src_str = src_file + str(line_number).rjust(4) + " " + source_line self.dso = None elif dso == self.dso: src_str = "" else: src_str = dso self.dso = dso self.line_number = line_number self.source_file_name = source_file_name print(start_str, src_str) def do_process_event(self, sample: perf.sample_event) -> None: """Process event and print info.""" cpu = getattr(sample, "sample_cpu", getattr(sample, "cpu", 0)) if cpu in self.switch_str: print(self.switch_str[cpu]) del self.switch_str[cpu] comm = "Unknown" if hasattr(self, 'session') and self.session: try: thread = self.session.find_thread(sample.sample_pid, sample.sample_tid) if thread: comm = thread.comm() or "Unknown" except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): pass # Python < 3.9 compatibility name = str(sample.evsel) if name.startswith("evsel("): name = name[6:-1] dso = (sample.dso or '[unknown]') symbol = (sample.symbol or '[unknown]') raw_buf = getattr(sample, 'raw_buf', b'') or b'' if name.startswith("instructions"): if self.src: self.print_srccode(comm, sample, symbol, dso, True) else: self.print_instructions_start(comm, sample) self.print_common_ip(sample, symbol, dso) elif name.startswith("branches"): if self.src: self.print_srccode(comm, sample, symbol, dso, False) else: self.print_common_start(comm, sample, name) self.print_common_ip(sample, symbol, dso) elif name == "ptwrite": self.print_common_start(comm, sample, name) self.print_ptwrite(raw_buf) self.print_common_ip(sample, symbol, dso) elif name == "cbr": self.print_common_start(comm, sample, name) self.print_cbr(raw_buf) self.print_common_ip(sample, symbol, dso) elif name == "mwait": self.print_common_start(comm, sample, name) self.print_mwait(raw_buf) self.print_common_ip(sample, symbol, dso) elif name == "pwre": self.print_common_start(comm, sample, name) self.print_pwre(raw_buf) self.print_common_ip(sample, symbol, dso) elif name == "exstop": self.print_common_start(comm, sample, name) self.print_exstop(raw_buf) self.print_common_ip(sample, symbol, dso) elif name == "pwrx": self.print_common_start(comm, sample, name) self.print_pwrx(raw_buf) self.print_common_ip(sample, symbol, dso) elif name == "psb": self.print_common_start(comm, sample, name) self.print_psb(raw_buf) self.print_common_ip(sample, symbol, dso) elif name == "evt": self.print_common_start(comm, sample, name) self.print_evt(raw_buf) self.print_common_ip(sample, symbol, dso) elif name == "iflag": self.print_common_start(comm, sample, name) self.print_iflag(raw_buf) self.print_common_ip(sample, symbol, dso) else: self.print_common_start(comm, sample, name) self.print_common_ip(sample, symbol, dso) def interleave_events(self, sample: perf.sample_event) -> None: """Interleave output to avoid garbled lines from different CPUs.""" self.cpu = sample.sample_cpu ts = sample.sample_time if self.time != ts: self.time = ts self.flush_stashed_output() self.output_pos = 0 with contextlib.redirect_stdout(io.StringIO()) as self.output: self.do_process_event(sample) self.stash_output() def stash_output(self) -> None: """Stash output for later flushing.""" output_str = self.output.getvalue()[self.output_pos:] n = len(output_str) if n: self.output_pos += n if self.cpu not in self.stash_dict: self.stash_dict[self.cpu] = [] self.stash_dict[self.cpu].append(output_str) if len(self.stash_dict[self.cpu]) > 1000: self.flush_stashed_output() def flush_stashed_output(self) -> None: """Flush stashed output.""" while self.stash_dict: cpus = list(self.stash_dict.keys()) for cpu in cpus: items = self.stash_dict[cpu] countdown = self.args.interleave while len(items) and countdown: sys.stdout.write(items[0]) del items[0] countdown -= 1 if not items: del self.stash_dict[cpu] def process_context_switch(self, event: perf.switch_event) -> None: """Process context switch.""" cpu = getattr(event, "sample_cpu", getattr(event, "cpu", 0)) pid = getattr(event, "sample_pid", getattr(event, "pid", 0)) tid = getattr(event, "sample_tid", getattr(event, "tid", 0)) ts = getattr(event, "sample_time", getattr(event, "time", 0)) np_pid = getattr(event, "next_prev_pid", None) np_tid = getattr(event, "next_prev_tid", None) machine_pid = getattr(event, "machine_pid", -1) vcpu = getattr(event, "vcpu", -1) misc = getattr(event, "misc", 0) out = bool(misc & (1 << 13)) out_preempt = bool(misc & (1 << 14)) if self.args.interleave: self.flush_stashed_output() if out: out_str = "Switch out " else: out_str = "Switch In " preempt_str = "preempt" if out_preempt else "" if machine_pid == -1: machine_str = "" elif vcpu == -1: machine_str = f"machine PID {machine_pid}" else: machine_str = f"machine PID {machine_pid} VCPU {vcpu}" np_str = f"{np_pid:5d}/{np_tid:<5d} " if np_pid is not None and np_tid is not None else "" c_str = (f"{out_str:>16s} {pid:5d}/{tid:<5d} [{cpu:03d}] " f"{ts // 1000000000:9d}.{ts % 1000000000:09d} " f"{np_str}{machine_str} {preempt_str}") if self.args.all_switch_events: print(c_str) else: self.switch_str[cpu] = c_str def process_event(self, sample: perf.sample_event) -> None: """Wrapper to handle interleaving and exceptions.""" try: if self.args.interleave: self.interleave_events(sample) else: self.do_process_event(sample) except BrokenPipeError: # Stop python printing broken pipe errors and traceback sys.stdout = open(os.devnull, 'w', encoding='utf-8') sys.exit(1) if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("-i", "--input", default="perf.data", help="Input file name") ap.add_argument("--insn-trace", action='store_true') ap.add_argument("--src-trace", action='store_true') ap.add_argument("--all-switch-events", action='store_true') ap.add_argument("--interleave", type=int, nargs='?', const=4, default=0) ap.add_argument("--itrace", default=os.environ.get("PERF_ITRACE"), help="itrace options") args = ap.parse_args() if args.itrace is None: if args.insn_trace or args.src_trace: args.itrace = "i0nsepwxI" else: args.itrace = "bepwxI" analyzer = IntelPTAnalyzer(args) try: # Note: Python API currently lacks auxtrace_error callbacks affecting # chronological interleaving try: analyzer.session = perf.session( perf.data(args.input), sample=analyzer.process_event, context_switch=analyzer.process_context_switch, itrace=args.itrace ) try: analyzer.session.process_events() except KeyboardInterrupt: pass finally: # Break the reference cycle between analyzer.session and the bound # analyzer.process_event / process_context_switch callbacks so the # underlying C perf.session object is freed. analyzer.session = None if args.interleave: analyzer.flush_stashed_output() print("End") except BrokenPipeError: sys.exit(0) except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError): import traceback traceback.print_exc() sys.exit(1)