summaryrefslogtreecommitdiff
path: root/tools/perf/python
diff options
context:
space:
mode:
Diffstat (limited to 'tools/perf/python')
-rwxr-xr-xtools/perf/python/SchedGui.py246
-rwxr-xr-xtools/perf/python/arm-cs-trace-disasm.py440
-rwxr-xr-xtools/perf/python/check-perf-trace.py223
-rwxr-xr-xtools/perf/python/compaction-times.py362
-rwxr-xr-xtools/perf/python/counting.py1
-rwxr-xr-xtools/perf/python/event_analyzing_sample.py317
-rwxr-xr-xtools/perf/python/export-to-postgresql.py1328
-rwxr-xr-xtools/perf/python/export-to-sqlite.py946
-rwxr-xr-xtools/perf/python/exported-sql-viewer.py5103
-rwxr-xr-xtools/perf/python/failed-syscalls-by-pid.py161
-rwxr-xr-xtools/perf/python/failed-syscalls.py94
-rwxr-xr-xtools/perf/python/flamegraph.py283
-rwxr-xr-xtools/perf/python/futex-contention.py107
-rwxr-xr-xtools/perf/python/gecko.py411
-rwxr-xr-xtools/perf/python/ilist.py86
-rwxr-xr-xtools/perf/python/intel-pt-events.py632
-rwxr-xr-xtools/perf/python/libxed.py123
-rwxr-xr-xtools/perf/python/mem-phys-addr.py136
-rwxr-xr-xtools/perf/python/net_dropmonitor.py173
-rwxr-xr-xtools/perf/python/netdev-times.py494
-rwxr-xr-xtools/perf/python/parallel-perf.py1250
-rw-r--r--tools/perf/python/perf.pyi122
-rwxr-xr-xtools/perf/python/perf_live.py10
-rwxr-xr-xtools/perf/python/powerpc-hcalls.py334
-rwxr-xr-xtools/perf/python/rw-by-file.py113
-rwxr-xr-xtools/perf/python/rw-by-pid.py205
-rwxr-xr-xtools/perf/python/rwtop.py256
-rwxr-xr-xtools/perf/python/sched-migration.py496
-rwxr-xr-xtools/perf/python/sctop.py253
-rwxr-xr-xtools/perf/python/stackcollapse.py145
-rwxr-xr-xtools/perf/python/stat-cpi.py231
-rwxr-xr-xtools/perf/python/syscall-counts-by-pid.py112
-rwxr-xr-xtools/perf/python/syscall-counts.py95
-rwxr-xr-xtools/perf/python/task-analyzer.py908
-rwxr-xr-xtools/perf/python/tracepoint.py5
-rwxr-xr-xtools/perf/python/treport.py807
-rwxr-xr-xtools/perf/python/ttimechart.py1806
-rwxr-xr-xtools/perf/python/twatch.py81
-rwxr-xr-xtools/perf/python/wakeup-latency.py103
39 files changed, 18886 insertions, 112 deletions
diff --git a/tools/perf/python/SchedGui.py b/tools/perf/python/SchedGui.py
new file mode 100755
index 000000000000..fa0d010fe528
--- /dev/null
+++ b/tools/perf/python/SchedGui.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# SchedGui.py - Python extension for perf script, basic GUI code for
+# traces drawing and overview.
+#
+# Copyright (C) 2010 by Frederic Weisbecker <fweisbec@gmail.com>
+#
+# Ported to modern directory structure.
+
+"""SchedGui.py - Python extension for perf script, basic GUI code for traces drawing and overview."""
+from __future__ import annotations
+
+import importlib
+from typing import Any
+
+class _DummyWx:
+ """Dummy wx module fallback when wxPython is not installed."""
+ Frame = object
+
+
+try:
+ wx: Any = importlib.import_module("wx")
+ WX_AVAILABLE = True
+except ImportError:
+ wx = _DummyWx
+ WX_AVAILABLE = False
+
+
+class RootFrame(wx.Frame):
+ """Main window frame for scheduling trace visualization."""
+ Y_OFFSET = 100
+ RECT_HEIGHT = 100
+ RECT_SPACE = 50
+ EVENT_MARKING_WIDTH = 5
+
+ def __init__(self, sched_tracer, title, parent=None, win_id=-1):
+ if not WX_AVAILABLE:
+ raise ImportError("You need to install the wxpython lib for this script")
+ wx.Frame.__init__(self, parent, win_id, title)
+
+ self.dc = None
+ (self.screen_width, self.screen_height) = wx.GetDisplaySize()
+ self.screen_width -= 10
+ self.screen_height -= 10
+ self.zoom = 0.5
+ self.scroll_scale = 20
+ self.sched_tracer = sched_tracer
+ self.sched_tracer.set_root_win(self)
+ (self.ts_start, self.ts_end) = sched_tracer.interval()
+ self.update_width_virtual()
+ self.nr_rects = sched_tracer.nr_rectangles() + 1
+ self.height_virtual = RootFrame.Y_OFFSET + \
+ (self.nr_rects * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE))
+
+ # whole window panel
+ self.panel = wx.Panel(self, size=(self.screen_width, self.screen_height))
+
+ # scrollable container
+ # Create SplitterWindow
+ self.splitter = wx.SplitterWindow(self.panel, style=wx.SP_3D)
+
+ # scrollable container (Top)
+ self.scroll = wx.ScrolledWindow(self.splitter)
+ self.scroll.SetScrollbars(self.scroll_scale, self.scroll_scale,
+ int(self.width_virtual // self.scroll_scale),
+ int(self.height_virtual // self.scroll_scale))
+ self.scroll.EnableScrolling(True, True)
+ self.scroll.SetFocus()
+
+ # scrollable drawing area
+ self.scroll_panel = wx.Panel(self.scroll,
+ size=(self.screen_width - 15, self.screen_height // 2))
+ self.scroll_panel.Bind(wx.EVT_PAINT, self.on_paint)
+ self.scroll_panel.Bind(wx.EVT_KEY_DOWN, self.on_key_press)
+ self.scroll_panel.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
+ self.scroll.Bind(wx.EVT_KEY_DOWN, self.on_key_press)
+ self.scroll.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
+
+ self.scroll_panel.SetSize(int(self.width_virtual), int(self.height_virtual))
+
+ # Create a separate panel for text (Bottom)
+ self.text_panel = wx.Panel(self.splitter)
+ self.text_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.txt = wx.TextCtrl(self.text_panel, -1, "Click a bar to see details",
+ style=wx.TE_MULTILINE)
+ self.text_sizer.Add(self.txt, 1, wx.EXPAND | wx.ALL, 5)
+ self.text_panel.SetSizer(self.text_sizer)
+
+ # Split the window
+ self.splitter.SplitHorizontally(self.scroll, self.text_panel, (self.screen_height * 3) // 4)
+
+ # Main sizer to layout splitter
+ self.main_sizer = wx.BoxSizer(wx.VERTICAL)
+ self.main_sizer.Add(self.splitter, 1, wx.EXPAND)
+ self.panel.SetSizer(self.main_sizer)
+
+ self.scroll.Fit()
+ self.Fit()
+
+ self.Show(True)
+
+ def us_to_px(self, val):
+ """Convert microseconds to pixels."""
+ return val / (10 ** 3) * self.zoom
+
+ def px_to_us(self, val):
+ """Convert pixels to microseconds."""
+ return (val / self.zoom) * (10 ** 3)
+
+ def scroll_start(self):
+ """Get scroll start position in pixels."""
+ (x, y) = self.scroll.GetViewStart()
+ return (x * self.scroll_scale, y * self.scroll_scale)
+
+ def scroll_start_us(self):
+ """Get scroll start position in microseconds."""
+ (x, _) = self.scroll_start()
+ return self.px_to_us(x)
+
+ def paint_rectangle_zone(self, nr, color, top_color, start, end):
+ """Draw a rectangle zone for a CPU."""
+ offset_px = self.us_to_px(start - self.ts_start)
+ width_px = self.us_to_px(end - start)
+
+ offset_py = RootFrame.Y_OFFSET + (nr * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE))
+ width_py = RootFrame.RECT_HEIGHT
+
+ dc = self.dc
+
+ if top_color is not None:
+ (r, g, b) = top_color
+ top_color = wx.Colour(r, g, b)
+ brush = wx.Brush(top_color, wx.SOLID)
+ dc.SetBrush(brush)
+ dc.DrawRectangle(int(offset_px), int(offset_py),
+ int(width_px), RootFrame.EVENT_MARKING_WIDTH)
+ width_py -= RootFrame.EVENT_MARKING_WIDTH
+ offset_py += RootFrame.EVENT_MARKING_WIDTH
+
+ (r, g, b) = color
+ color = wx.Colour(r, g, b)
+ brush = wx.Brush(color, wx.SOLID)
+ dc.SetBrush(brush)
+ dc.DrawRectangle(int(offset_px), int(offset_py), int(width_px), int(width_py))
+
+ def update_rectangles(self, start, end):
+ """Update rectangles in the given time window."""
+ start += self.ts_start
+ end += self.ts_start
+ self.sched_tracer.fill_zone(start, end)
+
+ def on_paint(self, event):
+ """Handle paint event."""
+ window = event.GetEventObject()
+ dc = wx.PaintDC(window)
+
+ # Clear background to avoid ghosting
+ dc.SetBackground(wx.Brush(window.GetBackgroundColour()))
+ dc.Clear()
+
+ self.dc = dc
+ try:
+ width = min(self.width_virtual, self.screen_width)
+ (x, _) = self.scroll_start()
+ start = self.px_to_us(x)
+ end = self.px_to_us(x + width)
+ self.update_rectangles(start, end)
+
+ # Draw CPU labels at the left edge of the visible area
+ (x_scroll, _) = self.scroll_start()
+ for nr in range(self.nr_rects):
+ offset_py = RootFrame.Y_OFFSET + (nr * (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE))
+ dc.DrawText(f"CPU {nr}", x_scroll + 10, offset_py + 10)
+ finally:
+ self.dc = None
+
+ def rect_from_ypixel(self, y):
+ y -= RootFrame.Y_OFFSET
+ rect = y // (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)
+ height = y % (RootFrame.RECT_HEIGHT + RootFrame.RECT_SPACE)
+
+ if rect < 0 or rect > self.nr_rects - 1 or height > RootFrame.RECT_HEIGHT:
+ return -1
+
+ return rect
+
+ def update_summary(self, txt):
+ self.txt.SetValue(txt)
+ self.text_panel.Layout()
+ self.splitter.Layout()
+ self.text_panel.Refresh()
+
+ def on_mouse_down(self, event):
+ pos = event.GetPosition()
+ x, y = pos.x, pos.y
+ rect = self.rect_from_ypixel(y)
+ if rect == -1:
+ return
+
+ t = self.px_to_us(x) + self.ts_start
+
+ self.sched_tracer.mouse_down(rect, t)
+
+ def update_width_virtual(self):
+ self.width_virtual = self.us_to_px(self.ts_end - self.ts_start)
+
+ def __zoom(self, x):
+ self.update_width_virtual()
+ (xpos, ypos) = self.scroll.GetViewStart()
+ xpos = int(self.us_to_px(x) // self.scroll_scale)
+ self.scroll_panel.SetSize((int(self.width_virtual), int(self.height_virtual)))
+ self.scroll.SetScrollbars(self.scroll_scale, self.scroll_scale,
+ int(self.width_virtual // self.scroll_scale),
+ int(self.height_virtual // self.scroll_scale),
+ xpos, ypos)
+ self.Refresh()
+
+ def zoom_in(self):
+ x = self.scroll_start_us()
+ self.zoom *= 2
+ self.__zoom(x)
+
+ def zoom_out(self):
+ x = self.scroll_start_us()
+ self.zoom /= 2
+ self.__zoom(x)
+
+ def on_key_press(self, event):
+ key = event.GetRawKeyCode()
+ if key == ord("+"):
+ self.zoom_in()
+ return
+ if key == ord("-"):
+ self.zoom_out()
+ return
+
+ key = event.GetKeyCode()
+ (x, y) = self.scroll.GetViewStart()
+ if key == wx.WXK_RIGHT:
+ self.scroll.Scroll(x + 1, y)
+ elif key == wx.WXK_LEFT:
+ self.scroll.Scroll(x - 1, y)
+ elif key == wx.WXK_DOWN:
+ self.scroll.Scroll(x, y + 1)
+ elif key == wx.WXK_UP:
+ self.scroll.Scroll(x, y - 1)
diff --git a/tools/perf/python/arm-cs-trace-disasm.py b/tools/perf/python/arm-cs-trace-disasm.py
new file mode 100755
index 000000000000..1812ccaf4b20
--- /dev/null
+++ b/tools/perf/python/arm-cs-trace-disasm.py
@@ -0,0 +1,440 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+arm-cs-trace-disasm.py: ARM CoreSight Trace Dump With Disassembler
+
+Author: Tor Jeremiassen <tor@ti.com>
+ Mathieu Poirier <mathieu.poirier@linaro.org>
+ Leo Yan <leo.yan@linaro.org>
+ Al Grant <Al.Grant@arm.com>
+
+Below are some example commands for using this script.
+Note a --kcore recording is required for accurate decode
+due to the alternatives patching mechanism. In addition to this,
+source line info comes from Perf, and when using kcore there is
+no debug info. The following lists the supported features in each mode:
+
++-----------+-----------------+------------------+------------------+
+| Recording | Accurate decode | Source line dump | Disassembly dump |
++-----------+-----------------+------------------+------------------+
+| --kcore | yes | no | yes |
+| normal | no | yes (inaccurate) | yes (inaccurate) |
++-----------+-----------------+------------------+------------------+
+
+Output disassembly with objdump and auto detect vmlinux
+(when running on same machine):
+ perf script arm-cs-trace-disasm -- -d
+
+Output disassembly with llvm-objdump:
+ perf script arm-cs-trace-disasm -- -d llvm-objdump-11 -k path/to/vmlinux
+
+Output accurate disassembly by passing kcore to script:
+ perf script arm-cs-trace-disasm -- -d -k perf.data/kcore_dir/kcore
+
+Output only source line and symbols:
+ perf script arm-cs-trace-disasm
+"""
+from __future__ import annotations
+
+import os
+from os import path
+import re
+from subprocess import CalledProcessError, check_output
+import argparse
+import platform
+import sys
+from typing import Dict, List, Optional
+
+import perf
+
+# Initialize global dicts and regular expression
+DISASM_CACHE: Dict[str, List[str]] = {}
+DISASM_RE = re.compile(r"^\s*([0-9a-fA-F]+):")
+DISASM_FUNC_RE = re.compile(r"^\s*([0-9a-fA-F]+)\s.*:")
+CACHE_SIZE = 1024
+class _State:
+ sample_idx: int = -1
+ source_file_name: Optional[str] = None
+ line_number: Optional[int] = None
+ dso: Optional[str] = None
+
+_STATE = _State()
+
+KVER = platform.release()
+VMLINUX_PATHS = [
+ f"/usr/lib/debug/boot/vmlinux-{KVER}.debug",
+ f"/usr/lib/debug/lib/modules/{KVER}/vmlinux",
+ f"/lib/modules/{KVER}/build/vmlinux",
+ f"/usr/lib/debug/boot/vmlinux-{KVER}",
+ f"/boot/vmlinux-{KVER}",
+ "/boot/vmlinux",
+ "vmlinux",
+]
+
+def default_objdump() -> str:
+ """Return the default objdump path from perf config or 'objdump'."""
+ try:
+ config = perf.config_get("annotate.objdump")
+ return str(config) if config else "objdump"
+ except (AttributeError, TypeError):
+ return "objdump"
+
+def find_vmlinux() -> Optional[str]:
+ """Find the vmlinux file in standard paths."""
+ if hasattr(find_vmlinux, "path"):
+ return getattr(find_vmlinux, "path")
+
+ for v in VMLINUX_PATHS:
+ if os.access(v, os.R_OK):
+ setattr(find_vmlinux, "path", v)
+ return v
+ setattr(find_vmlinux, "path", None)
+ return None
+
+def get_dso_file_path(dso_name: str, dso_build_id: str, vmlinux: Optional[str]) -> str:
+ """Return the path to the DSO file."""
+ # Locate DSO binaries in the perf build-id cache (~/.debug or PERF_BUILDID_DIR).
+ # Perf stores cached binaries under both <buildid_dir>/<dso_long_name>/<build_id>/elf
+ # and the canonical symlink/file layout <buildid_dir>/.build-id/<bid[:2]>/<bid[2:]>/elf.
+ buildid_dir = os.environ.get('PERF_BUILDID_DIR')
+ if not buildid_dir:
+ buildid_dir = os.path.join(os.environ.get('HOME', ''), '.debug')
+
+ if (dso_name in ("[kernel.kallsyms]", "vmlinux", "kcore")
+ or dso_name.endswith("/vmlinux") or dso_name.endswith("/kcore")):
+ if vmlinux:
+ return vmlinux
+ if dso_build_id and dso_build_id != "[unknown]" and len(dso_build_id) > 2:
+ bid_candidate = os.path.join(
+ buildid_dir, ".build-id", dso_build_id[:2], dso_build_id[2:], "elf"
+ )
+ if os.access(bid_candidate, os.R_OK):
+ return bid_candidate
+ for kname in (dso_name, "vmlinux", "[kernel.kallsyms]"):
+ candidate = os.path.join(buildid_dir, kname.lstrip("/"), dso_build_id, "elf")
+ if os.access(candidate, os.R_OK):
+ return candidate
+ return find_vmlinux() or dso_name
+
+ if dso_name == "[vdso]":
+ append = "/vdso"
+ else:
+ append = "/elf"
+
+ if dso_build_id and dso_build_id != "[unknown]" and len(dso_build_id) > 2:
+ bid_path = (buildid_dir + "/.build-id/" + dso_build_id[:2] + "/"
+ + dso_build_id[2:] + append).replace('//', '/')
+ if os.access(bid_path, os.R_OK):
+ return bid_path
+
+ dso_path = buildid_dir + "/" + dso_name + "/" + dso_build_id + append
+ # Replace duplicate slash chars to single slash char
+ dso_path = dso_path.replace('//', '/')
+ return dso_path
+
+def read_disam(dso_fname: str, dso_start: int, start_addr: int,
+ stop_addr: int, objdump: str) -> List[str]:
+ """Read disassembly from a DSO file using objdump."""
+ if stop_addr <= start_addr or (stop_addr - start_addr) > 0x100000:
+ return []
+ addr_range = f"{start_addr}:{stop_addr}:{dso_start}:{dso_fname}"
+
+ # Don't let the cache get too big, clear it when it hits max size
+ if len(DISASM_CACHE) > CACHE_SIZE:
+ DISASM_CACHE.clear()
+
+ if addr_range in DISASM_CACHE:
+ disasm_output = DISASM_CACHE[addr_range]
+ else:
+ start_addr = start_addr - dso_start
+ stop_addr = stop_addr - dso_start
+ disasm = [objdump, "-d", "-z",
+ f"--start-address={start_addr:#x}",
+ f"--stop-address={stop_addr:#x}"]
+ disasm += [dso_fname]
+ try:
+ disasm_output = check_output(disasm).decode('utf-8', errors='replace').split('\n')
+ except (CalledProcessError, OSError):
+ return []
+ if len(disasm_output) <= 512:
+ DISASM_CACHE[addr_range] = disasm_output
+
+ return disasm_output
+
+def print_disam(dso_fname: str, dso_start: int, start_addr: int,
+ stop_addr: int, objdump: str) -> None:
+ """Print disassembly for a given address range."""
+ for line in read_disam(dso_fname, dso_start, start_addr, stop_addr, objdump):
+ m = DISASM_FUNC_RE.search(line)
+ if m is None:
+ m = DISASM_RE.search(line)
+ if m is None:
+ continue
+ print(f"\t{line}")
+
+def print_sample(sample: perf.sample_event) -> None:
+ """Print sample details."""
+ print(f"Sample = {{ cpu: {sample.sample_cpu:04d} addr: {sample.sample_addr:016x} "
+ f"phys_addr: {sample.sample_phys_addr:016x} ip: {sample.sample_ip:016x} "
+ f"pid: {sample.sample_pid} tid: {sample.sample_tid} period: {sample.sample_period} "
+ f"time: {sample.sample_time} index: {_STATE.sample_idx}}}")
+
+def common_start_str(comm: str, sample: perf.sample_event) -> str:
+ """Return common start string for sample output."""
+ sec = int(sample.sample_time / 1000000000)
+ ns = sample.sample_time % 1000000000
+ cpu = sample.sample_cpu
+ pid = sample.sample_pid
+ tid = sample.sample_tid
+ return f"{comm:>16s} {pid:5d}/{tid:<5d} [{cpu:04d}] {sec:9d}.{ns:09d} "
+
+def print_srccode(comm: str, sample: perf.sample_event, symbol: str, dso: str) -> None:
+ """Print source code and symbols for a sample."""
+ ip = sample.sample_ip
+ if symbol == "[unknown]":
+ start_str = common_start_str(comm, sample) + f"{ip:x}".rjust(16).ljust(40)
+ else:
+ symoff = 0
+ symoff = getattr(sample, 'sym_offset', 0) or 0
+ offs = f"+{symoff:#x}" if symoff != 0 else ""
+ start_str = common_start_str(comm, sample) + (symbol + offs).ljust(40)
+
+ source_file_name, line_number, source_line = sample.srccode() or (None, 0, None)
+ if source_file_name:
+ if _STATE.line_number == line_number and _STATE.source_file_name == source_file_name:
+ src_str = ""
+ else:
+ if len(source_file_name) > 40:
+ src_file = f"...{source_file_name[-37:]} "
+ else:
+ src_file = source_file_name.ljust(41)
+
+ if source_line is None:
+ src_str = f"{src_file}{line_number:>4d} <source not found>"
+ else:
+ src_str = f"{src_file}{line_number:>4d} {source_line}"
+ _STATE.dso = None
+ elif dso == _STATE.dso:
+ src_str = ""
+ else:
+ src_str = dso
+ _STATE.dso = dso
+
+ _STATE.line_number = line_number
+ _STATE.source_file_name = source_file_name
+
+ print(start_str, src_str)
+
+class TraceDisasm:
+ """Class to handle trace disassembly."""
+ def __init__(self, cli_options: argparse.Namespace):
+ self.options = cli_options
+ self.sample_idx = -1
+ self.cpu_data: Dict[str, int] = {}
+ self.session: Optional[perf.session] = None
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process a single perf event."""
+ self.sample_idx += 1
+ _STATE.sample_idx = self.sample_idx
+
+ if self.options.start_time is not None and sample.sample_time < self.options.start_time:
+ return
+ if self.options.stop_time is not None and sample.sample_time > self.options.stop_time:
+ sys.exit(0)
+ if self.options.start_sample is not None and self.sample_idx < self.options.start_sample:
+ return
+ if self.options.stop_sample is not None and self.sample_idx > self.options.stop_sample:
+ sys.exit(0)
+
+ ev_name = str(sample.evsel)
+ if self.options.verbose:
+ print(f"Event type: {ev_name}")
+ print_sample(sample)
+
+ # Initialize CPU data if it's empty, and directly return back
+ # if this is the first tracing event for this CPU. This must happen
+ # before the dso == '[unknown]' check because the initial
+ # CS_ETM_TRACE_ON sample has ip == 0 (dso == '[unknown]') and
+ # carries the starting target address in sample.sample_addr.
+ if self.cpu_data.get(f"{sample.sample_cpu}addr") is None:
+ self.cpu_data[f"{sample.sample_cpu}addr"] = sample.sample_addr
+ return
+
+ dso = getattr(sample, 'dso_long_name', None) or sample.dso or '[unknown]'
+ symbol = sample.symbol or '[unknown]'
+ dso_bid = sample.dso_bid or '[unknown]'
+ dso_start = sample.map_start
+ dso_end = sample.map_end
+ map_pgoff = sample.map_pgoff or 0
+
+ comm = "[unknown]"
+ try:
+ if self.session:
+ thread_info = self.session.find_thread(sample.sample_pid, sample.sample_tid)
+ if thread_info:
+ comm = thread_info.comm() or "[unknown]"
+ except (TypeError, AttributeError):
+ pass
+
+ if dso == '[unknown]':
+ return
+
+ if dso_start is None or dso_end is None:
+ print(f"Failed to find valid dso map for dso {dso}")
+ return
+
+ if "instructions" in ev_name:
+ print_srccode(comm, sample, symbol, dso)
+ return
+
+ if "branches" not in ev_name:
+ return
+
+ self._process_branch(sample, comm, symbol, dso, dso_bid, dso_start, dso_end, map_pgoff)
+
+ def _process_branch(self, sample: perf.sample_event, comm: str, symbol: str, dso: str,
+ dso_bid: str, dso_start: int, dso_end: int, map_pgoff: int) -> None:
+ """Helper to process branch events."""
+ cpu = sample.sample_cpu
+ ip = sample.sample_ip
+ addr = sample.sample_addr
+
+ # The format for packet is:
+ #
+ # +------------+------------+------------+
+ # sample_prev: | addr | ip | cpu |
+ # +------------+------------+------------+
+ # sample_next: | addr | ip | cpu |
+ # +------------+------------+------------+
+ #
+ # Combine the two continuous packets to get the instruction range for
+ # sample_prev::cpu:
+ #
+ # [ sample_prev::addr .. sample_next::ip ]
+ #
+ # sample_prev::addr is stored into cpu_data and read back as
+ # 'start_addr' when the new packet arrives, and sample_next::ip + 4 is
+ # used for 'stop_addr' so objdump includes the branch instruction at
+ # sample_next::ip.
+ start_addr = self.cpu_data[str(cpu) + 'addr']
+ stop_addr = ip + 4
+
+ # Record for previous sample packet
+ self.cpu_data[str(cpu) + 'addr'] = addr
+
+ # Filter out zero start_address. Optionally identify CS_ETM_TRACE_ON packet
+ if start_addr == 0:
+ if stop_addr == 4 and self.options.verbose:
+ print(f"CPU{cpu}: CS_ETM_TRACE_ON packet is inserted")
+ return
+
+ if start_addr < dso_start or start_addr > dso_end:
+ print(f"Start address {start_addr:#x} is out of range [ {dso_start:#x} .. "
+ f"{dso_end:#x} ] for dso {dso}")
+ return
+
+ if stop_addr < dso_start or stop_addr > dso_end:
+ print(f"Stop address {stop_addr:#x} is out of range [ {dso_start:#x} .. "
+ f"{dso_end:#x} ] for dso {dso}")
+ return
+
+ if self.options.objdump is not None:
+ # Kernel images (vmlinux / kcore) and non-PIE executables linked at
+ # the default 0x400000 base already have their virtual load addresses
+ # baked into the ELF symbol/section headers read by objdump, so
+ # dso_vm_start and map_pgoff_local must be 0 to avoid subtracting
+ # map_start from virtual sample addresses.
+ if (dso in ("[kernel.kallsyms]", "vmlinux", "kcore")
+ or dso.endswith("/vmlinux") or dso.endswith("/kcore")):
+ dso_vm_start = 0
+ map_pgoff_local = 0
+ elif dso_start == 0x400000:
+ dso_vm_start = 0
+ map_pgoff_local = 0
+ else:
+ dso_vm_start = dso_start
+ map_pgoff_local = map_pgoff
+
+ dso_fname = get_dso_file_path(dso, dso_bid, self.options.vmlinux)
+ if path.exists(dso_fname):
+ print_disam(dso_fname, dso_vm_start, start_addr + map_pgoff_local,
+ stop_addr + map_pgoff_local, self.options.objdump)
+ else:
+ print(f"Failed to find dso {dso} for address range [ "
+ f"{start_addr + map_pgoff_local:#x} .. {stop_addr + map_pgoff_local:#x} ]")
+
+ print_srccode(comm, sample, symbol, dso)
+
+ def run(self) -> None:
+ """Run the trace disassembly session."""
+ input_file = self.options.input or "perf.data"
+ if input_file != "-" and not os.path.exists(input_file):
+ print(f"Error: {input_file} not found.", file=sys.stderr)
+ sys.exit(1)
+
+ print('ARM CoreSight Trace Data Assembler Dump')
+ try:
+ session_kwargs = {
+ "sample": self.process_event,
+ "itrace": self.options.itrace,
+ }
+ if self.options.vmlinux:
+ session_kwargs["vmlinux"] = self.options.vmlinux
+ try:
+ self.session = perf.session(perf.data(input_file), **session_kwargs)
+ except TypeError:
+ session_kwargs.pop("vmlinux", None)
+ self.session = perf.session(perf.data(input_file), **session_kwargs)
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e:
+ print(f"Error opening session: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ try:
+ self.session.process_events()
+ except (KeyboardInterrupt, SystemExit):
+ pass
+ finally:
+ # Break the reference cycle between self.session and the bound
+ # self.process_event callback so the C perf.session object is freed.
+ self.session = None
+ print('End')
+
+if __name__ == "__main__":
+ def int_arg(v: str) -> int:
+ """Helper for integer command line arguments."""
+ val = int(v)
+ if val < 0:
+ raise argparse.ArgumentTypeError("Argument must be a positive integer")
+ return val
+
+ arg_parser = argparse.ArgumentParser(description="ARM CoreSight Trace Dump With Disassembler")
+ arg_parser.add_argument("-i", "--input", help="input perf.data file")
+ arg_parser.add_argument("-k", "--vmlinux", default=os.environ.get("PERF_SYMBOL_VMLINUX"),
+ help="Set path to vmlinux file. Omit to autodetect")
+ arg_parser.add_argument("-d", "--objdump", nargs="?", const=default_objdump(),
+ help="Show disassembly. Can also be used to change the objdump path")
+ arg_parser.add_argument("-v", "--verbose", action="store_true", help="Enable debugging log")
+ arg_parser.add_argument("--start-time", type=int_arg,
+ help="Monotonic clock time of sample to start from.")
+ arg_parser.add_argument("--stop-time", type=int_arg,
+ help="Monotonic clock time of sample to stop at.")
+ arg_parser.add_argument("--itrace", default=os.environ.get("PERF_ITRACE") or "b",
+ help="Instruction tracing options.")
+ arg_parser.add_argument("--start-sample", type=int_arg,
+ help="Index of sample to start from.")
+ arg_parser.add_argument("--stop-sample", type=int_arg,
+ help="Index of sample to stop at.")
+
+ parsed_options = arg_parser.parse_args()
+ if (parsed_options.start_time is not None and parsed_options.stop_time is not None and
+ parsed_options.start_time >= parsed_options.stop_time):
+ print("--start-time must less than --stop-time")
+ sys.exit(2)
+ if (parsed_options.start_sample is not None and parsed_options.stop_sample is not None and
+ parsed_options.start_sample >= parsed_options.stop_sample):
+ print("--start-sample must less than --stop-sample")
+ sys.exit(2)
+
+ td = TraceDisasm(parsed_options)
+ td.run()
diff --git a/tools/perf/python/check-perf-trace.py b/tools/perf/python/check-perf-trace.py
new file mode 100755
index 000000000000..a7679a897cc7
--- /dev/null
+++ b/tools/perf/python/check-perf-trace.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Basic test of Python scripting support for perf.
+Ported from tools/perf/scripts/python/check-perf-trace.py
+"""
+from __future__ import annotations
+
+import argparse
+import collections
+import perf
+
+unhandled: collections.defaultdict[str, int] = collections.defaultdict(int)
+session = None
+
+softirq_vecs = {
+ 0: "HI_SOFTIRQ",
+ 1: "TIMER_SOFTIRQ",
+ 2: "NET_TX_SOFTIRQ",
+ 3: "NET_RX_SOFTIRQ",
+ 4: "BLOCK_SOFTIRQ",
+ 5: "IRQ_POLL_SOFTIRQ",
+ 6: "TASKLET_SOFTIRQ",
+ 7: "SCHED_SOFTIRQ",
+ 8: "HRTIMER_SOFTIRQ",
+ 9: "RCU_SOFTIRQ",
+}
+
+_GFP_DMA = 1 << 0
+_GFP_HIGHMEM = 1 << 1
+_GFP_DMA32 = 1 << 2
+_GFP_MOVABLE = 1 << 3
+_GFP_RECLAIMABLE = 1 << 4
+_GFP_HIGH = 1 << 5
+_GFP_IO = 1 << 6
+_GFP_FS = 1 << 7
+_GFP_ZERO = 1 << 8
+_GFP_DIRECT_RECLAIM = 1 << 10
+_GFP_KSWAPD_RECLAIM = 1 << 11
+_GFP_WRITE = 1 << 12
+_GFP_NOWARN = 1 << 13
+_GFP_RETRY_MAYFAIL = 1 << 14
+_GFP_NOFAIL = 1 << 15
+_GFP_NORETRY = 1 << 16
+_GFP_MEMALLOC = 1 << 17
+_GFP_COMP = 1 << 18
+_GFP_NOMEMALLOC = 1 << 19
+_GFP_HARDWALL = 1 << 20
+_GFP_THISNODE = 1 << 21
+_GFP_ACCOUNT = 1 << 22
+_GFP_ZEROTAGS = 1 << 23
+
+_GFP_RECLAIM = _GFP_DIRECT_RECLAIM | _GFP_KSWAPD_RECLAIM
+_GFP_KERNEL = _GFP_RECLAIM | _GFP_IO | _GFP_FS
+_GFP_USER = _GFP_KERNEL | _GFP_HARDWALL
+_GFP_HIGHUSER = _GFP_USER | _GFP_HIGHMEM
+_GFP_HIGHUSER_MOVABLE = _GFP_HIGHUSER | _GFP_MOVABLE
+_GFP_TRANSHUGE_LIGHT = (
+ _GFP_HIGHUSER_MOVABLE | _GFP_COMP | _GFP_NOMEMALLOC | _GFP_NOWARN
+) & ~_GFP_RECLAIM
+_GFP_TRANSHUGE = _GFP_TRANSHUGE_LIGHT | _GFP_DIRECT_RECLAIM
+
+GFP_FLAG_NAMES = [
+ (_GFP_TRANSHUGE, "GFP_TRANSHUGE"),
+ (_GFP_TRANSHUGE_LIGHT, "GFP_TRANSHUGE_LIGHT"),
+ (_GFP_HIGHUSER_MOVABLE, "GFP_HIGHUSER_MOVABLE"),
+ (_GFP_HIGHUSER, "GFP_HIGHUSER"),
+ (_GFP_USER, "GFP_USER"),
+ (_GFP_KERNEL | _GFP_ACCOUNT, "GFP_KERNEL_ACCOUNT"),
+ (_GFP_KERNEL, "GFP_KERNEL"),
+ (_GFP_RECLAIM | _GFP_IO, "GFP_NOFS"),
+ (_GFP_HIGH | _GFP_KSWAPD_RECLAIM, "GFP_ATOMIC"),
+ (_GFP_RECLAIM, "GFP_NOIO"),
+ (_GFP_KSWAPD_RECLAIM | _GFP_NOWARN, "GFP_NOWAIT"),
+ (_GFP_DMA, "GFP_DMA"),
+ (_GFP_DMA32, "GFP_DMA32"),
+ (_GFP_RECLAIM, "__GFP_RECLAIM"),
+ (_GFP_DMA, "__GFP_DMA"),
+ (_GFP_HIGHMEM, "__GFP_HIGHMEM"),
+ (_GFP_DMA32, "__GFP_DMA32"),
+ (_GFP_MOVABLE, "__GFP_MOVABLE"),
+ (_GFP_RECLAIMABLE, "__GFP_RECLAIMABLE"),
+ (_GFP_HIGH, "__GFP_HIGH"),
+ (_GFP_IO, "__GFP_IO"),
+ (_GFP_FS, "__GFP_FS"),
+ (_GFP_ZERO, "__GFP_ZERO"),
+ (_GFP_DIRECT_RECLAIM, "__GFP_DIRECT_RECLAIM"),
+ (_GFP_KSWAPD_RECLAIM, "__GFP_KSWAPD_RECLAIM"),
+ (_GFP_WRITE, "__GFP_WRITE"),
+ (_GFP_NOWARN, "__GFP_NOWARN"),
+ (_GFP_RETRY_MAYFAIL, "__GFP_RETRY_MAYFAIL"),
+ (_GFP_NOFAIL, "__GFP_NOFAIL"),
+ (_GFP_NORETRY, "__GFP_NORETRY"),
+ (_GFP_MEMALLOC, "__GFP_MEMALLOC"),
+ (_GFP_COMP, "__GFP_COMP"),
+ (_GFP_NOMEMALLOC, "__GFP_NOMEMALLOC"),
+ (_GFP_HARDWALL, "__GFP_HARDWALL"),
+ (_GFP_THISNODE, "__GFP_THISNODE"),
+ (_GFP_ACCOUNT, "__GFP_ACCOUNT"),
+ (_GFP_ZEROTAGS, "__GFP_ZEROTAGS"),
+]
+
+
+def trace_begin() -> None:
+ """Called at the start of trace processing."""
+ print("in trace_begin")
+
+def trace_end() -> None:
+ """Called at the end of trace processing."""
+ print_unhandled()
+ print("in trace_end")
+
+def symbol_str(event_name: str, field_name: str, value: int) -> str:
+ """Resolves symbol values to strings."""
+ # Note: The standalone Python API currently lacks dynamic libtraceevent
+ # formatting (equivalent to _perf_trace_context.symbol_str())
+ if event_name == "irq__softirq_entry" and field_name == "vec":
+ return softirq_vecs.get(value, str(value))
+ return str(value)
+
+def flag_str(event_name: str, field_name: str, value: int) -> str:
+ """Resolves flag values to strings."""
+ # Note: The standalone Python API currently lacks dynamic libtraceevent
+ # formatting (equivalent to _perf_trace_context.flag_str())
+ if event_name == "kmem__kmalloc" and field_name == "gfp_flags":
+ if value == 0:
+ return "none"
+ names = []
+ rem = value
+ for mask, name in GFP_FLAG_NAMES:
+ if (rem & mask) == mask:
+ names.append(name)
+ rem &= ~mask
+ if rem:
+ names.append(f"0x{rem:x}")
+ return "|".join(names)
+ return str(value)
+
+def print_header(event_name: str, sample: perf.sample_event) -> None:
+ """Prints common header for events."""
+ secs = sample.sample_time // 1000000000
+ nsecs = sample.sample_time % 1000000000
+ comm = "[unknown]"
+ try:
+ if session:
+ thread = session.find_thread(sample.sample_pid, sample.sample_tid)
+ if thread:
+ comm = thread.comm() or "[unknown]"
+ except (TypeError, AttributeError):
+ pass
+ print(f"{event_name:<20} {sample.sample_cpu:5} {secs:05}.{nsecs:09} "
+ f"{sample.sample_tid:8} {comm:<20} ", end=' ')
+
+def print_uncommon(sample: perf.sample_event) -> None:
+ """Prints uncommon fields for tracepoints."""
+ # Fallback to 0 if field not found (e.g. on older kernels or if not tracepoint)
+ pc = getattr(sample, 'common_preempt_count', 0)
+ flags = getattr(sample, 'common_flags', 0)
+ lock_depth = getattr(sample, 'common_lock_depth', 0)
+
+ print(f"common_preempt_count={pc}, common_flags={flags}, "
+ f"common_lock_depth={lock_depth}, ", end='')
+
+def irq__softirq_entry(sample: perf.sample_event) -> None:
+ """Handles irq:softirq_entry events."""
+ print_header("irq__softirq_entry", sample)
+ print_uncommon(sample)
+ print(f"vec={symbol_str('irq__softirq_entry', 'vec', getattr(sample, 'vec', 0))}")
+
+def kmem__kmalloc(sample: perf.sample_event) -> None:
+ """Handles kmem:kmalloc events."""
+ print_header("kmem__kmalloc", sample)
+ print_uncommon(sample)
+
+ print(f"call_site={getattr(sample, 'call_site', 0):#x}, "
+ f"ptr={getattr(sample, 'ptr', 0):#x}, "
+ f"bytes_req={getattr(sample, 'bytes_req', 0):d}, "
+ f"bytes_alloc={getattr(sample, 'bytes_alloc', 0):d}, "
+ f"gfp_flags={flag_str('kmem__kmalloc', 'gfp_flags', getattr(sample, 'gfp_flags', 0))}")
+
+def trace_unhandled(event_name: str) -> None:
+ """Tracks unhandled events."""
+ unhandled[event_name] += 1
+
+def print_unhandled() -> None:
+ """Prints summary of unhandled events."""
+ if not unhandled:
+ return
+ print("\nunhandled events:\n")
+ print(f"{'event':<40} {'count':>10}")
+ print("---------------------------------------- -----------")
+ for event_name, count in unhandled.items():
+ print(f"{event_name:<40} {count:10}")
+
+def process_event(sample: perf.sample_event) -> None:
+ """Callback for processing events."""
+ event_name = str(sample.evsel)
+ if event_name.startswith("evsel(") and event_name.endswith(")"):
+ event_name = event_name[6:-1]
+ if event_name.startswith("irq:softirq_entry"):
+ irq__softirq_entry(sample)
+ elif event_name.startswith("kmem:kmalloc"):
+ kmem__kmalloc(sample)
+ else:
+ trace_unhandled(event_name)
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser()
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ args = ap.parse_args()
+
+ trace_begin()
+ try:
+ session = perf.session(perf.data(args.input), sample=process_event)
+ try:
+ session.process_events()
+ except KeyboardInterrupt:
+ pass
+ trace_end()
+ finally:
+ # Break the reference cycle between the global session and the
+ # process_event callback so the C perf.session object is freed.
+ session = None
diff --git a/tools/perf/python/compaction-times.py b/tools/perf/python/compaction-times.py
new file mode 100755
index 000000000000..48d6af005c46
--- /dev/null
+++ b/tools/perf/python/compaction-times.py
@@ -0,0 +1,362 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Report time spent in memory compaction.
+
+Memory compaction is a feature in the Linux kernel that defragments memory
+by moving used pages to create larger contiguous blocks of free memory. This
+is particularly useful for allocating huge pages.
+
+This script processes trace events related to memory compaction and reports:
+- Total time spent in compaction (stall time).
+- Statistics for page migration (moved vs. failed).
+- Statistics for the free scanner (scanned vs. isolated pages).
+- Statistics for the migration scanner (scanned vs. isolated pages).
+
+Definitions:
+- **Compaction**: Defragmenting memory by moving allocated pages.
+- **Migration**: Moving pages from their current location to free pages found by the free scanner.
+- **Free Scanner**: Scans memory (typically from the end of a zone) to find free pages.
+- **Migration Scanner**: Scans memory (typically from the beginning of a zone)
+ to find pages to move.
+- **Isolated Pages**: Pages that have been temporarily removed from the buddy
+ system for migration or as migration targets.
+
+Ported from tools/perf/scripts/python/compaction-times.py to the modern perf Python module.
+"""
+from __future__ import annotations
+
+import argparse
+import enum
+import re
+import signal
+import sys
+from typing import Callable, Dict, List, Optional, Any
+import perf
+
+if hasattr(signal, "SIGPIPE"):
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
+
+class Popt(enum.IntEnum):
+ """Process display options."""
+ DISP_DFL = 0
+ DISP_PROC = 1
+ DISP_PROC_VERBOSE = 2
+
+class Topt(enum.IntFlag):
+ """Trace display options."""
+ DISP_TIME = 0
+ DISP_MIG = 1
+ DISP_ISOLFREE = 2
+ DISP_ISOLMIG = 4
+ DISP_ALL = DISP_MIG | DISP_ISOLFREE | DISP_ISOLMIG
+
+# Globals to satisfy pylint when accessed in functions before assignment in main.
+OPT_NS = True
+opt_disp = Topt.DISP_ALL
+opt_proc = Popt.DISP_DFL
+session = None
+
+def get_comm_filter(regex: re.Pattern) -> Callable[[int, str], bool]:
+ """Returns a filter function based on command regex."""
+ def filter_func(_pid: int, comm: str) -> bool:
+ regex_match = regex.search(comm)
+ return regex_match is None or regex_match.group() == ""
+ return filter_func
+
+def get_pid_filter(low_str: str, high_str: str) -> Callable[[int, str], bool]:
+ """Returns a filter function based on PID range."""
+ low = 0 if low_str == "" else int(low_str)
+ high = None if high_str == "" else int(high_str)
+
+ def filter_func(pid: int, _comm: str) -> bool:
+ return not (pid >= low and (high is None or pid <= high))
+ return filter_func
+
+def ns_to_time(ns: int) -> str:
+ """Format nanoseconds to string based on options."""
+ return f"{ns}ns" if OPT_NS else f"{round(ns, -3) // 1000}us"
+
+class Pair:
+ """Represents a pair of related counters (e.g., scanned vs isolated, moved vs failed)."""
+ def __init__(self, aval: int, bval: int,
+ alabel: Optional[str] = None, blabel: Optional[str] = None):
+ self.alabel = alabel
+ self.blabel = blabel
+ self.aval = aval
+ self.bval = bval
+
+ def __add__(self, rhs: 'Pair') -> 'Pair':
+ return Pair(self.aval + rhs.aval, self.bval + rhs.bval, self.alabel, self.blabel)
+
+ def __iadd__(self, rhs: 'Pair') -> 'Pair':
+ self.aval += rhs.aval
+ self.bval += rhs.bval
+ return self
+
+ def __str__(self) -> str:
+ return f"{self.alabel}={self.aval} {self.blabel}={self.bval}"
+
+class Cnode:
+ """Holds statistics for a single compaction event or an aggregated set of events."""
+ def __init__(self, ns: int):
+ self.ns = ns
+ self.migrated = Pair(0, 0, "moved", "failed")
+ self.fscan = Pair(0, 0, "scanned", "isolated")
+ self.mscan = Pair(0, 0, "scanned", "isolated")
+
+ def __add__(self, rhs: 'Cnode') -> 'Cnode':
+ res = Cnode(self.ns + rhs.ns)
+ res.migrated = self.migrated + rhs.migrated
+ res.fscan = self.fscan + rhs.fscan
+ res.mscan = self.mscan + rhs.mscan
+ return res
+
+ def __iadd__(self, rhs: 'Cnode') -> 'Cnode':
+ self.ns += rhs.ns
+ self.migrated += rhs.migrated
+ self.fscan += rhs.fscan
+ self.mscan += rhs.mscan
+ return self
+
+ def __str__(self) -> str:
+ prev = False
+ s = f"{ns_to_time(self.ns)} "
+ if opt_disp & Topt.DISP_MIG:
+ s += f"migration: {self.migrated}"
+ prev = True
+ if opt_disp & Topt.DISP_ISOLFREE:
+ s += f"{' ' if prev else ''}free_scanner: {self.fscan}"
+ prev = True
+ if opt_disp & Topt.DISP_ISOLMIG:
+ s += f"{' ' if prev else ''}migration_scanner: {self.mscan}"
+ return s
+
+ def complete(self, secs: int, nsecs: int) -> None:
+ """Complete the node with duration."""
+ self.ns = (secs * 1000000000 + nsecs) - self.ns
+
+ def increment(self, migrated: Optional[Pair], fscan: Optional[Pair],
+ mscan: Optional[Pair]) -> None:
+ """Increment statistics."""
+ if migrated is not None:
+ self.migrated += migrated
+ if fscan is not None:
+ self.fscan += fscan
+ if mscan is not None:
+ self.mscan += mscan
+
+class Chead:
+ """Aggregates compaction statistics per process (PID) and maintains total statistics."""
+ heads: Dict[int, 'Chead'] = {}
+ val = Cnode(0)
+ fobj: Optional[Any] = None
+
+ @classmethod
+ def add_filter(cls, fobj: Any) -> None:
+ """Add a filter object."""
+ cls.fobj = fobj
+
+ @classmethod
+ def create_pending(cls, pid: int, comm: str, start_secs: int, start_nsecs: int) -> None:
+ """Create a pending node for a process."""
+ filtered = False
+ try:
+ head = cls.heads[pid]
+ filtered = head.is_filtered()
+ except KeyError:
+ if cls.fobj is not None:
+ filtered = cls.fobj(pid, comm)
+ head = cls.heads[pid] = Chead(comm, pid, filtered)
+
+ if not filtered:
+ head.mark_pending(start_secs, start_nsecs)
+
+ @classmethod
+ def increment_pending(cls, pid: int, migrated: Optional[Pair],
+ fscan: Optional[Pair], mscan: Optional[Pair]) -> None:
+ """Increment pending stats for a process."""
+ if pid not in cls.heads:
+ return
+ head = cls.heads[pid]
+ if not head.is_filtered():
+ if head.is_pending():
+ head.do_increment(migrated, fscan, mscan)
+ else:
+ sys.stderr.write(f"missing start compaction event for pid {pid}\n")
+
+ @classmethod
+ def complete_pending(cls, pid: int, secs: int, nsecs: int) -> None:
+ """Complete pending stats for a process."""
+ if pid not in cls.heads:
+ return
+ head = cls.heads[pid]
+ if not head.is_filtered():
+ if head.is_pending():
+ head.make_complete(secs, nsecs)
+ else:
+ sys.stderr.write(f"missing start compaction event for pid {pid}\n")
+
+ @classmethod
+ def gen(cls):
+ """Generate heads for display."""
+ if opt_proc != Popt.DISP_DFL:
+ yield from cls.heads.values()
+
+ @classmethod
+ def get_total(cls) -> Cnode:
+ """Get total statistics."""
+ return cls.val
+
+ def __init__(self, comm: str, pid: int, filtered: bool):
+ self.comm = comm
+ self.pid = pid
+ self.val = Cnode(0)
+ self.pending: Optional[Cnode] = None
+ self.filtered = filtered
+ self.list: List[Cnode] = []
+
+ def mark_pending(self, secs: int, nsecs: int) -> None:
+ """Mark node as pending."""
+ self.pending = Cnode(secs * 1000000000 + nsecs)
+
+ def do_increment(self, migrated: Optional[Pair], fscan: Optional[Pair],
+ mscan: Optional[Pair]) -> None:
+ """Increment pending stats."""
+ if self.pending is not None:
+ self.pending.increment(migrated, fscan, mscan)
+
+ def make_complete(self, secs: int, nsecs: int) -> None:
+ """Make pending stats complete."""
+ if self.pending is not None:
+ self.pending.complete(secs, nsecs)
+ Chead.val += self.pending
+
+ if opt_proc != Popt.DISP_DFL:
+ self.val += self.pending
+
+ if opt_proc == Popt.DISP_PROC_VERBOSE:
+ self.list.append(self.pending)
+ self.pending = None
+
+ def enumerate(self) -> None:
+ """Enumerate verbose stats."""
+ if opt_proc == Popt.DISP_PROC_VERBOSE and not self.is_filtered():
+ for i, pelem in enumerate(self.list):
+ sys.stdout.write(f"{self.pid}[{self.comm}].{i+1}: {pelem}\n")
+
+ def is_pending(self) -> bool:
+ """Check if node is pending."""
+ return self.pending is not None
+
+ def is_filtered(self) -> bool:
+ """Check if node is filtered."""
+ return self.filtered
+
+ def display(self) -> None:
+ """Display stats."""
+ if not self.is_filtered():
+ sys.stdout.write(f"{self.pid}[{self.comm}]: {self.val}\n")
+
+def trace_end() -> None:
+ """Called at the end of trace processing."""
+ sys.stdout.write(f"total: {Chead.get_total()}\n")
+ for i in Chead.gen():
+ i.display()
+ i.enumerate()
+
+def process_event(sample: perf.sample_event) -> None:
+ """Callback for processing events."""
+ event_name = str(sample.evsel)
+ pid = sample.sample_tid
+ comm = "[unknown]"
+ try:
+ if session:
+ thread = session.find_thread(sample.sample_pid, pid)
+ if thread:
+ comm = thread.comm() or "[unknown]"
+ except (TypeError, AttributeError):
+ pass
+ secs = sample.sample_time // 1000000000
+ nsecs = sample.sample_time % 1000000000
+
+ if "evsel(compaction:mm_compaction_begin)" in event_name:
+ Chead.create_pending(pid, comm, secs, nsecs)
+ elif "evsel(compaction:mm_compaction_end)" in event_name:
+ Chead.complete_pending(pid, secs, nsecs)
+ elif "evsel(compaction:mm_compaction_migratepages)" in event_name:
+ nr_migrated = getattr(sample, "nr_migrated", 0)
+ nr_failed = getattr(sample, "nr_failed", 0)
+ Chead.increment_pending(pid, Pair(nr_migrated, nr_failed), None, None)
+ elif "evsel(compaction:mm_compaction_isolate_freepages)" in event_name:
+ nr_scanned = getattr(sample, "nr_scanned", 0)
+ nr_taken = getattr(sample, "nr_taken", 0)
+ Chead.increment_pending(pid, None, Pair(nr_scanned, nr_taken), None)
+ elif "evsel(compaction:mm_compaction_isolate_migratepages)" in event_name:
+ nr_scanned = getattr(sample, "nr_scanned", 0)
+ nr_taken = getattr(sample, "nr_taken", 0)
+ Chead.increment_pending(pid, None, None, Pair(nr_scanned, nr_taken))
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(description="Report time spent in compaction")
+ ap.add_argument("-p", action="store_true", help="display by process")
+ ap.add_argument("-pv", action="store_true", help="display by process (verbose)")
+ ap.add_argument("-u", action="store_true", help="display results in microseconds")
+ ap.add_argument("-t", action="store_true", help="display stall times only")
+ ap.add_argument("-m", action="store_true", help="display stats for migration")
+ ap.add_argument("-fs", action="store_true", help="display stats for free scanner")
+ ap.add_argument("-ms", action="store_true", help="display stats for migration scanner")
+ ap.add_argument("filter", nargs="?", help="pid|pid-range|comm-regex")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ args = ap.parse_args()
+
+ opt_proc = Popt.DISP_DFL
+ if args.pv:
+ opt_proc = Popt.DISP_PROC_VERBOSE
+ elif args.p:
+ opt_proc = Popt.DISP_PROC
+
+ OPT_NS = not args.u
+
+ opt_disp = Topt.DISP_ALL
+ if args.t or args.m or args.fs or args.ms:
+ opt_disp = Topt(0)
+ if args.t:
+ opt_disp |= Topt.DISP_TIME
+ if args.m:
+ opt_disp |= Topt.DISP_MIG
+ if args.fs:
+ opt_disp |= Topt.DISP_ISOLFREE
+ if args.ms:
+ opt_disp |= Topt.DISP_ISOLMIG
+
+ if args.filter:
+ PID_PATTERN = r"^(\d*)-(\d*)$|^(\d*)$"
+ pid_re = re.compile(PID_PATTERN)
+ match = pid_re.search(args.filter)
+ filter_obj: Any = None
+ if match is not None and match.group() != "":
+ if match.group(3) is not None:
+ filter_obj = get_pid_filter(match.group(3), match.group(3))
+ else:
+ filter_obj = get_pid_filter(match.group(1), match.group(2))
+ else:
+ try:
+ comm_re = re.compile(args.filter)
+ except re.error:
+ sys.stderr.write(f"invalid regex '{args.filter}'\n")
+ sys.exit(1)
+ filter_obj = get_comm_filter(comm_re)
+ Chead.add_filter(filter_obj)
+
+ try:
+ session = perf.session(perf.data(args.input), sample=process_event)
+ try:
+ session.process_events()
+ except KeyboardInterrupt:
+ pass
+ trace_end()
+ finally:
+ # Break the reference cycle between the global session and the
+ # process_event callback so the C perf.session object is freed.
+ session = None
diff --git a/tools/perf/python/counting.py b/tools/perf/python/counting.py
index 02121d2bb11d..9adbbeccdacd 100755
--- a/tools/perf/python/counting.py
+++ b/tools/perf/python/counting.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
+"""Example for counting perf events."""
# -*- python -*-
# -*- coding: utf-8 -*-
diff --git a/tools/perf/python/event_analyzing_sample.py b/tools/perf/python/event_analyzing_sample.py
new file mode 100755
index 000000000000..b4efee88d94b
--- /dev/null
+++ b/tools/perf/python/event_analyzing_sample.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+General event handler in Python, using SQLite to analyze events.
+
+The 2 database related functions in this script just show how to gather
+the basic information, and users can modify and write their own functions
+according to their specific requirement.
+
+The first function "show_general_events" just does a basic grouping for all
+generic events with the help of sqlite, and the 2nd one "show_pebs_ll" is
+for a x86 HW PMU event: PEBS with load latency data.
+
+Ported from tools/perf/scripts/python/event_analyzing_sample.py
+"""
+from __future__ import annotations
+
+import argparse
+import math
+import sqlite3
+import struct
+from typing import Any
+import perf
+
+# Event types, user could add more here
+EVTYPE_GENERIC = 0
+EVTYPE_PEBS = 1 # Basic PEBS event
+EVTYPE_PEBS_LL = 2 # PEBS event with load latency info
+EVTYPE_IBS = 3
+
+#
+# Currently we don't have good way to tell the event type, but by
+# the size of raw buffer, raw PEBS event with load latency data's
+# size is 176 bytes, while the pure PEBS event's size is 144 bytes.
+#
+def create_event(name, comm, dso, symbol, raw_buf):
+ """Create an event object based on raw buffer size."""
+ if len(raw_buf) == 144:
+ event = PebsEvent(name, comm, dso, symbol, raw_buf)
+ elif len(raw_buf) == 176:
+ event = PebsNHM(name, comm, dso, symbol, raw_buf)
+ else:
+ event = PerfEvent(name, comm, dso, symbol, raw_buf)
+
+ return event
+
+class PerfEvent:
+ """Base class for all perf event samples."""
+ event_num = 0
+ def __init__(self, name, comm, dso, symbol, raw_buf, ev_type=EVTYPE_GENERIC):
+ self.name = name
+ self.comm = comm
+ self.dso = dso
+ self.symbol = symbol
+ self.raw_buf = raw_buf
+ self.ev_type = ev_type
+ PerfEvent.event_num += 1
+
+ def show(self):
+ """Display PMU event info."""
+ print(f"PMU event: name={self.name:12s}, symbol={self.symbol:24s}, "
+ f"comm={self.comm:8s}, dso={self.dso:12s}")
+
+#
+# Basic Intel PEBS (Precise Event-based Sampling) event, whose raw buffer
+# contains the context info when that event happened: the EFLAGS and
+# linear IP info, as well as all the registers.
+#
+class PebsEvent(PerfEvent):
+ """Intel PEBS event."""
+ pebs_num = 0
+ def __init__(self, name, comm, dso, symbol, raw_buf, ev_type=EVTYPE_PEBS):
+ tmp_buf = raw_buf[0:80]
+ flags, ip, ax, bx, cx, dx, si, di, bp, sp = struct.unpack('<QQQQQQQQQQ', tmp_buf)
+ self.flags = flags
+ self.ip = ip
+ self.ax = ax
+ self.bx = bx
+ self.cx = cx
+ self.dx = dx
+ self.si = si
+ self.di = di
+ self.bp = bp
+ self.sp = sp
+
+ super().__init__(name, comm, dso, symbol, raw_buf, ev_type)
+ PebsEvent.pebs_num += 1
+ del tmp_buf
+
+#
+# Intel Nehalem and Westmere support PEBS plus Load Latency info which lie
+# in the four 64 bit words write after the PEBS data:
+# Status: records the IA32_PERF_GLOBAL_STATUS register value
+# DLA: Data Linear Address (EIP)
+# DSE: Data Source Encoding, where the latency happens, hit or miss
+# in L1/L2/L3 or IO operations
+# LAT: the actual latency in cycles
+#
+class PebsNHM(PebsEvent):
+ """Intel Nehalem/Westmere PEBS event with load latency."""
+ pebs_nhm_num = 0
+ def __init__(self, name, comm, dso, symbol, raw_buf, ev_type=EVTYPE_PEBS_LL):
+ tmp_buf = raw_buf[144:176]
+ status, dla, dse, lat = struct.unpack('<QQQQ', tmp_buf)
+ self.status = status
+ self.dla = dla
+ self.dse = dse
+ self.lat = lat
+
+ super().__init__(name, comm, dso, symbol, raw_buf, ev_type)
+ PebsNHM.pebs_nhm_num += 1
+ del tmp_buf
+
+session: Any = None
+
+class _DB:
+ con: sqlite3.Connection | None = None
+
+def trace_begin(db_path: str | None = None) -> None:
+ """Initialize database tables."""
+ print("In trace_begin:\n")
+ con = sqlite3.connect(db_path or ":memory:")
+ try:
+ # Drop any pre-existing tables so repeated runs do not accumulate duplicate events.
+ con.execute("drop table if exists gen_events;")
+ con.execute("drop table if exists pebs_ll;")
+
+ # Will create several tables at the start, pebs_ll is for PEBS data with
+ # load latency info, while gen_events is for general event.
+ con.execute("""
+ create table if not exists gen_events (
+ name text,
+ symbol text,
+ comm text,
+ dso text
+ );""")
+ con.execute("""
+ create table if not exists pebs_ll (
+ name text,
+ symbol text,
+ comm text,
+ dso text,
+ flags integer,
+ ip integer,
+ status integer,
+ dse integer,
+ dla integer,
+ lat integer
+ );""")
+ except BaseException:
+ con.close()
+ raise
+ _DB.con = con
+
+def insert_db(event: Any) -> None:
+ """Insert event into database."""
+ con = _DB.con
+ assert con is not None
+ if event.ev_type == EVTYPE_GENERIC:
+ con.execute("insert into gen_events values(?, ?, ?, ?)",
+ (event.name, event.symbol, event.comm, event.dso))
+ elif event.ev_type == EVTYPE_PEBS_LL:
+ # Python's sqlite3 module only accepts signed 64-bit integers (-2^63 to 2^63 - 1)
+ # and raises OverflowError for unsigned 64-bit kernel pointers (> 0x7fffffffffffffff).
+ # Convert unsigned 64-bit addresses to signed 64-bit two's-complement integers.
+ ip = event.ip - 0x10000000000000000 if event.ip > 0x7fffffffffffffff else event.ip
+ status = (event.status - 0x10000000000000000
+ if event.status > 0x7fffffffffffffff else event.status)
+ dla = event.dla - 0x10000000000000000 if event.dla > 0x7fffffffffffffff else event.dla
+ con.execute("insert into pebs_ll values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ (event.name, event.symbol, event.comm, event.dso, event.flags,
+ ip, status, event.dse, dla, event.lat))
+
+def process_event(sample: perf.sample_event) -> None:
+ """Callback for processing events."""
+ # Create and insert event object to a database so that user could
+ # do more analysis with simple database commands.
+
+ # Resolve comm, symbol, dso
+ comm = "Unknown_comm"
+ try:
+ if session is not None:
+ proc = session.find_thread(sample.sample_pid, sample.sample_tid)
+ if proc:
+ comm = proc.comm() or "Unknown_comm"
+ except TypeError:
+ pass
+
+ # Symbol and dso info are not always resolved
+ dso = sample.dso if hasattr(sample, 'dso') and sample.dso else "Unknown_dso"
+ symbol = sample.symbol if hasattr(sample, 'symbol') and sample.symbol else "Unknown_symbol"
+ name = str(sample.evsel)
+ if name.startswith("evsel("):
+ name = name[6:-1]
+
+ # Create the event object and insert it to the right table in database
+ try:
+ event = create_event(name, comm, dso, symbol, sample.raw_buf)
+ insert_db(event)
+ except (sqlite3.Error, ValueError, TypeError, OverflowError) as e:
+ print(f"Error creating/inserting event: {e}")
+
+def num2sym(num: int) -> str:
+ """Convert number to a histogram symbol (log2)."""
+ # As the event number may be very big, so we can't use linear way
+ # to show the histogram in real number, but use a log2 algorithm.
+ if num <= 0:
+ return ""
+ snum = '#' * (int(math.log(num, 2)) + 1)
+ return snum
+
+def show_general_events() -> None:
+ """Display statistics for general events."""
+ con = _DB.con
+ assert con is not None
+ count = con.execute("select count(*) from gen_events")
+ for t in count:
+ print(f"There is {t[0]} records in gen_events table")
+ if t[0] == 0:
+ return
+
+ print("Statistics about the general events grouped by thread/symbol/dso: \n")
+
+ # Group by thread
+ commq = con.execute("""
+ select comm, count(comm) from gen_events
+ group by comm order by -count(comm)
+ """)
+ print(f"\n{ 'comm':>16} {'number':>8} {'histogram':>16}\n{'='*42}")
+ for row in commq:
+ print(f"{row[0]:>16} {row[1]:>8} {num2sym(row[1])}")
+
+ # Group by symbol
+ print(f"\n{'symbol':>32} {'number':>8} {'histogram':>16}\n{'='*58}")
+ symbolq = con.execute("""
+ select symbol, count(symbol) from gen_events
+ group by symbol order by -count(symbol)
+ """)
+ for row in symbolq:
+ print(f"{row[0]:>32} {row[1]:>8} {num2sym(row[1])}")
+
+ # Group by dso
+ print(f"\n{'dso':>40} {'number':>8} {'histogram':>16}\n{'='*74}")
+ dsoq = con.execute("select dso, count(dso) from gen_events group by dso order by -count(dso)")
+ for row in dsoq:
+ print(f"{row[0]:>40} {row[1]:>8} {num2sym(row[1])}")
+
+def show_pebs_ll() -> None:
+ """Display statistics for PEBS load latency events."""
+ con = _DB.con
+ assert con is not None
+ # This function just shows the basic info, and we could do more with the
+ # data in the tables, like checking the function parameters when some
+ # big latency events happen.
+ count = con.execute("select count(*) from pebs_ll")
+ for t in count:
+ print(f"There is {t[0]} records in pebs_ll table")
+ if t[0] == 0:
+ return
+
+ print("Statistics about the PEBS Load Latency events grouped by thread/symbol/dse/latency: \n")
+
+ # Group by thread
+ commq = con.execute("select comm, count(comm) from pebs_ll group by comm order by -count(comm)")
+ print(f"\n{'comm':>16} {'number':>8} {'histogram':>16}\n{'='*42}")
+ for row in commq:
+ print(f"{row[0]:>16} {row[1]:>8} {num2sym(row[1])}")
+
+ # Group by symbol
+ print(f"\n{'symbol':>32} {'number':>8} {'histogram':>16}\n{'='*58}")
+ symbolq = con.execute("""
+ select symbol, count(symbol) from pebs_ll
+ group by symbol order by -count(symbol)
+ """)
+ for row in symbolq:
+ print(f"{row[0]:>32} {row[1]:>8} {num2sym(row[1])}")
+
+ # Group by dse
+ dseq = con.execute("select dse, count(dse) from pebs_ll group by dse order by -count(dse)")
+ print(f"\n{'dse':>32} {'number':>8} {'histogram':>16}\n{'='*58}")
+ for row in dseq:
+ print(f"{row[0]:>32} {row[1]:>8} {num2sym(row[1])}")
+
+ # Group by latency
+ latq = con.execute("select lat, count(lat) from pebs_ll group by lat order by lat")
+ print(f"\n{'latency':>32} {'number':>8} {'histogram':>16}\n{'='*58}")
+ for row in latq:
+ print(f"{str(row[0]):>32} {row[1]:>8} {num2sym(row[1])}")
+
+def trace_end() -> None:
+ """Called at the end of trace processing."""
+ print("In trace_end:\n")
+ if _DB.con:
+ try:
+ _DB.con.commit()
+ show_general_events()
+ show_pebs_ll()
+ finally:
+ _DB.con.close()
+ _DB.con = None
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(description="Analyze events with SQLite")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ ap.add_argument("-d", "--db", "--database", dest="database", default=None,
+ help="Database file name (defaults to an in-memory database)")
+ args = ap.parse_args()
+
+ try:
+ trace_begin(args.database)
+ session = perf.session(perf.data(args.input), sample=process_event)
+ session.process_events()
+ finally:
+ # Break the reference cycle between session and process_event (whose module
+ # globals reference session) so the C session object is deterministically freed.
+ session = None
+ trace_end()
diff --git a/tools/perf/python/export-to-postgresql.py b/tools/perf/python/export-to-postgresql.py
new file mode 100755
index 000000000000..9cac64dee361
--- /dev/null
+++ b/tools/perf/python/export-to-postgresql.py
@@ -0,0 +1,1328 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+r"""
+Export perf data to a postgresql database.
+
+This script has been ported to use the modern perf Python module and
+libpq via ctypes. It no longer requires PySide2 or QtSql for exporting.
+
+The script assumes postgresql is running on the local machine and that the
+user has postgresql permissions to create databases.
+
+An example of using this script with Intel PT:
+
+ $ perf record -e intel_pt//u ls
+ $ python export-to-postgresql.py -i perf.data -o pt_example
+
+To browse the database, psql can be used e.g.
+
+ $ psql pt_example
+ pt_example=# select * from samples_view where id < 100;
+ pt_example=# \d+
+ pt_example=# \d+ samples_view
+ pt_example=# \q
+
+An example of using the database is provided by the script
+exported-sql-viewer.py. Refer to that script for details.
+
+Tables:
+
+ The tables largely correspond to perf tools' data structures. They are
+ largely self-explanatory.
+
+ samples
+ 'samples' is the main table. It represents what instruction was
+ executing at a point in time when something (a selected event)
+ happened. The memory address is the instruction pointer or 'ip'.
+
+ branch_types
+ 'branch_types' provides descriptions for each type of branch.
+
+ comm_threads
+ 'comm_threads' shows how 'comms' relates to 'threads'.
+
+ comms
+ 'comms' contains a record for each 'comm' - the name given to the
+ executable that is running.
+
+ dsos
+ 'dsos' contains a record for each executable file or library.
+
+ machines
+ 'machines' can be used to distinguish virtual machines if
+ virtualization is supported.
+
+ selected_events
+ 'selected_events' contains a record for each kind of event that
+ has been sampled.
+
+ symbols
+ 'symbols' contains a record for each symbol. Only symbols that
+ have samples are present.
+
+ threads
+ 'threads' contains a record for each thread.
+
+Views:
+
+ Most of the tables have views for more friendly display. The views are:
+
+ comm_threads_view
+ dsos_view
+ machines_view
+ samples_view
+ symbols_view
+ threads_view
+
+Ported from tools/perf/scripts/python/export-to-postgresql.py
+"""
+
+from __future__ import annotations
+import typing
+import argparse
+from ctypes import CDLL, c_char_p, c_int, c_void_p, c_ubyte
+import ctypes.util
+import os
+import shutil
+import struct
+import tempfile
+import sys
+from typing import Any, Dict, Optional
+import perf
+
+# Need to access PostgreSQL C library directly to use COPY FROM STDIN
+libpq_name = ctypes.util.find_library("pq")
+if not libpq_name:
+ libpq_name = "libpq.so.5"
+
+try:
+ libpq = CDLL(libpq_name)
+except OSError as e:
+ print(f"Error loading {libpq_name}: {e}")
+ print("Please ensure PostgreSQL client library is installed.")
+ sys.exit(1)
+
+PQconnectdb = libpq.PQconnectdb
+PQconnectdb.restype = c_void_p
+PQconnectdb.argtypes = [c_char_p]
+PQfinish = libpq.PQfinish
+PQfinish.argtypes = [c_void_p]
+PQstatus = libpq.PQstatus
+PQstatus.restype = c_int
+PQstatus.argtypes = [c_void_p]
+libpq.PQerrorMessage.restype = c_char_p
+libpq.PQerrorMessage.argtypes = [c_void_p]
+PQexec = libpq.PQexec
+PQexec.restype = c_void_p
+PQexec.argtypes = [c_void_p, c_char_p]
+PQresultStatus = libpq.PQresultStatus
+PQresultStatus.restype = c_int
+PQresultStatus.argtypes = [c_void_p]
+PQputCopyData = libpq.PQputCopyData
+PQputCopyData.restype = c_int
+PQputCopyData.argtypes = [c_void_p, c_void_p, c_int]
+PQputCopyEnd = libpq.PQputCopyEnd
+PQputCopyEnd.restype = c_int
+PQputCopyEnd.argtypes = [c_void_p, c_void_p]
+PQgetResult = libpq.PQgetResult
+PQgetResult.restype = c_void_p
+PQgetResult.argtypes = [c_void_p]
+PQclear = libpq.PQclear
+PQclear.argtypes = [c_void_p]
+
+
+def toserverstr(s: str) -> bytes:
+ """Convert string to server encoding (UTF-8)."""
+ return bytes(s, "UTF_8")
+
+
+def toclientstr(s: str) -> bytes:
+ """Convert string to client encoding (UTF-8)."""
+ return bytes(s, "UTF_8")
+
+PERF_IP_FLAG_BRANCH = 1 << 0
+PERF_IP_FLAG_CALL = 1 << 1
+PERF_IP_FLAG_RETURN = 1 << 2
+PERF_IP_FLAG_CONDITIONAL = 1 << 3
+PERF_IP_FLAG_SYSCALLRET = 1 << 4
+PERF_IP_FLAG_ASYNC = 1 << 5
+PERF_IP_FLAG_INTERRUPT = 1 << 6
+PERF_IP_FLAG_TX_ABORT = 1 << 7
+PERF_IP_FLAG_TRACE_BEGIN = 1 << 8
+PERF_IP_FLAG_TRACE_END = 1 << 9
+PERF_IP_FLAG_IN_TX = 1 << 10
+PERF_IP_FLAG_VMENTRY = 1 << 11
+PERF_IP_FLAG_VMEXIT = 1 << 12
+
+BRANCH_TYPES = [
+ (0, "no branch"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL, "call"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN, "return"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CONDITIONAL, "conditional jump"),
+ (PERF_IP_FLAG_BRANCH, "unconditional jump"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_INTERRUPT, "software interrupt"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_INTERRUPT, "return from interrupt"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_SYSCALLRET, "system call"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_SYSCALLRET,
+ "return from system call"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_ASYNC, "asynchronous branch"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_ASYNC | PERF_IP_FLAG_INTERRUPT,
+ "hardware interrupt"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TX_ABORT, "transaction abort"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_BEGIN, "trace begin"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_END, "trace end"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMENTRY, "vm entry"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMEXIT, "vm exit"),
+]
+for _b_type, _b_name in list(BRANCH_TYPES):
+ if (_b_type == PERF_IP_FLAG_BRANCH or
+ (_b_type & (PERF_IP_FLAG_TRACE_BEGIN | PERF_IP_FLAG_TRACE_END))):
+ continue
+ BRANCH_TYPES.append((_b_type | PERF_IP_FLAG_TRACE_BEGIN, f"trace begin / {_b_name}"))
+ BRANCH_TYPES.append((_b_type | PERF_IP_FLAG_TRACE_END, f"{_b_name} / trace end"))
+
+class PostgresExporter:
+ """Handles PostgreSQL connection and exporting of perf events."""
+
+ def __init__(self, dbname: str):
+ self.dbname = dbname
+ self.conn = None
+ self.session: Optional[perf.session] = None
+ self.output_dir_name = tempfile.mkdtemp(prefix=dbname + "-perf-data-")
+ self.created_output_dir = True
+
+ self.file_header = struct.pack("!11sii", b"PGCOPY\n\377\r\n\0", 0, 0)
+ self.file_trailer = b"\377\377"
+
+ # Caches and counters grouped to reduce instance attributes
+ self.caches: Dict[str, dict] = {
+ 'machines': {},
+ 'threads': {},
+ 'comms': {},
+ 'dsos': {},
+ 'symbols': {},
+ 'events': {},
+ 'branch_types': {},
+ 'call_paths': {}
+ }
+ self.symbol_rows: Dict[int, tuple[int, int, int, int, str]] = {}
+
+ self.next_id = {
+ 'machine': 1,
+ 'thread': 1,
+ 'comm': 1,
+ 'dso': 1,
+ 'symbol': 1,
+ 'event': 1,
+ 'branch_type': 1,
+ 'comm_thread': 1,
+ 'call_path': 1,
+ 'sample': 1,
+ 'call': 1,
+ 'context_switch': 1
+ }
+
+ self.files: Dict[str, Any] = {}
+ self.unhandled_count = 0
+
+ def connect(self, db_to_use: str) -> None:
+ """Connect to database.
+
+ Reject URI schemes ('://') and key=value pairs ('=') to prevent
+ connection parameter injection, and escape backslashes and single
+ quotes per libpq single-quoted conninfo string rules.
+ """
+ if "://" in db_to_use or "=" in db_to_use:
+ raise ValueError(f"Invalid database name: {db_to_use}")
+ safe_db = db_to_use.replace('\\', '\\\\').replace("'", "\\'")
+ conn_str = toclientstr(f"dbname='{safe_db}'")
+ self.conn = PQconnectdb(conn_str)
+ if PQstatus(self.conn) != 0:
+ PQfinish(self.conn)
+ self.conn = None
+ raise RuntimeError(f"PQconnectdb failed for {db_to_use}")
+
+ def disconnect(self) -> None:
+ """Disconnect from database."""
+ if self.conn:
+ PQfinish(self.conn)
+ self.conn = None
+
+ def do_query(self, sql: str) -> None:
+ """Execute a query and check status."""
+ res = PQexec(self.conn, toserverstr(sql))
+ status = PQresultStatus(res)
+ PQclear(res)
+ if status not in (1, 2): # PGRES_COMMAND_OK, PGRES_TUPLES_OK
+ error_msg = libpq.PQerrorMessage(self.conn).decode('utf-8')
+ raise RuntimeError(f"Query failed: {sql}. Error: {error_msg}")
+
+ def open_output_file(self, file_name: str):
+ """Open intermediate binary file."""
+ path_name = self.output_dir_name + "/" + file_name
+ f = open(path_name, "wb+")
+ f.write(self.file_header)
+ return f
+
+ def close_output_file(self, f):
+ """Close intermediate binary file."""
+ f.write(self.file_trailer)
+ f.close()
+
+ def copy_output_file(self, path_name: str, table_name: str):
+ """Copy intermediate file to database."""
+ sql = f"COPY {table_name} FROM STDIN (FORMAT 'binary')"
+ res = PQexec(self.conn, toserverstr(sql))
+ if PQresultStatus(res) != 4: # PGRES_COPY_IN
+ PQclear(res)
+ err = libpq.PQerrorMessage(self.conn).decode()
+ raise RuntimeError(f"COPY FROM STDIN PQexec failed for {table_name}: {err}")
+ PQclear(res)
+
+ with open(path_name, "rb") as f:
+ data = f.read(65536)
+ while len(data) > 0:
+ c_data = (c_ubyte * len(data)).from_buffer_copy(data)
+ ret = PQputCopyData(self.conn, c_data, len(data))
+ if ret != 1:
+ raise RuntimeError(f"PQputCopyData failed for {table_name}")
+ data = f.read(65536)
+
+ ret = PQputCopyEnd(self.conn, None)
+ if ret != 1:
+ err = libpq.PQerrorMessage(self.conn).decode()
+ raise RuntimeError(f"PQputCopyEnd failed for {table_name}: {err}")
+
+ res = PQgetResult(self.conn)
+ while res:
+ status = PQresultStatus(res)
+ if status != 1: # PGRES_COMMAND_OK
+ error_msg = libpq.PQerrorMessage(self.conn).decode('utf-8')
+ PQclear(res)
+ raise RuntimeError(
+ f"COPY completion failed for {table_name}. Status {status}: {error_msg}"
+ )
+ PQclear(res)
+ res = PQgetResult(self.conn)
+
+ def setup_db(self) -> None:
+ """Create database and tables. MUST be called after init."""
+ if "://" in self.dbname or "=" in self.dbname:
+ raise ValueError(f"Invalid database name: {self.dbname}")
+ self.created_output_dir = True
+
+ self.connect('postgres')
+ try:
+ # SQL identifiers are double-quoted; escape embedded double quotes by doubling them.
+ db_name = self.dbname.replace('"', '""')
+ self.do_query(f'CREATE DATABASE "{db_name}"')
+ except Exception as db_error:
+ shutil.rmtree(self.output_dir_name, ignore_errors=True)
+ raise db_error
+ self.disconnect()
+
+ self.connect(self.dbname)
+ self.do_query("SET client_min_messages TO WARNING")
+
+ self.do_query("""
+ CREATE TABLE selected_events (
+ id bigint NOT NULL,
+ name varchar(80))
+ """)
+ self.do_query("""
+ CREATE TABLE machines (
+ id bigint NOT NULL,
+ pid integer,
+ root_dir varchar(4096))
+ """)
+ self.do_query("""
+ CREATE TABLE threads (
+ id bigint NOT NULL,
+ machine_id bigint,
+ process_id bigint,
+ pid integer,
+ tid integer)
+ """)
+ self.do_query("""
+ CREATE TABLE comms (
+ id bigint NOT NULL,
+ comm varchar(16),
+ c_thread_id bigint,
+ c_time bigint,
+ exec_flag boolean)
+ """)
+ self.do_query("""
+ CREATE TABLE comm_threads (
+ id bigint NOT NULL,
+ comm_id bigint,
+ thread_id bigint)
+ """)
+ self.do_query("""
+ CREATE TABLE dsos (
+ id bigint NOT NULL,
+ machine_id bigint,
+ short_name varchar(256),
+ long_name varchar(4096),
+ build_id varchar(64))
+ """)
+ self.do_query("""
+ CREATE TABLE symbols (
+ id bigint NOT NULL,
+ dso_id bigint,
+ sym_start bigint,
+ sym_end bigint,
+ binding integer,
+ name varchar(2048))
+ """)
+ self.do_query("""
+ CREATE TABLE branch_types (
+ id integer NOT NULL,
+ name varchar(80))
+ """)
+ self.do_query("""
+ CREATE TABLE samples (
+ id bigint NOT NULL,
+ evsel_id bigint,
+ machine_id bigint,
+ thread_id bigint,
+ comm_id bigint,
+ dso_id bigint,
+ symbol_id bigint,
+ sym_offset bigint,
+ ip bigint,
+ time bigint,
+ cpu integer,
+ to_dso_id bigint,
+ to_symbol_id bigint,
+ to_sym_offset bigint,
+ to_ip bigint,
+ period bigint,
+ weight bigint,
+ transaction_ bigint,
+ data_src bigint,
+ branch_type integer,
+ in_tx boolean,
+ call_path_id bigint,
+ insn_count bigint,
+ cyc_count bigint,
+ flags integer)
+ """)
+ self.do_query('''
+ CREATE TABLE context_switches (
+ id bigint,
+ machine_id bigint,
+ time bigint,
+ cpu integer,
+ thread_out_id bigint,
+ comm_out_id bigint,
+ thread_in_id bigint,
+ comm_in_id bigint,
+ flags integer
+ )
+ ''')
+ self.do_query('''
+ CREATE TABLE call_paths (
+ id bigint NOT NULL,
+ parent_id bigint,
+ symbol_id bigint,
+ ip bigint)
+ ''')
+ self.do_query('''
+ CREATE TABLE calls (
+ id bigint NOT NULL,
+ thread_id bigint,
+ comm_id bigint,
+ call_path_id bigint,
+ call_time bigint,
+ return_time bigint,
+ branch_count bigint,
+ call_id bigint,
+ return_id bigint,
+ parent_call_path_id bigint,
+ flags integer,
+ parent_id bigint,
+ insn_count bigint,
+ cyc_count bigint
+ )
+ ''')
+ self.do_query('''
+ CREATE TABLE ptwrite (
+ id bigint NOT NULL,
+ payload bigint,
+ exact_ip boolean
+ )
+ ''')
+ self.do_query('''
+ CREATE TABLE cbr (
+ id bigint NOT NULL,
+ cbr integer,
+ mhz integer,
+ percent integer
+ )
+ ''')
+ self.do_query('''
+ CREATE TABLE mwait (
+ id bigint NOT NULL,
+ hints integer,
+ extensions integer
+ )
+ ''')
+ self.do_query('''
+ CREATE TABLE pwre (
+ id bigint NOT NULL,
+ cstate integer,
+ subcstate integer,
+ hw boolean
+ )
+ ''')
+ self.do_query('''
+ CREATE TABLE exstop (
+ id bigint NOT NULL,
+ exact_ip boolean
+ )
+ ''')
+ self.do_query('''
+ CREATE TABLE pwrx (
+ id bigint NOT NULL,
+ deepest_cstate integer,
+ last_cstate integer,
+ wake_reason integer
+ )
+ ''')
+
+ self.files['evsel'] = self.open_output_file("evsel_table.bin")
+ self.files['machine'] = self.open_output_file("machine_table.bin")
+ self.files['thread'] = self.open_output_file("thread_table.bin")
+ self.files['comm'] = self.open_output_file("comm_table.bin")
+ self.files['comm_thread'] = self.open_output_file("comm_thread_table.bin")
+ self.files['dso'] = self.open_output_file("dso_table.bin")
+ self.files['symbol'] = self.open_output_file("symbol_table.bin")
+ self.files['branch_type'] = self.open_output_file("branch_type_table.bin")
+ self.files['sample'] = self.open_output_file("sample_table.bin")
+ self.files['context_switches'] = self.open_output_file("context_switches_table.bin")
+ self.files['call'] = self.open_output_file("call_table.bin")
+ self.files['call_path'] = self.open_output_file("call_path_table.bin")
+ self.files['ptwrite'] = self.open_output_file("ptwrite_table.bin")
+ self.files['cbr'] = self.open_output_file("cbr_table.bin")
+ self.files['mwait'] = self.open_output_file("mwait_table.bin")
+ self.files['pwre'] = self.open_output_file("pwre_table.bin")
+ self.files['exstop'] = self.open_output_file("exstop_table.bin")
+ self.files['pwrx'] = self.open_output_file("pwrx_table.bin")
+
+ self.write_evsel(0, "unknown")
+ self.write_machine(0, 0, "unknown")
+ self.write_thread(0, 0, 0, -1, -1)
+ self.write_comm(0, "unknown", 0, 0, 0)
+ self.write_dso(0, 0, "unknown", "unknown", "")
+ self.write_symbol(0, 0, 0, 0, 0, "unknown")
+ self.write_call_path(0, 0, 0, 0)
+ # Sentinel id = 0 rows in sample_table.bin and call_table.bin satisfy
+ # PostgreSQL FOREIGN KEY constraints (call_id/return_id -> samples.id
+ # and parent references) when a call or return event has no matching
+ # sample reference (0).
+ self.files['sample'].write(struct.pack(
+ "!hiqiqiqiqiqiqiqiQiQiQiIiqiqiQiQiQiQiQiQiiiBiqiQiQii",
+ 25, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 4, 0,
+ 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 4, 0, 1, 0, 8, 0, 8, 0, 8, 0, 4, 0
+ ))
+ self.files['call'].write(struct.pack(
+ "!hiqiqiqiqiqiqiqiqiqiqiiiqiqiq",
+ 14, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 8, 0, 4, 0, 8, 0, 8, 0, 8, 0
+ ))
+ for b_type, b_name in BRANCH_TYPES:
+ self.write_branch_type(b_type, b_name)
+
+ def write_branch_type(self, branch_type: int, name: str) -> None:
+ name_bytes = toserverstr(name)
+ n = len(name_bytes)
+ fmt = "!hiii" + str(n) + "s"
+ value = struct.pack(fmt, 2, 4, branch_type, n, name_bytes)
+ self.files['branch_type'].write(value)
+
+ def write_ptwrite(self, id_: int, raw_buf: bytes) -> None:
+ data = struct.unpack_from("<IQ", raw_buf)
+ flags, payload = data[0], data[1]
+ value = struct.pack("!hiqiQiB", 3, 8, id_, 8, payload, 1, flags & 1)
+ self.files['ptwrite'].write(value)
+
+ def write_cbr(self, id_: int, raw_buf: bytes) -> None:
+ data = struct.unpack_from("<BBBBII", raw_buf)
+ cbr_val, max_nonturbo, freq = data[0], data[2], data[4]
+ mhz = (freq + 500) // 1000
+ percent = ((cbr_val * 1000 // max_nonturbo) + 5) // 10 if max_nonturbo else 0
+ value = struct.pack("!hiqiiiiii", 4, 8, id_, 4, cbr_val, 4, int(mhz), 4, int(percent))
+ self.files['cbr'].write(value)
+
+ def write_mwait(self, id_: int, raw_buf: bytes) -> None:
+ data = struct.unpack_from("<IQ", raw_buf)
+ payload = data[1]
+ hints = payload & 0xff
+ extensions = (payload >> 32) & 0x3
+ value = struct.pack("!hiqiiii", 3, 8, id_, 4, hints, 4, extensions)
+ self.files['mwait'].write(value)
+
+ def write_pwre(self, id_: int, raw_buf: bytes) -> None:
+ data = struct.unpack_from("<IQ", raw_buf)
+ payload = data[1]
+ hw = (payload >> 7) & 1
+ cstate = (payload >> 12) & 0xf
+ subcstate = (payload >> 8) & 0xf
+ value = struct.pack("!hiqiiiiiB", 4, 8, id_, 4, cstate, 4, subcstate, 1, hw)
+ self.files['pwre'].write(value)
+
+ def write_exstop(self, id_: int, raw_buf: bytes) -> None:
+ data = struct.unpack_from("<I", raw_buf)
+ flags = data[0]
+ value = struct.pack("!hiqiB", 2, 8, id_, 1, flags & 1)
+ self.files['exstop'].write(value)
+
+ def write_pwrx(self, id_: int, raw_buf: bytes) -> None:
+ data = struct.unpack_from("<IQ", raw_buf)
+ payload = data[1]
+ deepest = payload & 0xf
+ last = (payload >> 4) & 0xf
+ wake = (payload >> 8) & 0xf
+ value = struct.pack("!hiqiiiiii", 4, 8, id_, 4, deepest, 4, last, 4, wake)
+ self.files['pwrx'].write(value)
+
+ def write_evsel(self, evsel_id: int, name: str) -> None:
+ """Write event to binary file."""
+ name_bytes = toserverstr(name)
+ n = len(name_bytes)
+ fmt = "!hiqi" + str(n) + "s"
+ value = struct.pack(fmt, 2, 8, evsel_id, n, name_bytes)
+ self.files['evsel'].write(value)
+
+ def write_machine(self, machine_id: int, pid: int, root_dir: str) -> None:
+ """Write machine to binary file."""
+ rd_bytes = toserverstr(root_dir)
+ n = len(rd_bytes)
+ fmt = "!hiqiii" + str(n) + "s"
+ value = struct.pack(fmt, 3, 8, machine_id, 4, pid, n, rd_bytes)
+ self.files['machine'].write(value)
+
+ def write_thread(self, thread_id: int, machine_id: int, process_id: int,
+ pid: int, tid: int) -> None:
+ """Write thread to binary file."""
+ value = struct.pack("!hiqiqiqiIiI", 5, 8, thread_id, 8, machine_id,
+ 8, process_id, 4, pid & 0xffffffff, 4, tid & 0xffffffff)
+ self.files['thread'].write(value)
+
+ def write_comm(self, comm_id: int, comm_str: str, thread_id: int,
+ time: int, exec_flag: int) -> None:
+ """Write comm to binary file."""
+ comm_bytes = toserverstr(comm_str)
+ n = len(comm_bytes)
+ fmt = "!hiqi" + str(n) + "s" + "iqiqiB"
+ value = struct.pack(fmt, 5, 8, comm_id, n, comm_bytes, 8,
+ thread_id, 8, time, 1, exec_flag)
+ self.files['comm'].write(value)
+
+ def write_comm_thread(self, comm_thread_id: int, comm_id: int,
+ thread_id: int) -> None:
+ """Write comm_thread to binary file."""
+ fmt = "!hiqiqiq"
+ value = struct.pack(fmt, 3, 8, comm_thread_id, 8, comm_id, 8, thread_id)
+ self.files['comm_thread'].write(value)
+
+ def write_dso(self, dso_id: int, machine_id: int, short_name: str,
+ long_name: str, build_id: str) -> None:
+ """Write DSO to binary file."""
+ sn_bytes = toserverstr(short_name)
+ ln_bytes = toserverstr(long_name)
+ bi_bytes = toserverstr(build_id)
+ n1, n2, n3 = len(sn_bytes), len(ln_bytes), len(bi_bytes)
+ fmt = "!hiqiqi" + str(n1) + "si" + str(n2) + "si" + str(n3) + "s"
+ value = struct.pack(fmt, 5, 8, dso_id, 8, machine_id, n1,
+ sn_bytes, n2, ln_bytes, n3, bi_bytes)
+ self.files['dso'].write(value)
+
+ def write_symbol(self, symbol_id: int, dso_id: int, sym_start: int,
+ sym_end: int, binding: int, symbol_name: str) -> None:
+ """Write symbol to binary file."""
+ name_bytes = toserverstr(symbol_name)
+ n = len(name_bytes)
+ fmt = "!hiqiqiQiQiii" + str(n) + "s"
+ value = struct.pack(fmt, 6, 8, symbol_id, 8, dso_id, 8,
+ sym_start, 8, sym_end, 4, binding, n, name_bytes)
+ self.files['symbol'].write(value)
+
+ def write_call_path(self, cp_id: int, parent_id: int, symbol_id: int,
+ ip: int) -> None:
+ """Write call path to binary file."""
+ fmt = "!hiqiqiqiQ"
+ value = struct.pack(fmt, 4, 8, cp_id, 8, parent_id, 8, symbol_id, 8, ip)
+ self.files['call_path'].write(value)
+
+ def write_sample(self, sample_id: int, evsel_id: int, machine_id: int, thread_id: int,
+ comm_id: int, dso_id: int, symbol_id: int,
+ sample: perf.sample_event, call_path_id: int) -> None:
+ """Write sample to binary file."""
+ addr_dso_id = self.get_dso_id(
+ sample.addr_dso or "Unknown_dso", sample.addr_dso or "Unknown_dso_long", "",
+ machine_id
+ )
+ addr_symbol_id = self.get_symbol_id(
+ addr_dso_id, sample.addr_symbol or "Unknown_symbol", 0, 0
+ )
+ value = struct.pack(
+ "!hiqiqiqiqiqiqiqiQiQiQiIiqiqiQiQiQiQiQiQiiiBiqiQiQii",
+ 25, 8, sample_id, 8, evsel_id, 8, machine_id, 8, thread_id, 8, comm_id,
+ 8, dso_id, 8, symbol_id, 8, getattr(sample, 'sym_offset', 0) or 0,
+ 8, getattr(sample, 'sample_ip', 0) or 0,
+ 8, getattr(sample, 'sample_time', 0) or 0,
+ 4, getattr(sample, 'sample_cpu', 0) & 0xffffffff,
+ 8, addr_dso_id,
+ 8, addr_symbol_id,
+ 8, getattr(sample, 'addr_sym_offset', 0) or 0,
+ 8, getattr(sample, 'sample_addr', 0) or 0,
+ 8, getattr(sample, 'sample_period', 0) or 0,
+ 8, getattr(sample, 'sample_weight', 0) or 0,
+ 8, getattr(sample, 'transaction', 0) or 0,
+ 8, getattr(sample, 'sample_data_src', 0) or 0 or 0,
+ 4, getattr(sample, 'branch_type', 0) or 0,
+ 1, getattr(sample, 'in_tx', 0) or 0,
+ 8, call_path_id,
+ 8, getattr(sample, 'sample_insn_count', 0) or 0 or 0,
+ 8, getattr(sample, 'sample_cyc_count', 0) or 0 or 0,
+ 4, getattr(sample, 'flags', 0) or 0
+ )
+ self.files['sample'].write(value)
+
+ def get_machine_id(self, machine_pid: Optional[int]) -> int:
+ """Get or create machine ID."""
+ if machine_pid is None or machine_pid <= 0 or machine_pid > 0x7fffffff:
+ machine_pid = -1
+ if machine_pid in self.caches['machines']:
+ return self.caches['machines'][machine_pid]
+ machine_id = self.next_id['machine']
+ self.next_id['machine'] += 1
+ self.write_machine(machine_id, machine_pid, "")
+ self.caches['machines'][machine_pid] = machine_id
+ return machine_id
+
+ def get_event_id(self, name: str) -> int:
+ """Get or create event ID."""
+ if name in self.caches['events']:
+ return self.caches['events'][name]
+ event_id = self.next_id['event']
+ self.write_evsel(event_id, name)
+ self.caches['events'][name] = event_id
+ self.next_id['event'] += 1
+ return event_id
+
+ def get_thread_id(self, machine_id: int, pid: Optional[int], tid: Optional[int]) -> int:
+ """Get or create thread ID."""
+ if pid is None:
+ pid = -1
+ elif pid > 0x7fffffff:
+ pid -= 0x100000000
+ if tid is None:
+ tid = -1
+ elif tid > 0x7fffffff:
+ tid -= 0x100000000
+ key = (machine_id, pid, tid)
+ if key in self.caches['threads']:
+ return self.caches['threads'][key]
+ process_id = self.get_thread_id(machine_id, pid, pid) if tid != pid else -1
+ thread_id = self.next_id['thread']
+ if process_id == -1: process_id = thread_id
+ self.write_thread(thread_id, machine_id, process_id, pid, tid)
+ self.caches['threads'][key] = thread_id
+ self.next_id['thread'] += 1
+ return thread_id
+
+ def get_comm_id(self, comm: str, thread_id: int) -> int:
+ """Get or create comm ID."""
+ c_key = (comm, thread_id)
+ if c_key in self.caches['comms']:
+ comm_id = self.caches['comms'][c_key]
+ else:
+ comm_id = self.next_id['comm']
+ self.write_comm(comm_id, comm, thread_id, 0, 0)
+ self.caches['comms'][c_key] = comm_id
+ self.next_id['comm'] += 1
+
+ key = (comm_id, thread_id)
+ if 'comm_threads' not in self.caches:
+ self.caches['comm_threads'] = {}
+ if key not in self.caches['comm_threads']:
+ comm_thread_id = self.next_id['comm_thread']
+ self.write_comm_thread(comm_thread_id, comm_id, thread_id)
+ self.caches['comm_threads'][key] = True
+ self.next_id['comm_thread'] += 1
+
+ return comm_id
+
+ def get_dso_id(self, short_name: str, long_name: str,
+ build_id: str, machine_id: int = 0) -> int:
+ """Get or create DSO ID."""
+ key = (machine_id, short_name, long_name, build_id)
+ if key in self.caches['dsos']:
+ return self.caches['dsos'][key]
+ short_key = (machine_id, short_name)
+ if not build_id and short_key in self.caches['dsos']:
+ return self.caches['dsos'][short_key]
+ dso_id = self.next_id['dso']
+ self.write_dso(dso_id, machine_id, short_name, long_name, build_id)
+ self.caches['dsos'][key] = dso_id
+ self.caches['dsos'][short_key] = dso_id
+ self.next_id['dso'] += 1
+ return dso_id
+
+ def get_symbol_id(self, dso_id: int, name: str, start: int,
+ end: int) -> int:
+ """Get or create symbol ID."""
+ key = (dso_id, name)
+ if key in self.caches['symbols']:
+ symbol_id, cached_start, cached_end = self.caches['symbols'][key]
+ if cached_start == 0 and cached_end == 0 and (start != 0 or end != 0):
+ self.caches['symbols'][key] = (symbol_id, start, end)
+ self.symbol_rows[symbol_id] = (dso_id, start, end, 0, name)
+ return symbol_id
+ symbol_id = self.next_id['symbol']
+ self.caches['symbols'][key] = (symbol_id, start, end)
+ self.symbol_rows[symbol_id] = (dso_id, start, end, 0, name)
+ self.next_id['symbol'] += 1
+ return symbol_id
+
+ def get_call_path_id(self, parent_id: int, symbol_id: int,
+ ip: int) -> int:
+ """Get or create call path ID."""
+ key = (parent_id, symbol_id, ip)
+ if key in self.caches['call_paths']:
+ return self.caches['call_paths'][key]
+ call_path_id = self.next_id['call_path']
+ self.write_call_path(call_path_id, parent_id, symbol_id, ip)
+ self.caches['call_paths'][key] = call_path_id
+ self.next_id['call_path'] += 1
+ return call_path_id
+
+ def process_context_switch(self, event: typing.Any) -> None:
+ """Callback for processing context switch events."""
+ misc = getattr(event, 'misc', 0)
+ out = bool(misc & (1 << 13))
+ out_preempt = bool(misc & (1 << 14))
+ flags = (1 if out else 0) | ((1 if out_preempt else 0) << 1)
+ machine_id = self.get_machine_id(getattr(event, 'machine_pid', None))
+
+ sample_pid = getattr(event, 'sample_pid', None)
+ if sample_pid is None:
+ sample_pid = -1
+ elif sample_pid > 0x7fffffff:
+ sample_pid -= 0x100000000
+ sample_tid = getattr(event, 'sample_tid', None)
+ if sample_tid is None:
+ sample_tid = -1
+ elif sample_tid > 0x7fffffff:
+ sample_tid -= 0x100000000
+ next_prev_pid = getattr(event, 'next_prev_pid', None)
+ if next_prev_pid is None:
+ next_prev_pid = -1
+ elif next_prev_pid > 0x7fffffff:
+ next_prev_pid -= 0x100000000
+ next_prev_tid = getattr(event, 'next_prev_tid', None)
+ if next_prev_tid is None:
+ next_prev_tid = -1
+ elif next_prev_tid > 0x7fffffff:
+ next_prev_tid -= 0x100000000
+
+ th_a_id = self.get_thread_id(machine_id, sample_pid, sample_tid)
+ comm_a = "Unknown_comm"
+ if self.session:
+ try:
+ proc = self.session.find_thread(sample_pid, sample_tid)
+ if proc:
+ comm_a_name = proc.comm() or "Unknown_comm"
+ if comm_a_name:
+ comm_a = comm_a_name
+ except TypeError:
+ pass
+ comm_a_id = self.get_comm_id(comm_a, th_a_id)
+
+ th_b_id = self.get_thread_id(machine_id, next_prev_pid, next_prev_tid)
+ comm_b = "Unknown_comm"
+ if self.session:
+ try:
+ proc = self.session.find_thread(next_prev_pid, next_prev_tid)
+ if proc:
+ comm_b_name = proc.comm() or "Unknown_comm"
+ if comm_b_name:
+ comm_b = comm_b_name
+ except TypeError:
+ pass
+ comm_b_id = self.get_comm_id(comm_b, th_b_id)
+
+ if out:
+ th_out_id = th_a_id
+ comm_out_id = comm_a_id
+ th_in_id = th_b_id
+ comm_in_id = comm_b_id
+ else:
+ th_out_id = th_b_id
+ comm_out_id = comm_b_id
+ th_in_id = th_a_id
+ comm_in_id = comm_a_id
+
+ cs_id = self.next_id['context_switch']
+ self.next_id['context_switch'] += 1
+ time = getattr(event, "time", getattr(event, "sample_time", 0))
+ cpu = getattr(event, "sample_cpu", 0) or 0
+ if cpu > 0x7fffffff:
+ cpu -= 0x100000000
+
+ # 9 columns total:
+ fmt = "!hiQiqiQiiiqiqiqiqii"
+ value = struct.pack(
+ fmt, 9, 8, cs_id, 8, machine_id, 8, time, 4, cpu,
+ 8, th_out_id, 8, comm_out_id, 8, th_in_id, 8, comm_in_id, 4, flags
+ )
+ self.files["context_switches"].write(value)
+
+ def get_call_path_ids(self, call_path: typing.Any, machine_id: int = 0) -> tuple[int, int]:
+ """Add a perf.callchain as call_path rows, return its id and parent's."""
+ parent_id = 0
+ call_path_id = 0
+ for node in call_path:
+ dso_name = node.dso or "Unknown_dso"
+ symbol_name = node.symbol or "Unknown_symbol"
+ dso_id = self.get_dso_id(dso_name, dso_name, "", machine_id)
+ symbol_id = self.get_symbol_id(dso_id, symbol_name, 0, 0)
+ parent_id = call_path_id
+ call_path_id = self.get_call_path_id(parent_id, symbol_id, node.ip)
+ return call_path_id, parent_id
+
+ def process_call_return(self, cr: perf.call_return) -> None:
+ """Callback for processing call_return events."""
+ machine_id = self.get_machine_id(getattr(cr, "machine_pid", 0) or 0)
+ thread_id = self.get_thread_id(machine_id,
+ cr.pid if cr.pid is not None else -1,
+ cr.tid if cr.tid is not None else -1)
+ comm_id = self.get_comm_id(cr.comm or "Unknown_comm", thread_id)
+ call_path_id, parent_call_path_id = self.get_call_path_ids(cr.call_path or [], machine_id)
+
+ fmt = "!hiqiqiqiqiqiqiqiqiqiqiiiqiqiq"
+ # call.id and call.parent_id use the db_id of the perf module, as a call
+ # is given an id before it returns its parent cannot be numbered here.
+ value = struct.pack(
+ fmt, 14, 8, cr.db_id, 8, thread_id, 8, comm_id, 8, call_path_id,
+ 8, cr.call_time, 8, cr.return_time, 8, cr.branch_count,
+ 8, cr.call_ref, 8, cr.return_ref,
+ 8, parent_call_path_id, 4, cr.flags, 8, cr.parent_id,
+ 8, cr.insn_count, 8, cr.cyc_count
+ )
+ self.files['call'].write(value)
+
+ def process_event(self, sample: typing.Any) -> None:
+ """Callback for processing events."""
+
+ machine_db_id = getattr(sample, 'machine_pid', 0)
+ machine_id = self.get_machine_id(machine_db_id)
+ thread_id = self.get_thread_id(machine_id, sample.sample_pid, sample.sample_tid)
+
+ comm = "Unknown_comm"
+ try:
+ if self.session is not None:
+ proc = self.session.find_thread(sample.sample_pid, sample.sample_tid)
+ if proc:
+ comm = proc.comm() or "Unknown_comm"
+ except TypeError:
+ pass
+ comm_id = self.get_comm_id(comm, thread_id)
+
+ dso_id = self.get_dso_id(
+ sample.dso or "Unknown_dso",
+ sample.dso_long_name or "Unknown_dso_long",
+ sample.dso_bid or "",
+ machine_id
+ )
+
+ symbol_id = self.get_symbol_id(
+ dso_id,
+ sample.symbol or "Unknown_symbol",
+ sample.sym_start or 0,
+ sample.sym_end or 0
+ )
+
+ call_path_id = 0
+ if hasattr(sample, 'callchain') and sample.callchain:
+ parent_id = 0
+ for node in reversed(sample.callchain):
+ node_dso = getattr(node, 'dso', None) or getattr(node, 'map', None)
+ node_symbol = getattr(node, 'symbol', None) or getattr(node, 'sym', None)
+
+ dso_name = "Unknown_dso"
+ if node_dso:
+ dso_name = (node_dso if isinstance(node_dso, str)
+ else getattr(node_dso, 'name', "Unknown_dso") or "Unknown_dso")
+
+ symbol_name = "Unknown_symbol"
+ if node_symbol:
+ symbol_name = (node_symbol if isinstance(node_symbol, str)
+ else getattr(node_symbol, 'name', "Unknown_symbol")
+ or "Unknown_symbol")
+
+ node_dso_id = self.get_dso_id(dso_name, dso_name, "", machine_id)
+ node_symbol_id = self.get_symbol_id(node_dso_id, symbol_name, 0, 0)
+
+ parent_id = self.get_call_path_id(parent_id, node_symbol_id, node.ip)
+ call_path_id = parent_id
+
+ sample_id = self.next_id['sample']
+ evsel_name = str(sample.evsel)
+ if evsel_name.startswith("evsel(") and evsel_name.endswith(")"):
+ evsel_name = evsel_name[6:-1]
+ self.write_sample(sample_id,
+ self.get_event_id(evsel_name),
+ machine_id, thread_id, comm_id, dso_id, symbol_id, sample,
+ call_path_id)
+
+ event_name_str = evsel_name
+ if hasattr(sample, "raw_buf"):
+ if event_name_str == "ptwrite":
+ self.write_ptwrite(sample_id, sample.raw_buf)
+ elif event_name_str == "cbr":
+ self.write_cbr(sample_id, sample.raw_buf)
+ elif event_name_str == "mwait":
+ self.write_mwait(sample_id, sample.raw_buf)
+ elif event_name_str == "pwre":
+ self.write_pwre(sample_id, sample.raw_buf)
+ elif event_name_str == "exstop":
+ self.write_exstop(sample_id, sample.raw_buf)
+ elif event_name_str == "pwrx":
+ self.write_pwrx(sample_id, sample.raw_buf)
+
+ self.next_id['sample'] += 1
+
+ def finalize(self) -> None:
+ """Copy files to database and add keys/views."""
+ print("Copying to database...")
+ for symbol_id, (dso_id, sym_start, sym_end, binding, name) in self.symbol_rows.items():
+ self.write_symbol(symbol_id, dso_id, sym_start, sym_end, binding, name)
+ for name, f in self.files.items():
+ self.close_output_file(f)
+
+ table_mapping = {
+ 'evsel': 'selected_events',
+ 'machine': 'machines',
+ 'thread': 'threads',
+ 'comm': 'comms',
+ 'comm_thread': 'comm_threads',
+ 'dso': 'dsos',
+ 'symbol': 'symbols',
+ 'branch_type': 'branch_types',
+ 'sample': 'samples',
+ 'call': 'calls',
+ 'call_path': 'call_paths',
+ 'context_switches': 'context_switches',
+ 'ptwrite': 'ptwrite',
+ 'cbr': 'cbr',
+ 'mwait': 'mwait',
+ 'pwre': 'pwre',
+ 'exstop': 'exstop',
+ 'pwrx': 'pwrx'
+ }
+ for name, f in self.files.items():
+ table_name = table_mapping.get(name, name + "s")
+ self.copy_output_file(f.name, table_name)
+
+ print("Removing intermediate files...")
+ for name, f in self.files.items():
+ os.unlink(f.name)
+ shutil.rmtree(self.output_dir_name, ignore_errors=True)
+
+ print("Adding primary keys")
+ self.do_query("ALTER TABLE selected_events ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE machines ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE threads ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE comms ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE comm_threads ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE dsos ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE symbols ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE branch_types ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE samples ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE call_paths ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE calls ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE ptwrite ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE cbr ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE mwait ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE pwre ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE exstop ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE pwrx ADD PRIMARY KEY (id)")
+ self.do_query("ALTER TABLE context_switches ADD PRIMARY KEY (id)")
+
+ print("Adding foreign keys")
+ self.do_query('ALTER TABLE threads '
+ 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),'
+ 'ADD CONSTRAINT processfk FOREIGN KEY (process_id) REFERENCES threads (id)')
+ self.do_query('ALTER TABLE comms '
+ 'ADD CONSTRAINT threadfk FOREIGN KEY (c_thread_id) REFERENCES threads (id)')
+ self.do_query('ALTER TABLE comm_threads '
+ 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),'
+ 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id)')
+ self.do_query('ALTER TABLE dsos '
+ 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id)')
+ self.do_query('ALTER TABLE symbols '
+ 'ADD CONSTRAINT dsofk FOREIGN KEY (dso_id) REFERENCES dsos (id)')
+ self.do_query('ALTER TABLE samples '
+ 'ADD CONSTRAINT evselfk FOREIGN KEY (evsel_id) '
+ 'REFERENCES selected_events (id),'
+ 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),'
+ 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id),'
+ 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),'
+ 'ADD CONSTRAINT dsofk FOREIGN KEY (dso_id) REFERENCES dsos (id),'
+ 'ADD CONSTRAINT symbolfk FOREIGN KEY (symbol_id) REFERENCES symbols (id),'
+ 'ADD CONSTRAINT todsofk FOREIGN KEY (to_dso_id) REFERENCES dsos (id),'
+ 'ADD CONSTRAINT tosymbolfk FOREIGN KEY (to_symbol_id) '
+ 'REFERENCES symbols (id)')
+ self.do_query('ALTER TABLE call_paths '
+ 'ADD CONSTRAINT parentfk FOREIGN KEY (parent_id) REFERENCES call_paths (id),'
+ 'ADD CONSTRAINT symbolfk FOREIGN KEY (symbol_id) REFERENCES symbols (id)')
+ self.do_query('ALTER TABLE calls '
+ 'ADD CONSTRAINT threadfk FOREIGN KEY (thread_id) REFERENCES threads (id),'
+ 'ADD CONSTRAINT commfk FOREIGN KEY (comm_id) REFERENCES comms (id),'
+ 'ADD CONSTRAINT call_pathfk FOREIGN KEY (call_path_id) '
+ 'REFERENCES call_paths (id),'
+ 'ADD CONSTRAINT callfk FOREIGN KEY (call_id) REFERENCES samples (id),'
+ 'ADD CONSTRAINT returnfk FOREIGN KEY (return_id) REFERENCES samples (id),'
+ 'ADD CONSTRAINT parent_call_pathfk FOREIGN KEY (parent_call_path_id) '
+ 'REFERENCES call_paths (id)')
+ # pcpid_idx, pid_idx, and comms.has_calls are required by
+ # exported-sql-viewer.py to filter the command dropdown to threads with
+ # call graphs and to expand parent/child call trees efficiently.
+ self.do_query('CREATE INDEX pcpid_idx ON calls (parent_call_path_id)')
+ self.do_query('CREATE INDEX pid_idx ON calls (parent_id)')
+ self.do_query('ALTER TABLE comms ADD has_calls boolean')
+ self.do_query('UPDATE comms SET has_calls = TRUE WHERE comms.id IN '
+ '(SELECT DISTINCT comm_id FROM calls)')
+ self.do_query('ALTER TABLE context_switches '
+ 'ADD CONSTRAINT machinefk FOREIGN KEY (machine_id) REFERENCES machines (id),'
+ 'ADD CONSTRAINT toutfk FOREIGN KEY (thread_out_id) REFERENCES threads (id),'
+ 'ADD CONSTRAINT tinfk FOREIGN KEY (thread_in_id) REFERENCES threads (id),'
+ 'ADD CONSTRAINT coutfk FOREIGN KEY (comm_out_id) REFERENCES comms (id),'
+ 'ADD CONSTRAINT cinfk FOREIGN KEY (comm_in_id) REFERENCES comms (id)')
+ self.do_query('ALTER TABLE ptwrite '
+ 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)')
+ self.do_query('ALTER TABLE cbr '
+ 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)')
+ self.do_query('ALTER TABLE mwait '
+ 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)')
+ self.do_query('ALTER TABLE pwre '
+ 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)')
+ self.do_query('ALTER TABLE exstop '
+ 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)')
+ self.do_query('ALTER TABLE pwrx '
+ 'ADD CONSTRAINT idfk FOREIGN KEY (id) REFERENCES samples (id)')
+
+ print("Creating views...")
+ self.do_query(
+ "CREATE VIEW machines_view AS "
+ "SELECT id, pid, root_dir, "
+ "CASE WHEN id=0 THEN 'unknown' WHEN pid=-1 THEN 'host' ELSE 'guest' END "
+ "AS host_or_guest FROM machines"
+ )
+ self.do_query(
+ "CREATE VIEW dsos_view AS "
+ "SELECT id, machine_id, "
+ "(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest, "
+ "short_name, long_name, build_id FROM dsos"
+ )
+ self.do_query(
+ "CREATE VIEW symbols_view AS "
+ "SELECT id, name, (SELECT short_name FROM dsos WHERE id=dso_id) AS dso, "
+ "dso_id, sym_start, sym_end, "
+ "CASE WHEN binding=0 THEN 'local' WHEN binding=1 THEN 'global' ELSE 'weak' END "
+ "AS binding FROM symbols"
+ )
+ self.do_query(
+ "CREATE VIEW threads_view AS "
+ "SELECT id, machine_id, "
+ "(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest, "
+ "process_id, pid, tid FROM threads"
+ )
+ self.do_query(
+ "CREATE VIEW comm_threads_view AS "
+ "SELECT comm_id, (SELECT comm FROM comms WHERE id = comm_id) AS command, "
+ "thread_id, (SELECT pid FROM threads WHERE id = thread_id) AS pid, "
+ "(SELECT tid FROM threads WHERE id = thread_id) AS tid FROM comm_threads"
+ )
+ self.do_query(
+ "CREATE VIEW call_paths_view AS "
+ "SELECT c.id, to_hex(c.ip) AS ip, c.symbol_id, "
+ "(SELECT name FROM symbols WHERE id = c.symbol_id) AS symbol, "
+ "(SELECT dso_id FROM symbols WHERE id = c.symbol_id) AS dso_id, "
+ "(SELECT dso FROM symbols_view WHERE id = c.symbol_id) AS dso_short_name, "
+ "c.parent_id, to_hex(p.ip) AS parent_ip, p.symbol_id AS parent_symbol_id, "
+ "(SELECT name FROM symbols WHERE id = p.symbol_id) AS parent_symbol, "
+ "(SELECT dso_id FROM symbols WHERE id = p.symbol_id) AS parent_dso_id, "
+ "(SELECT dso FROM symbols_view WHERE id = p.symbol_id) AS parent_dso_short_name "
+ "FROM call_paths c LEFT JOIN call_paths p ON p.id = c.parent_id"
+ )
+ self.do_query(
+ "CREATE VIEW calls_view AS "
+ "SELECT calls.id, thread_id, "
+ "(SELECT pid FROM threads WHERE id = thread_id) AS pid, "
+ "(SELECT tid FROM threads WHERE id = thread_id) AS tid, "
+ "(SELECT comm FROM comms WHERE id = comm_id) AS command, "
+ "call_path_id, to_hex(ip) AS ip, symbol_id, "
+ "(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, "
+ "call_time, return_time, return_time - call_time AS elapsed_time, "
+ "branch_count, insn_count, cyc_count, "
+ "CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) "
+ "ELSE CAST(insn_count AS FLOAT) / cyc_count END AS IPC, "
+ "call_id, return_id, "
+ "CASE WHEN flags=0 THEN '' WHEN flags=1 THEN 'no call' "
+ "WHEN flags=2 THEN 'no return' WHEN flags=3 THEN 'no call/return' "
+ "WHEN flags IN (4, 6) THEN 'jmp' ELSE CAST(flags AS VARCHAR(6)) END AS flags, "
+ "parent_call_path_id, calls.parent_id "
+ "FROM calls INNER JOIN call_paths ON call_paths.id = call_path_id"
+ )
+ self.do_query(
+ "CREATE VIEW samples_view AS "
+ "SELECT id, time, cpu, "
+ "(SELECT pid FROM threads WHERE id = thread_id) AS pid, "
+ "(SELECT tid FROM threads WHERE id = thread_id) AS tid, "
+ "(SELECT comm FROM comms WHERE id = comm_id) AS command, "
+ "(SELECT name FROM selected_events WHERE id = evsel_id) AS event, "
+ "to_hex(ip) AS ip_hex, "
+ "(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, sym_offset, "
+ "(SELECT short_name FROM dsos WHERE id = dso_id) AS dso_short_name, "
+ "to_hex(to_ip) AS to_ip_hex, "
+ "(SELECT name FROM symbols WHERE id = to_symbol_id) AS to_symbol, to_sym_offset, "
+ "(SELECT short_name FROM dsos WHERE id = to_dso_id) AS to_dso_short_name, "
+ "(SELECT name FROM branch_types WHERE id = branch_type) AS branch_type_name, "
+ "in_tx, call_path_id, insn_count, cyc_count, "
+ "CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) "
+ "ELSE CAST(insn_count AS FLOAT) / cyc_count END AS IPC, flags FROM samples"
+ )
+ self.do_query(
+ "CREATE VIEW context_switches_view AS "
+ "SELECT context_switches.id, context_switches.machine_id, "
+ "context_switches.time, context_switches.cpu, "
+ "th_out.pid AS pid_out, th_out.tid AS tid_out, comm_out.comm AS comm_out, "
+ "th_in.pid AS pid_in, th_in.tid AS tid_in, comm_in.comm AS comm_in, "
+ "CASE WHEN flags=0 THEN 'in' WHEN flags=1 THEN 'out' "
+ "WHEN flags=3 THEN 'out preempt' ELSE CAST(flags AS VARCHAR(6)) END AS flags "
+ "FROM context_switches "
+ "INNER JOIN threads AS th_out ON th_out.id = context_switches.thread_out_id "
+ "INNER JOIN threads AS th_in ON th_in.id = context_switches.thread_in_id "
+ "INNER JOIN comms AS comm_out ON comm_out.id = context_switches.comm_out_id "
+ "INNER JOIN comms AS comm_in ON comm_in.id = context_switches.comm_in_id"
+ )
+ self.do_query(
+ "CREATE VIEW ptwrite_view AS "
+ "SELECT ptwrite.id, time, cpu, to_hex(payload) AS payload_hex, "
+ "CASE WHEN exact_ip=FALSE THEN 'False' ELSE 'True' END AS exact_ip "
+ "FROM ptwrite INNER JOIN samples ON samples.id = ptwrite.id"
+ )
+ self.do_query(
+ "CREATE VIEW cbr_view AS "
+ "SELECT cbr.id, time, cpu, cbr, mhz, percent "
+ "FROM cbr INNER JOIN samples ON samples.id = cbr.id"
+ )
+ self.do_query(
+ "CREATE VIEW mwait_view AS "
+ "SELECT mwait.id, time, cpu, to_hex(hints) AS hints_hex, "
+ "to_hex(extensions) AS extensions_hex "
+ "FROM mwait INNER JOIN samples ON samples.id = mwait.id"
+ )
+ self.do_query(
+ "CREATE VIEW pwre_view AS "
+ "SELECT pwre.id, time, cpu, cstate, subcstate, "
+ "CASE WHEN hw=FALSE THEN 'False' ELSE 'True' END AS hw "
+ "FROM pwre INNER JOIN samples ON samples.id = pwre.id"
+ )
+ self.do_query(
+ "CREATE VIEW exstop_view AS "
+ "SELECT exstop.id, time, cpu, "
+ "CASE WHEN exact_ip=FALSE THEN 'False' ELSE 'True' END AS exact_ip "
+ "FROM exstop INNER JOIN samples ON samples.id = exstop.id"
+ )
+ self.do_query(
+ "CREATE VIEW pwrx_view AS "
+ "SELECT pwrx.id, time, cpu, deepest_cstate, last_cstate, "
+ "CASE WHEN wake_reason=1 THEN 'Interrupt' "
+ "WHEN wake_reason=2 THEN 'Timer Deadline' "
+ "WHEN wake_reason=4 THEN 'Monitored Address' "
+ "WHEN wake_reason=8 THEN 'HW' WHEN wake_reason=16 THEN 'Other' "
+ "ELSE CAST ( wake_reason AS VARCHAR(2) ) END AS wake_reason "
+ "FROM pwrx INNER JOIN samples ON samples.id = pwrx.id"
+ )
+ self.do_query(
+ "CREATE VIEW power_events_view AS "
+ "SELECT samples.id, samples.time, samples.cpu, selected_events.name AS event, "
+ "FORMAT('%6s', cbr.cbr) AS cbr, FORMAT('%6s', cbr.mhz) AS MHz, "
+ "FORMAT('%5s', cbr.percent) AS percent, to_hex(mwait.hints) AS hints_hex, "
+ "to_hex(mwait.extensions) AS extensions_hex, FORMAT('%3s', pwre.cstate) AS cstate, "
+ "FORMAT('%3s', pwre.subcstate) AS subcstate, "
+ "CASE WHEN pwre.hw=FALSE THEN 'False' WHEN pwre.hw=TRUE THEN 'True' "
+ "ELSE NULL END AS hw, "
+ "CASE WHEN exstop.exact_ip=FALSE THEN 'False' WHEN exstop.exact_ip=TRUE THEN 'True' "
+ "ELSE NULL END AS exact_ip, FORMAT('%3s', pwrx.deepest_cstate) AS deepest_cstate, "
+ "FORMAT('%3s', pwrx.last_cstate) AS last_cstate, "
+ "CASE WHEN pwrx.wake_reason=1 THEN 'Interrupt' "
+ "WHEN wake_reason=2 THEN 'Timer Deadline' "
+ "WHEN wake_reason=4 THEN 'Monitored Address' "
+ "WHEN wake_reason=8 THEN 'HW' WHEN wake_reason=16 THEN 'Other' "
+ "ELSE FORMAT('%2s', pwrx.wake_reason) END AS wake_reason "
+ "FROM cbr FULL JOIN mwait ON mwait.id = cbr.id "
+ "FULL JOIN pwre ON pwre.id = cbr.id FULL JOIN exstop ON exstop.id = cbr.id "
+ "FULL JOIN pwrx ON pwrx.id = cbr.id INNER JOIN samples ON samples.id = "
+ "coalesce(cbr.id, mwait.id, pwre.id, exstop.id, pwrx.id) "
+ "INNER JOIN selected_events ON selected_events.id = samples.evsel_id "
+ "ORDER BY samples.id"
+ )
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(
+ description="Export perf data to a postgresql database")
+ ap.add_argument("-i", "--input", default="perf.data",
+ help="Input file name")
+ ap.add_argument("-o", "--output", required=True,
+ help="Output database name")
+ ap.add_argument("--itrace", default=None,
+ help="itrace options, e.g. cr")
+ args = ap.parse_args()
+
+ exporter = PostgresExporter(args.output)
+
+ session = None
+ succeeded = False
+ caught_error = False
+ try:
+ exporter.setup_db()
+ session = perf.session(perf.data(args.input),
+ context_switch=exporter.process_context_switch,
+ sample=exporter.process_event,
+ call_return=exporter.process_call_return,
+ itrace=args.itrace)
+ exporter.session = session
+ try:
+ session.process_events()
+ finally:
+ # Break the C perf.session -> bound method -> exporter reference cycle.
+ exporter.session = None
+ exporter.finalize()
+ print(f"Successfully exported to {args.output}")
+ succeeded = True
+ except (OSError, RuntimeError, ValueError, KeyboardInterrupt) as e:
+ if not isinstance(e, (KeyboardInterrupt, SystemExit)):
+ import traceback
+ traceback.print_exc()
+ caught_error = True
+ finally:
+ exporter.session = None
+ for out_file in exporter.files.values():
+ try:
+ if not out_file.closed:
+ out_file.close()
+ except OSError:
+ pass
+ exporter.disconnect()
+ if not succeeded:
+ if (getattr(exporter, 'created_output_dir', False) and
+ os.path.exists(exporter.output_dir_name)):
+ shutil.rmtree(exporter.output_dir_name, ignore_errors=True)
+ if caught_error:
+ sys.exit(1)
diff --git a/tools/perf/python/export-to-sqlite.py b/tools/perf/python/export-to-sqlite.py
new file mode 100755
index 000000000000..0a8087e51037
--- /dev/null
+++ b/tools/perf/python/export-to-sqlite.py
@@ -0,0 +1,946 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Export perf data to a sqlite3 database.
+
+This script has been ported to use the modern perf Python module and the
+standard library sqlite3 module. It no longer requires PySide2 or QtSql
+for exporting.
+
+Examples of using this script with Intel PT:
+
+ $ perf record -e intel_pt//u ls
+ $ python export-to-sqlite.py -i perf.data -o pt_example
+
+To browse the database, sqlite3 can be used e.g.
+
+ $ sqlite3 pt_example
+ sqlite> .header on
+ sqlite> select * from samples_view where id < 10;
+ sqlite> .mode column
+ sqlite> select * from samples_view where id < 10;
+ sqlite> .tables
+ sqlite> .schema samples_view
+ sqlite> .quit
+
+An example of using the database is provided by the script
+exported-sql-viewer.py. Refer to that script for details.
+
+Ported from tools/perf/scripts/python/export-to-sqlite.py
+"""
+
+from __future__ import annotations
+import typing
+import argparse
+import os
+import shutil
+import sqlite3
+import struct
+import sys
+import tempfile
+from typing import Dict, Optional
+import perf
+
+
+PERF_IP_FLAG_BRANCH = 1 << 0
+PERF_IP_FLAG_CALL = 1 << 1
+PERF_IP_FLAG_RETURN = 1 << 2
+PERF_IP_FLAG_CONDITIONAL = 1 << 3
+PERF_IP_FLAG_SYSCALLRET = 1 << 4
+PERF_IP_FLAG_ASYNC = 1 << 5
+PERF_IP_FLAG_INTERRUPT = 1 << 6
+PERF_IP_FLAG_TX_ABORT = 1 << 7
+PERF_IP_FLAG_TRACE_BEGIN = 1 << 8
+PERF_IP_FLAG_TRACE_END = 1 << 9
+PERF_IP_FLAG_IN_TX = 1 << 10
+PERF_IP_FLAG_VMENTRY = 1 << 11
+PERF_IP_FLAG_VMEXIT = 1 << 12
+
+BRANCH_TYPES = [
+ (0, "no branch"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL, "call"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN, "return"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CONDITIONAL, "conditional jump"),
+ (PERF_IP_FLAG_BRANCH, "unconditional jump"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_INTERRUPT, "software interrupt"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_INTERRUPT, "return from interrupt"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_SYSCALLRET, "system call"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_RETURN | PERF_IP_FLAG_SYSCALLRET,
+ "return from system call"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_ASYNC, "asynchronous branch"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_ASYNC | PERF_IP_FLAG_INTERRUPT,
+ "hardware interrupt"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TX_ABORT, "transaction abort"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_BEGIN, "trace begin"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_TRACE_END, "trace end"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMENTRY, "vm entry"),
+ (PERF_IP_FLAG_BRANCH | PERF_IP_FLAG_CALL | PERF_IP_FLAG_VMEXIT, "vm exit"),
+]
+for _b_type, _b_name in list(BRANCH_TYPES):
+ if (_b_type == PERF_IP_FLAG_BRANCH or
+ (_b_type & (PERF_IP_FLAG_TRACE_BEGIN | PERF_IP_FLAG_TRACE_END))):
+ continue
+ BRANCH_TYPES.append((_b_type | PERF_IP_FLAG_TRACE_BEGIN, f"trace begin / {_b_name}"))
+ BRANCH_TYPES.append((_b_type | PERF_IP_FLAG_TRACE_END, f"{_b_name} / trace end"))
+
+
+class DatabaseExporter:
+ """Handles database connection and exporting of perf events."""
+
+ def __init__(self, db_path: str):
+ self.con = sqlite3.connect(db_path)
+ self.con.execute("PRAGMA journal_mode = MEMORY")
+ self.session: Optional[perf.session] = None
+ self.sample_count = 0
+
+ # Caches and counters grouped to reduce instance attributes
+ self.caches: Dict[str, dict] = {
+ 'machines': {},
+ 'threads': {},
+ 'comms': {},
+ 'dsos': {},
+ 'symbols': {},
+ 'events': {},
+ 'branch_types': {},
+ 'call_paths': {}
+ }
+
+ self.next_id = {
+ 'machine': 1,
+ 'thread': 1,
+ 'comm': 1,
+ 'dso': 1,
+ 'symbol': 1,
+ 'event': 1,
+ 'branch_type': 1,
+ 'call_path': 1
+ }
+
+ self.create_tables()
+
+ def create_tables(self) -> None:
+ """Create database tables."""
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS selected_events (
+ id INTEGER NOT NULL PRIMARY KEY,
+ name VARCHAR(80))
+ """)
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS machines (
+ id INTEGER NOT NULL PRIMARY KEY,
+ pid INTEGER,
+ root_dir VARCHAR(4096))
+ """)
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS threads (
+ id INTEGER NOT NULL PRIMARY KEY,
+ machine_id BIGINT,
+ process_id BIGINT,
+ pid INTEGER,
+ tid INTEGER)
+ """)
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS comms (
+ id INTEGER NOT NULL PRIMARY KEY,
+ comm VARCHAR(16),
+ c_thread_id BIGINT,
+ c_time BIGINT,
+ exec_flag BOOLEAN)
+ """)
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS comm_threads (
+ id INTEGER NOT NULL PRIMARY KEY,
+ comm_id BIGINT,
+ thread_id BIGINT)
+ """)
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS dsos (
+ id INTEGER NOT NULL PRIMARY KEY,
+ machine_id BIGINT,
+ short_name VARCHAR(256),
+ long_name VARCHAR(4096),
+ build_id VARCHAR(64))
+ """)
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS symbols (
+ id INTEGER NOT NULL PRIMARY KEY,
+ dso_id BIGINT,
+ sym_start BIGINT,
+ sym_end BIGINT,
+ binding INTEGER,
+ name VARCHAR(2048))
+ """)
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS branch_types (
+ id INTEGER NOT NULL PRIMARY KEY,
+ name VARCHAR(80))
+ """)
+ self.con.execute("""
+ CREATE TABLE IF NOT EXISTS samples (
+ id INTEGER NOT NULL PRIMARY KEY,
+ evsel_id BIGINT,
+ machine_id BIGINT,
+ thread_id BIGINT,
+ comm_id BIGINT,
+ dso_id BIGINT,
+ symbol_id BIGINT,
+ sym_offset BIGINT,
+ ip BIGINT,
+ time BIGINT,
+ cpu INTEGER,
+ to_dso_id BIGINT,
+ to_symbol_id BIGINT,
+ to_sym_offset BIGINT,
+ to_ip BIGINT,
+ period BIGINT,
+ weight BIGINT,
+ transaction_ BIGINT,
+ data_src BIGINT,
+ branch_type INTEGER,
+ in_tx BOOLEAN,
+ call_path_id BIGINT,
+ insn_count BIGINT,
+ cyc_count BIGINT,
+ flags INTEGER)
+ """)
+ self.con.execute('''
+ CREATE TABLE IF NOT EXISTS calls (
+ id INTEGER NOT NULL PRIMARY KEY,
+ thread_id BIGINT,
+ comm_id BIGINT,
+ call_path_id BIGINT,
+ call_time BIGINT,
+ return_time BIGINT,
+ branch_count BIGINT,
+ call_id BIGINT,
+ return_id BIGINT,
+ parent_call_path_id BIGINT,
+ flags INTEGER,
+ parent_id BIGINT,
+ insn_count BIGINT,
+ cyc_count BIGINT
+ )
+ ''')
+ self.con.execute('''
+ CREATE TABLE IF NOT EXISTS call_paths (
+ id INTEGER NOT NULL PRIMARY KEY,
+ parent_id BIGINT,
+ symbol_id BIGINT,
+ ip BIGINT
+ )
+ ''')
+
+ self.con.execute('''
+ CREATE TABLE IF NOT EXISTS cbr (
+ id integer primary key,
+ cbr integer,
+ mhz integer,
+ percent integer
+ )
+ ''')
+ self.con.execute('''
+ CREATE TABLE IF NOT EXISTS mwait (
+ id integer primary key,
+ hints integer,
+ extensions integer
+ )
+ ''')
+
+ self.con.execute('''
+ CREATE TABLE IF NOT EXISTS context_switches (
+ id integer primary key,
+ machine_id bigint,
+ time bigint,
+ cpu integer,
+ thread_out_id bigint,
+ comm_out_id bigint,
+ thread_in_id bigint,
+ comm_in_id bigint,
+ flags integer
+ )
+ ''')
+ self.con.execute(
+ "CREATE TABLE IF NOT EXISTS ptwrite ("
+ "id integer primary key, payload integer, exact_ip integer"
+ ")"
+ )
+ self.con.execute(
+ "CREATE TABLE IF NOT EXISTS pwre ("
+ "id integer primary key, hw integer, cstate integer, subcstate integer, "
+ "hw_name text, cstate_name text"
+ ")"
+ )
+ self.con.execute(
+ "CREATE TABLE IF NOT EXISTS exstop ("
+ "id integer primary key, exact_ip integer"
+ ")"
+ )
+ self.con.execute(
+ "CREATE TABLE IF NOT EXISTS pwrx ("
+ "id integer primary key, deepest_cstate integer, last_cstate integer, "
+ "wake_reason integer"
+ ")"
+ )
+
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS machines_view AS "
+ "SELECT id, pid, root_dir, "
+ "CASE WHEN id=0 THEN 'unknown' WHEN pid=-1 THEN 'host' ELSE 'guest' END "
+ "AS host_or_guest FROM machines"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS dsos_view AS "
+ "SELECT id, machine_id, "
+ "(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest, "
+ "short_name, long_name, build_id FROM dsos"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS symbols_view AS "
+ "SELECT id, name, (SELECT short_name FROM dsos WHERE id=dso_id) AS dso, "
+ "dso_id, sym_start, sym_end, "
+ "CASE WHEN binding=0 THEN 'local' WHEN binding=1 THEN 'global' ELSE 'weak' END "
+ "AS binding FROM symbols"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS threads_view AS "
+ "SELECT id, machine_id, "
+ "(SELECT host_or_guest FROM machines_view WHERE id = machine_id) AS host_or_guest, "
+ "process_id, pid, tid FROM threads"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS comm_threads_view AS "
+ "SELECT comm_id, (SELECT comm FROM comms WHERE id = comm_id) AS command, "
+ "thread_id, (SELECT pid FROM threads WHERE id = thread_id) AS pid, "
+ "(SELECT tid FROM threads WHERE id = thread_id) AS tid FROM comm_threads"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS call_paths_view AS "
+ "SELECT c.id, printf('%x', c.ip) AS ip, c.symbol_id, "
+ "(SELECT name FROM symbols WHERE id = c.symbol_id) AS symbol, "
+ "(SELECT dso_id FROM symbols WHERE id = c.symbol_id) AS dso_id, "
+ "(SELECT dso FROM symbols_view WHERE id = c.symbol_id) AS dso_short_name, "
+ "c.parent_id, printf('%x', p.ip) AS parent_ip, p.symbol_id AS parent_symbol_id, "
+ "(SELECT name FROM symbols WHERE id = p.symbol_id) AS parent_symbol, "
+ "(SELECT dso_id FROM symbols WHERE id = p.symbol_id) AS parent_dso_id, "
+ "(SELECT dso FROM symbols_view WHERE id = p.symbol_id) AS parent_dso_short_name "
+ "FROM call_paths c LEFT JOIN call_paths p ON p.id = c.parent_id"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS calls_view AS "
+ "SELECT calls.id, thread_id, "
+ "(SELECT pid FROM threads WHERE id = thread_id) AS pid, "
+ "(SELECT tid FROM threads WHERE id = thread_id) AS tid, "
+ "(SELECT comm FROM comms WHERE id = comm_id) AS command, "
+ "call_path_id, printf('%x', ip) AS ip, symbol_id, "
+ "(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, "
+ "call_time, return_time, return_time - call_time AS elapsed_time, "
+ "branch_count, insn_count, cyc_count, "
+ "CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) "
+ "ELSE CAST(insn_count AS FLOAT) / cyc_count END AS IPC, "
+ "call_id, return_id, "
+ "CASE WHEN flags=0 THEN '' WHEN flags=1 THEN 'no call' "
+ "WHEN flags=2 THEN 'no return' WHEN flags=3 THEN 'no call/return' "
+ "WHEN flags IN (4, 6) THEN 'jmp' ELSE flags END AS flags, "
+ "parent_call_path_id, calls.parent_id "
+ "FROM calls INNER JOIN call_paths ON call_paths.id = call_path_id"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS samples_view AS "
+ "SELECT id, time, cpu, "
+ "(SELECT pid FROM threads WHERE id = thread_id) AS pid, "
+ "(SELECT tid FROM threads WHERE id = thread_id) AS tid, "
+ "(SELECT comm FROM comms WHERE id = comm_id) AS command, "
+ "(SELECT name FROM selected_events WHERE id = evsel_id) AS event, "
+ "printf('%x', ip) AS ip_hex, "
+ "(SELECT name FROM symbols WHERE id = symbol_id) AS symbol, sym_offset, "
+ "(SELECT short_name FROM dsos WHERE id = dso_id) AS dso_short_name, "
+ "printf('%x', to_ip) AS to_ip_hex, "
+ "(SELECT name FROM symbols WHERE id = to_symbol_id) AS to_symbol, to_sym_offset, "
+ "(SELECT short_name FROM dsos WHERE id = to_dso_id) AS to_dso_short_name, "
+ "(SELECT name FROM branch_types WHERE id = branch_type) AS branch_type_name, "
+ "in_tx, call_path_id, insn_count, cyc_count, "
+ "CASE WHEN cyc_count=0 THEN CAST(0 AS FLOAT) "
+ "ELSE CAST(insn_count AS FLOAT) / cyc_count END AS IPC, flags FROM samples"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS ptwrite_view AS "
+ "SELECT ptwrite.id, time, cpu, printf('%x', payload) AS payload_hex, "
+ "CASE WHEN exact_ip=0 THEN 'False' ELSE 'True' END AS exact_ip "
+ "FROM ptwrite INNER JOIN samples ON samples.id = ptwrite.id"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS cbr_view AS "
+ "SELECT cbr.id, time, cpu, cbr, mhz, percent "
+ "FROM cbr INNER JOIN samples ON samples.id = cbr.id"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS mwait_view AS "
+ "SELECT mwait.id, time, cpu, printf('%x', hints) AS hints_hex, "
+ "printf('%x', extensions) AS extensions_hex "
+ "FROM mwait INNER JOIN samples ON samples.id = mwait.id"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS pwre_view AS "
+ "SELECT pwre.id, time, cpu, cstate, subcstate, "
+ "CASE WHEN hw=0 THEN 'False' ELSE 'True' END AS hw "
+ "FROM pwre INNER JOIN samples ON samples.id = pwre.id"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS exstop_view AS "
+ "SELECT exstop.id, time, cpu, "
+ "CASE WHEN exact_ip=0 THEN 'False' ELSE 'True' END AS exact_ip "
+ "FROM exstop INNER JOIN samples ON samples.id = exstop.id"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS pwrx_view AS "
+ "SELECT pwrx.id, time, cpu, deepest_cstate, last_cstate, "
+ "CASE WHEN wake_reason=1 THEN 'Interrupt' "
+ "WHEN wake_reason=2 THEN 'Timer Deadline' "
+ "WHEN wake_reason=4 THEN 'Monitored Address' "
+ "WHEN wake_reason=8 THEN 'HW' WHEN wake_reason=16 THEN 'Other' "
+ "ELSE wake_reason END AS wake_reason "
+ "FROM pwrx INNER JOIN samples ON samples.id = pwrx.id"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS power_events_view AS "
+ "SELECT samples.id, time, cpu, selected_events.name AS event, "
+ "CASE WHEN selected_events.name='cbr' THEN "
+ "(SELECT cbr FROM cbr WHERE cbr.id = samples.id) ELSE \"\" END AS cbr, "
+ "CASE WHEN selected_events.name='cbr' THEN "
+ "(SELECT mhz FROM cbr WHERE cbr.id = samples.id) ELSE \"\" END AS mhz, "
+ "CASE WHEN selected_events.name='cbr' THEN "
+ "(SELECT percent FROM cbr WHERE cbr.id = samples.id) ELSE \"\" END AS percent, "
+ "CASE WHEN selected_events.name='mwait' THEN "
+ "(SELECT printf('%x', hints) FROM mwait WHERE mwait.id = samples.id) "
+ "ELSE \"\" END AS hints_hex, "
+ "CASE WHEN selected_events.name='mwait' THEN "
+ "(SELECT printf('%x', extensions) FROM mwait WHERE mwait.id = samples.id) "
+ "ELSE \"\" END AS extensions_hex, "
+ "CASE WHEN selected_events.name='pwre' THEN "
+ "(SELECT cstate FROM pwre WHERE pwre.id = samples.id) ELSE \"\" END AS cstate, "
+ "CASE WHEN selected_events.name='pwre' THEN "
+ "(SELECT subcstate FROM pwre WHERE pwre.id = samples.id) ELSE \"\" END AS subcstate, "
+ "CASE WHEN selected_events.name='pwre' THEN "
+ "(SELECT hw FROM pwre WHERE pwre.id = samples.id) ELSE \"\" END AS hw, "
+ "CASE WHEN selected_events.name='exstop' THEN "
+ "(SELECT exact_ip FROM exstop WHERE exstop.id = samples.id) "
+ "ELSE \"\" END AS exact_ip, "
+ "CASE WHEN selected_events.name='pwrx' THEN "
+ "(SELECT deepest_cstate FROM pwrx WHERE pwrx.id = samples.id) "
+ "ELSE \"\" END AS deepest_cstate, "
+ "CASE WHEN selected_events.name='pwrx' THEN "
+ "(SELECT last_cstate FROM pwrx WHERE pwrx.id = samples.id) "
+ "ELSE \"\" END AS last_cstate, "
+ "CASE WHEN selected_events.name='pwrx' THEN "
+ "(SELECT wake_reason FROM pwrx WHERE pwrx.id = samples.id) "
+ "ELSE \"\" END AS wake_reason "
+ "FROM samples INNER JOIN selected_events ON selected_events.id = evsel_id "
+ "WHERE selected_events.name IN ('cbr','mwait','pwre','exstop','pwrx')"
+ )
+ self.con.execute(
+ "CREATE VIEW IF NOT EXISTS context_switches_view AS "
+ "SELECT context_switches.id, context_switches.machine_id, "
+ "context_switches.time, context_switches.cpu, "
+ "th_out.pid AS pid_out, th_out.tid AS tid_out, comm_out.comm AS comm_out, "
+ "th_in.pid AS pid_in, th_in.tid AS tid_in, comm_in.comm AS comm_in, "
+ "CASE WHEN flags=0 THEN 'in' WHEN flags=1 THEN 'out' "
+ "WHEN flags=3 THEN 'out preempt' ELSE flags END AS flags "
+ "FROM context_switches "
+ "INNER JOIN threads AS th_out ON th_out.id = context_switches.thread_out_id "
+ "INNER JOIN threads AS th_in ON th_in.id = context_switches.thread_in_id "
+ "INNER JOIN comms AS comm_out ON comm_out.id = context_switches.comm_out_id "
+ "INNER JOIN comms AS comm_in ON comm_in.id = context_switches.comm_in_id"
+ )
+
+ # id == 0 means unknown. It is easier to create records for them than
+ # replace the zeroes with NULLs
+ self.con.execute("INSERT OR IGNORE INTO selected_events VALUES (0, 'unknown')")
+ self.con.execute("INSERT OR IGNORE INTO machines VALUES (0, 0, 'unknown')")
+ self.con.execute("INSERT OR IGNORE INTO threads VALUES (0, 0, 0, -1, -1)")
+ self.con.execute("INSERT OR IGNORE INTO comms VALUES (0, 'unknown', 0, 0, 0)")
+ self.con.execute("INSERT OR IGNORE INTO dsos VALUES (0, 0, 'unknown', 'unknown', '')")
+ self.con.execute("INSERT OR IGNORE INTO call_paths VALUES (0, 0, 0, 0)")
+ self.con.execute("INSERT OR IGNORE INTO symbols VALUES (0, 0, 0, 0, 0, 'unknown')")
+ for b_type, b_name in BRANCH_TYPES:
+ self.con.execute("INSERT OR IGNORE INTO branch_types VALUES (?, ?)", (b_type, b_name))
+ self.caches['branch_types'][b_type] = b_name
+
+ self.caches['events']['unknown'] = 0
+ self.caches['threads'][(0, -1, -1)] = 0
+ self.caches['threads'][(0, 0, 0)] = 0
+ self.caches['comms'][('unknown', 0)] = 0
+ self.caches['dsos'][(0, 'unknown', 'unknown', '')] = 0
+ self.caches['symbols'][(0, 'unknown')] = (0, 0, 0)
+ # Initialize comm_threads mapping
+ self.comm_threads_cache: set[tuple[int, int]] = set()
+ self.next_comm_thread_id = 1
+
+ def _exec(self, sql: str, params: tuple[typing.Any, ...] = ()) -> sqlite3.Cursor:
+ """Execute SQL statement converting unsigned 64-bit ints to signed 64-bit.
+
+ Python's sqlite3 module only binds signed 64-bit integers (int64_t) and
+ raises OverflowError for unsigned 64-bit kernel addresses (>= 2^63).
+ Convert values in [2^63, 2^64) into two's-complement signed 64-bit ints.
+ """
+ conv_params = tuple(
+ p - 0x10000000000000000 if isinstance(p, int) and p >= 0x8000000000000000 else p
+ for p in params
+ )
+ return self.con.execute(sql, conv_params)
+
+ def get_machine_id(self, machine_pid: Optional[int]) -> int:
+ """Get or create machine ID."""
+ if machine_pid is None or machine_pid <= 0 or machine_pid > 0x7fffffff:
+ machine_pid = -1
+ if machine_pid in self.caches['machines']:
+ return self.caches['machines'][machine_pid]
+ machine_id = self.next_id['machine']
+ self.next_id['machine'] += 1
+ self._exec("INSERT INTO machines VALUES (?, ?, ?)",
+ (machine_id, machine_pid, ""))
+ self.caches['machines'][machine_pid] = machine_id
+ return machine_id
+
+ def get_event_id(self, name: str) -> int:
+ """Get or create event ID."""
+ if name in self.caches['events']:
+ return self.caches['events'][name]
+ event_id = self.next_id['event']
+ self._exec("INSERT INTO selected_events VALUES (?, ?)",
+ (event_id, name))
+ self.caches['events'][name] = event_id
+ self.next_id['event'] += 1
+ return event_id
+
+ def get_thread_id(self, machine_id: int, pid: Optional[int], tid: Optional[int]) -> int:
+ """Get or create thread ID."""
+ if pid is None:
+ pid = -1
+ elif pid > 0x7fffffff:
+ pid -= 0x100000000
+ if tid is None:
+ tid = -1
+ elif tid > 0x7fffffff:
+ tid -= 0x100000000
+ key = (machine_id, pid, tid)
+ if key in self.caches['threads']:
+ return self.caches['threads'][key]
+ thread_id = self.next_id['thread']
+ self.next_id['thread'] += 1
+ self.caches['threads'][key] = thread_id
+ process_id = thread_id if pid == tid else self.get_thread_id(machine_id, pid, pid)
+ self._exec("INSERT INTO threads VALUES (?, ?, ?, ?, ?)",
+ (thread_id, machine_id, process_id, pid, tid))
+ return thread_id
+
+ def get_comm_id(self, comm: str, thread_id: int) -> int:
+ """Get or create comm ID."""
+ key = (comm, thread_id)
+ if key not in self.caches['comms']:
+ comm_id = self.next_id['comm']
+ self._exec("INSERT INTO comms VALUES (?, ?, ?, ?, ?)",
+ (comm_id, comm, thread_id, 0, 0))
+ self.caches['comms'][key] = comm_id
+ self.next_id['comm'] += 1
+ comm_id = self.caches['comms'][key]
+ mapping_key = (comm_id, thread_id)
+ if mapping_key not in self.comm_threads_cache:
+ comm_thread_id = self.next_comm_thread_id
+ self._exec("INSERT INTO comm_threads VALUES (?, ?, ?)",
+ (comm_thread_id, comm_id, thread_id))
+ self.comm_threads_cache.add(mapping_key)
+ self.next_comm_thread_id += 1
+ return comm_id
+
+ def get_dso_id(self, short_name: str, long_name: str,
+ build_id: str, machine_id: int = 0) -> int:
+ """Get or create DSO ID."""
+ key = (machine_id, short_name, long_name, build_id)
+ if key in self.caches['dsos']:
+ return self.caches['dsos'][key]
+ short_key = (machine_id, short_name)
+ if not build_id and short_key in self.caches['dsos']:
+ return self.caches['dsos'][short_key]
+ dso_id = self.next_id['dso']
+ self._exec("INSERT INTO dsos VALUES (?, ?, ?, ?, ?)",
+ (dso_id, machine_id, short_name, long_name, build_id))
+ self.caches['dsos'][key] = dso_id
+ self.caches['dsos'][short_key] = dso_id
+ self.next_id['dso'] += 1
+ return dso_id
+
+ def get_symbol_id(self, dso_id: int, name: str, start: int,
+ end: int) -> int:
+ """Get or create symbol ID."""
+ key = (dso_id, name)
+ if key in self.caches['symbols']:
+ symbol_id, cached_start, cached_end = self.caches['symbols'][key]
+ if cached_start == 0 and cached_end == 0 and (start != 0 or end != 0):
+ self._exec("UPDATE symbols SET sym_start = ?, sym_end = ? WHERE id = ?",
+ (start, end, symbol_id))
+ self.caches['symbols'][key] = (symbol_id, start, end)
+ return symbol_id
+ symbol_id = self.next_id['symbol']
+ self._exec("INSERT INTO symbols VALUES (?, ?, ?, ?, ?, ?)",
+ (symbol_id, dso_id, start, end, 0, name))
+ self.caches['symbols'][key] = (symbol_id, start, end)
+ self.next_id['symbol'] += 1
+ return symbol_id
+
+ def get_call_path_id(self, parent_id: int, symbol_id: int,
+ ip: int) -> int:
+ """Get or create call path ID."""
+ key = (parent_id, symbol_id, ip)
+ if key in self.caches['call_paths']:
+ return self.caches['call_paths'][key]
+ call_path_id = self.next_id['call_path']
+ self._exec("INSERT INTO call_paths VALUES (?, ?, ?, ?)",
+ (call_path_id, parent_id, symbol_id, ip))
+ self.caches['call_paths'][key] = call_path_id
+ self.next_id['call_path'] += 1
+ return call_path_id
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Callback for processing events."""
+
+ machine_pid = getattr(sample, 'machine_pid', 0)
+ machine_id = self.get_machine_id(machine_pid)
+ thread_id = self.get_thread_id(machine_id, sample.sample_pid, sample.sample_tid)
+
+ comm = "Unknown_comm"
+ try:
+ if self.session is not None:
+ proc = self.session.find_thread(sample.sample_pid, sample.sample_tid)
+ if proc:
+ comm_name = proc.comm()
+ if comm_name:
+ comm = comm_name
+ except TypeError:
+ pass
+ comm_id = self.get_comm_id(comm, thread_id)
+
+ build_id = sample.dso_bid or ""
+ dso_id = self.get_dso_id(
+ sample.dso or "Unknown_dso",
+ sample.dso_long_name or "Unknown_dso_long",
+ build_id,
+ machine_id
+ )
+
+ symbol_id = self.get_symbol_id(
+ dso_id,
+ sample.symbol or "Unknown_symbol",
+ sample.sym_start or 0,
+ sample.sym_end or 0
+ )
+
+ # Handle callchain
+ call_path_id = 0
+ if hasattr(sample, 'callchain') and sample.callchain:
+ parent_id = 0
+ for node in reversed(sample.callchain):
+ dso_name = node.dso or "Unknown_dso"
+ symbol_name = node.symbol or "Unknown_symbol"
+
+ node_dso_id = self.get_dso_id(dso_name, dso_name, "", machine_id)
+ node_symbol_id = self.get_symbol_id(node_dso_id, symbol_name, 0, 0)
+
+ parent_id = self.get_call_path_id(parent_id, node_symbol_id, node.ip)
+ call_path_id = parent_id
+ else:
+ call_path_id = 0
+
+ # Insert sample
+ event_name_str = str(sample.evsel)
+ if event_name_str.startswith("evsel(") and event_name_str.endswith(")"):
+ event_name_str = event_name_str[6:-1]
+ cursor = self._exec("""
+ INSERT INTO samples VALUES (
+ NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
+ )
+ """, (
+ self.get_event_id(event_name_str),
+ machine_id, thread_id, comm_id,
+ dso_id, symbol_id,
+ getattr(sample, 'sym_offset', 0) or 0,
+ sample.sample_ip, sample.sample_time, sample.sample_cpu,
+ self.get_dso_id(sample.addr_dso or "Unknown_dso",
+ sample.addr_dso or "Unknown_dso_long", "", machine_id),
+ self.get_symbol_id(
+ self.get_dso_id(sample.addr_dso or "Unknown_dso",
+ sample.addr_dso or "Unknown_dso_long", "", machine_id),
+ sample.addr_symbol or "Unknown_symbol", 0, 0
+ ),
+ sample.addr_sym_offset or 0,
+ sample.sample_addr or 0,
+ sample.sample_period or 0,
+ sample.sample_weight or 0,
+ sample.transaction or 0,
+ sample.sample_data_src,
+ sample.branch_type or 0,
+ sample.in_tx or 0,
+ call_path_id,
+ sample.sample_insn_count,
+ sample.sample_cyc_count,
+ getattr(sample, "flags", 0) # flags
+ ))
+ sample_id = cursor.lastrowid
+
+ # Handle Intel PT specific raw payloads mathematically equivalent to
+ # legacy synth_data unpacking
+
+ if event_name_str == "ptwrite" and hasattr(sample, "raw_buf"):
+ try:
+ flags, payload = struct.unpack_from("<IQ", sample.raw_buf)
+ self._exec("INSERT INTO ptwrite VALUES (?, ?, ?)",
+ (sample_id, payload, flags & 1))
+ except struct.error:
+ pass
+
+ elif event_name_str == "cbr" and hasattr(sample, "raw_buf"):
+ try:
+ data = struct.unpack_from("<BBBBII", sample.raw_buf)
+ cbr_val, max_nonturbo, freq = data[0], data[2], data[4]
+ MHz = (freq + 500) // 1000
+ percent = ((cbr_val * 1000 // max_nonturbo) + 5) // 10 if max_nonturbo else 0
+ self._exec("INSERT INTO cbr VALUES (?, ?, ?, ?)",
+ (sample_id, cbr_val, MHz, percent))
+ except struct.error:
+ pass
+
+ elif event_name_str == "mwait" and hasattr(sample, "raw_buf"):
+ try:
+ flags, payload = struct.unpack_from("<IQ", sample.raw_buf)
+ hints = payload & 0xff
+ extensions = (payload >> 32) & 0x3
+ self._exec("INSERT INTO mwait VALUES (?, ?, ?)",
+ (sample_id, hints, extensions))
+ except struct.error:
+ pass
+
+ elif event_name_str == "pwre" and hasattr(sample, "raw_buf"):
+ try:
+ flags, payload = struct.unpack_from("<IQ", sample.raw_buf)
+ hw = (payload >> 7) & 1
+ cstate = (payload >> 12) & 0xf
+ subcstate = (payload >> 8) & 0xf
+ self._exec("INSERT INTO pwre (id, cstate, subcstate, hw) VALUES (?, ?, ?, ?)",
+ (sample_id, cstate, subcstate, hw))
+ except struct.error:
+ pass
+
+ elif event_name_str == "exstop" and hasattr(sample, "raw_buf"):
+ try:
+ flags = struct.unpack_from("<I", sample.raw_buf)[0]
+ self._exec("INSERT INTO exstop VALUES (?, ?)",
+ (sample_id, flags & 1))
+ except struct.error:
+ pass
+
+ elif event_name_str == "pwrx" and hasattr(sample, "raw_buf"):
+ try:
+ flags, payload = struct.unpack_from("<IQ", sample.raw_buf)
+ deepest_cstate = payload & 0xf
+ last_cstate = (payload >> 4) & 0xf
+ wake_reason = (payload >> 8) & 0xf
+ self._exec("INSERT INTO pwrx VALUES (?, ?, ?, ?)",
+ (sample_id, deepest_cstate, last_cstate, wake_reason))
+ except struct.error:
+ pass
+
+ self.sample_count += 1
+ if self.sample_count % 10000 == 0:
+ self.commit()
+
+ def get_call_path_ids(self, call_path: typing.Any, machine_id: int = 0) -> tuple[int, int]:
+ """Add a perf.callchain as call_paths rows, return its id and parent's."""
+ parent_id = 0
+ call_path_id = 0
+ for node in call_path:
+ dso_name = node.dso or "Unknown_dso"
+ symbol_name = node.symbol or "Unknown_symbol"
+ dso_id = self.get_dso_id(dso_name, dso_name, "", machine_id)
+ symbol_id = self.get_symbol_id(dso_id, symbol_name, 0, 0)
+ parent_id = call_path_id
+ call_path_id = self.get_call_path_id(parent_id, symbol_id, node.ip)
+ return call_path_id, parent_id
+
+ def process_call_return(self, cr: perf.call_return) -> None:
+ """Callback for processing call_return events."""
+ machine_id = self.get_machine_id(getattr(cr, "machine_pid", None))
+ thread_id = self.get_thread_id(machine_id, cr.pid, cr.tid)
+ comm_id = self.get_comm_id(cr.comm or "Unknown_comm", thread_id)
+ call_path_id, parent_call_path_id = self.get_call_path_ids(cr.call_path or [], machine_id)
+ # calls.id and calls.parent_id use the db_id of the perf module, as a
+ # call is given an id before it returns its parent cannot be numbered
+ # here.
+ self._exec(
+ "INSERT INTO calls VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ (cr.db_id, thread_id, comm_id, call_path_id,
+ cr.call_time, cr.return_time,
+ cr.branch_count, cr.call_ref,
+ cr.return_ref, parent_call_path_id,
+ cr.flags, cr.parent_id,
+ cr.insn_count, cr.cyc_count)
+ )
+
+ def process_context_switch(self, event: typing.Any) -> None:
+ """Callback for processing context switch events."""
+ misc = getattr(event, 'misc', 0)
+ out = bool(misc & (1 << 13)) # PERF_RECORD_MISC_SWITCH_OUT
+ out_preempt = bool(misc & (1 << 14)) # PERF_RECORD_MISC_SWITCH_OUT_PREEMPT
+ flags = (1 if out else 0) | ((1 if out_preempt else 0) << 1)
+ machine_pid = getattr(event, 'machine_pid', None)
+ machine_id = self.get_machine_id(machine_pid)
+
+ sample_pid = getattr(event, 'sample_pid', None)
+ if sample_pid is None:
+ sample_pid = -1
+ elif sample_pid > 0x7fffffff:
+ sample_pid -= 0x100000000
+ sample_tid = getattr(event, 'sample_tid', None)
+ if sample_tid is None:
+ sample_tid = -1
+ elif sample_tid > 0x7fffffff:
+ sample_tid -= 0x100000000
+ next_prev_pid = getattr(event, 'next_prev_pid', None)
+ if next_prev_pid is None:
+ next_prev_pid = -1
+ elif next_prev_pid > 0x7fffffff:
+ next_prev_pid -= 0x100000000
+ next_prev_tid = getattr(event, 'next_prev_tid', None)
+ if next_prev_tid is None:
+ next_prev_tid = -1
+ elif next_prev_tid > 0x7fffffff:
+ next_prev_tid -= 0x100000000
+
+ th_a_id = self.get_thread_id(machine_id, sample_pid, sample_tid)
+ comm_a = "Unknown_comm"
+ if self.session:
+ try:
+ proc = self.session.find_thread(sample_pid, sample_tid)
+ if proc:
+ comm_a_name = proc.comm()
+ if comm_a_name:
+ comm_a = comm_a_name
+ except TypeError:
+ pass
+ comm_a_id = self.get_comm_id(comm_a, th_a_id)
+
+ th_b_id = self.get_thread_id(machine_id, next_prev_pid, next_prev_tid)
+ comm_b = "Unknown_comm"
+ if self.session:
+ try:
+ proc = self.session.find_thread(next_prev_pid, next_prev_tid)
+ if proc:
+ comm_b_name = proc.comm()
+ if comm_b_name:
+ comm_b = comm_b_name
+ except TypeError:
+ pass
+ comm_b_id = self.get_comm_id(comm_b, th_b_id)
+
+ if out:
+ th_out_id = th_a_id
+ comm_out_id = comm_a_id
+ th_in_id = th_b_id
+ comm_in_id = comm_b_id
+ else:
+ th_out_id = th_b_id
+ comm_out_id = comm_b_id
+ th_in_id = th_a_id
+ comm_in_id = comm_a_id
+
+ self._exec(
+ "INSERT INTO context_switches VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)",
+ (machine_id, getattr(event, 'sample_time', 0), getattr(event, 'sample_cpu', 0) or 0,
+ th_out_id, comm_out_id, th_in_id, comm_in_id, flags)
+ )
+
+ def finish(self) -> None:
+ """Create indexes and update summary metadata after processing events.
+
+ pcpid_idx, pid_idx, and comms.has_calls are required by
+ exported-sql-viewer.py to filter the command dropdown to threads with
+ call graphs and to expand parent/child call trees efficiently.
+ """
+ self.con.execute("CREATE INDEX IF NOT EXISTS pcpid_idx ON calls (parent_call_path_id)")
+ self.con.execute("CREATE INDEX IF NOT EXISTS pid_idx ON calls (parent_id)")
+ self.con.execute("ALTER TABLE comms ADD has_calls boolean")
+ self.con.execute("UPDATE comms SET has_calls = 1 WHERE comms.id IN "
+ "(SELECT DISTINCT comm_id FROM calls)")
+
+ def commit(self) -> None:
+ """Commit transaction."""
+ self.con.commit()
+
+ def close(self) -> None:
+ """Close connection."""
+ self.con.close()
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(
+ description="Export perf data to a sqlite3 database")
+ ap.add_argument("-i", "--input", default="perf.data",
+ help="Input file name")
+ ap.add_argument("-o", "--output", default="perf.db",
+ help="Output database name")
+ ap.add_argument("--itrace", help="Instruction Tracing options (e.g. crpt)")
+ args = ap.parse_args()
+
+ try:
+ fd = os.open(args.output, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
+ except FileExistsError:
+ print(f"Error: {args.output} already exists")
+ sys.exit(1)
+
+ tmp_dir = None
+ exporter = None
+ succeeded = False
+ try:
+ tmp_dir = tempfile.mkdtemp(prefix="perf-export-sqlite-")
+ tmp_db = os.path.join(tmp_dir, "perf.db")
+ exporter = DatabaseExporter(tmp_db)
+ session = perf.session(perf.data(args.input),
+ sample=exporter.process_event,
+ context_switch=exporter.process_context_switch,
+ call_return=exporter.process_call_return,
+ itrace=args.itrace)
+ exporter.session = session
+ try:
+ session.process_events()
+ finally:
+ # Break the C perf.session -> bound method -> exporter reference cycle.
+ exporter.session = None
+ exporter.finish()
+ exporter.commit()
+ exporter.close()
+ exporter = None
+ with open(tmp_db, "rb") as src, os.fdopen(fd, "wb", closefd=False) as dst:
+ shutil.copyfileobj(src, dst)
+ succeeded = True
+ print(f"Successfully exported to {args.output}")
+ except (OSError, RuntimeError, ValueError, sqlite3.Error):
+ import traceback
+ traceback.print_exc()
+ finally:
+ if exporter is not None:
+ exporter.session = None
+ try:
+ exporter.close()
+ except (OSError, sqlite3.Error):
+ pass
+ try:
+ os.close(fd)
+ except OSError:
+ pass
+ if tmp_dir is not None:
+ shutil.rmtree(tmp_dir, ignore_errors=True)
+ if not succeeded:
+ try:
+ os.remove(args.output)
+ except OSError:
+ pass
+ if not succeeded:
+ sys.exit(1)
diff --git a/tools/perf/python/exported-sql-viewer.py b/tools/perf/python/exported-sql-viewer.py
new file mode 100755
index 000000000000..f31aa45d9a54
--- /dev/null
+++ b/tools/perf/python/exported-sql-viewer.py
@@ -0,0 +1,5103 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: GPL-2.0
+"""exported-sql-viewer.py: view data from sql database."""
+# Copyright (c) 2014-2018, Intel Corporation.
+
+# To use this script you will need to have exported data using either the
+# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those
+# scripts for details.
+#
+# Following on from the example in the export scripts, a
+# call-graph can be displayed for the pt_example database like this:
+#
+# python exported-sql-viewer.py pt_example
+#
+# Note that for PostgreSQL, this script supports connecting to remote databases
+# by setting hostname, port, username, password, and dbname e.g.
+#
+# python exported-sql-viewer.py \
+# "hostname=myhost username=myuser password=mypassword dbname=pt_example"
+#
+# The result is a GUI window with a tree representing a context-sensitive
+# call-graph. Expanding a couple of levels of the tree and adjusting column
+# widths to suit will display something like:
+#
+# Call Graph: pt_example
+# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
+# v- ls
+# v- 2638:2638
+# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
+# |- unknown unknown 1 13198 0.1 1 0.0
+# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
+# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
+# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
+# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
+# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
+# >- __libc_csu_init ls 1 10354 0.1 10 0.0
+# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
+# v- main ls 1 8182043 99.6 180254 99.9
+#
+# Points to note:
+# The top level is a command name (comm)
+# The next level is a thread (pid:tid)
+# Subsequent levels are functions
+# 'Count' is the number of calls
+# 'Time' is the elapsed time until the function returns
+# Percentages are relative to the level above
+# 'Branch Count' is the total number of branches for that function and all
+# functions that it calls
+
+# There is also a "All branches" report, which displays branches and
+# possibly disassembly. However, presently, the only supported disassembler is
+# Intel XED, and additionally the object code must be present in perf build ID
+# cache. To use Intel XED, libxed.so must be present. To build and install
+# libxed.so:
+# git clone https://github.com/intelxed/mbuild.git mbuild
+# git clone https://github.com/intelxed/xed
+# cd xed
+# ./mfile.py --share
+# sudo ./mfile.py --prefix=/usr/local install
+# sudo ldconfig
+#
+# Example report:
+#
+# Time CPU Command PID TID Branch Type In Tx Branch
+# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
+# 7fab593ea260 48 89 e7 mov %rsp, %rdi
+# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
+# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
+# 7fab593ea260 48 89 e7 mov %rsp, %rdi
+# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930
+# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
+# 7fab593ea930 55 pushq %rbp
+# 7fab593ea931 48 89 e5 mov %rsp, %rbp
+# 7fab593ea934 41 57 pushq %r15
+# 7fab593ea936 41 56 pushq %r14
+# 7fab593ea938 41 55 pushq %r13
+# 7fab593ea93a 41 54 pushq %r12
+# 7fab593ea93c 53 pushq %rbx
+# 7fab593ea93d 48 89 fb mov %rdi, %rbx
+# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp
+# 7fab593ea944 0f 31 rdtsc
+# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx
+# 7fab593ea94a 89 c0 mov %eax, %eax
+# 7fab593ea94c 48 09 c2 or %rax, %rdx
+# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
+# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
+# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
+# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
+# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip)
+# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
+
+from __future__ import print_function
+
+import sys
+# Only change warnings if the python -W option was not used
+if not sys.warnoptions:
+ import warnings
+ # PySide2 causes deprecation warnings, ignore them.
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
+import argparse
+import weakref
+import threading
+import pickle
+glb_nsz = 16
+import re
+import os
+import random
+import copy
+import math
+from libxed import LibXED
+
+import importlib
+from typing import Any
+
+class _QtBase:
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ pass
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
+ return self
+ def __getattr__(self, name: str) -> Any:
+ return _QtBase()
+ def mousePressEvent(self, event: Any) -> None:
+ pass
+ def mouseMoveEvent(self, event: Any) -> None:
+ pass
+ def mouseReleaseEvent(self, event: Any) -> None:
+ pass
+ def resizeEvent(self, event: Any) -> None:
+ pass
+ def changeEvent(self, event: Any) -> None:
+ pass
+
+class _QtWidget(_QtBase):
+ pass
+
+class _QtMdiSubWindow(_QtWidget):
+ pass
+
+QAbstractItemModel: Any = _QtBase
+QAbstractItemView: Any = _QtBase
+QAbstractTableModel: Any = _QtBase
+QAction: Any = _QtBase
+QApplication: Any = _QtBase
+QCheckBox: Any = _QtBase
+QColor: Any = _QtBase
+QComboBox: Any = _QtBase
+QDialog: Any = _QtWidget
+QEvent: Any = _QtBase
+QFont: Any = _QtBase
+QFontMetrics: Any = _QtBase
+QGraphicsItem: Any = _QtBase
+QGraphicsLineItem: Any = _QtBase
+QGraphicsScene: Any = _QtBase
+QGraphicsSimpleTextItem: Any = _QtBase
+QGraphicsView: Any = _QtBase
+QGridLayout: Any = _QtBase
+QHBoxLayout: Any = _QtBase
+QKeySequence: Any = _QtBase
+QLabel: Any = _QtBase
+QLineEdit: Any = _QtBase
+QMainWindow: Any = _QtWidget
+QMdiArea: Any = _QtBase
+QMdiSubWindow: Any = _QtMdiSubWindow
+QMenu: Any = _QtBase
+QMessageBox: Any = _QtBase
+QModelIndex: Any = _QtBase
+QObject: Any = _QtBase
+QPalette: Any = _QtBase
+QPoint: Any = _QtBase
+QPointF: Any = _QtBase
+QProgressBar: Any = _QtBase
+QPushButton: Any = _QtBase
+QRect: Any = _QtBase
+QRectF: Any = _QtBase
+QRubberBand: Any = _QtBase
+QSize: Any = _QtBase
+QSizePolicy: Any = _QtBase
+QSortFilterProxyModel: Any = _QtBase
+QSpinBox: Any = _QtBase
+QSplitter: Any = _QtWidget
+QSqlDatabase: Any = _QtBase
+QSqlQuery: Any = _QtBase
+QStyle: Any = _QtBase
+Qt: Any = _QtBase
+QTableView: Any = _QtWidget
+QTextBrowser: Any = _QtWidget
+QThread: Any = _QtBase
+QToolButton: Any = _QtBase
+QTreeView: Any = _QtWidget
+QVBoxLayout: Any = _QtBase
+qVersion: Any = _QtBase
+QWidget: Any = _QtWidget
+Signal: Any = _QtBase
+
+pyside_version_1 = False
+for _pkg in ("PySide6", "PySide2"):
+ try:
+ for _mod_name in ("QtCore", "QtGui", "QtSql", "QtWidgets"):
+ _mod = importlib.import_module(f"{_pkg}.{_mod_name}")
+ for _sym in dir(_mod):
+ if not _sym.startswith("_"):
+ globals()[_sym] = getattr(_mod, _sym)
+ break
+ except ImportError:
+ pass
+
+
+from decimal import Decimal, ROUND_HALF_UP
+from ctypes import create_string_buffer, addressof, sizeof, \
+ c_void_p, c_bool, c_char, c_longlong
+from multiprocessing import Process, Array, Value, Event
+
+
+def printerr(*args, **keyword_args):
+ print(*args, file=sys.stderr, **keyword_args)
+
+# Data formatting helpers
+
+def tohex(ip):
+ if ip < 0:
+ ip += 1 << 64
+ return "%x" % ip
+
+def offstr(offset):
+ if offset:
+ return "+0x%x" % offset
+ return ""
+
+def dsoname(name):
+ if name == "[kernel.kallsyms]":
+ return "[kernel]"
+ return name
+
+def findnth(s, sub, n, offs=0):
+ pos = s.find(sub)
+ if pos < 0:
+ return pos
+ if n <= 1:
+ return offs + pos
+ return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
+
+# Percent to one decimal place
+
+def PercentToOneDP(n, d):
+ if not d:
+ return "0.0"
+ x = (n * Decimal(100)) / d
+ return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
+
+# Helper for queries that must not fail
+
+def QueryExec(query, stmt):
+ ret = query.exec_(stmt)
+ if not ret:
+ raise RuntimeError("Query failed: " + query.lastError().text())
+
+# Background thread
+
+class Thread(QThread):
+
+ done = Signal(object)
+
+ def __init__(self, task, param=None, parent=None):
+ super(Thread, self).__init__(parent)
+ self.task = task
+ self.param = param
+
+ def run(self):
+ while True:
+ if self.param is None:
+ done, result = self.task()
+ else:
+ done, result = self.task(self.param)
+ self.done.emit(result)
+ if done:
+ break
+
+# Tree data model
+
+class TreeModel(QAbstractItemModel):
+
+ def __init__(self, glb, params, parent=None):
+ super(TreeModel, self).__init__(parent)
+ self.glb = glb
+ self.params = params
+ self.root = self.GetRoot()
+ self.last_row_read = 0
+
+ def Item(self, parent):
+ if parent.isValid():
+ return parent.internalPointer()
+ else:
+ return self.root
+
+ def rowCount(self, parent):
+ result = self.Item(parent).childCount()
+ if result < 0:
+ result = 0
+ self.dataChanged.emit(parent, parent)
+ return result
+
+ def hasChildren(self, parent):
+ return self.Item(parent).hasChildren()
+
+ def headerData(self, section, orientation, role):
+ if role == Qt.TextAlignmentRole:
+ return self.columnAlignment(section)
+ if role != Qt.DisplayRole:
+ return None
+ if orientation != Qt.Horizontal:
+ return None
+ return self.columnHeader(section)
+
+ def parent(self, child):
+ child_item = child.internalPointer()
+ if child_item is self.root:
+ return QModelIndex()
+ parent_item = child_item.getParentItem()
+ return self.createIndex(parent_item.getRow(), 0, parent_item)
+
+ def index(self, row, column, parent):
+ child_item = self.Item(parent).getChildItem(row)
+ return self.createIndex(row, column, child_item)
+
+ def DisplayData(self, item, index):
+ return item.getData(index.column())
+
+ def FetchIfNeeded(self, row):
+ if row > self.last_row_read:
+ self.last_row_read = row
+ if row + 10 >= self.root.child_count:
+ self.fetcher.Fetch(glb_chunk_sz)
+
+ def columnAlignment(self, _column):
+ return Qt.AlignLeft
+
+ def columnFont(self, _column):
+ return None
+
+ def data(self, index, role):
+ if role == Qt.TextAlignmentRole:
+ return self.columnAlignment(index.column())
+ if role == Qt.FontRole:
+ return self.columnFont(index.column())
+ if role != Qt.DisplayRole:
+ return None
+ item = index.internalPointer()
+ return self.DisplayData(item, index)
+
+# Table data model
+
+class TableModel(QAbstractTableModel):
+
+ def __init__(self, parent=None):
+ super(TableModel, self).__init__(parent)
+ self.child_count = 0
+ self.child_items = []
+ self.last_row_read = 0
+
+ def Item(self, parent):
+ if parent.isValid():
+ return parent.internalPointer()
+ else:
+ return self
+
+ def rowCount(self, _parent):
+ return self.child_count
+
+ def headerData(self, section, orientation, role):
+ if role == Qt.TextAlignmentRole:
+ return self.columnAlignment(section)
+ if role != Qt.DisplayRole:
+ return None
+ if orientation != Qt.Horizontal:
+ return None
+ return self.columnHeader(section)
+
+ def index(self, row, column, _parent):
+ return self.createIndex(row, column, self.child_items[row])
+
+ def DisplayData(self, item, index):
+ return item.getData(index.column())
+
+ def FetchIfNeeded(self, row):
+ if row > self.last_row_read:
+ self.last_row_read = row
+ if row + 10 >= self.child_count:
+ self.fetcher.Fetch(glb_chunk_sz)
+
+ def columnAlignment(self, _column):
+ return Qt.AlignLeft
+
+ def columnFont(self, _column):
+ return None
+
+ def data(self, index, role):
+ if role == Qt.TextAlignmentRole:
+ return self.columnAlignment(index.column())
+ if role == Qt.FontRole:
+ return self.columnFont(index.column())
+ if role != Qt.DisplayRole:
+ return None
+ item = index.internalPointer()
+ return self.DisplayData(item, index)
+
+# Model cache
+
+model_cache: Any = weakref.WeakValueDictionary()
+model_cache_lock = threading.Lock()
+
+def LookupCreateModel(model_name, create_fn):
+ model_cache_lock.acquire()
+ try:
+ model = model_cache[model_name]
+ except KeyError:
+ model = None
+ if model is None:
+ model = create_fn()
+ model_cache[model_name] = model
+ model_cache_lock.release()
+ return model
+
+def LookupModel(model_name):
+ model_cache_lock.acquire()
+ try:
+ model = model_cache[model_name]
+ except KeyError:
+ model = None
+ model_cache_lock.release()
+ return model
+
+# Find bar
+
+class FindBar():
+
+ def __init__(self, parent, finder, is_reg_expr=False):
+ self.finder = finder
+ self.context = []
+ self.last_value = None
+ self.last_pattern = None
+
+ label = QLabel("Find:")
+ label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+ self.textbox = QComboBox()
+ self.textbox.setEditable(True)
+ self.textbox.currentIndexChanged.connect(self.ValueChanged)
+
+ self.progress = QProgressBar()
+ self.progress.setRange(0, 0)
+ self.progress.hide()
+
+ if is_reg_expr:
+ self.pattern = QCheckBox("Regular Expression")
+ else:
+ self.pattern = QCheckBox("Pattern")
+ self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+ self.next_button = QToolButton()
+ self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
+ self.next_button.released.connect(lambda: self.NextPrev(1))
+
+ self.prev_button = QToolButton()
+ self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
+ self.prev_button.released.connect(lambda: self.NextPrev(-1))
+
+ self.close_button = QToolButton()
+ self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
+ self.close_button.released.connect(self.Deactivate)
+
+ self.hbox = QHBoxLayout()
+ self.hbox.setContentsMargins(0, 0, 0, 0)
+
+ self.hbox.addWidget(label)
+ self.hbox.addWidget(self.textbox)
+ self.hbox.addWidget(self.progress)
+ self.hbox.addWidget(self.pattern)
+ self.hbox.addWidget(self.next_button)
+ self.hbox.addWidget(self.prev_button)
+ self.hbox.addWidget(self.close_button)
+
+ self.bar = QWidget()
+ self.bar.setLayout(self.hbox)
+ self.bar.hide()
+
+ def Widget(self):
+ return self.bar
+
+ def Activate(self):
+ self.bar.show()
+ self.textbox.lineEdit().selectAll()
+ self.textbox.setFocus()
+
+ def Deactivate(self):
+ self.bar.hide()
+
+ def Busy(self):
+ self.textbox.setEnabled(False)
+ self.pattern.hide()
+ self.next_button.hide()
+ self.prev_button.hide()
+ self.progress.show()
+
+ def Idle(self):
+ self.textbox.setEnabled(True)
+ self.progress.hide()
+ self.pattern.show()
+ self.next_button.show()
+ self.prev_button.show()
+
+ def Find(self, direction):
+ value = self.textbox.currentText()
+ pattern = self.pattern.isChecked()
+ self.last_value = value
+ self.last_pattern = pattern
+ self.finder.Find(value, direction, pattern, self.context)
+
+ def ValueChanged(self):
+ _value = self.textbox.currentText()
+ pattern = self.pattern.isChecked()
+ index = self.textbox.currentIndex()
+ data = self.textbox.itemData(index)
+ # Store the pattern in the combo box to keep it with the text value
+ if data == None:
+ self.textbox.setItemData(index, pattern)
+ else:
+ self.pattern.setChecked(data)
+ self.Find(0)
+
+ def NextPrev(self, direction):
+ value = self.textbox.currentText()
+ pattern = self.pattern.isChecked()
+ if value != self.last_value:
+ index = self.textbox.findText(value)
+ # Allow for a button press before the value has been added to the combo box
+ if index < 0:
+ index = self.textbox.count()
+ self.textbox.addItem(value, pattern)
+ self.textbox.setCurrentIndex(index)
+ return
+ else:
+ self.textbox.setItemData(index, pattern)
+ elif pattern != self.last_pattern:
+ # Keep the pattern recorded in the combo box up to date
+ index = self.textbox.currentIndex()
+ self.textbox.setItemData(index, pattern)
+ self.Find(direction)
+
+ def NotFound(self):
+ QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
+
+# Context-sensitive call graph data model item base
+
+class CallGraphLevelItemBase(object):
+ def Select(self) -> None:
+ pass
+ def data(self, _column: int, _role: int) -> Any:
+ return None
+
+ def __init__(self, glb, params, row, parent_item):
+ self.glb = glb
+ self.params = params
+ self.row = row
+ self.parent_item = parent_item
+ self.query_done = False
+ self.child_count = 0
+ self.child_items = []
+ if parent_item:
+ self.level = parent_item.level + 1
+ else:
+ self.level = 0
+
+ def getChildItem(self, row):
+ return self.child_items[row]
+
+ def getParentItem(self):
+ return self.parent_item
+
+ def getRow(self):
+ return self.row
+
+ def childCount(self):
+ if not self.query_done:
+ self.Select()
+ if not self.child_count:
+ return -1
+ return self.child_count
+
+ def hasChildren(self):
+ if not self.query_done:
+ return True
+ return self.child_count > 0
+
+ def getData(self, column):
+ return self.data[column]
+
+# Context-sensitive call graph data model level 2+ item base
+
+class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
+
+ def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
+ super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
+ self.comm_id = comm_id
+ self.thread_id = thread_id
+ self.call_path_id = call_path_id
+ self.insn_cnt = insn_cnt
+ self.cyc_cnt = cyc_cnt
+ self.branch_count = branch_count
+ self.time = time
+
+ def Select(self):
+ self.query_done = True
+ query = QSqlQuery(self.glb.db)
+ if self.params.have_ipc:
+ ipc_str = ", SUM(insn_count), SUM(cyc_count)"
+ else:
+ ipc_str = ""
+ QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
+ " FROM calls"
+ " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
+ " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
+ " INNER JOIN dsos ON symbols.dso_id = dsos.id"
+ " WHERE parent_call_path_id = " + str(self.call_path_id) +
+ " AND comm_id = " + str(self.comm_id) +
+ " AND thread_id = " + str(self.thread_id) +
+ " GROUP BY call_path_id, name, short_name"
+ " ORDER BY call_path_id")
+ while query.next():
+ if self.params.have_ipc:
+ insn_cnt = int(query.value(5))
+ cyc_cnt = int(query.value(6))
+ branch_count = int(query.value(7))
+ else:
+ insn_cnt = 0
+ cyc_cnt = 0
+ branch_count = int(query.value(5))
+ child_item = CallGraphLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)
+ self.child_items.append(child_item)
+ self.child_count += 1
+
+# Context-sensitive call graph data model level three item
+
+class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
+
+ def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item):
+ super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
+ dso = dsoname(dso)
+ if self.params.have_ipc:
+ insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
+ cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
+ br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
+ ipc = CalcIPC(cyc_cnt, insn_cnt)
+ self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
+ else:
+ self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
+ self.dbid = call_path_id
+
+# Context-sensitive call graph data model level two item
+
+class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
+
+ def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
+ super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)
+ if self.params.have_ipc:
+ self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
+ else:
+ self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
+ self.dbid = thread_id
+
+ def Select(self):
+ super(CallGraphLevelTwoItem, self).Select()
+ for child_item in self.child_items:
+ self.time += child_item.time
+ self.insn_cnt += child_item.insn_cnt
+ self.cyc_cnt += child_item.cyc_cnt
+ self.branch_count += child_item.branch_count
+ for child_item in self.child_items:
+ child_item.data[4] = PercentToOneDP(child_item.time, self.time)
+ if self.params.have_ipc:
+ child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
+ child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
+ child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
+ else:
+ child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
+
+# Context-sensitive call graph data model level one item
+
+class CallGraphLevelOneItem(CallGraphLevelItemBase):
+
+ def __init__(self, glb, params, row, comm_id, comm, parent_item):
+ super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)
+ if self.params.have_ipc:
+ self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
+ else:
+ self.data = [comm, "", "", "", "", "", ""]
+ self.dbid = comm_id
+
+ def Select(self):
+ self.query_done = True
+ query = QSqlQuery(self.glb.db)
+ QueryExec(query, "SELECT thread_id, pid, tid"
+ " FROM comm_threads"
+ " INNER JOIN threads ON thread_id = threads.id"
+ " WHERE comm_id = " + str(self.dbid))
+ while query.next():
+ child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
+ self.child_items.append(child_item)
+ self.child_count += 1
+
+# Context-sensitive call graph data model root item
+
+class CallGraphRootItem(CallGraphLevelItemBase):
+
+ def __init__(self, glb, params):
+ super(CallGraphRootItem, self).__init__(glb, params, 0, None)
+ self.dbid = 0
+ self.query_done = True
+ if_has_calls = ""
+ if IsSelectable(glb.db, "comms", columns = "has_calls"):
+ if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
+ query = QSqlQuery(glb.db)
+ QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
+ while query.next():
+ if not query.value(0):
+ continue
+ child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
+ self.child_items.append(child_item)
+ self.child_count += 1
+
+# Call graph model parameters
+
+class CallGraphModelParams():
+
+ def __init__(self, glb, _parent=None):
+ self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")
+
+# Context-sensitive call graph data model base
+
+class CallGraphModelBase(TreeModel):
+
+ def __init__(self, glb, parent=None):
+ super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)
+
+ def FindSelect(self, value, pattern, query):
+ if pattern:
+ # postgresql and sqlite pattern patching differences:
+ # postgresql LIKE is case sensitive but sqlite LIKE is not
+ # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
+ # postgresql supports ILIKE which is case insensitive
+ # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
+ if not self.glb.dbref.is_sqlite3:
+ # Escape % and _
+ s = value.replace("%", "\\%")
+ s = s.replace("_", "\\_")
+ # Translate * and ? into SQL LIKE pattern characters % and _
+ if sys.version_info[0] == 3:
+ trans = str.maketrans("*?", "%_")
+ else:
+ trans = bytes.maketrans("*?", "%_")
+ match = " LIKE '" + str(s).translate(trans) + "'"
+ else:
+ match = " GLOB '" + str(value) + "'"
+ else:
+ match = " = '" + str(value) + "'"
+ self.DoFindSelect(query, match)
+
+ def Found(self, query, found):
+ if found:
+ return self.FindPath(query)
+ return []
+
+ def FindValue(self, value, pattern, query, last_value, last_pattern):
+ if last_value == value and pattern == last_pattern:
+ found = query.first()
+ else:
+ self.FindSelect(value, pattern, query)
+ found = query.next()
+ return self.Found(query, found)
+
+ def FindNext(self, query):
+ found = query.next()
+ if not found:
+ found = query.first()
+ return self.Found(query, found)
+
+ def FindPrev(self, query):
+ found = query.previous()
+ if not found:
+ found = query.last()
+ return self.Found(query, found)
+
+ def FindThread(self, c):
+ if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
+ ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
+ elif c.direction > 0:
+ ids = self.FindNext(c.query)
+ else:
+ ids = self.FindPrev(c.query)
+ return (True, ids)
+
+ def Find(self, value, direction, pattern, context, callback):
+ class Context():
+ def __init__(self, *x):
+ self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
+ def Update(self, *x):
+ self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
+ if len(context):
+ context[0].Update(value, direction, pattern)
+ else:
+ context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
+ # Use a thread so the UI is not blocked during the SELECT
+ thread = Thread(self.FindThread, context[0])
+ thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
+ thread.start()
+
+ def FindDone(self, _thread, callback, ids):
+ callback(ids)
+
+# Context-sensitive call graph data model
+
+class CallGraphModel(CallGraphModelBase):
+
+ def GetRoot(self):
+ return CallGraphRootItem(self.glb, self.params)
+
+ def columnCount(self, _parent=None):
+ if self.params.have_ipc:
+ return 12
+ else:
+ return 7
+
+ def columnHeader(self, column):
+ if self.params.have_ipc:
+ headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
+ else:
+ headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
+ return headers[column]
+
+ def columnAlignment(self, column):
+ if self.params.have_ipc:
+ alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
+ else:
+ alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
+ return alignment[column]
+
+ def DoFindSelect(self, query, match):
+ QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
+ " FROM calls"
+ " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
+ " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
+ " WHERE calls.id <> 0"
+ " AND symbols.name" + match +
+ " GROUP BY comm_id, thread_id, call_path_id"
+ " ORDER BY comm_id, thread_id, call_path_id")
+
+ def FindPath(self, query):
+ # Turn the query result into a list of ids that the tree view can walk
+ # to open the tree at the right place.
+ ids = []
+ parent_id = query.value(0)
+ while parent_id:
+ ids.insert(0, parent_id)
+ q2 = QSqlQuery(self.glb.db)
+ QueryExec(q2, "SELECT parent_id"
+ " FROM call_paths"
+ " WHERE id = " + str(parent_id))
+ if not q2.next():
+ break
+ parent_id = q2.value(0)
+ # The call path root is not used
+ if ids[0] == 1:
+ del ids[0]
+ ids.insert(0, query.value(2))
+ ids.insert(0, query.value(1))
+ return ids
+
+# Call tree data model level 2+ item base
+
+class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
+
+ def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
+ super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
+ self.comm_id = comm_id
+ self.thread_id = thread_id
+ self.calls_id = calls_id
+ self.call_time = call_time
+ self.time = time
+ self.insn_cnt = insn_cnt
+ self.cyc_cnt = cyc_cnt
+ self.branch_count = branch_count
+
+ def Select(self):
+ self.query_done = True
+ if self.calls_id == 0:
+ comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
+ else:
+ comm_thread = ""
+ if self.params.have_ipc:
+ ipc_str = ", insn_count, cyc_count"
+ else:
+ ipc_str = ""
+ query = QSqlQuery(self.glb.db)
+ QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"
+ " FROM calls"
+ " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
+ " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
+ " INNER JOIN dsos ON symbols.dso_id = dsos.id"
+ " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
+ " ORDER BY call_time, calls.id")
+ while query.next():
+ if self.params.have_ipc:
+ insn_cnt = int(query.value(5))
+ cyc_cnt = int(query.value(6))
+ branch_count = int(query.value(7))
+ else:
+ insn_cnt = 0
+ cyc_cnt = 0
+ branch_count = int(query.value(5))
+ child_item = CallTreeLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)
+ self.child_items.append(child_item)
+ self.child_count += 1
+
+# Call tree data model level three item
+
+class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
+
+ def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
+ super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item)
+ dso = dsoname(dso)
+ if self.params.have_ipc:
+ insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
+ cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
+ br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
+ ipc = CalcIPC(cyc_cnt, insn_cnt)
+ self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
+ else:
+ self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
+ self.dbid = calls_id
+
+# Call tree data model level two item
+
+class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
+
+ def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
+ super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item)
+ if self.params.have_ipc:
+ self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
+ else:
+ self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
+ self.dbid = thread_id
+
+ def Select(self):
+ super(CallTreeLevelTwoItem, self).Select()
+ for child_item in self.child_items:
+ self.time += child_item.time
+ self.insn_cnt += child_item.insn_cnt
+ self.cyc_cnt += child_item.cyc_cnt
+ self.branch_count += child_item.branch_count
+ for child_item in self.child_items:
+ child_item.data[4] = PercentToOneDP(child_item.time, self.time)
+ if self.params.have_ipc:
+ child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
+ child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
+ child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
+ else:
+ child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
+
+# Call tree data model level one item
+
+class CallTreeLevelOneItem(CallGraphLevelItemBase):
+
+ def __init__(self, glb, params, row, comm_id, comm, parent_item):
+ super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
+ if self.params.have_ipc:
+ self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
+ else:
+ self.data = [comm, "", "", "", "", "", ""]
+ self.dbid = comm_id
+
+ def Select(self):
+ self.query_done = True
+ query = QSqlQuery(self.glb.db)
+ QueryExec(query, "SELECT thread_id, pid, tid"
+ " FROM comm_threads"
+ " INNER JOIN threads ON thread_id = threads.id"
+ " WHERE comm_id = " + str(self.dbid))
+ while query.next():
+ child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
+ self.child_items.append(child_item)
+ self.child_count += 1
+
+# Call tree data model root item
+
+class CallTreeRootItem(CallGraphLevelItemBase):
+
+ def __init__(self, glb, params):
+ super(CallTreeRootItem, self).__init__(glb, params, 0, None)
+ self.dbid = 0
+ self.query_done = True
+ if_has_calls = ""
+ if IsSelectable(glb.db, "comms", columns = "has_calls"):
+ if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
+ query = QSqlQuery(glb.db)
+ QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
+ while query.next():
+ if not query.value(0):
+ continue
+ child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
+ self.child_items.append(child_item)
+ self.child_count += 1
+
+# Call Tree data model
+
+class CallTreeModel(CallGraphModelBase):
+
+ def GetRoot(self):
+ return CallTreeRootItem(self.glb, self.params)
+
+ def columnCount(self, _parent=None):
+ if self.params.have_ipc:
+ return 12
+ else:
+ return 7
+
+ def columnHeader(self, column):
+ if self.params.have_ipc:
+ headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
+ else:
+ headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
+ return headers[column]
+
+ def columnAlignment(self, column):
+ if self.params.have_ipc:
+ alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
+ else:
+ alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
+ return alignment[column]
+
+ def DoFindSelect(self, query, match):
+ QueryExec(query, "SELECT calls.id, comm_id, thread_id"
+ " FROM calls"
+ " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
+ " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
+ " WHERE calls.id <> 0"
+ " AND symbols.name" + match +
+ " ORDER BY comm_id, thread_id, call_time, calls.id")
+
+ def FindPath(self, query):
+ # Turn the query result into a list of ids that the tree view can walk
+ # to open the tree at the right place.
+ ids = []
+ parent_id = query.value(0)
+ while parent_id:
+ ids.insert(0, parent_id)
+ q2 = QSqlQuery(self.glb.db)
+ QueryExec(q2, "SELECT parent_id"
+ " FROM calls"
+ " WHERE id = " + str(parent_id))
+ if not q2.next():
+ break
+ parent_id = q2.value(0)
+ ids.insert(0, query.value(2))
+ ids.insert(0, query.value(1))
+ return ids
+
+# Vertical layout
+
+class HBoxLayout(QHBoxLayout):
+
+ def __init__(self, *children):
+ super(HBoxLayout, self).__init__()
+
+ self.layout().setContentsMargins(0, 0, 0, 0)
+ for child in children:
+ if child.isWidgetType():
+ self.layout().addWidget(child)
+ else:
+ self.layout().addLayout(child)
+
+# Horizontal layout
+
+class VBoxLayout(QVBoxLayout):
+
+ def __init__(self, *children):
+ super(VBoxLayout, self).__init__()
+
+ self.layout().setContentsMargins(0, 0, 0, 0)
+ for child in children:
+ if child.isWidgetType():
+ self.layout().addWidget(child)
+ else:
+ self.layout().addLayout(child)
+
+# Vertical layout widget
+
+class VBox():
+
+ def __init__(self, *children):
+ self.vbox = QWidget()
+ self.vbox.setLayout(VBoxLayout(*children))
+
+ def Widget(self):
+ return self.vbox
+
+# Tree window base
+
+class TreeWindowBase(QMdiSubWindow):
+
+ def __init__(self, parent=None):
+ super(TreeWindowBase, self).__init__(parent)
+
+ self.model = None
+ self.find_bar = None
+
+ self.view = QTreeView()
+ self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
+ self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
+
+ self.context_menu = TreeContextMenu(self.view)
+
+ def DisplayFound(self, ids):
+ if not len(ids):
+ return False
+ parent = QModelIndex()
+ for dbid in ids:
+ found = False
+ n = self.model.rowCount(parent)
+ for row in range(n):
+ child = self.model.index(row, 0, parent)
+ if child.internalPointer().dbid == dbid:
+ found = True
+ self.view.setExpanded(parent, True)
+ self.view.setCurrentIndex(child)
+ parent = child
+ break
+ if not found:
+ break
+ return found
+
+ def Find(self, value, direction, pattern, context):
+ self.view.setFocus()
+ self.find_bar.Busy()
+ self.model.Find(value, direction, pattern, context, self.FindDone)
+
+ def FindDone(self, ids):
+ found = True
+ if not self.DisplayFound(ids):
+ found = False
+ self.find_bar.Idle()
+ if not found:
+ self.find_bar.NotFound()
+
+
+# Context-sensitive call graph window
+
+class CallGraphWindow(TreeWindowBase):
+
+ def __init__(self, glb, parent=None):
+ super(CallGraphWindow, self).__init__(parent)
+
+ self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
+
+ self.view.setModel(self.model)
+
+ for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
+ self.view.setColumnWidth(c, w)
+
+ self.find_bar = FindBar(self, self)
+
+ self.vbox = VBox(self.view, self.find_bar.Widget())
+
+ self.setWidget(self.vbox.Widget())
+
+ AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
+
+# Call tree window
+
+class CallTreeWindow(TreeWindowBase):
+
+ def __init__(self, glb, parent=None, thread_at_time=None):
+ super(CallTreeWindow, self).__init__(parent)
+
+ self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
+
+ self.view.setModel(self.model)
+
+ for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
+ self.view.setColumnWidth(c, w)
+
+ self.find_bar = FindBar(self, self)
+
+ self.vbox = VBox(self.view, self.find_bar.Widget())
+
+ self.setWidget(self.vbox.Widget())
+
+ AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
+
+ if thread_at_time:
+ self.DisplayThreadAtTime(*thread_at_time)
+
+ def DisplayThreadAtTime(self, comm_id, thread_id, time):
+ parent = QModelIndex()
+ for dbid in (comm_id, thread_id):
+ found = False
+ n = self.model.rowCount(parent)
+ for row in range(n):
+ child = self.model.index(row, 0, parent)
+ if child.internalPointer().dbid == dbid:
+ found = True
+ self.view.setExpanded(parent, True)
+ self.view.setCurrentIndex(child)
+ parent = child
+ break
+ if not found:
+ return
+ found = False
+ while True:
+ n = self.model.rowCount(parent)
+ if not n:
+ return
+ last_child = None
+ for row in range(n):
+ self.view.setExpanded(parent, True)
+ child = self.model.index(row, 0, parent)
+ child_call_time = child.internalPointer().call_time
+ if child_call_time < time:
+ last_child = child
+ elif child_call_time == time:
+ self.view.setCurrentIndex(child)
+ return
+ elif child_call_time > time:
+ break
+ if not last_child:
+ if not found:
+ child = self.model.index(0, 0, parent)
+ self.view.setExpanded(parent, True)
+ self.view.setCurrentIndex(child)
+ return
+ found = True
+ self.view.setExpanded(parent, True)
+ self.view.setCurrentIndex(last_child)
+ parent = last_child
+
+# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
+
+def ExecComm(db, thread_id, time):
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
+ " FROM comm_threads"
+ " INNER JOIN comms ON comms.id = comm_threads.comm_id"
+ " WHERE comm_threads.thread_id = " + str(thread_id) +
+ " ORDER BY comms.c_time, comms.id")
+ first = None
+ last = None
+ while query.next():
+ if first is None:
+ first = query.value(0)
+ if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
+ last = query.value(0)
+ if not(last is None):
+ return last
+ return first
+
+# Container for (x, y) data
+
+class XY():
+ def __init__(self, x=0, y=0):
+ self.x = x
+ self.y = y
+
+ def __str__(self):
+ return "XY({}, {})".format(str(self.x), str(self.y))
+
+# Container for sub-range data
+
+class Subrange():
+ def __init__(self, lo=0, hi=0):
+ self.lo = lo
+ self.hi = hi
+
+ def __str__(self):
+ return "Subrange({}, {})".format(str(self.lo), str(self.hi))
+
+# Graph data region base class
+
+class GraphDataRegion(object):
+
+ def __init__(self, key, title = "", ordinal = ""):
+ self.key = key
+ self.title = title
+ self.ordinal = ordinal
+
+# Function to sort GraphDataRegion
+
+def GraphDataRegionOrdinal(data_region):
+ return data_region.ordinal
+
+# Attributes for a graph region
+
+class GraphRegionAttribute():
+
+ def __init__(self, colour):
+ self.colour = colour
+
+# Switch graph data region represents a task
+
+class SwitchGraphDataRegion(GraphDataRegion):
+
+ def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
+ super(SwitchGraphDataRegion, self).__init__(key)
+
+ self.title = str(pid) + " / " + str(tid) + " " + comm
+ # Order graph legend within exec comm by pid / tid / time
+ self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
+ self.exec_comm_id = exec_comm_id
+ self.pid = pid
+ self.tid = tid
+ self.comm = comm
+ self.thread_id = thread_id
+ self.comm_id = comm_id
+
+# Graph data point
+
+class GraphDataPoint():
+
+ def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
+ self.data = data
+ self.index = index
+ self.x = x
+ self.y = y
+ self.altx = altx
+ self.alty = alty
+ self.hregion = hregion
+ self.vregion = vregion
+
+# Graph data (single graph) base class
+
+class GraphData(object):
+
+ def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
+ self.collection = collection
+ self.points = []
+ self.xbase = xbase
+ self.ybase = ybase
+ self.title = ""
+
+ def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
+ index = len(self.points)
+
+ x = float(Decimal(x) - self.xbase)
+ y = float(Decimal(y) - self.ybase)
+
+ self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
+
+ def XToData(self, x):
+ return Decimal(x) + self.xbase
+
+ def YToData(self, y):
+ return Decimal(y) + self.ybase
+
+# Switch graph data (for one CPU)
+
+class SwitchGraphData(GraphData):
+
+ def __init__(self, db, collection, cpu, xbase):
+ super(SwitchGraphData, self).__init__(collection, xbase)
+
+ self.cpu = cpu
+ self.title = "CPU " + str(cpu)
+ self.SelectSwitches(db)
+
+ def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT id, c_time"
+ " FROM comms"
+ " WHERE c_thread_id = " + str(thread_id) +
+ " AND exec_flag = " + self.collection.glb.dbref.TRUE +
+ " AND c_time >= " + str(start_time) +
+ " AND c_time <= " + str(end_time) +
+ " ORDER BY c_time, id")
+ while query.next():
+ comm_id = query.value(0)
+ if comm_id == last_comm_id:
+ continue
+ time = query.value(1)
+ hregion = self.HRegion(db, thread_id, comm_id, time)
+ self.AddPoint(time, 1000, None, None, hregion)
+
+ def SelectSwitches(self, db):
+ last_time = None
+ last_comm_id = None
+ last_thread_id = None
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
+ " FROM context_switches"
+ " WHERE machine_id = " + str(self.collection.machine_id) +
+ " AND cpu = " + str(self.cpu) +
+ " ORDER BY time, id")
+ while query.next():
+ flags = int(query.value(5))
+ if flags & 1:
+ # Schedule-out: detect and add exec's
+ if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
+ self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
+ continue
+ # Schedule-in: add data point
+ if len(self.points) == 0:
+ start_time = self.collection.glb.StartTime(self.collection.machine_id)
+ hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
+ self.AddPoint(start_time, 1000, None, None, hregion)
+ time = query.value(0)
+ comm_id = query.value(4)
+ thread_id = query.value(2)
+ hregion = self.HRegion(db, thread_id, comm_id, time)
+ self.AddPoint(time, 1000, None, None, hregion)
+ last_time = time
+ last_comm_id = comm_id
+ last_thread_id = thread_id
+
+ def NewHRegion(self, db, key, thread_id, comm_id, time):
+ exec_comm_id = ExecComm(db, thread_id, time)
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
+ if query.next():
+ pid = query.value(0)
+ tid = query.value(1)
+ else:
+ pid = -1
+ tid = -1
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
+ if query.next():
+ comm = query.value(0)
+ else:
+ comm = ""
+ return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
+
+ def HRegion(self, db, thread_id, comm_id, time):
+ key = str(thread_id) + ":" + str(comm_id)
+ hregion = self.collection.LookupHRegion(key)
+ if hregion is None:
+ hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
+ self.collection.AddHRegion(key, hregion)
+ return hregion
+
+# Graph data collection (multiple related graphs) base class
+
+class GraphDataCollection(object):
+
+ def __init__(self, glb):
+ self.glb = glb
+ self.data = []
+ self.hregions = {}
+ self.xrangelo = None
+ self.xrangehi = None
+ self.yrangelo = None
+ self.yrangehi = None
+ self.dp = XY(0, 0)
+
+ def AddGraphData(self, data):
+ self.data.append(data)
+
+ def LookupHRegion(self, key):
+ if key in self.hregions:
+ return self.hregions[key]
+ return None
+
+ def AddHRegion(self, key, hregion):
+ self.hregions[key] = hregion
+
+# Switch graph data collection (SwitchGraphData for each CPU)
+
+class SwitchGraphDataCollection(GraphDataCollection):
+
+ def __init__(self, glb, db, machine_id):
+ super(SwitchGraphDataCollection, self).__init__(glb)
+
+ self.machine_id = machine_id
+ self.cpus = self.SelectCPUs(db)
+
+ self.xrangelo = glb.StartTime(machine_id)
+ self.xrangehi = glb.FinishTime(machine_id)
+
+ self.yrangelo = Decimal(0)
+ self.yrangehi = Decimal(1000)
+
+ for cpu in self.cpus:
+ self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
+
+ def SelectCPUs(self, db):
+ cpus = []
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT DISTINCT cpu"
+ " FROM context_switches"
+ " WHERE machine_id = " + str(self.machine_id))
+ while query.next():
+ cpus.append(int(query.value(0)))
+ return sorted(cpus)
+
+# Switch graph data graphics item displays the graphed data
+
+class SwitchGraphDataGraphicsItem(QGraphicsItem):
+
+ def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
+ super(SwitchGraphDataGraphicsItem, self).__init__(parent)
+
+ self.data = data
+ self.graph_width = graph_width
+ self.graph_height = graph_height
+ self.attrs = attrs
+ self.event_handler = event_handler
+ self.setAcceptHoverEvents(True)
+
+ def boundingRect(self):
+ return QRectF(0, 0, self.graph_width, self.graph_height)
+
+ def PaintPoint(self, painter, last, x):
+ if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
+ if last.x < self.attrs.subrange.x.lo:
+ x0 = self.attrs.subrange.x.lo
+ else:
+ x0 = last.x
+ if x > self.attrs.subrange.x.hi:
+ x1 = self.attrs.subrange.x.hi
+ else:
+ x1 = x - 1
+ x0 = self.attrs.XToPixel(x0)
+ x1 = self.attrs.XToPixel(x1)
+
+ y0 = self.attrs.YToPixel(last.y)
+
+ colour = self.attrs.region_attributes[last.hregion.key].colour
+
+ width = x1 - x0 + 1
+ if width < 2:
+ painter.setPen(colour)
+ painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
+ else:
+ painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
+
+ def paint(self, painter, _option, _widget):
+ last = None
+ for point in self.data.points:
+ self.PaintPoint(painter, last, point.x)
+ if point.x > self.attrs.subrange.x.hi:
+ break
+ last = point
+ self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
+
+ def BinarySearchPoint(self, target):
+ lower_pos = 0
+ higher_pos = len(self.data.points)
+ while True:
+ pos = int((lower_pos + higher_pos) / 2)
+ val = self.data.points[pos].x
+ if target >= val:
+ lower_pos = pos
+ else:
+ higher_pos = pos
+ if higher_pos <= lower_pos + 1:
+ return lower_pos
+
+ def XPixelToData(self, x):
+ x = self.attrs.PixelToX(x)
+ if x < self.data.points[0].x:
+ x = 0
+ pos = 0
+ low = True
+ else:
+ pos = self.BinarySearchPoint(x)
+ low = False
+ return (low, pos, self.data.XToData(x))
+
+ def EventToData(self, event):
+ no_data = (None,) * 4
+ if len(self.data.points) < 1:
+ return no_data
+ x = event.pos().x()
+ if x < 0:
+ return no_data
+ _low0, pos0, time_from = self.XPixelToData(x)
+ low1, pos1, time_to = self.XPixelToData(x + 1)
+ hregions = set()
+ hregion_times = []
+ if not low1:
+ for i in range(pos0, pos1 + 1):
+ hregion = self.data.points[i].hregion
+ hregions.add(hregion)
+ if i == pos0:
+ time = time_from
+ else:
+ time = self.data.XToData(self.data.points[i].x)
+ hregion_times.append((hregion, time))
+ return (time_from, time_to, hregions, hregion_times)
+
+ def hoverMoveEvent(self, event):
+ time_from, time_to, hregions, _hregion_times = self.EventToData(event)
+ if time_from is not None:
+ self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
+
+ def hoverLeaveEvent(self, _event):
+ self.event_handler.NoPointEvent()
+
+ def mousePressEvent(self, event):
+ if event.button() != Qt.RightButton:
+ super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
+ return
+ _time_from, _time_to, _hregions, hregion_times = self.EventToData(event)
+ if hregion_times:
+ self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
+
+# X-axis graphics item
+
+class XAxisGraphicsItem(QGraphicsItem):
+
+ def __init__(self, width, parent=None):
+ super(XAxisGraphicsItem, self).__init__(parent)
+
+ self.width = width
+ self.max_mark_sz = 4
+ self.height = self.max_mark_sz + 1
+
+ def boundingRect(self):
+ return QRectF(0, 0, self.width, self.height)
+
+ def Step(self):
+ attrs = self.parentItem().attrs
+ subrange = attrs.subrange.x
+ t = subrange.hi - subrange.lo
+ s = (3.0 * t) / self.width
+ n = 1.0
+ while s > n:
+ n = n * 10.0
+ return n
+
+ def PaintMarks(self, painter, at_y, lo, hi, step, i):
+ attrs = self.parentItem().attrs
+ x = lo
+ while x <= hi:
+ xp = attrs.XToPixel(x)
+ if i % 10:
+ if i % 5:
+ sz = 1
+ else:
+ sz = 2
+ else:
+ sz = self.max_mark_sz
+ i = 0
+ painter.drawLine(xp, at_y, xp, at_y + sz)
+ x += step
+ i += 1
+
+ def paint(self, painter, _option, _widget):
+ # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
+ painter.drawLine(0, 0, self.width - 1, 0)
+ n = self.Step()
+ attrs = self.parentItem().attrs
+ subrange = attrs.subrange.x
+ if subrange.lo:
+ x_offset = n - (subrange.lo % n)
+ else:
+ x_offset = 0.0
+ x = subrange.lo + x_offset
+ i = (x / n) % 10
+ self.PaintMarks(painter, 0, x, subrange.hi, n, i)
+
+ def ScaleDimensions(self):
+ n = self.Step()
+ attrs = self.parentItem().attrs
+ lo = attrs.subrange.x.lo
+ hi = (n * 10.0) + lo
+ width = attrs.XToPixel(hi)
+ if width > 500:
+ width = 0
+ return (n, lo, hi, width)
+
+ def PaintScale(self, painter, at_x, at_y):
+ n, lo, hi, width = self.ScaleDimensions()
+ if not width:
+ return
+ painter.drawLine(at_x, at_y, at_x + width, at_y)
+ self.PaintMarks(painter, at_y, lo, hi, n, 0)
+
+ def ScaleWidth(self):
+ _n, _lo, _hi, width = self.ScaleDimensions()
+ return width
+
+ def ScaleHeight(self):
+ return self.height
+
+ def ScaleUnit(self):
+ return self.Step() * 10
+
+# Scale graphics item base class
+
+class ScaleGraphicsItem(QGraphicsItem):
+
+ def __init__(self, axis, parent=None):
+ super(ScaleGraphicsItem, self).__init__(parent)
+ self.axis = axis
+
+ def boundingRect(self):
+ scale_width = self.axis.ScaleWidth()
+ if not scale_width:
+ return QRectF()
+ return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
+
+ def paint(self, painter, _option, _widget):
+ scale_width = self.axis.ScaleWidth()
+ if not scale_width:
+ return
+ self.axis.PaintScale(painter, 0, 5)
+ x = scale_width + 4
+ painter.drawText(QPointF(x, 10), self.Text())
+
+ def Unit(self):
+ return self.axis.ScaleUnit()
+
+ def Text(self):
+ return ""
+
+# Switch graph scale graphics item
+
+class SwitchScaleGraphicsItem(ScaleGraphicsItem):
+
+ def Text(self):
+ unit = self.Unit()
+ if unit >= 1000000000:
+ unit = int(unit / 1000000000)
+ us = "s"
+ elif unit >= 1000000:
+ unit = int(unit / 1000000)
+ us = "ms"
+ elif unit >= 1000:
+ unit = int(unit / 1000)
+ us = "us"
+ else:
+ unit = int(unit)
+ us = "ns"
+ return " = " + str(unit) + " " + us
+
+# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
+
+class SwitchGraphGraphicsItem(QGraphicsItem):
+
+ def __init__(self, collection, data, attrs, event_handler, first, parent=None):
+ super(SwitchGraphGraphicsItem, self).__init__(parent)
+ self.collection = collection
+ self.data = data
+ self.attrs = attrs
+ self.event_handler = event_handler
+
+ margin = 20
+ title_width = 50
+
+ self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
+
+ self.title_graphics.setPos(margin, margin)
+ graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
+ graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
+
+ self.graph_origin_x = margin + title_width + margin
+ self.graph_origin_y = graph_height + margin
+
+ _x_axis_size = 1
+ y_axis_size = 1
+ self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
+
+ self.x_axis = XAxisGraphicsItem(graph_width, self)
+ self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
+
+ if first:
+ self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
+ self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
+
+ self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
+
+ self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
+ self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
+
+ self.width = self.graph_origin_x + graph_width + margin
+ self.height = self.graph_origin_y + margin
+
+ self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
+ self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
+
+ if parent and 'EnableRubberBand' in dir(parent):
+ parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
+
+ def boundingRect(self):
+ return QRectF(0, 0, self.width, self.height)
+
+ def paint(self, painter, option, widget):
+ pass
+
+ def RBXToPixel(self, x):
+ return self.attrs.PixelToX(x - self.graph_origin_x)
+
+ def RBXRangeToPixel(self, x0, x1):
+ return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
+
+ def RBPixelToTime(self, x):
+ if x < self.data.points[0].x:
+ return self.data.XToData(0)
+ return self.data.XToData(x)
+
+ def RBEventTimes(self, x0, x1):
+ x0, x1 = self.RBXRangeToPixel(x0, x1)
+ time_from = self.RBPixelToTime(x0)
+ time_to = self.RBPixelToTime(x1)
+ return (time_from, time_to)
+
+ def RBEvent(self, x0, x1):
+ time_from, time_to = self.RBEventTimes(x0, x1)
+ self.event_handler.RangeEvent(time_from, time_to)
+
+ def RBMoveEvent(self, x0, x1):
+ if x1 < x0:
+ x0, x1 = x1, x0
+ self.RBEvent(x0, x1)
+
+ def RBReleaseEvent(self, x0, x1, selection_state):
+ if x1 < x0:
+ x0, x1 = x1, x0
+ x0, x1 = self.RBXRangeToPixel(x0, x1)
+ self.event_handler.SelectEvent(x0, x1, selection_state)
+
+# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
+
+class VerticalBracketGraphicsItem(QGraphicsItem):
+
+ def __init__(self, parent=None):
+ super(VerticalBracketGraphicsItem, self).__init__(parent)
+
+ self.width = 0
+ self.height = 0
+ self.hide()
+
+ def SetSize(self, width, height):
+ self.width = width + 1
+ self.height = height + 1
+
+ def boundingRect(self):
+ return QRectF(0, 0, self.width, self.height)
+
+ def paint(self, painter, _option, _widget):
+ colour = QColor(255, 255, 0, 32)
+ painter.fillRect(0, 0, self.width, self.height, colour)
+ x1 = self.width - 1
+ y1 = self.height - 1
+ painter.drawLine(0, 0, x1, 0)
+ painter.drawLine(0, 0, 0, 3)
+ painter.drawLine(x1, 0, x1, 3)
+ painter.drawLine(0, y1, x1, y1)
+ painter.drawLine(0, y1, 0, y1 - 3)
+ painter.drawLine(x1, y1, x1, y1 - 3)
+
+# Graphics item to contain graphs arranged vertically
+
+class VertcalGraphSetGraphicsItem(QGraphicsItem):
+
+ def __init__(self, collection, attrs, event_handler, child_class, parent=None):
+ super(VertcalGraphSetGraphicsItem, self).__init__(parent)
+
+ self.collection = collection
+
+ self.top = 10
+
+ self.width = 0
+ self.height = self.top
+
+ self.rubber_band = None
+ self.rb_enabled = False
+ self.rb_in_view = False
+ self.rb_xlo = 0
+ self.rb_xhi = 0
+ self.rb_event_handler = None
+ self.rb_origin = None
+
+ first = True
+ for data in collection.data:
+ child = child_class(collection, data, attrs, event_handler, first, self)
+ child.setPos(0, self.height + 1)
+ rect = child.boundingRect()
+ if rect.right() > self.width:
+ self.width = rect.right()
+ self.height = self.height + rect.bottom() + 1
+ first = False
+
+ self.bracket = VerticalBracketGraphicsItem(self)
+
+ def EnableRubberBand(self, xlo, xhi, rb_event_handler):
+ if self.rb_enabled:
+ return
+ self.rb_enabled = True
+ self.rb_in_view = False
+ self.setAcceptedMouseButtons(Qt.LeftButton)
+ self.rb_xlo = xlo
+ self.rb_xhi = xhi
+ self.rb_event_handler = rb_event_handler
+
+ def mousePressEvent(self, event):
+ if self.rb_enabled:
+ self.MousePressEvent(event)
+
+ def mouseMoveEvent(self, event):
+ if self.rb_enabled:
+ self.MouseMoveEvent(event)
+
+ def mouseReleaseEvent(self, event):
+ if self.rb_enabled:
+ self.MouseReleaseEvent(event)
+
+ def boundingRect(self):
+ return QRectF(0, 0, self.width, self.height)
+
+ def paint(self, painter, option, widget):
+ pass
+
+ def RubberBandParent(self):
+ scene = self.scene()
+ view = scene.views()[0]
+ viewport = view.viewport()
+ return viewport
+
+ def RubberBandSetGeometry(self, rect):
+ scene_rectf = self.mapRectToScene(QRectF(rect))
+ scene = self.scene()
+ view = scene.views()[0]
+ poly = view.mapFromScene(scene_rectf)
+ self.rubber_band.setGeometry(poly.boundingRect())
+
+ def SetSelection(self, selection_state):
+ if self.rubber_band:
+ if selection_state:
+ self.RubberBandSetGeometry(selection_state)
+ self.rubber_band.show()
+ else:
+ self.rubber_band.hide()
+
+ def SetBracket(self, rect):
+ if rect:
+ x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
+ self.bracket.setPos(x, y)
+ self.bracket.SetSize(width, height)
+ self.bracket.show()
+ else:
+ self.bracket.hide()
+
+ def RubberBandX(self, event):
+ x = event.pos().toPoint().x()
+ if x < self.rb_xlo:
+ x = self.rb_xlo
+ elif x > self.rb_xhi:
+ x = self.rb_xhi
+ else:
+ self.rb_in_view = True
+ return x
+
+ def RubberBandRect(self, x):
+ if self.rb_origin.x() <= x:
+ width = x - self.rb_origin.x()
+ rect = QRect(self.rb_origin, QSize(width, self.height))
+ else:
+ width = self.rb_origin.x() - x
+ top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
+ rect = QRect(top_left, QSize(width, self.height))
+ return rect
+
+ def MousePressEvent(self, event):
+ self.rb_in_view = False
+ x = self.RubberBandX(event)
+ self.rb_origin = QPoint(x, self.top)
+ if self.rubber_band is None:
+ self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
+ self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
+ if self.rb_in_view:
+ self.rubber_band.show()
+ self.rb_event_handler.RBMoveEvent(x, x)
+ else:
+ self.rubber_band.hide()
+
+ def MouseMoveEvent(self, event):
+ x = self.RubberBandX(event)
+ rect = self.RubberBandRect(x)
+ self.RubberBandSetGeometry(rect)
+ if self.rb_in_view:
+ self.rubber_band.show()
+ self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
+
+ def MouseReleaseEvent(self, event):
+ x = self.RubberBandX(event)
+ if self.rb_in_view:
+ selection_state = self.RubberBandRect(x)
+ else:
+ selection_state = None
+ self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
+
+# Switch graph legend data model
+
+class SwitchGraphLegendModel(QAbstractTableModel):
+
+ def __init__(self, collection, region_attributes, parent=None):
+ super(SwitchGraphLegendModel, self).__init__(parent)
+
+ self.region_attributes = region_attributes
+
+ self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
+ self.child_count = len(self.child_items)
+
+ self.highlight_set = set()
+
+ self.column_headers = ("pid", "tid", "comm")
+
+ def rowCount(self, _parent):
+ return self.child_count
+
+ def headerData(self, section, orientation, role):
+ if role != Qt.DisplayRole:
+ return None
+ if orientation != Qt.Horizontal:
+ return None
+ return self.columnHeader(section)
+
+ def index(self, row, column, _parent):
+ return self.createIndex(row, column, self.child_items[row])
+
+ def columnCount(self, _parent=None):
+ return len(self.column_headers)
+
+ def columnHeader(self, column):
+ return self.column_headers[column]
+
+ def data(self, index, role):
+ if role == Qt.BackgroundRole:
+ child = self.child_items[index.row()]
+ if child in self.highlight_set:
+ return self.region_attributes[child.key].colour
+ return None
+ if role == Qt.ForegroundRole:
+ child = self.child_items[index.row()]
+ if child in self.highlight_set:
+ return QColor(255, 255, 255)
+ return self.region_attributes[child.key].colour
+ if role != Qt.DisplayRole:
+ return None
+ hregion = self.child_items[index.row()]
+ col = index.column()
+ if col == 0:
+ return hregion.pid
+ if col == 1:
+ return hregion.tid
+ if col == 2:
+ return hregion.comm
+ return None
+
+ def SetHighlight(self, row, _set_highlight):
+ child = self.child_items[row]
+ top_left = self.createIndex(row, 0, child)
+ bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
+ self.dataChanged.emit(top_left, bottom_right)
+
+ def Highlight(self, highlight_set):
+ for row in range(self.child_count):
+ child = self.child_items[row]
+ if child in self.highlight_set:
+ if child not in highlight_set:
+ self.SetHighlight(row, False)
+ elif child in highlight_set:
+ self.SetHighlight(row, True)
+ self.highlight_set = highlight_set
+
+# Switch graph legend is a table
+
+class SwitchGraphLegend(QWidget):
+
+ def __init__(self, collection, region_attributes, parent=None):
+ super(SwitchGraphLegend, self).__init__(parent)
+
+ self.data_model = SwitchGraphLegendModel(collection, region_attributes)
+
+ self.model = QSortFilterProxyModel()
+ self.model.setSourceModel(self.data_model)
+
+ self.view = QTableView()
+ self.view.setModel(self.model)
+ self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.view.verticalHeader().setVisible(False)
+ self.view.sortByColumn(-1, Qt.AscendingOrder)
+ self.view.setSortingEnabled(True)
+ self.view.resizeColumnsToContents()
+ self.view.resizeRowsToContents()
+
+ self.vbox = VBoxLayout(self.view)
+ self.setLayout(self.vbox)
+
+ sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
+ sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
+ self.saved_size = sz1
+
+ def resizeEvent(self, event):
+ self.saved_size = self.size().width()
+ super(SwitchGraphLegend, self).resizeEvent(event)
+
+ def Highlight(self, highlight_set):
+ self.data_model.Highlight(highlight_set)
+ self.update()
+
+ def changeEvent(self, event):
+ if event.type() == QEvent.FontChange:
+ self.view.resizeRowsToContents()
+ self.view.resizeColumnsToContents()
+ # Need to resize rows again after column resize
+ self.view.resizeRowsToContents()
+ super(SwitchGraphLegend, self).changeEvent(event)
+
+# Random colour generation
+
+def RGBColourTooLight(r, g, _b):
+ if g > 230:
+ return True
+ if g <= 160:
+ return False
+ if r <= 180 and g <= 180:
+ return False
+ if r < 60:
+ return False
+ return True
+
+def GenerateColours(x):
+ cs = [0]
+ for i in range(1, x):
+ cs.append(int((255.0 / i) + 0.5))
+ colours = []
+ for r in cs:
+ for g in cs:
+ for b in cs:
+ # Exclude black and colours that look too light against a white background
+ if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
+ continue
+ colours.append(QColor(r, g, b))
+ return colours
+
+def GenerateNColours(n):
+ for x in range(2, n + 2):
+ colours = GenerateColours(x)
+ if len(colours) >= n:
+ return colours
+ return []
+
+def GenerateNRandomColours(n, seed):
+ colours = GenerateNColours(n)
+ random.seed(seed)
+ random.shuffle(colours)
+ return colours
+
+# Graph attributes, in particular the scale and subrange that change when zooming
+
+class GraphAttributes():
+
+ def __init__(self, scale, subrange, region_attributes, dp):
+ self.scale = scale
+ self.subrange = subrange
+ self.region_attributes = region_attributes
+ # Rounding avoids errors due to finite floating point precision
+ self.dp = dp # data decimal places
+ self.Update()
+
+ def XToPixel(self, x):
+ return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
+
+ def YToPixel(self, y):
+ return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
+
+ def PixelToXRounded(self, px):
+ return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
+
+ def PixelToYRounded(self, py):
+ return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
+
+ def PixelToX(self, px):
+ x = self.PixelToXRounded(px)
+ if self.pdp.x == 0:
+ rt = self.XToPixel(x)
+ if rt > px:
+ return x - 1
+ return x
+
+ def PixelToY(self, py):
+ y = self.PixelToYRounded(py)
+ if self.pdp.y == 0:
+ rt = self.YToPixel(y)
+ if rt > py:
+ return y - 1
+ return y
+
+ def ToPDP(self, dp, scale):
+ # Calculate pixel decimal places:
+ # (10 ** dp) is the minimum delta in the data
+ # scale it to get the minimum delta in pixels
+ # log10 gives the number of decimals places negatively
+ # subtrace 1 to divide by 10
+ # round to the lower negative number
+ # change the sign to get the number of decimals positively
+ x = math.log10((10 ** dp) * scale)
+ if x < 0:
+ x -= 1
+ x = -int(math.floor(x) - 0.1)
+ else:
+ x = 0
+ return x
+
+ def Update(self):
+ x = self.ToPDP(self.dp.x, self.scale.x)
+ y = self.ToPDP(self.dp.y, self.scale.y)
+ self.pdp = XY(x, y) # pixel decimal places
+
+# Switch graph splitter which divides the CPU graphs from the legend
+
+class SwitchGraphSplitter(QSplitter):
+
+ def __init__(self, parent=None):
+ super(SwitchGraphSplitter, self).__init__(parent)
+
+ self.first_time = False
+
+ def resizeEvent(self, event):
+ if self.first_time:
+ self.first_time = False
+ sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
+ sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
+ sz0 = self.size().width() - self.handleWidth() - sz1
+ self.setSizes([sz0, sz1])
+ elif not(self.widget(1).saved_size is None):
+ sz1 = self.widget(1).saved_size
+ sz0 = self.size().width() - self.handleWidth() - sz1
+ self.setSizes([sz0, sz1])
+ super(SwitchGraphSplitter, self).resizeEvent(event)
+
+# Graph widget base class
+
+class GraphWidget(QWidget):
+
+ graph_title_changed = Signal(object)
+
+ def __init__(self, parent=None):
+ super(GraphWidget, self).__init__(parent)
+
+ def GraphTitleChanged(self, title):
+ self.graph_title_changed.emit(title)
+
+ def Title(self):
+ return ""
+
+# Display time in s, ms, us or ns
+
+def ToTimeStr(val):
+ val = Decimal(val)
+ if val >= 1000000000:
+ return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
+ if val >= 1000000:
+ return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
+ if val >= 1000:
+ return "{} us".format((val / 1000).quantize(Decimal("0.001")))
+ return "{} ns".format(val.quantize(Decimal("1")))
+
+# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
+
+class SwitchGraphWidget(GraphWidget):
+
+ def __init__(self, glb, collection, parent=None):
+ super(SwitchGraphWidget, self).__init__(parent)
+
+ self.glb = glb
+ self.collection = collection
+
+ self.back_state = []
+ self.forward_state = []
+ self.selection_state = (None, None)
+ self.fwd_rect = None
+ self.start_time = self.glb.StartTime(collection.machine_id)
+
+ i = 0
+ hregions = collection.hregions.values()
+ colours = GenerateNRandomColours(len(hregions), 1013)
+ region_attributes = {}
+ for hregion in hregions:
+ if hregion.pid == 0 and hregion.tid == 0:
+ region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
+ else:
+ region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
+ i = i + 1
+
+ # Default to entire range
+ xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
+ ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
+ subrange = XY(xsubrange, ysubrange)
+
+ scale = self.GetScaleForRange(subrange)
+
+ self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
+
+ self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
+
+ self.scene = QGraphicsScene()
+ self.scene.addItem(self.item)
+
+ self.view = QGraphicsView(self.scene)
+ self.view.centerOn(0, 0)
+ self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
+
+ self.legend = SwitchGraphLegend(collection, region_attributes)
+
+ self.splitter = SwitchGraphSplitter()
+ self.splitter.addWidget(self.view)
+ self.splitter.addWidget(self.legend)
+
+ self.point_label = QLabel("")
+ self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
+
+ self.back_button = QToolButton()
+ self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
+ self.back_button.setDisabled(True)
+ self.back_button.released.connect(self.Back)
+
+ self.forward_button = QToolButton()
+ self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
+ self.forward_button.setDisabled(True)
+ self.forward_button.released.connect(self.Forward)
+
+ self.zoom_button = QToolButton()
+ self.zoom_button.setText("Zoom")
+ self.zoom_button.setDisabled(True)
+ self.zoom_button.released.connect(self.Zoom)
+
+ self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
+
+ self.vbox = VBoxLayout(self.splitter, self.hbox)
+
+ self.setLayout(self.vbox)
+
+ def GetScaleForRangeX(self, xsubrange):
+ # Default graph 1000 pixels wide
+ dflt = 1000.0
+ r = xsubrange.hi - xsubrange.lo
+ return dflt / r
+
+ def GetScaleForRangeY(self, ysubrange):
+ # Default graph 50 pixels high
+ dflt = 50.0
+ r = ysubrange.hi - ysubrange.lo
+ return dflt / r
+
+ def GetScaleForRange(self, subrange):
+ # Default graph 1000 pixels wide, 50 pixels high
+ xscale = self.GetScaleForRangeX(subrange.x)
+ yscale = self.GetScaleForRangeY(subrange.y)
+ return XY(xscale, yscale)
+
+ def PointEvent(self, cpu, time_from, _time_to, hregions):
+ text = "CPU: " + str(cpu)
+ time_from = time_from.quantize(Decimal(1))
+ rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
+ text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
+ self.point_label.setText(text)
+ self.legend.Highlight(hregions)
+
+ def RightClickEvent(self, _cpu, hregion_times, pos):
+ if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
+ return
+ menu = QMenu(self.view)
+ for hregion, time in hregion_times:
+ thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
+ menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
+ menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
+ menu.exec_(pos)
+
+ def RightClickSelect(self, args):
+ CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
+
+ def NoPointEvent(self):
+ self.point_label.setText("")
+ self.legend.Highlight({})
+
+ def RangeEvent(self, time_from, time_to):
+ time_from = time_from.quantize(Decimal(1))
+ time_to = time_to.quantize(Decimal(1))
+ if time_to <= time_from:
+ self.point_label.setText("")
+ return
+ rel_time_from = time_from - self.start_time
+ rel_time_to = time_to - self.start_time
+ text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
+ text = text + " duration: " + ToTimeStr(time_to - time_from)
+ self.point_label.setText(text)
+
+ def BackState(self):
+ return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
+
+ def PushBackState(self):
+ state = copy.deepcopy(self.BackState())
+ self.back_state.append(state)
+ self.back_button.setEnabled(True)
+
+ def PopBackState(self):
+ self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
+ self.attrs.Update()
+ if not self.back_state:
+ self.back_button.setDisabled(True)
+
+ def PushForwardState(self):
+ state = copy.deepcopy(self.BackState())
+ self.forward_state.append(state)
+ self.forward_button.setEnabled(True)
+
+ def PopForwardState(self):
+ self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
+ self.attrs.Update()
+ if not self.forward_state:
+ self.forward_button.setDisabled(True)
+
+ def Title(self):
+ time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
+ time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
+ rel_time_from = time_from - self.start_time
+ rel_time_to = time_to - self.start_time
+ title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
+ title = title + " (" + ToTimeStr(time_to - time_from) + ")"
+ return title
+
+ def Update(self):
+ selected_subrange, selection_state = self.selection_state
+ self.item.SetSelection(selection_state)
+ self.item.SetBracket(self.fwd_rect)
+ self.zoom_button.setDisabled(selected_subrange is None)
+ self.GraphTitleChanged(self.Title())
+ self.item.update(self.item.boundingRect())
+
+ def Back(self):
+ if not self.back_state:
+ return
+ self.PushForwardState()
+ self.PopBackState()
+ self.Update()
+
+ def Forward(self):
+ if not self.forward_state:
+ return
+ self.PushBackState()
+ self.PopForwardState()
+ self.Update()
+
+ def SelectEvent(self, x0, x1, selection_state):
+ if selection_state is None:
+ selected_subrange = None
+ else:
+ if x1 - x0 < 1.0:
+ x1 += 1.0
+ selected_subrange = Subrange(x0, x1)
+ self.selection_state = (selected_subrange, selection_state)
+ self.zoom_button.setDisabled(selected_subrange is None)
+
+ def Zoom(self):
+ selected_subrange, selection_state = self.selection_state
+ if selected_subrange is None:
+ return
+ self.fwd_rect = selection_state
+ self.item.SetSelection(None)
+ self.PushBackState()
+ self.attrs.subrange.x = selected_subrange
+ self.forward_state = []
+ self.forward_button.setDisabled(True)
+ self.selection_state = (None, None)
+ self.fwd_rect = None
+ self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
+ self.attrs.Update()
+ self.Update()
+
+# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
+
+class SlowInitClass():
+
+ def __init__(self, glb, title, init_fn):
+ self.init_fn = init_fn
+ self.done = False
+ self.result = None
+
+ self.msg_box = QMessageBox(glb.mainwindow)
+ self.msg_box.setText("Initializing " + title + ". Please wait.")
+ self.msg_box.setWindowTitle("Initializing " + title)
+ self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
+
+ self.init_thread = Thread(self.ThreadFn, glb)
+ self.init_thread.done.connect(self.Done, Qt.QueuedConnection)
+
+ self.init_thread.start()
+
+ def Done(self):
+ self.msg_box.done(0)
+
+ def ThreadFn(self, glb):
+ conn_name = "SlowInitClass" + str(os.getpid())
+ db, _dbname = glb.dbref.Open(conn_name)
+ self.result = self.init_fn(db)
+ self.done = True
+ return (True, 0)
+
+ def Result(self):
+ while not self.done:
+ self.msg_box.exec_()
+ self.init_thread.wait()
+ return self.result
+
+def SlowInit(glb, title, init_fn):
+ init = SlowInitClass(glb, title, init_fn)
+ return init.Result()
+
+# Time chart by CPU window
+
+class TimeChartByCPUWindow(QMdiSubWindow):
+
+ def __init__(self, glb, parent=None):
+ super(TimeChartByCPUWindow, self).__init__(parent)
+
+ self.glb = glb
+ self.machine_id = glb.HostMachineId()
+ self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
+
+ collection = LookupModel(self.collection_name)
+ if collection is None:
+ collection = SlowInit(glb, "Time Chart", self.Init)
+
+ self.widget = SwitchGraphWidget(glb, collection, self)
+ self.view = self.widget
+
+ self.base_title = "Time Chart by CPU"
+ self.setWindowTitle(self.base_title + self.widget.Title())
+ self.widget.graph_title_changed.connect(self.GraphTitleChanged)
+
+ self.setWidget(self.widget)
+
+ AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
+
+ def Init(self, db):
+ return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
+
+ def GraphTitleChanged(self, title):
+ self.setWindowTitle(self.base_title + " : " + title)
+
+# Child data item finder
+
+class ChildDataItemFinder():
+
+ def __init__(self, root):
+ self.root = root
+ self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
+ self.rows = []
+ self.pos = 0
+
+ def FindSelect(self):
+ self.rows = []
+ if self.pattern:
+ pattern = re.compile(self.value)
+ for child in self.root.child_items:
+ for column_data in child.data:
+ if re.search(pattern, str(column_data)) is not None:
+ self.rows.append(child.row)
+ break
+ else:
+ for child in self.root.child_items:
+ for column_data in child.data:
+ if self.value in str(column_data):
+ self.rows.append(child.row)
+ break
+
+ def FindValue(self):
+ self.pos = 0
+ if self.last_value != self.value or self.pattern != self.last_pattern:
+ self.FindSelect()
+ if not len(self.rows):
+ return -1
+ return self.rows[self.pos]
+
+ def FindThread(self):
+ if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
+ row = self.FindValue()
+ elif len(self.rows):
+ if self.direction > 0:
+ self.pos += 1
+ if self.pos >= len(self.rows):
+ self.pos = 0
+ else:
+ self.pos -= 1
+ if self.pos < 0:
+ self.pos = len(self.rows) - 1
+ row = self.rows[self.pos]
+ else:
+ row = -1
+ return (True, row)
+
+ def Find(self, value, direction, pattern, _context, callback):
+ self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
+ # Use a thread so the UI is not blocked
+ thread = Thread(self.FindThread)
+ thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
+ thread.start()
+
+ def FindDone(self, _thread, callback, row):
+ callback(row)
+
+# Number of database records to fetch in one go
+
+glb_chunk_sz = 10000
+
+# Background process for SQL data fetcher
+
+class SQLFetcherProcess():
+
+ def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
+ # Need a unique connection name
+ conn_name = "SQLFetcher" + str(os.getpid())
+ self.db, _dbname = dbref.Open(conn_name)
+ self.sql = sql
+ self.buffer = buffer
+ self.head = head
+ self.tail = tail
+ self.fetch_count = fetch_count
+ self.fetching_done = fetching_done
+ self.process_target = process_target
+ self.wait_event = wait_event
+ self.fetched_event = fetched_event
+ self.prep = prep
+ self.query = QSqlQuery(self.db)
+ self.query_limit = 0 if "$$last_id$$" in sql else 2
+ self.last_id = -1
+ self.fetched = 0
+ self.more = True
+ self.local_head = self.head.value
+ self.local_tail = self.tail.value
+
+ def Select(self):
+ if self.query_limit:
+ if self.query_limit == 1:
+ return
+ self.query_limit -= 1
+ stmt = self.sql.replace("$$last_id$$", str(self.last_id))
+ QueryExec(self.query, stmt)
+
+ def Next(self):
+ if not self.query.next():
+ self.Select()
+ if not self.query.next():
+ return None
+ self.last_id = self.query.value(0)
+ return self.prep(self.query)
+
+ def WaitForTarget(self):
+ while True:
+ self.wait_event.clear()
+ target = self.process_target.value
+ if target > self.fetched or target < 0:
+ break
+ self.wait_event.wait()
+ return target
+
+ def HasSpace(self, sz):
+ if self.local_tail <= self.local_head:
+ space = len(self.buffer) - self.local_head
+ if space > sz:
+ return True
+ if space >= glb_nsz:
+ # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
+ nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
+ self.buffer[self.local_head : self.local_head + len(nd)] = nd
+ self.local_head = 0
+ if self.local_tail - self.local_head > sz:
+ return True
+ return False
+
+ def WaitForSpace(self, sz):
+ if self.HasSpace(sz):
+ return
+ while True:
+ self.wait_event.clear()
+ self.local_tail = self.tail.value
+ if self.HasSpace(sz):
+ return
+ self.wait_event.wait()
+
+ def AddToBuffer(self, obj):
+ d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
+ n = len(d)
+ nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
+ sz = n + glb_nsz
+ self.WaitForSpace(sz)
+ pos = self.local_head
+ self.buffer[pos : pos + len(nd)] = nd
+ self.buffer[pos + glb_nsz : pos + sz] = d
+ self.local_head += sz
+
+ def FetchBatch(self, batch_size):
+ fetched = 0
+ while batch_size > fetched:
+ obj = self.Next()
+ if obj is None:
+ self.more = False
+ break
+ self.AddToBuffer(obj)
+ fetched += 1
+ if fetched:
+ self.fetched += fetched
+ with self.fetch_count.get_lock():
+ self.fetch_count.value += fetched
+ self.head.value = self.local_head
+ self.fetched_event.set()
+
+ def Run(self):
+ while self.more:
+ target = self.WaitForTarget()
+ if target < 0:
+ break
+ batch_size = min(glb_chunk_sz, target - self.fetched)
+ self.FetchBatch(batch_size)
+ self.fetching_done.value = True
+ self.fetched_event.set()
+
+def SQLFetcherFn(*x):
+ process = SQLFetcherProcess(*x)
+ process.Run()
+
+# SQL data fetcher
+
+class SQLFetcher(QObject):
+
+ done = Signal(object)
+
+ def __init__(self, glb, sql, prep, process_data, parent=None):
+ super(SQLFetcher, self).__init__(parent)
+ self.process_data = process_data
+ self.more = True
+ self.target = 0
+ self.last_target = 0
+ self.fetched = 0
+ self.buffer_size = 16 * 1024 * 1024
+ self.buffer = Array(c_char, self.buffer_size, lock=False)
+ self.head = Value(c_longlong)
+ self.tail = Value(c_longlong)
+ self.local_tail = 0
+ self.fetch_count = Value(c_longlong)
+ self.fetching_done = Value(c_bool)
+ self.last_count = 0
+ self.process_target = Value(c_longlong)
+ self.wait_event = Event()
+ self.fetched_event = Event()
+ glb.AddInstanceToShutdownOnExit(self)
+ self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep))
+ self.process.start()
+ self.thread = Thread(self.Thread)
+ self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
+ self.thread.start()
+
+ def Shutdown(self):
+ # Tell the thread and process to exit
+ self.process_target.value = -1
+ self.wait_event.set()
+ self.more = False
+ self.fetching_done.value = True
+ self.fetched_event.set()
+
+ def Thread(self):
+ if not self.more:
+ return True, 0
+ while True:
+ self.fetched_event.clear()
+ fetch_count = self.fetch_count.value
+ if fetch_count != self.last_count:
+ break
+ if self.fetching_done.value:
+ self.more = False
+ return True, 0
+ self.fetched_event.wait()
+ count = fetch_count - self.last_count
+ self.last_count = fetch_count
+ self.fetched += count
+ return False, count
+
+ def Fetch(self, nr):
+ if not self.more:
+ # -1 inidcates there are no more
+ return -1
+ result = self.fetched
+ extra = result + nr - self.target
+ if extra > 0:
+ self.target += extra
+ # process_target < 0 indicates shutting down
+ if self.process_target.value >= 0:
+ self.process_target.value = self.target
+ self.wait_event.set()
+ return result
+
+ def RemoveFromBuffer(self):
+ pos = self.local_tail
+ if len(self.buffer) - pos < glb_nsz:
+ pos = 0
+ n = pickle.loads(self.buffer[pos : pos + glb_nsz])
+ if n == 0:
+ pos = 0
+ n = pickle.loads(self.buffer[0 : glb_nsz])
+ pos += glb_nsz
+ obj = pickle.loads(self.buffer[pos : pos + n])
+ self.local_tail = pos + n
+ return obj
+
+ def ProcessData(self, count):
+ for _i in range(count):
+ obj = self.RemoveFromBuffer()
+ self.process_data(obj)
+ self.tail.value = self.local_tail
+ self.wait_event.set()
+ self.done.emit(count)
+
+# Fetch more records bar
+
+class FetchMoreRecordsBar():
+
+ def __init__(self, model, parent):
+ self.model = model
+
+ self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
+ self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+ self.fetch_count = QSpinBox()
+ self.fetch_count.setRange(1, 1000000)
+ self.fetch_count.setValue(10)
+ self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+ self.fetch = QPushButton("Go!")
+ self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+ self.fetch.released.connect(self.FetchMoreRecords)
+
+ self.progress = QProgressBar()
+ self.progress.setRange(0, 100)
+ self.progress.hide()
+
+ self.done_label = QLabel("All records fetched")
+ self.done_label.hide()
+
+ self.spacer = QLabel("")
+
+ self.close_button = QToolButton()
+ self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
+ self.close_button.released.connect(self.Deactivate)
+
+ self.hbox = QHBoxLayout()
+ self.hbox.setContentsMargins(0, 0, 0, 0)
+
+ self.hbox.addWidget(self.label)
+ self.hbox.addWidget(self.fetch_count)
+ self.hbox.addWidget(self.fetch)
+ self.hbox.addWidget(self.spacer)
+ self.hbox.addWidget(self.progress)
+ self.hbox.addWidget(self.done_label)
+ self.hbox.addWidget(self.close_button)
+
+ self.bar = QWidget()
+ self.bar.setLayout(self.hbox)
+ self.bar.show()
+
+ self.in_progress = False
+ self.start = 0
+ self.model.progress.connect(self.Progress)
+
+ self.done = False
+
+ if not model.HasMoreRecords():
+ self.Done()
+
+ def Widget(self):
+ return self.bar
+
+ def Activate(self):
+ self.bar.show()
+ self.fetch.setFocus()
+
+ def Deactivate(self):
+ self.bar.hide()
+
+ def Enable(self, enable):
+ self.fetch.setEnabled(enable)
+ self.fetch_count.setEnabled(enable)
+
+ def Busy(self):
+ self.Enable(False)
+ self.fetch.hide()
+ self.spacer.hide()
+ self.progress.show()
+
+ def Idle(self):
+ self.in_progress = False
+ self.Enable(True)
+ self.progress.hide()
+ self.fetch.show()
+ self.spacer.show()
+
+ def Target(self):
+ return self.fetch_count.value() * glb_chunk_sz
+
+ def Done(self):
+ self.done = True
+ self.Idle()
+ self.label.hide()
+ self.fetch_count.hide()
+ self.fetch.hide()
+ self.spacer.hide()
+ self.done_label.show()
+
+ def Progress(self, count):
+ if self.in_progress:
+ if count:
+ percent = ((count - self.start) * 100) / self.Target()
+ if percent >= 100:
+ self.Idle()
+ else:
+ self.progress.setValue(percent)
+ if not count:
+ # Count value of zero means no more records
+ self.Done()
+
+ def FetchMoreRecords(self):
+ if self.done:
+ return
+ self.progress.setValue(0)
+ self.Busy()
+ self.in_progress = True
+ self.start = self.model.FetchMoreRecords(self.Target())
+
+# Brance data model level two item
+
+class BranchLevelTwoItem():
+
+ def __init__(self, row, col, text, parent_item):
+ self.row = row
+ self.parent_item = parent_item
+ self.data = [""] * (col + 1)
+ self.data[col] = text
+ self.level = 2
+
+ def getParentItem(self):
+ return self.parent_item
+
+ def getRow(self):
+ return self.row
+
+ def childCount(self):
+ return 0
+
+ def hasChildren(self):
+ return False
+
+ def getData(self, column):
+ return self.data[column]
+
+# Brance data model level one item
+
+class BranchLevelOneItem():
+
+ def __init__(self, glb, row, data, parent_item):
+ self.glb = glb
+ self.row = row
+ self.parent_item = parent_item
+ self.child_count = 0
+ self.child_items = []
+ self.data = data[1:]
+ self.dbid = data[0]
+ self.level = 1
+ self.query_done = False
+ self.br_col = len(self.data) - 1
+
+ def getChildItem(self, row):
+ return self.child_items[row]
+
+ def getParentItem(self):
+ return self.parent_item
+
+ def getRow(self):
+ return self.row
+
+ def Select(self):
+ self.query_done = True
+
+ if not self.glb.have_disassembler:
+ return
+
+ query = QSqlQuery(self.glb.db)
+
+ QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
+ " FROM samples"
+ " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
+ " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
+ " WHERE samples.id = " + str(self.dbid))
+ if not query.next():
+ return
+ cpu = query.value(0)
+ dso = query.value(1)
+ sym = query.value(2)
+ if dso == 0 or sym == 0:
+ return
+ off = query.value(3)
+ short_name = query.value(4)
+ long_name = query.value(5)
+ build_id = query.value(6)
+ sym_start = query.value(7)
+ ip = query.value(8)
+
+ QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
+ " FROM samples"
+ " INNER JOIN symbols ON samples.symbol_id = symbols.id"
+ " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
+ " ORDER BY samples.id"
+ " LIMIT 1")
+ if not query.next():
+ return
+ if query.value(0) != dso:
+ # Cannot disassemble from one dso to another
+ return
+ bsym = query.value(1)
+ boff = query.value(2)
+ bsym_start = query.value(3)
+ if bsym == 0:
+ return
+ tot = bsym_start + boff + 1 - sym_start - off
+ if tot <= 0 or tot > 16384:
+ return
+
+ inst = self.glb.disassembler.Instruction()
+ f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
+ if not f:
+ return
+ mode = 0 if Is64Bit(f) else 1
+ self.glb.disassembler.SetMode(inst, mode)
+
+ buf_sz = tot + 16
+ buf = create_string_buffer(tot + 16)
+ f.seek(sym_start + off)
+ buf.value = f.read(buf_sz)
+ buf_ptr = addressof(buf)
+ i = 0
+ while tot > 0:
+ cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
+ if cnt:
+ byte_str = tohex(ip).rjust(16)
+ for k in range(cnt):
+ byte_str += " %02x" % ord(buf[i])
+ i += 1
+ while k < 15:
+ byte_str += " "
+ k += 1
+ self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
+ self.child_count += 1
+ else:
+ return
+ buf_ptr += cnt
+ tot -= cnt
+ buf_sz -= cnt
+ ip += cnt
+
+ def childCount(self):
+ if not self.query_done:
+ self.Select()
+ if not self.child_count:
+ return -1
+ return self.child_count
+
+ def hasChildren(self):
+ if not self.query_done:
+ return True
+ return self.child_count > 0
+
+ def getData(self, column):
+ return self.data[column]
+
+# Brance data model root item
+
+class BranchRootItem():
+
+ def __init__(self):
+ self.child_count = 0
+ self.child_items = []
+ self.level = 0
+
+ def getChildItem(self, row):
+ return self.child_items[row]
+
+ def getParentItem(self):
+ return None
+
+ def getRow(self):
+ return 0
+
+ def childCount(self):
+ return self.child_count
+
+ def hasChildren(self):
+ return self.child_count > 0
+
+ def getData(self, _column):
+ return ""
+
+# Calculate instructions per cycle
+
+def CalcIPC(cyc_cnt, insn_cnt):
+ if cyc_cnt and insn_cnt:
+ ipc = Decimal(float(insn_cnt) / cyc_cnt)
+ ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
+ else:
+ ipc = "0"
+ return ipc
+
+# Branch data preparation
+
+def BranchDataPrepBr(query, data):
+ data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
+ " (" + dsoname(query.value(11)) + ")" + " -> " +
+ tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
+ " (" + dsoname(query.value(15)) + ")")
+
+def BranchDataPrepIPC(query, data):
+ insn_cnt = query.value(16)
+ cyc_cnt = query.value(17)
+ ipc = CalcIPC(cyc_cnt, insn_cnt)
+ data.append(insn_cnt)
+ data.append(cyc_cnt)
+ data.append(ipc)
+
+def BranchDataPrep(query):
+ data = []
+ for i in range(0, 8):
+ data.append(query.value(i))
+ BranchDataPrepBr(query, data)
+ return data
+
+def BranchDataPrepWA(query):
+ data = []
+ data.append(query.value(0))
+ # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
+ data.append("{:>19}".format(query.value(1)))
+ for i in range(2, 8):
+ data.append(query.value(i))
+ BranchDataPrepBr(query, data)
+ return data
+
+def BranchDataWithIPCPrep(query):
+ data = []
+ for i in range(0, 8):
+ data.append(query.value(i))
+ BranchDataPrepIPC(query, data)
+ BranchDataPrepBr(query, data)
+ return data
+
+def BranchDataWithIPCPrepWA(query):
+ data = []
+ data.append(query.value(0))
+ # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
+ data.append("{:>19}".format(query.value(1)))
+ for i in range(2, 8):
+ data.append(query.value(i))
+ BranchDataPrepIPC(query, data)
+ BranchDataPrepBr(query, data)
+ return data
+
+# Branch data model
+
+class BranchModel(TreeModel):
+
+ progress = Signal(object)
+
+ def __init__(self, glb, event_id, where_clause, parent=None):
+ super(BranchModel, self).__init__(glb, None, parent)
+ self.event_id = event_id
+ self.more = True
+ self.populated = 0
+ self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
+ if self.have_ipc:
+ select_ipc = ", insn_count, cyc_count"
+ prep_fn = BranchDataWithIPCPrep
+ prep_wa_fn = BranchDataWithIPCPrepWA
+ else:
+ select_ipc = ""
+ prep_fn = BranchDataPrep
+ prep_wa_fn = BranchDataPrepWA
+ sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
+ " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
+ " ip, symbols.name, sym_offset, dsos.short_name,"
+ " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
+ + select_ipc +
+ " FROM samples"
+ " INNER JOIN comms ON comm_id = comms.id"
+ " INNER JOIN threads ON thread_id = threads.id"
+ " INNER JOIN branch_types ON branch_type = branch_types.id"
+ " INNER JOIN symbols ON symbol_id = symbols.id"
+ " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
+ " INNER JOIN dsos ON samples.dso_id = dsos.id"
+ " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
+ " WHERE samples.id > $$last_id$$" + where_clause +
+ " AND evsel_id = " + str(self.event_id) +
+ " ORDER BY samples.id"
+ " LIMIT " + str(glb_chunk_sz))
+ if pyside_version_1 and sys.version_info[0] == 3:
+ prep = prep_fn
+ else:
+ prep = prep_wa_fn
+ self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
+ self.fetcher.done.connect(self.Update)
+ self.fetcher.Fetch(glb_chunk_sz)
+
+ def GetRoot(self):
+ return BranchRootItem()
+
+ def columnCount(self, _parent=None):
+ if self.have_ipc:
+ return 11
+ else:
+ return 8
+
+ def columnHeader(self, column):
+ if self.have_ipc:
+ return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
+ else:
+ return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
+
+ def columnFont(self, column):
+ if self.have_ipc:
+ br_col = 10
+ else:
+ br_col = 7
+ if column != br_col:
+ return None
+ return QFont("Monospace")
+
+ def DisplayData(self, item, index):
+ if item.level == 1:
+ self.FetchIfNeeded(item.row)
+ return item.getData(index.column())
+
+ def AddSample(self, data):
+ child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
+ self.root.child_items.append(child)
+ self.populated += 1
+
+ def Update(self, fetched):
+ if not fetched:
+ self.more = False
+ self.progress.emit(0)
+ child_count = self.root.child_count
+ count = self.populated - child_count
+ if count > 0:
+ parent = QModelIndex()
+ self.beginInsertRows(parent, child_count, child_count + count - 1)
+ self.insertRows(child_count, count, parent)
+ self.root.child_count += count
+ self.endInsertRows()
+ self.progress.emit(self.root.child_count)
+
+ def FetchMoreRecords(self, count):
+ current = self.root.child_count
+ if self.more:
+ self.fetcher.Fetch(count)
+ else:
+ self.progress.emit(0)
+ return current
+
+ def HasMoreRecords(self):
+ return self.more
+
+# Report Variables
+
+class ReportVars():
+
+ def __init__(self, name = "", where_clause = "", limit = ""):
+ self.name = name
+ self.where_clause = where_clause
+ self.limit = limit
+
+ def UniqueId(self):
+ return str(self.where_clause + ";" + self.limit)
+
+# Branch window
+
+class BranchWindow(QMdiSubWindow):
+
+ def __init__(self, glb, event_id, report_vars, parent=None):
+ super(BranchWindow, self).__init__(parent)
+
+ model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
+
+ self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
+
+ self.view = QTreeView()
+ self.view.setUniformRowHeights(True)
+ self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
+ self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
+ self.view.setModel(self.model)
+
+ self.ResizeColumnsToContents()
+
+ self.context_menu = TreeContextMenu(self.view)
+
+ self.find_bar = FindBar(self, self, True)
+
+ self.finder = ChildDataItemFinder(self.model.root)
+
+ self.fetch_bar = FetchMoreRecordsBar(self.model, self)
+
+ self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
+
+ self.setWidget(self.vbox.Widget())
+
+ AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
+
+ def ResizeColumnToContents(self, column, n):
+ # Using the view's resizeColumnToContents() here is extrememly slow
+ # so implement a crude alternative
+ mm = "MM" if column else "MMMM"
+ font = self.view.font()
+ metrics = QFontMetrics(font)
+ max_w = 0
+ for row in range(n):
+ val = self.model.root.child_items[row].data[column]
+ width = metrics.width(str(val) + mm)
+ max_w = width if width > max_w else max_w
+ val = self.model.columnHeader(column)
+ width = metrics.width(str(val) + mm)
+ max_w = width if width > max_w else max_w
+ self.view.setColumnWidth(column, max_w)
+
+ def ResizeColumnsToContents(self):
+ n = min(self.model.root.child_count, 100)
+ if n < 1:
+ # No data yet, so connect a signal to notify when there is
+ self.model.rowsInserted.connect(self.UpdateColumnWidths)
+ return
+ columns = self.model.columnCount()
+ for i in range(columns):
+ self.ResizeColumnToContents(i, n)
+
+ def UpdateColumnWidths(self, *_x):
+ # This only needs to be done once, so disconnect the signal now
+ self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
+ self.ResizeColumnsToContents()
+
+ def Find(self, value, direction, pattern, context):
+ self.view.setFocus()
+ self.find_bar.Busy()
+ self.finder.Find(value, direction, pattern, context, self.FindDone)
+
+ def FindDone(self, row):
+ self.find_bar.Idle()
+ if row >= 0:
+ self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
+ else:
+ self.find_bar.NotFound()
+
+# Line edit data item
+
+class LineEditDataItem(object):
+
+ def __init__(self, glb, label, placeholder_text, parent, item_id = "", default = ""):
+ self.glb = glb
+ self.label = label
+ self.placeholder_text = placeholder_text
+ self.parent = parent
+ self.id = item_id
+
+ self.value = default
+
+ self.widget = QLineEdit(default)
+ self.widget.editingFinished.connect(self.Validate)
+ self.widget.textChanged.connect(self.Invalidate)
+ self.red = False
+ self.error = ""
+ self.validated = True
+
+ if placeholder_text:
+ self.widget.setPlaceholderText(placeholder_text)
+
+ def TurnTextRed(self):
+ if not self.red:
+ palette = QPalette()
+ palette.setColor(QPalette.Text,Qt.red)
+ self.widget.setPalette(palette)
+ self.red = True
+
+ def TurnTextNormal(self):
+ if self.red:
+ palette = QPalette()
+ self.widget.setPalette(palette)
+ self.red = False
+
+ def InvalidValue(self, value):
+ self.value = ""
+ self.TurnTextRed()
+ self.error = self.label + " invalid value '" + value + "'"
+ self.parent.ShowMessage(self.error)
+
+ def Invalidate(self):
+ self.validated = False
+
+ def DoValidate(self, input_string):
+ self.value = input_string.strip()
+
+ def Validate(self):
+ self.validated = True
+ self.error = ""
+ self.TurnTextNormal()
+ self.parent.ClearMessage()
+ input_string = self.widget.text()
+ if not len(input_string.strip()):
+ self.value = ""
+ return
+ self.DoValidate(input_string)
+
+ def IsValid(self):
+ if not self.validated:
+ self.Validate()
+ if len(self.error):
+ self.parent.ShowMessage(self.error)
+ return False
+ return True
+
+ def IsNumber(self, value):
+ try:
+ x = int(value)
+ except ValueError:
+ x = 0
+ return str(x) == value
+
+# Non-negative integer ranges dialog data item
+
+class NonNegativeIntegerRangesDataItem(LineEditDataItem):
+
+ def __init__(self, glb, label, placeholder_text, column_name, parent):
+ super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
+
+ self.column_name = column_name
+
+ def DoValidate(self, input_string):
+ singles = []
+ ranges = []
+ for value in [x.strip() for x in input_string.split(",")]:
+ if "-" in value:
+ vrange = value.split("-")
+ if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
+ return self.InvalidValue(value)
+ ranges.append(vrange)
+ else:
+ if not self.IsNumber(value):
+ return self.InvalidValue(value)
+ singles.append(value)
+ ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
+ if len(singles):
+ ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
+ self.value = " OR ".join(ranges)
+
+# Positive integer dialog data item
+
+class PositiveIntegerDataItem(LineEditDataItem):
+
+ def DoValidate(self, input_string):
+ if not self.IsNumber(input_string.strip()):
+ return self.InvalidValue(input_string)
+ value = int(input_string.strip())
+ if value <= 0:
+ return self.InvalidValue(input_string)
+ self.value = str(value)
+
+# Dialog data item converted and validated using a SQL table
+
+class SQLTableDataItem(LineEditDataItem):
+
+ def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
+ super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
+
+ self.table_name = table_name
+ self.match_column = match_column
+ self.column_name1 = column_name1
+ self.column_name2 = column_name2
+
+ def ValueToIds(self, value):
+ ids = []
+ query = QSqlQuery(self.glb.db)
+ stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
+ ret = query.exec_(stmt)
+ if ret:
+ while query.next():
+ ids.append(str(query.value(0)))
+ return ids
+
+ def DoValidate(self, input_string):
+ all_ids = []
+ for value in [x.strip() for x in input_string.split(",")]:
+ ids = self.ValueToIds(value)
+ if len(ids):
+ all_ids.extend(ids)
+ else:
+ return self.InvalidValue(value)
+ self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
+ if self.column_name2:
+ self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
+
+# Sample time ranges dialog data item converted and validated using 'samples' SQL table
+
+class SampleTimeRangesDataItem(LineEditDataItem):
+
+ def __init__(self, glb, label, placeholder_text, column_name, parent):
+ self.column_name = column_name
+
+ self.last_id = 0
+ self.first_time = 0
+ self.last_time = 2 ** 64
+
+ query = QSqlQuery(glb.db)
+ QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
+ if query.next():
+ self.last_id = int(query.value(0))
+ self.first_time = int(glb.HostStartTime())
+ self.last_time = int(glb.HostFinishTime())
+ if placeholder_text:
+ placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
+
+ super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
+
+ def IdBetween(self, query, lower_id, higher_id, order):
+ QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
+ if query.next():
+ return True, int(query.value(0))
+ else:
+ return False, 0
+
+ def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
+ query = QSqlQuery(self.glb.db)
+ while True:
+ next_id = int((lower_id + higher_id) / 2)
+ QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
+ if not query.next():
+ ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
+ if not ok:
+ ok, dbid = self.IdBetween(query, next_id, higher_id, "")
+ if not ok:
+ return str(higher_id)
+ next_id = dbid
+ QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
+ next_time = int(query.value(0))
+ if get_floor:
+ if target_time > next_time:
+ lower_id = next_id
+ else:
+ higher_id = next_id
+ if higher_id <= lower_id + 1:
+ return str(higher_id)
+ else:
+ if target_time >= next_time:
+ lower_id = next_id
+ else:
+ higher_id = next_id
+ if higher_id <= lower_id + 1:
+ return str(lower_id)
+
+ def ConvertRelativeTime(self, val):
+ mult = 1
+ suffix = val[-2:]
+ if suffix == "ms":
+ mult = 1000000
+ elif suffix == "us":
+ mult = 1000
+ elif suffix == "ns":
+ mult = 1
+ else:
+ return val
+ val = val[:-2].strip()
+ if not self.IsNumber(val):
+ return val
+ val = int(val) * mult
+ if val >= 0:
+ val += self.first_time
+ else:
+ val += self.last_time
+ return str(val)
+
+ def ConvertTimeRange(self, vrange):
+ if vrange[0] == "":
+ vrange[0] = str(self.first_time)
+ if vrange[1] == "":
+ vrange[1] = str(self.last_time)
+ vrange[0] = self.ConvertRelativeTime(vrange[0])
+ vrange[1] = self.ConvertRelativeTime(vrange[1])
+ if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
+ return False
+ beg_range = max(int(vrange[0]), self.first_time)
+ end_range = min(int(vrange[1]), self.last_time)
+ if beg_range > self.last_time or end_range < self.first_time:
+ return False
+ vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
+ vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
+ return True
+
+ def AddTimeRange(self, value, ranges):
+ n = value.count("-")
+ if n == 1:
+ pass
+ elif n == 2:
+ if value.split("-")[1].strip() == "":
+ n = 1
+ elif n == 3:
+ n = 2
+ else:
+ return False
+ pos = findnth(value, "-", n)
+ vrange = [value[:pos].strip() ,value[pos+1:].strip()]
+ if self.ConvertTimeRange(vrange):
+ ranges.append(vrange)
+ return True
+ return False
+
+ def DoValidate(self, input_string):
+ ranges = []
+ for value in [x.strip() for x in input_string.split(",")]:
+ if not self.AddTimeRange(value, ranges):
+ return self.InvalidValue(value)
+ ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
+ self.value = " OR ".join(ranges)
+
+# Report Dialog Base
+
+class ReportDialogBase(QDialog):
+
+ def __init__(self, glb, title, items, partial, parent=None):
+ super(ReportDialogBase, self).__init__(parent)
+
+ self.glb = glb
+
+ self.report_vars = ReportVars()
+
+ self.setWindowTitle(title)
+ self.setMinimumWidth(600)
+
+ self.data_items = [x(glb, self) for x in items]
+
+ self.partial = partial
+
+ self.grid = QGridLayout()
+
+ for row in range(len(self.data_items)):
+ self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
+ self.grid.addWidget(self.data_items[row].widget, row, 1)
+
+ self.status = QLabel()
+
+ self.ok_button = QPushButton("Ok", self)
+ self.ok_button.setDefault(True)
+ self.ok_button.released.connect(self.Ok)
+ self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+ self.cancel_button = QPushButton("Cancel", self)
+ self.cancel_button.released.connect(self.reject)
+ self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
+
+ self.hbox = QHBoxLayout()
+ #self.hbox.addStretch()
+ self.hbox.addWidget(self.status)
+ self.hbox.addWidget(self.ok_button)
+ self.hbox.addWidget(self.cancel_button)
+
+ self.vbox = QVBoxLayout()
+ self.vbox.addLayout(self.grid)
+ self.vbox.addLayout(self.hbox)
+
+ self.setLayout(self.vbox)
+
+ def Ok(self):
+ rep_vars = self.report_vars
+ for d in self.data_items:
+ if d.id == "REPORTNAME":
+ rep_vars.name = d.value
+ if not rep_vars.name:
+ self.ShowMessage("Report name is required")
+ return
+ for d in self.data_items:
+ if not d.IsValid():
+ return
+ for d in self.data_items[1:]:
+ if d.id == "LIMIT":
+ rep_vars.limit = d.value
+ elif len(d.value):
+ if len(rep_vars.where_clause):
+ rep_vars.where_clause += " AND "
+ rep_vars.where_clause += d.value
+ if len(rep_vars.where_clause):
+ if self.partial:
+ rep_vars.where_clause = " AND ( " + rep_vars.where_clause + " ) "
+ else:
+ rep_vars.where_clause = " WHERE " + rep_vars.where_clause + " "
+ self.accept()
+
+ def ShowMessage(self, msg):
+ self.status.setText("<font color=#FF0000>" + msg)
+
+ def ClearMessage(self):
+ self.status.setText("")
+
+# Selected branch report creation dialog
+
+class SelectedBranchDialog(ReportDialogBase):
+
+ def __init__(self, glb, parent=None):
+ title = "Selected Branches"
+ items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
+ lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
+ lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
+ lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
+ lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
+ lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
+ lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
+ lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
+ lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
+ super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
+
+# Event list
+
+def GetEventList(db):
+ events = []
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
+ while query.next():
+ events.append(query.value(0))
+ return events
+
+# Is a table selectable
+
+def IsSelectable(db, table, sql = "", columns = "*"):
+ query = QSqlQuery(db)
+ try:
+ QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
+ except RuntimeError:
+ return False
+ return True
+
+# SQL table data model item
+
+class SQLTableItem():
+
+ def __init__(self, row, data):
+ self.row = row
+ self.data = data
+
+ def getData(self, column):
+ return self.data[column]
+
+# SQL table data model
+
+class SQLTableModel(TableModel):
+
+ progress = Signal(object)
+
+ def __init__(self, glb, sql, column_headers, parent=None):
+ super(SQLTableModel, self).__init__(parent)
+ self.glb = glb
+ self.more = True
+ self.populated = 0
+ self.column_headers = column_headers
+ self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
+ self.fetcher.done.connect(self.Update)
+ self.fetcher.Fetch(glb_chunk_sz)
+
+ def DisplayData(self, item, index):
+ self.FetchIfNeeded(item.row)
+ return item.getData(index.column())
+
+ def AddSample(self, data):
+ child = SQLTableItem(self.populated, data)
+ self.child_items.append(child)
+ self.populated += 1
+
+ def Update(self, fetched):
+ if not fetched:
+ self.more = False
+ self.progress.emit(0)
+ child_count = self.child_count
+ count = self.populated - child_count
+ if count > 0:
+ parent = QModelIndex()
+ self.beginInsertRows(parent, child_count, child_count + count - 1)
+ self.insertRows(child_count, count, parent)
+ self.child_count += count
+ self.endInsertRows()
+ self.progress.emit(self.child_count)
+
+ def FetchMoreRecords(self, count):
+ current = self.child_count
+ if self.more:
+ self.fetcher.Fetch(count)
+ else:
+ self.progress.emit(0)
+ return current
+
+ def HasMoreRecords(self):
+ return self.more
+
+ def columnCount(self, _parent=None):
+ return len(self.column_headers)
+
+ def columnHeader(self, column):
+ return self.column_headers[column]
+
+ def SQLTableDataPrep(self, query, count):
+ data = []
+ for i in range(count):
+ data.append(query.value(i))
+ return data
+
+# SQL automatic table data model
+
+class SQLAutoTableModel(SQLTableModel):
+
+ def __init__(self, glb, table_name, parent=None):
+ sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
+ if table_name == "comm_threads_view":
+ # For now, comm_threads_view has no id column
+ sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
+ column_headers = []
+ query = QSqlQuery(glb.db)
+ if glb.dbref.is_sqlite3:
+ QueryExec(query, "PRAGMA table_info(" + table_name + ")")
+ while query.next():
+ column_headers.append(query.value(1))
+ if table_name == "sqlite_master":
+ sql = "SELECT * FROM " + table_name
+ else:
+ if table_name[:19] == "information_schema.":
+ sql = "SELECT * FROM " + table_name
+ select_table_name = table_name[19:]
+ schema = "information_schema"
+ else:
+ select_table_name = table_name
+ schema = "public"
+ QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
+ while query.next():
+ column_headers.append(query.value(0))
+ if pyside_version_1 and sys.version_info[0] == 3:
+ if table_name == "samples_view":
+ self.SQLTableDataPrep = self.samples_view_DataPrep
+ if table_name == "samples":
+ self.SQLTableDataPrep = self.samples_DataPrep
+ super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
+
+ def samples_view_DataPrep(self, query, count):
+ data = []
+ data.append(query.value(0))
+ # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
+ data.append("{:>19}".format(query.value(1)))
+ for i in range(2, count):
+ data.append(query.value(i))
+ return data
+
+ def samples_DataPrep(self, query, count):
+ data = []
+ for i in range(9):
+ data.append(query.value(i))
+ # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
+ data.append("{:>19}".format(query.value(9)))
+ for i in range(10, count):
+ data.append(query.value(i))
+ return data
+
+# Base class for custom ResizeColumnsToContents
+
+class ResizeColumnsToContentsBase(QObject):
+
+ def __init__(self, parent=None):
+ super(ResizeColumnsToContentsBase, self).__init__(parent)
+
+ def ResizeColumnToContents(self, column, n):
+ # Using the view's resizeColumnToContents() here is extrememly slow
+ # so implement a crude alternative
+ font = self.view.font()
+ metrics = QFontMetrics(font)
+ max_w = 0
+ for row in range(n):
+ val = self.data_model.child_items[row].data[column]
+ width = metrics.width(str(val) + "MM")
+ max_w = width if width > max_w else max_w
+ val = self.data_model.columnHeader(column)
+ width = metrics.width(str(val) + "MM")
+ max_w = width if width > max_w else max_w
+ self.view.setColumnWidth(column, max_w)
+
+ def ResizeColumnsToContents(self):
+ n = min(self.data_model.child_count, 100)
+ if n < 1:
+ # No data yet, so connect a signal to notify when there is
+ self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
+ return
+ columns = self.data_model.columnCount()
+ for i in range(columns):
+ self.ResizeColumnToContents(i, n)
+
+ def UpdateColumnWidths(self, *_x):
+ # This only needs to be done once, so disconnect the signal now
+ self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
+ self.ResizeColumnsToContents()
+
+# Convert value to CSV
+
+def ToCSValue(val):
+ if '"' in val:
+ val = val.replace('"', '""')
+ if "," in val or '"' in val:
+ val = '"' + val + '"'
+ return val
+
+# Key to sort table model indexes by row / column, assuming fewer than 1000 columns
+
+glb_max_cols = 1000
+
+def RowColumnKey(a):
+ return a.row() * glb_max_cols + a.column()
+
+# Copy selected table cells to clipboard
+
+def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
+ indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
+ idx_cnt = len(indexes)
+ if not idx_cnt:
+ return
+ if idx_cnt == 1:
+ with_hdr=False
+ min_row = indexes[0].row()
+ max_row = indexes[0].row()
+ min_col = indexes[0].column()
+ max_col = indexes[0].column()
+ for i in indexes:
+ min_row = min(min_row, i.row())
+ max_row = max(max_row, i.row())
+ min_col = min(min_col, i.column())
+ max_col = max(max_col, i.column())
+ if max_col > glb_max_cols:
+ raise RuntimeError("glb_max_cols is too low")
+ max_width = [0] * (1 + max_col - min_col)
+ for i in indexes:
+ c = i.column() - min_col
+ max_width[c] = max(max_width[c], len(str(i.data())))
+ text = ""
+ pad = ""
+ sep = ""
+ if with_hdr:
+ model = indexes[0].model()
+ for col in range(min_col, max_col + 1):
+ val = model.headerData(col, Qt.Horizontal, Qt.DisplayRole)
+ if as_csv:
+ text += sep + ToCSValue(val)
+ sep = ","
+ else:
+ c = col - min_col
+ max_width[c] = max(max_width[c], len(val))
+ width = max_width[c]
+ align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
+ if align & Qt.AlignRight:
+ val = val.rjust(width)
+ text += pad + sep + val
+ pad = " " * (width - len(val))
+ sep = " "
+ text += "\n"
+ pad = ""
+ sep = ""
+ last_row = min_row
+ for i in indexes:
+ if i.row() > last_row:
+ last_row = i.row()
+ text += "\n"
+ pad = ""
+ sep = ""
+ if as_csv:
+ text += sep + ToCSValue(str(i.data()))
+ sep = ","
+ else:
+ width = max_width[i.column() - min_col]
+ if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
+ val = str(i.data()).rjust(width)
+ else:
+ val = str(i.data())
+ text += pad + sep + val
+ pad = " " * (width - len(val))
+ sep = " "
+ QApplication.clipboard().setText(text)
+
+def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
+ indexes = view.selectedIndexes()
+ if not len(indexes):
+ return
+
+ selection = view.selectionModel()
+
+ first = None
+ for i in indexes:
+ above = view.indexAbove(i)
+ if not selection.isSelected(above):
+ first = i
+ break
+
+ if first is None:
+ raise RuntimeError("CopyTreeCellsToClipboard internal error")
+
+ model = first.model()
+ row_cnt = 0
+ col_cnt = model.columnCount(first)
+ max_width = [0] * col_cnt
+
+ indent_sz = 2
+ indent_str = " " * indent_sz
+
+ expanded_mark_sz = 2
+ if sys.version_info[0] == 3:
+ expanded_mark = "\u25BC "
+ not_expanded_mark = "\u25B6 "
+ else:
+ expanded_mark = chr(0x25BC) + " "
+ not_expanded_mark = chr(0x25B6) + " "
+ leaf_mark = " "
+
+ if not as_csv:
+ pos = first
+ while True:
+ row_cnt += 1
+ row = pos.row()
+ for c in range(col_cnt):
+ i = pos.sibling(row, c)
+ if c:
+ n = len(str(i.data()))
+ else:
+ n = len(str(i.data()).strip())
+ n += (i.internalPointer().level - 1) * indent_sz
+ n += expanded_mark_sz
+ max_width[c] = max(max_width[c], n)
+ pos = view.indexBelow(pos)
+ if not selection.isSelected(pos):
+ break
+
+ text = ""
+ pad = ""
+ sep = ""
+ if with_hdr:
+ for c in range(col_cnt):
+ val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
+ if as_csv:
+ text += sep + ToCSValue(val)
+ sep = ","
+ else:
+ max_width[c] = max(max_width[c], len(val))
+ width = max_width[c]
+ align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
+ if align & Qt.AlignRight:
+ val = val.rjust(width)
+ text += pad + sep + val
+ pad = " " * (width - len(val))
+ sep = " "
+ text += "\n"
+ pad = ""
+ sep = ""
+
+ pos = first
+ while True:
+ row = pos.row()
+ for c in range(col_cnt):
+ i = pos.sibling(row, c)
+ val = str(i.data())
+ if not c:
+ if model.hasChildren(i):
+ if view.isExpanded(i):
+ mark = expanded_mark
+ else:
+ mark = not_expanded_mark
+ else:
+ mark = leaf_mark
+ val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
+ if as_csv:
+ text += sep + ToCSValue(val)
+ sep = ","
+ else:
+ width = max_width[c]
+ if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
+ val = val.rjust(width)
+ text += pad + sep + val
+ pad = " " * (width - len(val))
+ sep = " "
+ pos = view.indexBelow(pos)
+ if not selection.isSelected(pos):
+ break
+ text = text.rstrip() + "\n"
+ pad = ""
+ sep = ""
+
+ QApplication.clipboard().setText(text)
+
+def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
+ view.CopyCellsToClipboard(view, as_csv, with_hdr)
+
+def CopyCellsToClipboardHdr(view):
+ CopyCellsToClipboard(view, False, True)
+
+def CopyCellsToClipboardCSV(view):
+ CopyCellsToClipboard(view, True, True)
+
+# Context menu
+
+class ContextMenu(object):
+
+ def __init__(self, view):
+ self.view = view
+ self.view.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.view.customContextMenuRequested.connect(self.ShowContextMenu)
+
+ def ShowContextMenu(self, pos):
+ menu = QMenu(self.view)
+ self.AddActions(menu)
+ menu.exec_(self.view.mapToGlobal(pos))
+
+ def AddCopy(self, menu):
+ menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
+ menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
+
+ def AddActions(self, menu):
+ self.AddCopy(menu)
+
+class TreeContextMenu(ContextMenu):
+
+ def AddActions(self, menu):
+ i = self.view.currentIndex()
+ text = str(i.data()).strip()
+ if len(text):
+ menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
+ self.AddCopy(menu)
+
+# Table window
+
+class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
+
+ def __init__(self, glb, table_name, parent=None):
+ super(TableWindow, self).__init__(parent)
+
+ self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
+
+ self.model = QSortFilterProxyModel()
+ self.model.setSourceModel(self.data_model)
+
+ self.view = QTableView()
+ self.view.setModel(self.model)
+ self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.view.verticalHeader().setVisible(False)
+ self.view.sortByColumn(-1, Qt.AscendingOrder)
+ self.view.setSortingEnabled(True)
+ self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
+ self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
+
+ self.ResizeColumnsToContents()
+
+ self.context_menu = ContextMenu(self.view)
+
+ self.find_bar = FindBar(self, self, True)
+
+ self.finder = ChildDataItemFinder(self.data_model)
+
+ self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
+
+ self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
+
+ self.setWidget(self.vbox.Widget())
+
+ AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
+
+ def Find(self, value, direction, pattern, context):
+ self.view.setFocus()
+ self.find_bar.Busy()
+ self.finder.Find(value, direction, pattern, context, self.FindDone)
+
+ def FindDone(self, row):
+ self.find_bar.Idle()
+ if row >= 0:
+ self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
+ else:
+ self.find_bar.NotFound()
+
+# Table list
+
+def GetTableList(glb):
+ tables = []
+ query = QSqlQuery(glb.db)
+ if glb.dbref.is_sqlite3:
+ QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
+ else:
+ QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
+ while query.next():
+ tables.append(query.value(0))
+ if glb.dbref.is_sqlite3:
+ tables.append("sqlite_master")
+ else:
+ tables.append("information_schema.tables")
+ tables.append("information_schema.views")
+ tables.append("information_schema.columns")
+ return tables
+
+# Top Calls data model
+
+class TopCallsModel(SQLTableModel):
+
+ def __init__(self, glb, report_vars, parent=None):
+ text = ""
+ if not glb.dbref.is_sqlite3:
+ text = "::text"
+ limit = ""
+ if len(report_vars.limit):
+ limit = " LIMIT " + report_vars.limit
+ sql = ("SELECT comm, pid, tid, name,"
+ " CASE"
+ " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
+ " ELSE short_name"
+ " END AS dso,"
+ " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
+ " CASE"
+ " WHEN (calls.flags = 1) THEN 'no call'" + text +
+ " WHEN (calls.flags = 2) THEN 'no return'" + text +
+ " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
+ " ELSE ''" + text +
+ " END AS flags"
+ " FROM calls"
+ " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
+ " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
+ " INNER JOIN dsos ON symbols.dso_id = dsos.id"
+ " INNER JOIN comms ON calls.comm_id = comms.id"
+ " INNER JOIN threads ON calls.thread_id = threads.id" +
+ report_vars.where_clause +
+ " ORDER BY elapsed_time DESC" +
+ limit
+ )
+ column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
+ self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
+ super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
+
+ def columnAlignment(self, column):
+ return self.alignment[column]
+
+# Top Calls report creation dialog
+
+class TopCallsDialog(ReportDialogBase):
+
+ def __init__(self, glb, parent=None):
+ title = "Top Calls by Elapsed Time"
+ items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
+ lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
+ lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
+ lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
+ lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
+ lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
+ lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
+ lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
+ super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
+
+# Top Calls window
+
+class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
+
+ def __init__(self, glb, report_vars, parent=None):
+ super(TopCallsWindow, self).__init__(parent)
+
+ self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
+ self.model = self.data_model
+
+ self.view = QTableView()
+ self.view.setModel(self.model)
+ self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
+ self.view.verticalHeader().setVisible(False)
+ self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
+ self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
+
+ self.context_menu = ContextMenu(self.view)
+
+ self.ResizeColumnsToContents()
+
+ self.find_bar = FindBar(self, self, True)
+
+ self.finder = ChildDataItemFinder(self.model)
+
+ self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
+
+ self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
+
+ self.setWidget(self.vbox.Widget())
+
+ AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
+
+ def Find(self, value, direction, pattern, context):
+ self.view.setFocus()
+ self.find_bar.Busy()
+ self.finder.Find(value, direction, pattern, context, self.FindDone)
+
+ def FindDone(self, row):
+ self.find_bar.Idle()
+ if row >= 0:
+ self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
+ else:
+ self.find_bar.NotFound()
+
+# Action Definition
+
+def CreateAction(label, tip, callback, parent=None, shortcut=None):
+ action = QAction(label, parent)
+ if shortcut != None:
+ action.setShortcuts(shortcut)
+ action.setStatusTip(tip)
+ action.triggered.connect(callback)
+ return action
+
+# Typical application actions
+
+def CreateExitAction(app, parent=None):
+ return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
+
+# Typical MDI actions
+
+def CreateCloseActiveWindowAction(mdi_area):
+ return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
+
+def CreateCloseAllWindowsAction(mdi_area):
+ return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
+
+def CreateTileWindowsAction(mdi_area):
+ return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
+
+def CreateCascadeWindowsAction(mdi_area):
+ return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
+
+def CreateNextWindowAction(mdi_area):
+ return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
+
+def CreatePreviousWindowAction(mdi_area):
+ return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
+
+# Typical MDI window menu
+
+class WindowMenu():
+
+ def __init__(self, mdi_area, menu):
+ self.mdi_area = mdi_area
+ self.window_menu = menu.addMenu("&Windows")
+ self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
+ self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
+ self.tile_windows = CreateTileWindowsAction(mdi_area)
+ self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
+ self.next_window = CreateNextWindowAction(mdi_area)
+ self.previous_window = CreatePreviousWindowAction(mdi_area)
+ self.window_menu.aboutToShow.connect(self.Update)
+
+ def Update(self):
+ self.window_menu.clear()
+ sub_window_count = len(self.mdi_area.subWindowList())
+ have_sub_windows = sub_window_count != 0
+ self.close_active_window.setEnabled(have_sub_windows)
+ self.close_all_windows.setEnabled(have_sub_windows)
+ self.tile_windows.setEnabled(have_sub_windows)
+ self.cascade_windows.setEnabled(have_sub_windows)
+ self.next_window.setEnabled(have_sub_windows)
+ self.previous_window.setEnabled(have_sub_windows)
+ self.window_menu.addAction(self.close_active_window)
+ self.window_menu.addAction(self.close_all_windows)
+ self.window_menu.addSeparator()
+ self.window_menu.addAction(self.tile_windows)
+ self.window_menu.addAction(self.cascade_windows)
+ self.window_menu.addSeparator()
+ self.window_menu.addAction(self.next_window)
+ self.window_menu.addAction(self.previous_window)
+ if sub_window_count == 0:
+ return
+ self.window_menu.addSeparator()
+ nr = 1
+ for sub_window in self.mdi_area.subWindowList():
+ label = str(nr) + " " + sub_window.name
+ if nr < 10:
+ label = "&" + label
+ action = self.window_menu.addAction(label)
+ action.setCheckable(True)
+ action.setChecked(sub_window == self.mdi_area.activeSubWindow())
+ action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
+ self.window_menu.addAction(action)
+ nr += 1
+
+ def setActiveSubWindow(self, nr):
+ self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
+
+# Help text
+
+glb_help_text = """
+<h1>Contents</h1>
+<style>
+p.c1 {
+ text-indent: 40px;
+}
+p.c2 {
+ text-indent: 80px;
+}
+}
+</style>
+<p class=c1><a href=#reports>1. Reports</a></p>
+<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
+<p class=c2><a href=#calltree>1.2 Call Tree</a></p>
+<p class=c2><a href=#allbranches>1.3 All branches</a></p>
+<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
+<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
+<p class=c1><a href=#charts>2. Charts</a></p>
+<p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
+<p class=c1><a href=#tables>3. Tables</a></p>
+<h1 id=reports>1. Reports</h1>
+<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
+The result is a GUI window with a tree representing a context-sensitive
+call-graph. Expanding a couple of levels of the tree and adjusting column
+widths to suit will display something like:
+<pre>
+ Call Graph: pt_example
+Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
+v- ls
+ v- 2638:2638
+ v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
+ |- unknown unknown 1 13198 0.1 1 0.0
+ >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
+ >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
+ v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
+ >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
+ >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
+ >- __libc_csu_init ls 1 10354 0.1 10 0.0
+ |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
+ v- main ls 1 8182043 99.6 180254 99.9
+</pre>
+<h3>Points to note:</h3>
+<ul>
+<li>The top level is a command name (comm)</li>
+<li>The next level is a thread (pid:tid)</li>
+<li>Subsequent levels are functions</li>
+<li>'Count' is the number of calls</li>
+<li>'Time' is the elapsed time until the function returns</li>
+<li>Percentages are relative to the level above</li>
+<li>'Branch Count' is the total number of branches for that function and all functions that it calls
+</ul>
+<h3>Find</h3>
+Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
+The pattern matching symbols are ? for any character and * for zero or more characters.
+<h2 id=calltree>1.2 Call Tree</h2>
+The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
+Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
+<h2 id=allbranches>1.3 All branches</h2>
+The All branches report displays all branches in chronological order.
+Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
+<h3>Disassembly</h3>
+Open a branch to display disassembly. This only works if:
+<ol>
+<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
+<li>The object code is available. Currently, only the perf build ID cache is searched for object code.
+The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
+One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
+or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
+</ol>
+<h4 id=xed>Intel XED Setup</h4>
+To use Intel XED, libxed.so must be present. To build and install libxed.so:
+<pre>
+git clone https://github.com/intelxed/mbuild.git mbuild
+git clone https://github.com/intelxed/xed
+cd xed
+./mfile.py --share
+sudo ./mfile.py --prefix=/usr/local install
+sudo ldconfig
+</pre>
+<h3>Instructions per Cycle (IPC)</h3>
+If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
+<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
+Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
+In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
+since the previous displayed 'IPC'.
+<h3>Find</h3>
+Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
+Refer to Python documentation for the regular expression syntax.
+All columns are searched, but only currently fetched rows are searched.
+<h2 id=selectedbranches>1.4 Selected branches</h2>
+This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
+by various selection criteria. A dialog box displays available criteria which are AND'ed together.
+<h3>1.4.1 Time ranges</h3>
+The time ranges hint text shows the total time range. Relative time ranges can also be entered in
+ms, us or ns. Also, negative values are relative to the end of trace. Examples:
+<pre>
+ 81073085947329-81073085958238 From 81073085947329 to 81073085958238
+ 100us-200us From 100us to 200us
+ 10ms- From 10ms to the end
+ -100ns The first 100ns
+ -10ms- The last 10ms
+</pre>
+N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
+<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
+The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
+The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
+If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
+<h1 id=charts>2. Charts</h1>
+<h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
+This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
+<h3>Features</h3>
+<ol>
+<li>Mouse over to highight the task and show the time</li>
+<li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
+<li>Go back and forward by pressing the arrow buttons</li>
+<li>If call information is available, right-click to show a call tree opened to that task and time.
+Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
+</li>
+</ol>
+<h3>Important</h3>
+The graph can be misleading in the following respects:
+<ol>
+<li>The graph shows the first task on each CPU as running from the beginning of the time range.
+Because tracing might start on different CPUs at different times, that is not necessarily the case.
+Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
+<li>Similarly, the last task on each CPU can be showing running longer than it really was.
+Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
+<li>When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window</li>
+</ol>
+<h1 id=tables>3. Tables</h1>
+The Tables menu shows all tables and views in the database. Most tables have an associated view
+which displays the information in a more friendly way. Not all data for large tables is fetched
+immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
+but that can be slow for large tables.
+<p>There are also tables of database meta-information.
+For SQLite3 databases, the sqlite_master table is included.
+For PostgreSQL databases, information_schema.tables/views/columns are included.
+<h3>Find</h3>
+Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
+Refer to Python documentation for the regular expression syntax.
+All columns are searched, but only currently fetched rows are searched.
+<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
+will go to the next/previous result in id order, instead of display order.
+"""
+
+# Help window
+
+class HelpWindow(QMdiSubWindow):
+
+ def __init__(self, glb, parent=None):
+ super(HelpWindow, self).__init__(parent)
+
+ self.text = QTextBrowser()
+ self.text.setHtml(glb_help_text)
+ self.text.setReadOnly(True)
+ self.text.setOpenExternalLinks(True)
+
+ self.setWidget(self.text)
+
+ AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
+
+# Main window that only displays the help text
+
+class HelpOnlyWindow(QMainWindow):
+
+ def __init__(self, parent=None):
+ super(HelpOnlyWindow, self).__init__(parent)
+
+ self.setMinimumSize(200, 100)
+ self.resize(800, 600)
+ self.setWindowTitle("Exported SQL Viewer Help")
+ self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
+
+ self.text = QTextBrowser()
+ self.text.setHtml(glb_help_text)
+ self.text.setReadOnly(True)
+ self.text.setOpenExternalLinks(True)
+
+ self.setCentralWidget(self.text)
+
+# PostqreSQL server version
+
+def PostqreSQLServerVersion(db):
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT VERSION()")
+ if query.next():
+ v_str = query.value(0)
+ v_list = v_str.strip().split(" ")
+ if v_list[0] == "PostgreSQL" and v_list[2] == "on":
+ return v_list[1]
+ return v_str
+ return "Unknown"
+
+# SQLite version
+
+def SQLiteVersion(db):
+ query = QSqlQuery(db)
+ QueryExec(query, "SELECT sqlite_version()")
+ if query.next():
+ return query.value(0)
+ return "Unknown"
+
+# About dialog
+
+class AboutDialog(QDialog):
+
+ def __init__(self, glb, parent=None):
+ super(AboutDialog, self).__init__(parent)
+
+ self.setWindowTitle("About Exported SQL Viewer")
+ self.setMinimumWidth(300)
+
+ pyside_version = "1" if pyside_version_1 else "2"
+
+ text = "<pre>"
+ text += "Python version: " + sys.version.split(" ")[0] + "\n"
+ text += "PySide version: " + pyside_version + "\n"
+ text += "Qt version: " + qVersion() + "\n"
+ if glb.dbref.is_sqlite3:
+ text += "SQLite version: " + SQLiteVersion(glb.db) + "\n"
+ else:
+ text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
+ text += "</pre>"
+
+ self.text = QTextBrowser()
+ self.text.setHtml(text)
+ self.text.setReadOnly(True)
+ self.text.setOpenExternalLinks(True)
+
+ self.vbox = QVBoxLayout()
+ self.vbox.addWidget(self.text)
+
+ self.setLayout(self.vbox)
+
+# Font resize
+
+def ResizeFont(widget, diff):
+ font = widget.font()
+ sz = font.pointSize()
+ font.setPointSize(sz + diff)
+ widget.setFont(font)
+
+def ShrinkFont(widget):
+ ResizeFont(widget, -1)
+
+def EnlargeFont(widget):
+ ResizeFont(widget, 1)
+
+# Unique name for sub-windows
+
+def NumberedWindowName(name, nr):
+ if nr > 1:
+ name += " <" + str(nr) + ">"
+ return name
+
+def UniqueSubWindowName(mdi_area, name):
+ nr = 1
+ while True:
+ unique_name = NumberedWindowName(name, nr)
+ ok = True
+ for sub_window in mdi_area.subWindowList():
+ if sub_window.name == unique_name:
+ ok = False
+ break
+ if ok:
+ return unique_name
+ nr += 1
+
+# Add a sub-window
+
+def AddSubWindow(mdi_area, sub_window, name):
+ unique_name = UniqueSubWindowName(mdi_area, name)
+ sub_window.setMinimumSize(200, 100)
+ sub_window.resize(800, 600)
+ sub_window.setWindowTitle(unique_name)
+ sub_window.setAttribute(Qt.WA_DeleteOnClose)
+ sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
+ sub_window.name = unique_name
+ mdi_area.addSubWindow(sub_window)
+ sub_window.show()
+
+# Main window
+
+class MainWindow(QMainWindow):
+
+ def __init__(self, glb, parent=None):
+ super(MainWindow, self).__init__(parent)
+
+ self.glb = glb
+
+ self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
+ self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
+ self.setMinimumSize(200, 100)
+
+ self.mdi_area = QMdiArea()
+ self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+ self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
+
+ self.setCentralWidget(self.mdi_area)
+
+ menu = self.menuBar()
+
+ file_menu = menu.addMenu("&File")
+ file_menu.addAction(CreateExitAction(glb.app, self))
+
+ edit_menu = menu.addMenu("&Edit")
+ edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
+ edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
+ edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
+ edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
+ edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
+ edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
+
+ reports_menu = menu.addMenu("&Reports")
+ if IsSelectable(glb.db, "calls"):
+ reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
+
+ if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
+ reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
+
+ self.EventMenu(GetEventList(glb.db), reports_menu)
+
+ if IsSelectable(glb.db, "calls"):
+ reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
+
+ if IsSelectable(glb.db, "context_switches"):
+ charts_menu = menu.addMenu("&Charts")
+ charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
+
+ self.TableMenu(GetTableList(glb), menu)
+
+ self.window_menu = WindowMenu(self.mdi_area, menu)
+
+ help_menu = menu.addMenu("&Help")
+ help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
+ help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
+
+ def Try(self, fn):
+ win = self.mdi_area.activeSubWindow()
+ if win:
+ try:
+ fn(win.view)
+ except AttributeError:
+ pass
+
+ def CopyToClipboard(self):
+ self.Try(CopyCellsToClipboardHdr)
+
+ def CopyToClipboardCSV(self):
+ self.Try(CopyCellsToClipboardCSV)
+
+ def Find(self):
+ win = self.mdi_area.activeSubWindow()
+ if win:
+ try:
+ win.find_bar.Activate()
+ except AttributeError:
+ pass
+
+ def FetchMoreRecords(self):
+ win = self.mdi_area.activeSubWindow()
+ if win:
+ try:
+ win.fetch_bar.Activate()
+ except AttributeError:
+ pass
+
+ def ShrinkFont(self):
+ self.Try(ShrinkFont)
+
+ def EnlargeFont(self):
+ self.Try(EnlargeFont)
+
+ def EventMenu(self, events, reports_menu):
+ branches_events = 0
+ for event in events:
+ event = event.split(":")[0]
+ if event == "branches":
+ branches_events += 1
+ dbid = 0
+ for event in events:
+ dbid += 1
+ event = event.split(":")[0]
+ if event == "branches":
+ label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
+ reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
+ label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
+ reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
+
+ def TimeChartByCPU(self):
+ TimeChartByCPUWindow(self.glb, self)
+
+ def TableMenu(self, tables, menu):
+ table_menu = menu.addMenu("&Tables")
+ for table in tables:
+ table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
+
+ def NewCallGraph(self):
+ CallGraphWindow(self.glb, self)
+
+ def NewCallTree(self):
+ CallTreeWindow(self.glb, self)
+
+ def NewTopCalls(self):
+ dialog = TopCallsDialog(self.glb, self)
+ ret = dialog.exec_()
+ if ret:
+ TopCallsWindow(self.glb, dialog.report_vars, self)
+
+ def NewBranchView(self, event_id):
+ BranchWindow(self.glb, event_id, ReportVars(), self)
+
+ def NewSelectedBranchView(self, event_id):
+ dialog = SelectedBranchDialog(self.glb, self)
+ ret = dialog.exec_()
+ if ret:
+ BranchWindow(self.glb, event_id, dialog.report_vars, self)
+
+ def NewTableView(self, table_name):
+ TableWindow(self.glb, table_name, self)
+
+ def Help(self):
+ HelpWindow(self.glb, self)
+
+ def About(self):
+ dialog = AboutDialog(self.glb, self)
+ dialog.exec_()
+
+def TryOpen(file_name):
+ try:
+ return open(file_name, "rb")
+ except OSError:
+ return None
+
+def Is64Bit(f):
+ result = sizeof(c_void_p)
+ # ELF support only
+ pos = f.tell()
+ f.seek(0)
+ header = f.read(7)
+ f.seek(pos)
+ magic = header[0:4]
+ if sys.version_info[0] == 2:
+ eclass = ord(header[4])
+ encoding = ord(header[5])
+ version = ord(header[6])
+ else:
+ eclass = header[4]
+ encoding = header[5]
+ version = header[6]
+ if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
+ result = True if eclass == 2 else False
+ return result
+
+# Global data
+
+class Glb():
+
+ def __init__(self, dbref, db, dbname):
+ self.dbref = dbref
+ self.db = db
+ self.dbname = dbname
+ self.home_dir = os.path.expanduser("~")
+ self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
+ if self.buildid_dir:
+ self.buildid_dir += "/.build-id/"
+ else:
+ self.buildid_dir = self.home_dir + "/.debug/.build-id/"
+ self.app = None
+ self.mainwindow = None
+ self.instances_to_shutdown_on_exit = weakref.WeakSet()
+ try:
+ self.disassembler = LibXED()
+ self.have_disassembler = True
+ except (OSError, RuntimeError, AttributeError):
+ self.have_disassembler = False
+ self.host_machine_id = 0
+ self.host_start_time = 0
+ self.host_finish_time = 0
+
+ def FileFromBuildId(self, build_id):
+ file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
+ return TryOpen(file_name)
+
+ def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
+ # Assume current machine i.e. no support for virtualization
+ if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
+ file_name = os.getenv("PERF_KCORE")
+ f = TryOpen(file_name) if file_name else None
+ if f:
+ return f
+ # For now, no special handling if long_name is /proc/kcore
+ f = TryOpen(long_name)
+ if f:
+ return f
+ f = self.FileFromBuildId(build_id)
+ if f:
+ return f
+ return None
+
+ def AddInstanceToShutdownOnExit(self, instance):
+ self.instances_to_shutdown_on_exit.add(instance)
+
+ # Shutdown any background processes or threads
+ def ShutdownInstances(self):
+ for x in self.instances_to_shutdown_on_exit:
+ try:
+ x.Shutdown()
+ except AttributeError:
+ pass
+
+ def GetHostMachineId(self):
+ query = QSqlQuery(self.db)
+ QueryExec(query, "SELECT id FROM machines WHERE pid = -1")
+ if query.next():
+ self.host_machine_id = query.value(0)
+ else:
+ self.host_machine_id = 0
+ return self.host_machine_id
+
+ def HostMachineId(self):
+ if self.host_machine_id:
+ return self.host_machine_id
+ return self.GetHostMachineId()
+
+ def SelectValue(self, sql):
+ query = QSqlQuery(self.db)
+ try:
+ QueryExec(query, sql)
+ except RuntimeError:
+ return None
+ if query.next():
+ return Decimal(query.value(0))
+ return None
+
+ def SwitchesMinTime(self, machine_id):
+ return self.SelectValue("SELECT time"
+ " FROM context_switches"
+ " WHERE time != 0 AND machine_id = " + str(machine_id) +
+ " ORDER BY id LIMIT 1")
+
+ def SwitchesMaxTime(self, machine_id):
+ return self.SelectValue("SELECT time"
+ " FROM context_switches"
+ " WHERE time != 0 AND machine_id = " + str(machine_id) +
+ " ORDER BY id DESC LIMIT 1")
+
+ def SamplesMinTime(self, machine_id):
+ return self.SelectValue("SELECT time"
+ " FROM samples"
+ " WHERE time != 0 AND machine_id = " + str(machine_id) +
+ " ORDER BY id LIMIT 1")
+
+ def SamplesMaxTime(self, machine_id):
+ return self.SelectValue("SELECT time"
+ " FROM samples"
+ " WHERE time != 0 AND machine_id = " + str(machine_id) +
+ " ORDER BY id DESC LIMIT 1")
+
+ def CallsMinTime(self, machine_id):
+ return self.SelectValue("SELECT calls.call_time"
+ " FROM calls"
+ " INNER JOIN threads ON threads.id = calls.thread_id"
+ " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +
+ " ORDER BY calls.id LIMIT 1")
+
+ def CallsMaxTime(self, machine_id):
+ return self.SelectValue("SELECT calls.return_time"
+ " FROM calls"
+ " INNER JOIN threads ON threads.id = calls.thread_id"
+ " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +
+ " ORDER BY calls.return_time DESC LIMIT 1")
+
+ def GetStartTime(self, machine_id):
+ t0 = self.SwitchesMinTime(machine_id)
+ t1 = self.SamplesMinTime(machine_id)
+ t2 = self.CallsMinTime(machine_id)
+ if t0 is None or (not(t1 is None) and t1 < t0):
+ t0 = t1
+ if t0 is None or (not(t2 is None) and t2 < t0):
+ t0 = t2
+ return t0
+
+ def GetFinishTime(self, machine_id):
+ t0 = self.SwitchesMaxTime(machine_id)
+ t1 = self.SamplesMaxTime(machine_id)
+ t2 = self.CallsMaxTime(machine_id)
+ if t0 is None or (not(t1 is None) and t1 > t0):
+ t0 = t1
+ if t0 is None or (not(t2 is None) and t2 > t0):
+ t0 = t2
+ return t0
+
+ def HostStartTime(self):
+ if self.host_start_time:
+ return self.host_start_time
+ self.host_start_time = self.GetStartTime(self.HostMachineId())
+ return self.host_start_time
+
+ def HostFinishTime(self):
+ if self.host_finish_time:
+ return self.host_finish_time
+ self.host_finish_time = self.GetFinishTime(self.HostMachineId())
+ return self.host_finish_time
+
+ def StartTime(self, machine_id):
+ if machine_id == self.HostMachineId():
+ return self.HostStartTime()
+ return self.GetStartTime(machine_id)
+
+ def FinishTime(self, machine_id):
+ if machine_id == self.HostMachineId():
+ return self.HostFinishTime()
+ return self.GetFinishTime(machine_id)
+
+# Database reference
+
+class DBRef():
+
+ def __init__(self, is_sqlite3, dbname):
+ self.is_sqlite3 = is_sqlite3
+ self.dbname = dbname
+ self.TRUE = "TRUE"
+ self.FALSE = "FALSE"
+ # SQLite prior to version 3.23 does not support TRUE and FALSE
+ if self.is_sqlite3:
+ self.TRUE = "1"
+ self.FALSE = "0"
+
+ def Open(self, connection_name):
+ dbname = self.dbname
+ if self.is_sqlite3:
+ db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
+ else:
+ db = QSqlDatabase.addDatabase("QPSQL", connection_name)
+ opts = dbname.split()
+ for opt in opts:
+ if "=" in opt:
+ opt = opt.split("=")
+ if opt[0] == "hostname":
+ db.setHostName(opt[1])
+ elif opt[0] == "port":
+ db.setPort(int(opt[1]))
+ elif opt[0] == "username":
+ db.setUserName(opt[1])
+ elif opt[0] == "password":
+ db.setPassword(opt[1])
+ elif opt[0] == "dbname":
+ dbname = opt[1]
+ else:
+ dbname = opt
+
+ db.setDatabaseName(dbname)
+ if not db.open():
+ raise RuntimeError("Failed to open database " + dbname + " error: " + db.lastError().text())
+ return db, dbname
+
+# Main
+
+def Main():
+ usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
+ " or: exported-sql-viewer.py --help-only"
+ ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
+ ap.add_argument("--pyside-version-1", action='store_true')
+ ap.add_argument("dbname", nargs="?")
+ ap.add_argument("--help-only", action='store_true')
+ args = ap.parse_args()
+
+ if args.help_only:
+ app = QApplication(sys.argv)
+ mainwindow = HelpOnlyWindow()
+ mainwindow.show()
+ err = app.exec_()
+ sys.exit(err)
+
+ dbname = args.dbname
+ if dbname is None:
+ ap.print_usage()
+ print("Too few arguments")
+ sys.exit(1)
+
+ is_sqlite3 = False
+ try:
+ f = open(dbname, "rb")
+ if f.read(15) == b'SQLite format 3':
+ is_sqlite3 = True
+ f.close()
+ except OSError:
+ pass
+
+ dbref = DBRef(is_sqlite3, dbname)
+ db, dbname = dbref.Open("main")
+ glb = Glb(dbref, db, dbname)
+ app = QApplication(sys.argv)
+ glb.app = app
+ mainwindow = MainWindow(glb)
+ glb.mainwindow = mainwindow
+ mainwindow.show()
+ err = app.exec_()
+ glb.ShutdownInstances()
+ db.close()
+ sys.exit(err)
+
+if __name__ == "__main__":
+ Main()
diff --git a/tools/perf/python/failed-syscalls-by-pid.py b/tools/perf/python/failed-syscalls-by-pid.py
new file mode 100755
index 000000000000..51824a929ea4
--- /dev/null
+++ b/tools/perf/python/failed-syscalls-by-pid.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Displays system-wide failed system call totals, broken down by pid.
+If a [comm] or [pid] arg is specified, only syscalls called by it are displayed.
+
+Ported from tools/perf/scripts/python/failed-syscalls-by-pid.py
+"""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import os
+import sys
+from typing import Optional
+import perf
+
+
+def strerror(nr: int, e_machine: Optional[int] = None) -> str:
+ """Return error string for a given errno, accounting for target e_machine."""
+ return perf.arch_strerrno(nr, e_machine) or f"Unknown {nr} errno"
+
+
+class SyscallAnalyzer:
+ """Analyzes failed syscalls and aggregates counts."""
+
+ def __init__(self, for_comm: Optional[str] = None, for_pid: Optional[int] = None):
+ self.for_comm = for_comm
+ self.for_pid = for_pid
+ self.session: Optional[perf.session] = None
+ self.syscalls: dict[tuple[str, int, int, int], int] = defaultdict(int)
+ machine = os.uname().machine
+ self.host_64_bit = ("64" in machine or machine in ("s390x", "alpha")
+ or sys.maxsize > 0xffffffff)
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process raw_syscalls:sys_exit and syscalls:sys_exit events."""
+ event_name = str(sample.evsel)
+ if "raw_syscalls:sys_exit" not in event_name and "syscalls:sys_exit" not in event_name:
+ return
+
+ pid = sample.sample_tid
+ comm = "Unknown"
+ if hasattr(self, 'session') and self.session:
+ try:
+ thread = self.session.find_thread(sample.sample_pid, pid)
+ if thread:
+ comm = thread.comm() or "Unknown"
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError):
+ pass
+
+ if self.for_comm is not None and comm != self.for_comm:
+ return
+ if self.for_pid is not None and pid != self.for_pid:
+ return
+
+ ret = getattr(sample, "ret", 0)
+ is_64_bit = (getattr(self.session, "is_64_bit", self.host_64_bit)
+ if self.session else self.host_64_bit)
+ # Tracepoint fields may be exposed as unsigned 32-bit or 64-bit integers.
+ # Guard the 32-bit 0xfffff000..0xffffffff conversion with 'not is_64_bit' so valid
+ # 64-bit user-space addresses in that range (e.g. mmap/brk) are not misclassified.
+ if ret > 0:
+ if not is_64_bit and 0xfffff000 <= ret <= 0xffffffff:
+ ret -= 0x100000000
+ elif ret >= 0xfffffffffffff000:
+ ret -= 0x10000000000000000
+
+ # Linux kernel syscalls only return error codes in the [-4095, -1] (-MAX_ERRNO)
+ # range; other negative signed values are valid unsigned user-space pointers.
+ if -4095 <= ret < 0:
+ syscall_id = getattr(sample, "__syscall_nr", -1)
+ if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
+ syscall_id = getattr(sample, "nr", -1)
+ if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
+ syscall_id = getattr(sample, "sys_id", -1)
+ if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
+ syscall_id = getattr(sample, "id", -1)
+
+ # Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000).
+ raw_sc_id = syscall_id & ~0x40000000
+ if 0 <= raw_sc_id <= 0xffff:
+ self.syscalls[(comm, pid, syscall_id, ret)] += 1
+
+ def print_summary(self) -> None:
+ """Print aggregated statistics."""
+ if self.for_comm is not None:
+ print(f"\nsyscall errors for {self.for_comm}:\n")
+ elif self.for_pid is not None:
+ print(f"\nsyscall errors for PID {self.for_pid}:\n")
+ else:
+ print("\nsyscall errors:\n")
+
+ print(f"{'comm [pid]':<30} {'count':>10}")
+ print(f"{'-' * 30:<30} {'-' * 10:>10}")
+
+ sorted_keys = sorted(
+ self.syscalls.keys(),
+ key=lambda k: (k[0], k[1], k[2], -self.syscalls[k])
+ )
+ current_comm_pid = None
+ current_syscall = None
+ emach = getattr(self.session, "e_machine", 0) or 0
+ for comm, pid, syscall_id, ret in sorted_keys:
+ if current_comm_pid != (comm, pid):
+ print(f"\n{comm} [{pid}]")
+ current_comm_pid = (comm, pid)
+ current_syscall = None
+ if current_syscall != syscall_id:
+ raw_sc_id = syscall_id & ~0x40000000
+ try:
+ if emach:
+ name = perf.syscall_name(raw_sc_id, emach) or str(syscall_id)
+ else:
+ name = perf.syscall_name(raw_sc_id) or str(syscall_id)
+ except AttributeError:
+ name = str(syscall_id)
+ print(f" syscall: {name:<16}")
+ current_syscall = syscall_id
+ err_str = strerror(ret, emach)
+ count = self.syscalls[(comm, pid, syscall_id, ret)]
+ print(f" err = {err_str:<20} {count:10d}")
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(
+ description="Displays system-wide failed system call totals, "
+ "broken down by pid.")
+ ap.add_argument("-i", "--input", default="perf.data",
+ help="Input file name")
+ ap.add_argument("filter", nargs="?", help="COMM or PID to filter by")
+ args = ap.parse_args()
+
+ F_COMM = None
+ F_PID = None
+
+ if args.filter:
+ try:
+ F_PID = int(args.filter)
+ except ValueError:
+ F_COMM = args.filter
+
+ analyzer = SyscallAnalyzer(F_COMM, F_PID)
+ session = None
+
+ try:
+ print("Press control+C to stop and show the summary")
+ session = perf.session(perf.data(args.input), sample=analyzer.process_event)
+ analyzer.session = session
+ session.process_events()
+ analyzer.print_summary()
+ except KeyboardInterrupt:
+ analyzer.print_summary()
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e:
+ print(f"Error processing events: {e}")
+ sys.exit(1)
+ finally:
+ # Break reference cycles between perf.session and analyzer.process_event
+ # because perf.session is a C extension type without cyclic GC (tp_traverse).
+ analyzer.session = None
+ session = None
diff --git a/tools/perf/python/failed-syscalls.py b/tools/perf/python/failed-syscalls.py
new file mode 100755
index 000000000000..6d1fa4e5ef89
--- /dev/null
+++ b/tools/perf/python/failed-syscalls.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""Failed system call counts."""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import sys
+from typing import Optional
+import perf
+
+class FailedSyscalls:
+ """Tracks and displays failed system call totals."""
+ def __init__(self, comm: Optional[str] = None) -> None:
+ self.failed_syscalls: dict[str, int] = defaultdict(int)
+ self.for_comm = comm
+ self.session: Optional[perf.session] = None
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process sys_exit events."""
+ event_name = str(sample.evsel)
+ if not event_name.startswith("evsel(syscalls:sys_exit") and \
+ not event_name.startswith("evsel(raw_syscalls:sys_exit"):
+ return
+
+ try:
+ ret = sample.ret
+ except AttributeError:
+ print("ERROR: tracepoint fields missed", file=sys.stderr)
+ sys.exit(1)
+
+ if ret > 0:
+ if ret >= 0xfffffffffffff000: # 64-bit negative errors
+ ret -= 0x10000000000000000
+ elif 0xfffff000 <= ret <= 0xffffffff: # 32-bit negative errors
+ assert self.session is not None
+ if not self.session.is_64_bit:
+ ret -= 0x100000000
+
+ # Linux kernel syscalls only return error codes in the [-4095, -1] (-MAX_ERRNO)
+ # range; other negative signed values are valid unsigned user-space addresses.
+ if not -4095 <= ret < 0:
+ return
+
+ assert self.session is not None
+ try:
+ thread = self.session.find_thread(sample.sample_pid, sample.sample_tid)
+ comm = (thread.comm() if thread else None) or "unknown"
+ except (TypeError, AttributeError):
+ # find_thread returns None when the thread isn't known.
+ comm = "unknown"
+
+ if self.for_comm and comm != self.for_comm:
+ return
+
+ self.failed_syscalls[comm] += 1
+
+ def print_totals(self) -> None:
+ """Print summary table."""
+ print("\nfailed syscalls by comm:\n")
+ print(f"{'comm':<20s} {'# errors':>10s}")
+ print(f"{'-'*20} {'-'*10}")
+
+ for comm, val in sorted(self.failed_syscalls.items(),
+ key=lambda kv: (-kv[1], kv[0])):
+ print(f"{comm:<20s} {val:10d}")
+
+ def run(self, input_file: str) -> None:
+ """Run the session."""
+ self.session = perf.session(perf.data(input_file), sample=self.process_event)
+ try:
+ self.session.process_events()
+ finally:
+ # Break the reference cycle between perf.session and self.process_event
+ # because perf.session lacks cyclic GC support (tp_traverse).
+ self.session = None
+ self.print_totals()
+
+def main() -> None:
+ """Main function."""
+ parser = argparse.ArgumentParser(description="Trace failed syscalls")
+ parser.add_argument("comm", nargs="?", help="Filter by command name")
+ parser.add_argument("-i", "--input", default="perf.data", help="Input file")
+ args = parser.parse_args()
+
+ analyzer = FailedSyscalls(args.comm)
+ try:
+ analyzer.run(args.input)
+ except IOError as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/flamegraph.py b/tools/perf/python/flamegraph.py
new file mode 100755
index 000000000000..1b06a11fa86c
--- /dev/null
+++ b/tools/perf/python/flamegraph.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+flamegraph.py - create flame graphs from perf samples using perf python module
+"""
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import re
+import subprocess
+import sys
+import urllib.request
+from typing import Dict, Optional, Union
+import perf
+
+MINIMAL_HTML = """<head>
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.css" integrity="sha256-fXXWjLZigZo3QBJIhjEA3Z2zUfS+vzMQhF2Rlvurfio=" crossorigin="anonymous">
+</head>
+<body>
+ <div id="chart"></div>
+ <script type="text/javascript" src="https://d3js.org/d3.v7.js" integrity="sha256-DA1YviEGwU4kL8dbwJoXRngAYwdRWAZl9ulVKgwGTqI=" crossorigin="anonymous"></script>
+ <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.min.js" integrity="sha256-GEbx5jbtG9LDL8h9HiTWOv9+Txhim0u9gZNR8J6iI1M=" crossorigin="anonymous"></script>
+ <script type="text/javascript">
+ const stacks = [/** @flamegraph_json **/];
+ // Note, options is unused.
+ const options = [/** @options_json **/];
+
+ var chart = flamegraph();
+ d3.select("#chart")
+ .datum(stacks[0])
+ .call(chart);
+ </script>
+</body>
+"""
+
+class Node:
+ """A node in the flame graph tree."""
+ def __init__(self, name: str, libtype: str):
+ self.name = name
+ self.libtype = libtype
+ self.value: int = 0
+ self.children: dict[str, Node] = {}
+
+ def to_json(self) -> Dict[str, Union[str, int, list[Dict]]]:
+ """Convert the node to a JSON-serializable dictionary."""
+ return {
+ "n": self.name,
+ "l": self.libtype,
+ "v": self.value,
+ "c": [x.to_json() for x in self.children.values()]
+ }
+
+
+class FlameGraphCLI:
+ """Command-line interface for generating flame graphs."""
+ def __init__(self, args):
+ self.args = args
+ self.stack = Node("all", "root")
+ self.session = None
+
+ @staticmethod
+ def get_libtype_from_dso(dso: Optional[str]) -> str:
+ """Determine the library type from the DSO name."""
+ if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux") or dso == "[kernel]"):
+ return "kernel"
+ return ""
+
+ @staticmethod
+ def find_or_create_node(node: Node, name: str, libtype: str) -> Node:
+ """Find a child node with the given name or create a new one."""
+ if name in node.children:
+ return node.children[name]
+ child = Node(name, libtype)
+ node.children[name] = child
+ return child
+
+ def process_event(self, sample) -> None:
+ """Process a single perf sample event."""
+ if self.args.event_name and self.args.event_name not in str(sample.evsel):
+ return
+
+ pid = sample.sample_pid
+ dso_type = ""
+ try:
+ thread = self.session.find_thread(sample.sample_pid, sample.sample_tid)
+ comm = (thread.comm() if thread else None) or "[unknown]"
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError):
+ comm = "[unknown]"
+
+ if pid == 0:
+ comm = comm if comm != "[unknown]" else "swapper"
+ dso_type = "kernel"
+ else:
+ comm = f"{comm} ({pid})"
+
+ node = self.find_or_create_node(self.stack, comm, dso_type)
+
+ callchain = sample.callchain
+ if callchain:
+ # We want to traverse from root to leaf.
+ # perf callchain iterator gives leaf to root.
+ # We collect them and reverse.
+ frames = list(callchain)
+ for entry in reversed(frames):
+ name = entry.symbol or "[unknown]"
+ libtype = self.get_libtype_from_dso(entry.dso)
+ node = self.find_or_create_node(node, name, libtype)
+ else:
+ # Fallback if no callchain
+ name = (sample.symbol or '[unknown]')
+ libtype = self.get_libtype_from_dso((sample.dso or '[unknown]'))
+ node = self.find_or_create_node(node, name, libtype)
+
+ node.value += 1
+
+ def get_report_header(self) -> str:
+ """Get the header from the perf report."""
+ try:
+ input_file = self.args.input or "perf.data"
+ if input_file == "-":
+ return ""
+ mode = os.stat(input_file).st_mode
+ import stat
+ if stat.S_ISFIFO(mode) or stat.S_ISCHR(mode):
+ return ""
+ output = subprocess.check_output(["perf", "report", "--header-only", "-i", input_file])
+ result = output.decode("utf-8")
+ if self.args.event_name:
+ result += "\nFocused event: " + self.args.event_name
+ return result
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError,
+ subprocess.CalledProcessError):
+ return ""
+
+ def run(self) -> None:
+ """Run the flame graph generation."""
+ input_file = self.args.input or "perf.data"
+ if input_file != "-" and not os.path.exists(input_file):
+ print(f"Error: {input_file} not found. (try 'perf record' first)", file=sys.stderr)
+ sys.exit(1)
+
+ try:
+ self.session = perf.session(perf.data(input_file),
+ sample=self.process_event)
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e:
+ print(f"Error opening session: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ try:
+ self.session.process_events()
+ finally:
+ # perf.session is a C extension type that holds a strong reference to the
+ # bound method self.process_event (which references self) and does not
+ # implement cyclic GC (tp_traverse). Explicitly clear self.session to break
+ # the reference cycle and free the underlying session resources deterministically.
+ self.session = None
+
+ stacks_json = json.dumps(self.stack, default=lambda x: x.to_json())
+ # Escape HTML special characters to prevent XSS
+ stacks_json = stacks_json.replace("<", "\\u003c") \
+ .replace(">", "\\u003e").replace("&", "\\u0026")
+
+ if self.args.format == "html":
+ report_header = self.get_report_header()
+ options = {
+ "colorscheme": self.args.colorscheme,
+ "context": report_header
+ }
+ options_json = json.dumps(options)
+ options_json = options_json.replace("<", "\\u003c") \
+ .replace(">", "\\u003e").replace("&", "\\u0026")
+
+ template = self.args.template
+ template_sha256sum = None
+ output_str = None
+
+ if not os.path.isfile(template):
+ if template.startswith("http://") or template.startswith("https://"):
+ if not self.args.allow_download:
+ print("Warning: Downloading templates is disabled. "
+ "Use --allow-download.", file=sys.stderr)
+ template = None
+ else:
+ print(f"Warning: Template file '{template}' not found.", file=sys.stderr)
+ if self.args.allow_download:
+ print("Using default CDN template.", file=sys.stderr)
+ template = (
+ "https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/"
+ "d3-flamegraph-base.html"
+ )
+ template_sha256sum = (
+ "f6a4aa7edffda4fb9bd71eb0eb75bc44d0bb34cd9efbd053f6095bc5c28d702b"
+ )
+ else:
+ template = None
+
+ use_minimal = False
+ try:
+ if not template:
+ use_minimal = True
+ elif template.startswith(("http://", "https://")):
+ with urllib.request.urlopen(template) as url_template:
+ output_str = "".join([l.decode("utf-8") for l in url_template.readlines()])
+ else:
+ with open(template, "r", encoding="utf-8") as f:
+ output_str = f.read()
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as err:
+ print(f"Error reading template {template}: {err}\n", file=sys.stderr)
+ use_minimal = True
+
+ if use_minimal:
+ print("Using internal minimal HTML that refers to d3's web site. JavaScript " +
+ "loaded this way from a local file may be blocked unless your " +
+ "browser has relaxed permissions. Run with '--allow-download' to fetch " +
+ "the full D3 HTML template.", file=sys.stderr)
+ output_str = MINIMAL_HTML
+
+ elif template_sha256sum:
+ assert output_str is not None
+ download_sha256sum = hashlib.sha256(
+ output_str.encode("utf-8")
+ ).hexdigest()
+ if download_sha256sum != template_sha256sum:
+ s = None
+ while s not in ["y", "n"]:
+ try:
+ s = input(f"""Unexpected template sha256sum.
+{download_sha256sum} != {template_sha256sum}, for:
+{template}
+continue?[yn] """).lower()
+ except EOFError:
+ s = "n"
+ if s == "n":
+ sys.exit(1)
+
+ assert output_str is not None
+ replacements = {
+ "/** @options_json **/": options_json,
+ "/** @flamegraph_json **/": stacks_json,
+ }
+ # Use a lambda replacement so re.sub does not interpret backslashes
+ # in the JSON strings (such as \n or \u003c) as regex escape sequences.
+ output_str = re.sub(
+ r"/\*\* @(?:options_json|flamegraph_json) \*\*/",
+ lambda m: replacements[m.group(0)],
+ output_str,
+ )
+ output_fn = self.args.output or "flamegraph.html"
+ else:
+ output_str = stacks_json
+ output_fn = self.args.output or "stacks.json"
+
+ if output_fn == "-":
+ with open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out:
+ out.write(output_str)
+ else:
+ print(f"dumping data to {output_fn}")
+ with open(output_fn, "w", encoding="utf-8") as out:
+ out.write(output_str)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Create flame graphs using perf python module.")
+ parser.add_argument("-f", "--format", default="html", choices=["json", "html"],
+ help="output file format")
+ parser.add_argument("-o", "--output", help="output file name")
+ parser.add_argument("--template",
+ default="/usr/share/d3-flame-graph/d3-flamegraph-base.html",
+ help="path to flame graph HTML template")
+ parser.add_argument("--colorscheme", default="blue-green",
+ help="flame graph color scheme", choices=["blue-green", "orange"])
+ parser.add_argument("-i", "--input", help="input perf.data file")
+ parser.add_argument("--allow-download", default=False, action="store_true",
+ help="allow unprompted downloading of HTML template")
+ parser.add_argument("-e", "--event", default="", dest="event_name", type=str,
+ help="specify the event to generate flamegraph for")
+
+ cli_args = parser.parse_args()
+ cli = FlameGraphCLI(cli_args)
+ cli.run()
diff --git a/tools/perf/python/futex-contention.py b/tools/perf/python/futex-contention.py
new file mode 100755
index 000000000000..097663d929b7
--- /dev/null
+++ b/tools/perf/python/futex-contention.py
@@ -0,0 +1,107 @@
+#!/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
diff --git a/tools/perf/python/gecko.py b/tools/perf/python/gecko.py
new file mode 100755
index 000000000000..3427e0770156
--- /dev/null
+++ b/tools/perf/python/gecko.py
@@ -0,0 +1,411 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+gecko.py - Convert perf record output to Firefox's gecko profile format
+"""
+from __future__ import annotations
+
+import argparse
+import functools
+import json
+import os
+import sys
+import tempfile
+import threading
+import urllib.parse
+import uuid
+import webbrowser
+from dataclasses import dataclass, field
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from typing import Dict, List, NamedTuple, Optional, Tuple
+
+import perf
+
+
+# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L156
+class Frame(NamedTuple):
+ """A single stack frame in the gecko profile format."""
+ string_id: int
+ relevantForJS: bool
+ innerWindowID: int
+ implementation: None
+ optimizations: None
+ line: None
+ column: None
+ category: int
+ subcategory: Optional[int]
+
+
+# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L216
+class Stack(NamedTuple):
+ """A single stack in the gecko profile format."""
+ prefix_id: Optional[int]
+ frame_id: int
+
+
+# https://github.com/firefox-devtools/profiler/blob/53970305b51b9b472e26d7457fee1d66cd4e2737/src/types/gecko-profile.js#L90
+class Sample(NamedTuple):
+ """A single sample in the gecko profile format."""
+ stack_id: Optional[int]
+ time_ms: float
+ responsiveness: int
+
+
+@dataclass
+class Tables:
+ """Interned tables for the gecko profile format."""
+ frame_table: List[Frame] = field(default_factory=list)
+ string_table: List[str] = field(default_factory=list)
+ string_map: Dict[str, int] = field(default_factory=dict)
+ stack_table: List[Stack] = field(default_factory=list)
+ stack_map: Dict[Tuple[Optional[int], int], int] = field(default_factory=dict)
+ frame_map: Dict[str, int] = field(default_factory=dict)
+
+
+@dataclass
+class Thread:
+ """A builder for a profile of the thread."""
+ comm: str
+ pid: int
+ tid: int
+ user_category: int
+ kernel_category: int
+ samples: List[Sample] = field(default_factory=list)
+ tables: Tables = field(default_factory=Tables)
+
+ def _intern_stack(self, frame_id: int, prefix_id: Optional[int]) -> int:
+ """Gets a matching stack, or saves the new stack. Returns a Stack ID."""
+ key = (prefix_id, frame_id)
+ stack_id = self.tables.stack_map.get(key)
+ if stack_id is None:
+ stack_id = len(self.tables.stack_table)
+ self.tables.stack_table.append(Stack(prefix_id=prefix_id, frame_id=frame_id))
+ self.tables.stack_map[key] = stack_id
+ return stack_id
+
+ def _intern_string(self, string: str) -> int:
+ """Gets a matching string, or saves the new string. Returns a String ID."""
+ string_id = self.tables.string_map.get(string)
+ if string_id is not None:
+ return string_id
+ string_id = len(self.tables.string_table)
+ self.tables.string_table.append(string)
+ self.tables.string_map[string] = string_id
+ return string_id
+
+ def _intern_frame(self, frame_str: str) -> int:
+ """Gets a matching stack frame, or saves the new frame. Returns a Frame ID."""
+ frame_id = self.tables.frame_map.get(frame_str)
+ if frame_id is not None:
+ return frame_id
+ frame_id = len(self.tables.frame_table)
+ self.tables.frame_map[frame_str] = frame_id
+ string_id = self._intern_string(frame_str)
+
+ category = self.user_category
+ if (frame_str.find('kallsyms') != -1 or
+ frame_str.find('/vmlinux') != -1 or
+ frame_str.endswith('.ko)')):
+ category = self.kernel_category
+
+ self.tables.frame_table.append(Frame(
+ string_id=string_id,
+ relevantForJS=False,
+ innerWindowID=0,
+ implementation=None,
+ optimizations=None,
+ line=None,
+ column=None,
+ category=category,
+ subcategory=None,
+ ))
+ return frame_id
+
+ def add_sample(self, comm: str, stack: List[str], time_ms: float) -> None:
+ """Add a timestamped stack trace sample to the thread builder."""
+ if self.comm != comm:
+ self.comm = comm
+
+ prefix_stack_id: Optional[int] = None
+ for frame in stack:
+ frame_id = self._intern_frame(frame)
+ prefix_stack_id = self._intern_stack(frame_id, prefix_stack_id)
+
+ if prefix_stack_id is not None:
+ self.samples.append(Sample(stack_id=prefix_stack_id,
+ time_ms=time_ms,
+ responsiveness=0))
+
+ def to_json_dict(self) -> Dict:
+ """Converts current Thread to GeckoThread JSON format."""
+ return {
+ "tid": self.tid,
+ "pid": self.pid,
+ "name": self.comm,
+ "markers": {
+ "schema": {
+ "name": 0,
+ "startTime": 1,
+ "endTime": 2,
+ "phase": 3,
+ "category": 4,
+ "data": 5,
+ },
+ "data": [],
+ },
+ "samples": {
+ "schema": {
+ "stack": 0,
+ "time": 1,
+ "responsiveness": 2,
+ },
+ "data": self.samples
+ },
+ "frameTable": {
+ "schema": {
+ "location": 0,
+ "relevantForJS": 1,
+ "innerWindowID": 2,
+ "implementation": 3,
+ "optimizations": 4,
+ "line": 5,
+ "column": 6,
+ "category": 7,
+ "subcategory": 8,
+ },
+ "data": self.tables.frame_table,
+ },
+ "stackTable": {
+ "schema": {
+ "prefix": 0,
+ "frame": 1,
+ },
+ "data": self.tables.stack_table,
+ },
+ "stringTable": self.tables.string_table,
+ "registerTime": 0,
+ "unregisterTime": None,
+ "processType": "default",
+ }
+
+
+class CORSRequestHandler(SimpleHTTPRequestHandler):
+ """Enable CORS for requests from profiler.firefox.com."""
+ def __init__(self, *args, allowed_file: Optional[str] = None, **kwargs):
+ self.allowed_file = allowed_file
+ super().__init__(*args, **kwargs)
+
+ def end_headers(self):
+ self.send_header('Access-Control-Allow-Origin', 'https://profiler.firefox.com')
+ super().end_headers()
+
+ def do_GET(self):
+ if self.allowed_file and self.path.split('?')[0] != f'/{self.allowed_file}':
+ self.send_error(403, "Access denied")
+ return
+ super().do_GET()
+
+ def list_directory(self, path):
+ self.send_error(403, "Directory listing forbidden")
+ return None
+
+
+@dataclass
+class CategoryData:
+ """Category configuration for the gecko profile."""
+ user_index: int = 0
+ kernel_index: int = 1
+ categories: List[Dict] = field(default_factory=list)
+
+
+class GeckoCLI:
+ """Command-line interface for converting perf data to Gecko format."""
+ def __init__(self, args: argparse.Namespace) -> None:
+ self.args = args
+ self.tid_to_thread: Dict[int, Thread] = {}
+ self.start_time_ms: Optional[float] = None
+ self.session: Optional[perf.session] = None
+ self.product = args.product
+ self.cat_data = CategoryData(
+ categories=[
+ {
+ "name": 'User',
+ "color": args.user_color,
+ "subcategories": ['Other']
+ },
+ {
+ "name": 'Kernel',
+ "color": args.kernel_color,
+ "subcategories": ['Other']
+ },
+ ]
+ )
+
+ def process_event(self, sample) -> None:
+ """Process a single perf sample event."""
+ if self.args.event_name and self.args.event_name not in str(sample.evsel):
+ return
+
+ # sample_time is in nanoseconds. Gecko wants milliseconds.
+ time_ms = sample.sample_time / 1000000.0
+ pid = sample.sample_pid
+ tid = sample.sample_tid
+
+ if self.start_time_ms is None:
+ self.start_time_ms = time_ms
+
+ try:
+ thread_info = self.session.find_thread(sample.sample_pid, sample.sample_tid) if self.session else None
+ comm = (thread_info.comm() if thread_info is not None else None) or "[unknown]"
+ except AttributeError:
+ comm = "[unknown]"
+
+ stack = []
+ callchain = sample.callchain
+ if callchain:
+ for entry in callchain:
+ symbol = entry.symbol or "[unknown]"
+ dso = entry.dso or "[unknown]"
+ stack.append(f"{symbol} (in {dso})")
+ # Reverse because Gecko wants root first.
+ stack.reverse()
+ else:
+ symbol = sample.symbol or "[unknown]"
+ dso = sample.dso or "[unknown]"
+ stack.append(f"{symbol} (in {dso})")
+
+ thread = self.tid_to_thread.get(tid)
+ if thread is None:
+ thread = Thread(comm=comm, pid=pid, tid=tid,
+ user_category=self.cat_data.user_index,
+ kernel_category=self.cat_data.kernel_index)
+ self.tid_to_thread[tid] = thread
+ thread.add_sample(comm=comm, stack=stack, time_ms=time_ms)
+
+ def run(self) -> None:
+ """Run the conversion process."""
+ input_file = self.args.input or "perf.data"
+ if input_file != "-" and not os.path.exists(input_file):
+ print(f"Error: {input_file} not found.", file=sys.stderr)
+ sys.exit(1)
+
+ try:
+ self.session = perf.session(perf.data(input_file), sample=self.process_event)
+ except (OSError, RuntimeError, ValueError) as e:
+ print(f"Error opening session: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ if self.session:
+ try:
+ self.session.process_events()
+ finally:
+ # Break the reference cycle between perf.session and self.process_event
+ # because perf.session lacks cyclic GC support (tp_traverse).
+ self.session = None
+
+ threads = [t.to_json_dict() for t in self.tid_to_thread.values()]
+
+ gecko_profile = {
+ "meta": {
+ "interval": 1,
+ "processType": 0,
+ "product": self.product,
+ "stackwalk": 1,
+ "debug": 0,
+ "gcpoison": 0,
+ "asyncstack": 1,
+ "startTime": self.start_time_ms or 0,
+ "shutdownTime": None,
+ "version": 24,
+ "presymbolicated": True,
+ "categories": self.cat_data.categories,
+ "markerSchema": [],
+ },
+ "libs": [],
+ "threads": threads,
+ "processes": [],
+ "pausedRanges": [],
+ }
+
+ output_file = self.args.save_only
+ if output_file is None:
+ self._write_and_launch(gecko_profile)
+ else:
+ print(f'[ perf gecko: Captured and wrote into {output_file} ]')
+ with open(output_file, 'w', encoding='utf-8') as f:
+ json.dump(gecko_profile, f, indent=2)
+
+ def _write_and_launch(self, profile: Dict) -> None:
+ """Write the profile to a file and launch the Firefox profiler."""
+ print("Starting Firefox Profiler on your default browser...")
+
+ with tempfile.TemporaryDirectory() as tmp_dir_name:
+ profile_name = f'gecko_profile_{uuid.uuid4().hex}.json'
+ filename = os.path.join(tmp_dir_name, profile_name)
+
+ with open(filename, 'w', encoding='utf-8') as f:
+ json.dump(profile, f, indent=2)
+
+ handler = functools.partial(
+ CORSRequestHandler, directory=tmp_dir_name, allowed_file=profile_name
+ )
+ try:
+ httpd = HTTPServer(('127.0.0.1', 0), handler)
+ except OSError as e:
+ print(f"Error starting HTTP server: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ port = httpd.server_port
+
+ def start_server():
+ httpd.serve_forever()
+
+ thread = threading.Thread(target=start_server, daemon=True)
+ thread.start()
+
+ safe_string = urllib.parse.quote_plus(f'http://127.0.0.1:{port}/{profile_name}')
+ url = f'https://profiler.firefox.com/from-url/{safe_string}'
+ print(f"Please open the following URL in your browser to view the profile:\n\n{url}\n")
+ launch_html = os.path.join(tmp_dir_name, 'launch.html')
+ with open(launch_html, 'w', encoding='utf-8') as f:
+ f.write(f'<!DOCTYPE html><html><head>'
+ f'<meta http-equiv="refresh" content="0;url={url}">'
+ f'<script>window.location.replace({json.dumps(url)});</script>'
+ f'</head><body>Redirecting to Firefox Profiler...</body></html>')
+ if not webbrowser.open(f'file://{launch_html}'):
+ print("Failed to open browser, please open the URL manually.")
+
+ print(f'[ perf gecko: Captured and wrote into {filename} ]')
+ print("Press Ctrl+C to stop the local server.")
+ try:
+ # Keep the main thread alive so the daemon thread can serve requests
+ stop_event = threading.Event()
+ while True:
+ stop_event.wait(1)
+ except KeyboardInterrupt:
+ print("\nStopping server...")
+ httpd.shutdown()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Convert perf.data to Firefox's Gecko Profile format"
+ )
+ parser.add_argument('--product', default='perf', help='Product name (e.g. perf)')
+ parser.add_argument('--user-color', default='yellow',
+ help='Color for the User category',
+ choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red',
+ 'grey', 'magenta'])
+ parser.add_argument('--kernel-color', default='orange',
+ help='Color for the Kernel category',
+ choices=['yellow', 'blue', 'purple', 'green', 'orange', 'red',
+ 'grey', 'magenta'])
+ parser.add_argument('--save-only',
+ help='Save the output to a file instead of opening Firefox\'s profiler')
+ parser.add_argument("-i", "--input", help="input perf.data file")
+ parser.add_argument("-e", "--event", default="", dest="event_name", type=str,
+ help="specify the event to generate gecko profile for")
+
+ cli_args = parser.parse_args()
+ cli = GeckoCLI(cli_args)
+ cli.run()
diff --git a/tools/perf/python/ilist.py b/tools/perf/python/ilist.py
index 0d757ddb4795..86bd65452e6e 100755
--- a/tools/perf/python/ilist.py
+++ b/tools/perf/python/ilist.py
@@ -2,11 +2,13 @@
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
"""Interactive perf list."""
-from abc import ABC, abstractmethod
+from __future__ import annotations
+
import argparse
+from abc import ABC, abstractmethod
from dataclasses import dataclass
import math
-from typing import Any, Dict, Optional, Tuple
+from typing import Any, Optional
import perf
from textual import on
from textual.app import App, ComposeResult
@@ -19,7 +21,7 @@ from textual.widgets import Button, Footer, Header, Input, Label, Sparkline, Sta
from textual.widgets.tree import TreeNode
-def get_info(info: Dict[str, str], key: str):
+def get_info(info: dict[str, Any], key: str):
return (info[key] + "\n") if key in info else ""
@@ -80,7 +82,7 @@ class Metric(TreeValue):
try:
val = evlist.compute_metric(self.metric_name, cpu, thread)
return 0 if math.isnan(val) else val
- except:
+ except (OSError, ValueError, RuntimeError, TypeError):
# Be tolerant of failures to compute metrics on particular CPUs/threads.
return 0
@@ -140,7 +142,7 @@ class ErrorScreen(ModalScreen[bool]):
def compose(self) -> ComposeResult:
yield Button(f"Error: {self.error}", variant="primary", id="error")
- def on_button_pressed(self, event: Button.Pressed) -> None:
+ def on_button_pressed(self) -> None:
self.dismiss(True)
@@ -234,7 +236,7 @@ class IListApp(App):
def __init__(self, interval: float) -> None:
self.interval = interval
- self.evlist = None
+ self.evlist: Optional[perf.evlist] = None
self.selected: Optional[TreeValue] = None
self.search_results: list[TreeNode[TreeValue]] = []
self.cur_search_result: TreeNode[TreeValue] | None = None
@@ -284,18 +286,18 @@ class IListApp(App):
"""Sets the focus after the SearchScreen is dismissed."""
search_label = self.query_one("#active_search", Label)
- search_label.display = True if event else False
+ search_label.display = bool(event)
if not event:
return
event = event.lower()
search_label.update(f'Searching for events matching "{event}"')
- tree: Tree[str] = self.query_one("#root", Tree)
+ tree: Tree[TreeValue] = self.query_one("#root", Tree)
- def find_search_results(event: str, node: TreeNode[str],
+ def find_search_results(event: str, node: TreeNode[TreeValue],
cursor_seen: bool = False,
- match_after_cursor: Optional[TreeNode[str]] = None
- ) -> Tuple[bool, Optional[TreeNode[str]]]:
+ match_after_cursor: Optional[TreeNode[TreeValue]] = None
+ ) -> tuple[bool, Optional[TreeNode[TreeValue]]]:
"""Find nodes that match the search remembering the one after the cursor."""
if not cursor_seen and node == tree.cursor_node:
cursor_seen = True
@@ -332,7 +334,7 @@ class IListApp(App):
def action_collapse(self) -> None:
"""Collapse the part of the tree currently on."""
- tree: Tree[str] = self.query_one("#root", Tree)
+ tree: Tree[TreeValue] = self.query_one("#root", Tree)
node = tree.cursor_node
if node and node.parent:
node.parent.collapse_all()
@@ -343,31 +345,34 @@ class IListApp(App):
if not self.selected or not self.evlist:
return
- def update_count(cpu: int, count: int):
+ def update_count(cpu: int, count: int | float):
# Update the raw count display.
- counter: Label = self.query(f"#counter_cpu{cpu}" if cpu >= 0 else "#counter_total")
- if not counter:
+ counter_query = self.query(f"#counter_cpu{cpu}" if cpu >= 0 else "#counter_total")
+ if not counter_query:
return
- counter = counter.first(Label)
+ counter: Label = counter_query.first(Label)
counter.update(str(count))
# Update the sparkline.
- line: Sparkline = self.query(f"#sparkline_cpu{cpu}" if cpu >= 0 else "#sparkline_total")
- if not line:
+ line_query = self.query(f"#sparkline_cpu{cpu}" if cpu >= 0 else "#sparkline_total")
+ if not line_query:
return
- line = line.first(Sparkline)
+ line: Sparkline = line_query.first(Sparkline)
# If there are more events than the width, remove the front event.
- if len(line.data) > line.size.width:
- line.data.pop(0)
- line.data.append(count)
- line.mutate_reactive(Sparkline.data)
+ if line.data is not None:
+ if len(line.data) > line.size.width:
+ line.data = line.data[1:]
+ line.data = list(line.data) + [float(count)]
+ else:
+ line.data = [float(count)]
+ line.refresh()
# Update the total and each CPU counts, assume there's just 1 evsel.
- total = 0
+ total: float = 0.0
self.evlist.disable()
for evsel in self.evlist:
for cpu in evsel.cpus():
- aggr = 0
+ aggr: float = 0.0
for thread in evsel.threads():
aggr += self.selected.value(self.evlist, evsel, cpu, thread)
update_count(cpu, aggr)
@@ -414,7 +419,7 @@ class IListApp(App):
if self.evlist:
self.evlist.open()
self.evlist.enable()
- except:
+ except (OSError, ValueError, RuntimeError):
self.evlist = None
if not self.evlist:
@@ -424,16 +429,16 @@ class IListApp(App):
# Add spark lines for all the CPUs. Note, must be done after
# open so that the evlist CPUs have been computed by propagate
# maps.
- line = CounterSparkline(cpu=-1)
- lines.mount(line)
+ line_sp = CounterSparkline(cpu=-1)
+ lines.mount(line_sp)
for cpu in self.evlist.all_cpus():
- line = CounterSparkline(cpu)
- lines.mount(line)
- line = Counter(cpu=-1)
- lines.mount(line)
+ c_sp = CounterSparkline(cpu)
+ lines.mount(c_sp)
+ c_val = Counter(cpu=-1)
+ lines.mount(c_val)
for cpu in self.evlist.all_cpus():
- line = Counter(cpu)
- lines.mount(line)
+ c_val2 = Counter(cpu)
+ lines.mount(c_val2)
def compose(self) -> ComposeResult:
"""Draws the app."""
@@ -445,7 +450,7 @@ class IListApp(App):
pmu_name = pmu.name().lower()
pmu_node = pmus.add(pmu_name)
try:
- for event in sorted(pmu.events(), key=lambda x: x["name"]):
+ for event in sorted(pmu.events(), key=lambda x: x.get("name", "")):
if "deprecated" in event:
continue
if "name" in event:
@@ -455,21 +460,22 @@ class IListApp(App):
data=PmuEvent(pmu_name, e))
else:
pmu_node.add_leaf(e, data=PmuEvent(pmu_name, e))
- except:
+ except (OSError, ValueError, RuntimeError, KeyError, TypeError):
# Reading events may fail with EPERM, ignore.
pass
metrics = tree.root.add("Metrics")
- groups = set()
+ groups: set[str] = set()
for metric in perf.metrics():
groups.update(metric["MetricGroup"])
- def add_metrics_to_tree(node: TreeNode[TreeValue], parent: str, pmu: str = None):
+ def add_metrics_to_tree(node: TreeNode[TreeValue], parent: str, pmu: Optional[str] = None):
for metric in sorted(perf.metrics(), key=lambda x: x["MetricName"]):
- metric_pmu = metric.get('PMU')
+ metric_pmu_raw = metric.get('PMU')
+ metric_pmu = str(metric_pmu_raw) if metric_pmu_raw else ''
if pmu and metric_pmu and metric_pmu != pmu:
continue
if parent in metric["MetricGroup"]:
- name = metric["MetricName"]
+ name = str(metric["MetricName"])
display_name = name
if metric_pmu:
display_name += f" ({metric_pmu})"
diff --git a/tools/perf/python/intel-pt-events.py b/tools/perf/python/intel-pt-events.py
new file mode 100755
index 000000000000..c233a29eb8f9
--- /dev/null
+++ b/tools/perf/python/intel-pt-events.py
@@ -0,0 +1,632 @@
+#!/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("<IQ", raw_buf)
+ flags = data[0]
+ payload = data[1]
+ except struct.error:
+ return
+ exact_ip = flags & 1
+ try:
+ s = payload.to_bytes(8, "little").decode("ascii").rstrip("\x00")
+ if not s.isprintable():
+ s = ""
+ except (UnicodeDecodeError, ValueError):
+ s = ""
+ print(f"IP: {exact_ip} payload: {payload:#x} {s}", end=' ')
+
+ def print_cbr(self, raw_buf: bytes) -> None:
+ """Print CBR data."""
+ if len(raw_buf) < 12:
+ return
+ try:
+ data = struct.unpack_from("<BBBBII", raw_buf)
+ except struct.error:
+ return
+ cbr = data[0]
+ f = (data[4] + 500) // 1000
+ if data[2] == 0:
+ return
+ p = ((cbr * 1000 // data[2]) + 5) // 10
+ print(f"{cbr:3d} freq: {f:4d} MHz ({p:3d}%)", end=' ')
+
+ def print_mwait(self, raw_buf: bytes) -> None:
+ """Print MWAIT data."""
+ try:
+ data = struct.unpack_from("<IQ", raw_buf)
+ except struct.error:
+ return
+ payload = data[1]
+ hints = payload & 0xff
+ extensions = (payload >> 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("<IQ", raw_buf)
+ except struct.error:
+ return
+ payload = data[1]
+ hw = (payload >> 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("<I", raw_buf)
+ except struct.error:
+ return
+ flags = data[0]
+ exact_ip = flags & 1
+ print(f"IP: {exact_ip}", end=' ')
+
+ def print_pwrx(self, raw_buf: bytes) -> None:
+ """Print PWRX data."""
+ try:
+ data = struct.unpack_from("<IQ", raw_buf)
+ except struct.error:
+ return
+ payload = data[1]
+ deepest_cstate = payload & 0xf
+ last_cstate = (payload >> 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("<IQ", raw_buf)
+ except struct.error:
+ return
+ offset = data[1]
+ print(f"offset: {offset:#x}", end=' ')
+
+ def print_evt(self, raw_buf: bytes) -> 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("<BBH", raw_buf)
+ except struct.error:
+ return
+ typ = data[0] & 0x1f
+ ip_flag = (data[0] & 0x80) >> 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("<QQ", raw_buf, pos)
+ except struct.error:
+ return
+ et = data[0] & 0x3f
+ s = glb_evd[et]
+ if s:
+ print(f"{s}: {data[1]:#x}", end=' ')
+ else:
+ print(f"EVD_{et}: {data[1]:#x}", end=' ')
+ pos += 16
+
+ def print_iflag(self, raw_buf: bytes) -> None:
+ """Print IFLAG data."""
+ try:
+ data = struct.unpack_from("<IQ", raw_buf)
+ except struct.error:
+ return
+ iflag = data[0] & 1
+ old_iflag = iflag ^ 1
+ via_branch = data[0] & 2
+ s = "via" if via_branch else "non"
+ print(f"IFLAG: {old_iflag}->{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) + " <source not found>"
+ 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)
diff --git a/tools/perf/python/libxed.py b/tools/perf/python/libxed.py
new file mode 100755
index 000000000000..900bd3e0184b
--- /dev/null
+++ b/tools/perf/python/libxed.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Python wrapper for libxed.so
+Ported from tools/perf/scripts/python/libxed.py
+"""
+from __future__ import annotations
+
+from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \
+ c_void_p, c_byte, c_int, c_uint, c_ulonglong
+
+# To use Intel XED, libxed.so must be present. To build and install
+# libxed.so:
+# git clone https://github.com/intelxed/mbuild.git mbuild
+# git clone https://github.com/intelxed/xed
+# cd xed
+# ./mfile.py --share
+# sudo ./mfile.py --prefix=/usr/local install
+# sudo ldconfig
+#
+
+
+class XedStateT(Structure):
+ """xed_state_t structure."""
+ _fields_ = [
+ ("mode", c_int),
+ ("width", c_int)
+ ]
+
+
+class XEDInstruction():
+ """Represents a decoded instruction."""
+
+ def __init__(self, libxed):
+ # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
+ xedd_t = c_byte * 512
+ self.xedd = xedd_t()
+ self.xedp = addressof(self.xedd)
+ libxed.xed_decoded_inst_zero(self.xedp)
+ self.state = XedStateT()
+ self.statep = addressof(self.state)
+ # Buffer for disassembled instruction text
+ self.buffer = create_string_buffer(256)
+ self.bufferp = addressof(self.buffer)
+
+
+class LibXED():
+ """Wrapper for libxed.so."""
+
+ def __init__(self):
+ try:
+ self.libxed = CDLL("libxed.so")
+ except OSError:
+ self.libxed = None
+ if not self.libxed:
+ try:
+ self.libxed = CDLL("/usr/local/lib/libxed.so")
+ except OSError:
+ self.libxed = None
+
+ if not self.libxed:
+ raise ImportError("libxed.so not found. Please install Intel XED.")
+
+ self.xed_tables_init = self.libxed.xed_tables_init
+ self.xed_tables_init.restype = None
+ self.xed_tables_init.argtypes = []
+
+ self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
+ self.xed_decoded_inst_zero.restype = None
+ self.xed_decoded_inst_zero.argtypes = [c_void_p]
+
+ self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
+ self.xed_operand_values_set_mode.restype = None
+ self.xed_operand_values_set_mode.argtypes = [c_void_p, c_void_p]
+
+ self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
+ self.xed_decoded_inst_zero_keep_mode.restype = None
+ self.xed_decoded_inst_zero_keep_mode.argtypes = [c_void_p]
+
+ self.xed_decode = self.libxed.xed_decode
+ self.xed_decode.restype = c_int
+ self.xed_decode.argtypes = [c_void_p, c_void_p, c_uint]
+
+ self.xed_format_context = self.libxed.xed_format_context
+ self.xed_format_context.restype = c_uint
+ self.xed_format_context.argtypes = [
+ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p
+ ]
+
+ self.xed_decoded_inst_get_length = self.libxed.xed_decoded_inst_get_length
+ self.xed_decoded_inst_get_length.restype = c_uint
+ self.xed_decoded_inst_get_length.argtypes = [c_void_p]
+
+ self.xed_tables_init()
+
+ def instruction(self):
+ """Create a new XEDInstruction."""
+ return XEDInstruction(self)
+
+ def set_mode(self, inst, mode):
+ """Set 32-bit or 64-bit mode."""
+ if mode:
+ inst.state.mode = 4 # 32-bit
+ inst.state.width = 4 # 4 bytes
+ else:
+ inst.state.mode = 1 # 64-bit
+ inst.state.width = 8 # 8 bytes
+ self.xed_operand_values_set_mode(inst.xedp, inst.statep)
+
+ def disassemble_one(self, inst, bytes_ptr, bytes_cnt, ip):
+ """Disassemble one instruction."""
+ self.xed_decoded_inst_zero_keep_mode(inst.xedp)
+ err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
+ if err:
+ return 0, ""
+ # Use AT&T mode (2), alternative is Intel (3)
+ ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
+ if not ok:
+ return 0, ""
+
+ result = inst.buffer.value.decode('utf-8')
+ # Return instruction length and the disassembled instruction text
+ return self.xed_decoded_inst_get_length(inst.xedp), result
diff --git a/tools/perf/python/mem-phys-addr.py b/tools/perf/python/mem-phys-addr.py
new file mode 100755
index 000000000000..53ef7e9a14b5
--- /dev/null
+++ b/tools/perf/python/mem-phys-addr.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""mem-phys-addr.py: Resolve physical address samples"""
+from __future__ import annotations
+import argparse
+import bisect
+import collections
+from dataclasses import dataclass
+import re
+from typing import (Dict, List, Optional)
+
+import perf
+
+@dataclass(frozen=True)
+class IomemEntry:
+ """Read from a line in /proc/iomem"""
+ begin: int
+ end: int
+ indent: int
+ label: str
+
+ def __lt__(self, other) -> bool:
+ if isinstance(other, int):
+ return self.begin < other
+ return self.begin < other.begin
+
+ def __gt__(self, other) -> bool:
+ if isinstance(other, int):
+ return self.begin > other
+ return self.begin > other.begin
+
+# Physical memory layout from /proc/iomem. Key is the indent and then
+# a list of ranges.
+iomem: Dict[int, List[IomemEntry]] = collections.defaultdict(list)
+# Child nodes from the iomem parent.
+children: Dict[IomemEntry, List[IomemEntry]] = collections.defaultdict(list)
+# Maximum indent seen before an entry in the iomem file.
+_STATE: Dict[str, int] = {"max_indent": 0}
+# Per-event counts for each range of memory.
+event_counts: Dict[str, collections.Counter] = collections.defaultdict(collections.Counter)
+
+def parse_iomem(iomem_path: str):
+ """Populate iomem from iomem file"""
+ with open(iomem_path, 'r', encoding='ascii') as f:
+ for line in f:
+ line = line.rstrip('\n')
+ if not line or line.isspace():
+ continue
+ indent = 0
+ while indent < len(line) and line[indent] == ' ':
+ indent += 1
+ _STATE["max_indent"] = max(_STATE["max_indent"], indent)
+ m = re.split('-|:', line, maxsplit=2)
+ if len(m) < 3:
+ continue
+ begin = int(m[0].strip(), 16)
+ end = int(m[1].strip(), 16)
+ label = m[2].strip()
+ entry = IomemEntry(begin, end, indent, label)
+ # Before adding entry, search for a parent node using its begin.
+ if indent > 0:
+ parent = find_memory_type(begin)
+ assert parent, f"Given indent expected a parent for {label}"
+ children[parent].append(entry)
+ iomem[indent].append(entry)
+
+def find_memory_type(phys_addr) -> Optional[IomemEntry]:
+ """Search iomem for the range containing phys_addr with the maximum indent"""
+ for i in range(_STATE["max_indent"], -1, -1):
+ if i not in iomem:
+ continue
+ position = bisect.bisect_right(iomem[i], phys_addr)
+ if position == 0:
+ continue
+ iomem_entry = iomem[i][position-1]
+ if iomem_entry.begin <= phys_addr <= iomem_entry.end:
+ return iomem_entry
+ return None
+
+def _print_entries(entries, load_mem_type_cnt, total):
+ """Print counts from parents down to their children"""
+ for entry in sorted(entries,
+ key=lambda e: (-load_mem_type_cnt[e], e.begin)):
+ count = load_mem_type_cnt[entry]
+ if count > 0:
+ mem_type = ' ' * entry.indent + f"{entry.begin:x}-{entry.end:x} : {entry.label}"
+ percent = 100 * count / total
+ print(f"{mem_type:<40} {count:>10} {percent:>10.1f}")
+ _print_entries(children[entry], load_mem_type_cnt, total)
+
+def print_memory_type():
+ """Print the resolved memory types and their counts."""
+ if not event_counts:
+ print("No valid physical address samples found in perf data.")
+ return
+
+ for event_name, load_mem_type_cnt in event_counts.items():
+ print(f"Event: {event_name}")
+ print(f"{'Memory type':<40} {'count':>10} {'percentage':>10}")
+ print(f"{'-' * 40:<40} {'-' * 10:>10} {'-' * 10:>10}")
+ total = sum(load_mem_type_cnt.values())
+ if total == 0:
+ continue
+
+ # Add count from children into the parent.
+ for i in range(_STATE["max_indent"], -1, -1):
+ if i not in iomem:
+ continue
+ for entry in iomem[i]:
+ for child in children[entry]:
+ if load_mem_type_cnt[child] > 0:
+ load_mem_type_cnt[entry] += load_mem_type_cnt[child]
+
+ _print_entries(iomem[0], load_mem_type_cnt, total)
+ print()
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(description="Resolve physical address samples")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ ap.add_argument("--iomem", default="/proc/iomem", help="Path to iomem file")
+ args = ap.parse_args()
+
+ def process_event(sample):
+ """Process a single sample event."""
+ phys_addr = sample.sample_phys_addr or 0
+ if not phys_addr:
+ return
+ entry = find_memory_type(phys_addr)
+ if entry:
+ event_name = str(sample.evsel)
+ if event_name.startswith("evsel(") and event_name.endswith(")"):
+ event_name = event_name[6:-1]
+ event_counts[event_name][entry] += 1
+
+ parse_iomem(args.iomem)
+ perf.session(perf.data(args.input), sample=process_event).process_events()
+ print_memory_type()
diff --git a/tools/perf/python/net_dropmonitor.py b/tools/perf/python/net_dropmonitor.py
new file mode 100755
index 000000000000..fff637b5c7e9
--- /dev/null
+++ b/tools/perf/python/net_dropmonitor.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Monitor the system for dropped packets and produce a report of drop locations and counts.
+Ported from tools/perf/scripts/python/net_dropmonitor.py
+"""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import os
+import sys
+from typing import Tuple
+import perf
+
+
+class DropMonitor:
+ """Monitors dropped packets and aggregates counts by location."""
+
+ def __init__(self, kallsyms_path: str | None = None) -> None:
+ self.drop_log: dict[int, int] = defaultdict(int)
+ self.kallsyms: list[Tuple[int, str]] = []
+ self.kallsyms_parsed = False
+ self.resolved_syms: dict[int, Tuple[str, int]] = {}
+ self.callchain_syms: dict[int, str] = {}
+ self.kallsyms_path = (
+ kallsyms_path
+ or os.environ.get("PERF_SYMBOL_KALLSYMS")
+ or "/proc/kallsyms"
+ )
+
+ def _parse_kallsyms(self) -> None:
+ """Parse the kallsyms file and map kernel addresses to function symbols."""
+ self.kallsyms.clear()
+ self.kallsyms_parsed = True
+ try:
+ with open(self.kallsyms_path, "r", encoding="utf-8") as f:
+ for line in f:
+ parts = line.split()
+ if len(parts) >= 3 and parts[1] in ('t', 'T', 'w', 'W'):
+ addr = int(parts[0], 16)
+ if addr > 0:
+ self.kallsyms.append((addr, parts[2]))
+ self.kallsyms.sort(key=lambda x: x[0])
+ except (FileNotFoundError, PermissionError):
+ print(f"Failed to read {self.kallsyms_path}. Symbols will not be resolved.")
+
+ def _get_sym(self, loc: int) -> Tuple[str, int]:
+ """Resolve a memory location using session symbols or the kallsyms map."""
+ # Priority order:
+ # 1. Exact symbols with offsets resolved directly from the perf.data session
+ # (resolved_syms).
+ # 2. Symbols captured from the sample's callchain in the trace file
+ # (callchain_syms) before falling back to self.kallsyms, so offline
+ # trace symbols are not overwritten by the live host's /proc/kallsyms.
+ # 3. Binary search in self.kallsyms (from --kallsyms or /proc/kallsyms).
+ if loc in self.resolved_syms:
+ return self.resolved_syms[loc]
+ if loc in self.callchain_syms:
+ res = (self.callchain_syms[loc], 0)
+ self.resolved_syms[loc] = res
+ return res
+ if not self.kallsyms:
+ return f"{loc:#x}", 0
+
+ start = 0
+ end = len(self.kallsyms) - 1
+ while start < end:
+ mid = (start + end) // 2
+ if self.kallsyms[mid][0] <= loc < self.kallsyms[mid+1][0]:
+ start = mid
+ break
+ if loc < self.kallsyms[mid][0]:
+ end = mid - 1
+ else:
+ start = mid + 1
+
+ sym_addr, sym_name = self.kallsyms[start]
+ if loc >= sym_addr:
+ res = (sym_name, loc - sym_addr)
+ self.resolved_syms[loc] = res
+ return res
+ return f"{loc:#x}", 0
+
+ def print_drop_table(self) -> None:
+ """Print aggregated results."""
+ if not self.drop_log:
+ print(f"{'LOCATION':>25} {'OFFSET':>25} {'COUNT':>25}")
+ return
+
+ if (not self.kallsyms_parsed
+ and any(loc not in self.resolved_syms and loc not in self.callchain_syms
+ for loc in self.drop_log)):
+ print("Gathering kallsyms data")
+ self._parse_kallsyms()
+
+ print(f"{'LOCATION':>25} {'OFFSET':>25} {'COUNT':>25}")
+ sorted_keys = sorted(self.drop_log.keys())
+ for sloc in sorted_keys:
+ sym, off = self._get_sym(sloc)
+ print(f"{sym:>25} {off:>25d} {self.drop_log[sloc]:>25d}")
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process a single sample event."""
+ if "skb:kfree_skb" not in str(sample.evsel):
+ return
+
+ location = getattr(sample, "location", None)
+ if location is not None:
+ self.drop_log[location] += 1
+ if location not in self.resolved_syms:
+ sym = getattr(sample, "symbol", None)
+ if getattr(sample, "sample_ip", 0) == location and sym and sym != "[unknown]":
+ self.resolved_syms[location] = (
+ sym,
+ getattr(sample, "sym_offset", 0) or 0,
+ )
+ self.callchain_syms.pop(location, None)
+ else:
+ for entry in getattr(sample, "callchain", []) or []:
+ if isinstance(entry, dict):
+ entry_ip = entry.get("ip")
+ sym_info = entry.get("sym")
+ sym_name = sym_info.get("name") if isinstance(sym_info, dict) else None
+ sym_start = sym_info.get("start") if isinstance(sym_info, dict) else None
+ sym_off = (max(0, location - sym_start)
+ if sym_start is not None else None)
+ else:
+ entry_ip = getattr(entry, "ip", None)
+ entry_sym = getattr(entry, "sym", None)
+ sym_name = (
+ getattr(entry_sym, "name", None)
+ or getattr(entry, "symbol", None)
+ )
+ sym_off = getattr(entry, "sym_offset", None)
+ if sym_off is None:
+ sym_start = getattr(entry_sym, "start", None)
+ if sym_start is not None:
+ sym_off = max(0, location - sym_start)
+ if entry_ip == location and sym_name and sym_name != "[unknown]":
+ if sym_off is not None:
+ self.resolved_syms[location] = (sym_name, sym_off)
+ self.callchain_syms.pop(location, None)
+ else:
+ self.callchain_syms[location] = sym_name
+ break
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(
+ description="Monitor the system for dropped packets and produce a "
+ "report of drop locations and counts.")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ ap.add_argument("-k", "--kallsyms", default=None,
+ help="Path to kallsyms file for offline symbol resolution")
+ args = ap.parse_args()
+
+ monitor = DropMonitor(kallsyms_path=args.kallsyms)
+ session = None
+
+ try:
+ session = perf.session(perf.data(args.input), sample=monitor.process_event,
+ kallsyms=args.kallsyms)
+ session.process_events()
+ except KeyboardInterrupt:
+ print("\nStopping trace...")
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e:
+ print(f"Error processing events: {e}")
+ sys.exit(1)
+ finally:
+ session = None
+
+ monitor.print_drop_table()
diff --git a/tools/perf/python/netdev-times.py b/tools/perf/python/netdev-times.py
new file mode 100755
index 000000000000..f0f829987447
--- /dev/null
+++ b/tools/perf/python/netdev-times.py
@@ -0,0 +1,494 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Display a process of packets and processed time.
+It helps us to investigate networking or network device.
+
+Ported from tools/perf/scripts/python/netdev-times.py
+"""
+
+from __future__ import annotations
+import argparse
+from collections import defaultdict
+import sys
+from typing import Optional
+import perf
+
+# Format for displaying rx packet processing
+PF_IRQ_ENTRY = " irq_entry(+%.3fmsec irq=%d:%s)"
+PF_SOFT_ENTRY = " softirq_entry(+%.3fmsec)"
+PF_NAPI_POLL = " napi_poll_exit(+%.3fmsec %s)"
+PF_JOINT = " |"
+PF_WJOINT = " | |"
+PF_NET_RECV = " |---netif_receive_skb(+%.3fmsec skb=%x len=%d)"
+PF_NET_RX = " |---netif_rx(+%.3fmsec skb=%x)"
+PF_CPY_DGRAM = " | skb_copy_datagram_iovec(+%.3fmsec %d:%s)"
+PF_KFREE_SKB = " | kfree_skb(+%.3fmsec location=%x)"
+PF_CONS_SKB = " | consume_skb(+%.3fmsec)"
+
+
+class NetDevTimesAnalyzer:
+ """Analyzes network device events and prints charts."""
+
+ def __init__(self, cfg: argparse.Namespace):
+ self.args = cfg
+ self.session: Optional[perf.session] = None
+ self.show_tx = cfg.tx or (not cfg.tx and not cfg.rx)
+ self.show_rx = cfg.rx or (not cfg.tx and not cfg.rx)
+ self.dev = cfg.dev
+ self.debug = cfg.debug
+ self.buffer_budget = 65536
+ self.irq_dic: dict[int, list[dict]] = defaultdict(list)
+ self.net_rx_dic: dict[int, dict] = {}
+ self.receive_hunk_list: list[dict] = []
+ self.rx_skb_list: list[dict] = []
+ self.tx_queue_list: list[dict] = []
+ self.tx_xmit_list: list[dict] = []
+ self.tx_free_list: list[dict] = []
+
+ self.buffer_budget = 65536
+ self.of_count_rx_skb_list = 0
+ self.of_count_tx_queue_list = 0
+ self.of_count_tx_xmit_list = 0
+
+ def diff_msec(self, src: int, dst: int) -> float:
+ """Calculate a time interval(msec) from src(nsec) to dst(nsec)."""
+ return (dst - src) / 1000000.0
+
+ def print_transmit(self, hunk: dict) -> None:
+ """Display a process of transmitting a packet."""
+ if self.dev and hunk['dev'].find(self.dev) < 0:
+ return
+ queue_t_sec = hunk['queue_t'] // 1000000000
+ queue_t_usec = hunk['queue_t'] % 1000000000 // 1000
+ print(f"{hunk['dev']:7s} {hunk['len']:5d} "
+ f"{queue_t_sec:6d}.{queue_t_usec:06d}sec "
+ f"{self.diff_msec(hunk['queue_t'], hunk['xmit_t']):12.3f}msec "
+ f"{self.diff_msec(hunk['xmit_t'], hunk['free_t']):12.3f}msec")
+
+ def print_receive(self, hunk: dict) -> None:
+ """Display a process of received packets and interrupts."""
+ show_hunk = False
+ irq_list = hunk['irq_list']
+ if not irq_list:
+ return
+ cpu = irq_list[0]['cpu']
+ base_t = irq_list[0]['irq_ent_t']
+
+ if self.dev:
+ for irq in irq_list:
+ if irq['name'].find(self.dev) >= 0:
+ show_hunk = True
+ break
+ else:
+ show_hunk = True
+
+ if not show_hunk:
+ return
+
+ base_t_sec = base_t // 1000000000
+ base_t_usec = base_t % 1000000000 // 1000
+ print(f"{base_t_sec}.{base_t_usec:06d}sec cpu={cpu}")
+ for irq in irq_list:
+ print(PF_IRQ_ENTRY %
+ (self.diff_msec(base_t, irq['irq_ent_t']),
+ irq['irq'], irq['name']))
+ print(PF_JOINT)
+ irq_event_list = irq['event_list']
+ for irq_event in irq_event_list:
+ if irq_event['event'] == 'netif_rx':
+ print(PF_NET_RX %
+ (self.diff_msec(base_t, irq_event['time']),
+ irq_event['skbaddr']))
+ print(PF_JOINT)
+
+ print(PF_SOFT_ENTRY % self.diff_msec(base_t, hunk['sirq_ent_t']))
+ print(PF_JOINT)
+ event_list = hunk['event_list']
+ for i, event in enumerate(event_list):
+ if event['event_name'] == 'napi_poll':
+ print(PF_NAPI_POLL %
+ (self.diff_msec(base_t, event['event_t']),
+ event['dev']))
+ if i == len(event_list) - 1:
+ print("")
+ else:
+ print(PF_JOINT)
+ else:
+ print(PF_NET_RECV %
+ (self.diff_msec(base_t, event['event_t']),
+ event['skbaddr'],
+ event['len']))
+ if 'handle' in event:
+ print(PF_WJOINT)
+ if event['handle'] == "kfree_skb":
+ print(PF_KFREE_SKB %
+ (self.diff_msec(base_t, event['comm_t']),
+ event['location']))
+ elif event['handle'] == "consume_skb":
+ print(PF_CONS_SKB %
+ self.diff_msec(base_t, event['comm_t']))
+ elif event['handle'] == "skb_copy_datagram_iovec":
+ print(PF_CPY_DGRAM %
+ (self.diff_msec(base_t, event['comm_t']),
+ event['pid'], event['comm']))
+ print(PF_JOINT)
+
+ def handle_irq_handler_entry(self, event: dict) -> None:
+ """Handle irq:irq_handler_entry event."""
+ time = event['time']
+ cpu = event['cpu']
+ irq = event['irq']
+ irq_name = event['irq_name']
+ irq_record = {'irq': irq, 'name': irq_name, 'cpu': cpu,
+ 'irq_ent_t': time, 'event_list': []}
+ self.irq_dic[cpu].append(irq_record)
+
+ def handle_irq_handler_exit(self, event: dict) -> None:
+ """Handle irq:irq_handler_exit event."""
+ time = event['time']
+ cpu = event['cpu']
+ irq = event['irq']
+ if cpu not in self.irq_dic or not self.irq_dic[cpu]:
+ return
+ if irq != self.irq_dic[cpu][-1]['irq']:
+ return
+ irq_record = self.irq_dic[cpu].pop()
+ irq_record['irq_ext_t'] = time
+ # if an irq doesn't include NET_RX softirq, drop.
+ if irq_record['event_list']:
+ self.irq_dic[cpu].append(irq_record)
+
+ def handle_irq_softirq_raise(self, event: dict) -> None:
+ """Handle irq:softirq_raise event."""
+ time = event['time']
+ cpu = event['cpu']
+ if cpu not in self.irq_dic or not self.irq_dic[cpu]:
+ return
+ irq_record = self.irq_dic[cpu].pop()
+ irq_record['event_list'].append({'time': time, 'event': 'sirq_raise'})
+ self.irq_dic[cpu].append(irq_record)
+
+ def handle_irq_softirq_entry(self, event: dict) -> None:
+ """Handle irq:softirq_entry event."""
+ time = event['time']
+ cpu = event['cpu']
+ self.net_rx_dic[cpu] = {'sirq_ent_t': time, 'event_list': []}
+
+ def handle_irq_softirq_exit(self, event: dict) -> None:
+ """Handle irq:softirq_exit event."""
+ time = event['time']
+ cpu = event['cpu']
+ irq_list = None
+ event_list = None
+ sirq_ent_t = None
+
+ if cpu in self.irq_dic:
+ irq_list = self.irq_dic[cpu]
+ del self.irq_dic[cpu]
+ if cpu in self.net_rx_dic:
+ sirq_ent_t = self.net_rx_dic[cpu]['sirq_ent_t']
+ event_list = self.net_rx_dic[cpu]['event_list']
+ del self.net_rx_dic[cpu]
+ if not irq_list or event_list is None or sirq_ent_t is None:
+ return
+ rec_data = {'sirq_ent_t': sirq_ent_t, 'sirq_ext_t': time,
+ 'irq_list': irq_list, 'event_list': event_list}
+ self.receive_hunk_list.append(rec_data)
+
+ def handle_napi_poll(self, event: dict) -> None:
+ """Handle napi:napi_poll event."""
+ time = event['time']
+ cpu = event['cpu']
+ dev_name = event['dev_name']
+ work = event['work']
+ budget = event['budget']
+ if cpu in self.net_rx_dic:
+ event_list = self.net_rx_dic[cpu]['event_list']
+ rec_data = {'event_name': 'napi_poll',
+ 'dev': dev_name, 'event_t': time,
+ 'work': work, 'budget': budget}
+ event_list.append(rec_data)
+
+ def handle_netif_rx(self, event: dict) -> None:
+ """Handle net:netif_rx event."""
+ time = event['time']
+ cpu = event['cpu']
+ skbaddr = event['skbaddr']
+ skblen = event['skblen']
+ dev_name = event['dev_name']
+ if cpu not in self.irq_dic or not self.irq_dic[cpu]:
+ return
+ irq_record = self.irq_dic[cpu].pop()
+ irq_record['event_list'].append({'time': time, 'event': 'netif_rx',
+ 'skbaddr': skbaddr, 'skblen': skblen,
+ 'dev_name': dev_name})
+ self.irq_dic[cpu].append(irq_record)
+
+ def handle_netif_receive_skb(self, event: dict) -> None:
+ """Handle net:netif_receive_skb event."""
+ time = event['time']
+ cpu = event['cpu']
+ skbaddr = event['skbaddr']
+ skblen = event['skblen']
+ if cpu in self.net_rx_dic:
+ rec_data = {'event_name': 'netif_receive_skb',
+ 'event_t': time, 'skbaddr': skbaddr, 'len': skblen}
+ event_list = self.net_rx_dic[cpu]['event_list']
+ event_list.append(rec_data)
+ self.rx_skb_list.insert(0, rec_data)
+ if len(self.rx_skb_list) > self.buffer_budget:
+ self.rx_skb_list.pop()
+ self.of_count_rx_skb_list += 1
+
+ def handle_net_dev_queue(self, event: dict) -> None:
+ """Handle net:net_dev_queue event."""
+ time = event['time']
+ skbaddr = event['skbaddr']
+ skblen = event['skblen']
+ dev_name = event['dev_name']
+ skb = {'dev': dev_name, 'skbaddr': skbaddr, 'len': skblen, 'queue_t': time}
+ self.tx_queue_list.insert(0, skb)
+ if len(self.tx_queue_list) > self.buffer_budget:
+ self.tx_queue_list.pop()
+ self.of_count_tx_queue_list += 1
+
+ def handle_net_dev_xmit(self, event: dict) -> None:
+ """Handle net:net_dev_xmit event."""
+ time = event['time']
+ skbaddr = event['skbaddr']
+ rc = event['rc']
+ if rc == 0: # NETDEV_TX_OK
+ for i, skb in enumerate(self.tx_queue_list):
+ if skb['skbaddr'] == skbaddr:
+ skb['xmit_t'] = time
+ self.tx_xmit_list.insert(0, skb)
+ del self.tx_queue_list[i]
+ if len(self.tx_xmit_list) > self.buffer_budget:
+ self.tx_xmit_list.pop()
+ self.of_count_tx_xmit_list += 1
+ return
+
+ def handle_kfree_skb(self, event: dict) -> None:
+ """Handle skb:kfree_skb event."""
+ time = event['time']
+ skbaddr = event['skbaddr']
+ comm = event['comm']
+ pid = event['pid']
+ location = event['location']
+ for i, skb in enumerate(self.tx_queue_list):
+ if skb['skbaddr'] == skbaddr:
+ del self.tx_queue_list[i]
+ return
+ for i, skb in enumerate(self.tx_xmit_list):
+ if skb['skbaddr'] == skbaddr:
+ skb['free_t'] = time
+ self.tx_free_list.append(skb)
+ del self.tx_xmit_list[i]
+ return
+ for i, rec_data in enumerate(self.rx_skb_list):
+ if rec_data['skbaddr'] == skbaddr:
+ rec_data.update({'handle': "kfree_skb",
+ 'comm': comm, 'pid': pid, 'comm_t': time, 'location': location})
+ del self.rx_skb_list[i]
+ return
+
+ def handle_consume_skb(self, event: dict) -> None:
+ """Handle skb:consume_skb event."""
+ time = event['time']
+ skbaddr = event['skbaddr']
+ for i, skb in enumerate(self.tx_xmit_list):
+ if skb['skbaddr'] == skbaddr:
+ skb['free_t'] = time
+ self.tx_free_list.append(skb)
+ del self.tx_xmit_list[i]
+ return
+ for i, rec_data in enumerate(self.rx_skb_list):
+ if rec_data['skbaddr'] == skbaddr:
+ rec_data.update({'handle': "consume_skb", 'comm_t': time})
+ del self.rx_skb_list[i]
+ return
+
+ def handle_skb_copy_datagram_iovec(self, event: dict) -> None:
+ """Handle skb:skb_copy_datagram_iovec event."""
+ time = event['time']
+ skbaddr = event['skbaddr']
+ comm = event['comm']
+ pid = event['pid']
+ for i, rec_data in enumerate(self.rx_skb_list):
+ if skbaddr == rec_data['skbaddr']:
+ rec_data.update({'handle': "skb_copy_datagram_iovec",
+ 'comm': comm, 'pid': pid, 'comm_t': time})
+ del self.rx_skb_list[i]
+ return
+
+ def print_summary(self) -> None:
+ """Print charts."""
+
+ # display receive hunks
+ if self.show_rx:
+ for hunk in self.receive_hunk_list:
+ self.print_receive(hunk)
+
+ # display transmit hunks
+ if self.show_tx:
+ print(" dev len Qdisc "
+ " netdevice free")
+ for hunk in self.tx_free_list:
+ self.print_transmit(hunk)
+
+ if self.debug:
+ print("debug buffer status")
+ print("----------------------------")
+ print(f"xmit Qdisc:remain:{len(self.tx_queue_list)} "
+ f"overflow:{self.of_count_tx_queue_list}")
+ print(f"xmit netdevice:remain:{len(self.tx_xmit_list)} "
+ f"overflow:{self.of_count_tx_xmit_list}")
+ print(f"receive:remain:{len(self.rx_skb_list)} "
+ f"overflow:{self.of_count_rx_skb_list}")
+
+ def handle_single_event(self, event: dict) -> None:
+ """Handle a single processed event."""
+ name = event['name']
+ if name == 'irq:softirq_exit':
+ self.handle_irq_softirq_exit(event)
+ elif name == 'irq:softirq_entry':
+ self.handle_irq_softirq_entry(event)
+ elif name == 'irq:softirq_raise':
+ self.handle_irq_softirq_raise(event)
+ elif name == 'irq:irq_handler_entry':
+ self.handle_irq_handler_entry(event)
+ elif name == 'irq:irq_handler_exit':
+ self.handle_irq_handler_exit(event)
+ elif name == 'napi:napi_poll':
+ self.handle_napi_poll(event)
+ elif name == 'net:netif_receive_skb':
+ self.handle_netif_receive_skb(event)
+ elif name == 'net:netif_rx':
+ self.handle_netif_rx(event)
+ elif name == 'skb:skb_copy_datagram_iovec':
+ self.handle_skb_copy_datagram_iovec(event)
+ elif name == 'net:net_dev_queue':
+ self.handle_net_dev_queue(event)
+ elif name == 'net:net_dev_xmit':
+ self.handle_net_dev_xmit(event)
+ elif name == 'skb:kfree_skb':
+ self.handle_kfree_skb(event)
+ elif name == 'skb:consume_skb':
+ self.handle_consume_skb(event)
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process events directly on-the-fly."""
+ name = str(sample.evsel)
+ ev_name = name[6:-1] if name.startswith("evsel(") else name
+ pid = sample.sample_pid
+ if hasattr(self, 'session') and self.session:
+ try:
+ thread = self.session.find_thread(sample.sample_pid, sample.sample_tid)
+ comm = (thread.comm() if thread else None) or "[unknown]"
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError):
+ comm = "[unknown]"
+ else:
+ comm = "Unknown"
+ event_data = {
+ 'name': ev_name,
+ 'time': sample.sample_time,
+ 'cpu': sample.sample_cpu,
+ 'pid': pid,
+ 'comm': comm,
+ }
+
+ # Extract specific fields based on event type
+ if ev_name.startswith("irq:softirq_"):
+ event_data['vec'] = getattr(sample, "vec", 0)
+ # Filter for NET_RX
+ if event_data['vec'] != 3: # NET_RX_SOFTIRQ is usually 3
+ return
+ elif ev_name == "irq:irq_handler_entry":
+ event_data['irq'] = getattr(sample, "irq", -1)
+ event_data['irq_name'] = getattr(sample, "name", "[unknown]")
+ elif ev_name == "irq:irq_handler_exit":
+ event_data['irq'] = getattr(sample, "irq", -1)
+ event_data['ret'] = getattr(sample, "ret", 0)
+ elif ev_name == "napi:napi_poll":
+ event_data['napi'] = getattr(sample, "napi", 0)
+ event_data['dev_name'] = getattr(sample, "dev_name", "[unknown]")
+ event_data['work'] = getattr(sample, "work", 0)
+ event_data['budget'] = getattr(sample, "budget", 0)
+ elif ev_name in ("net:netif_receive_skb", "net:netif_rx",
+ "net:net_dev_queue"):
+ event_data['skbaddr'] = getattr(sample, "skbaddr", 0)
+ event_data['skblen'] = getattr(sample, "len", 0)
+ event_data['dev_name'] = getattr(sample, "name", "[unknown]")
+ elif ev_name == "net:net_dev_xmit":
+ event_data['skbaddr'] = getattr(sample, "skbaddr", 0)
+ event_data['skblen'] = getattr(sample, "len", 0)
+ event_data['rc'] = getattr(sample, "rc", 0)
+ event_data['dev_name'] = getattr(sample, "name", "[unknown]")
+ elif ev_name == "skb:kfree_skb":
+ event_data['skbaddr'] = getattr(sample, "skbaddr", 0)
+ event_data['location'] = getattr(sample, "location", 0)
+ event_data['protocol'] = getattr(sample, "protocol", 0)
+ event_data['reason'] = getattr(sample, "reason", 0)
+ elif ev_name == "skb:consume_skb":
+ event_data['skbaddr'] = getattr(sample, "skbaddr", 0)
+ event_data['location'] = getattr(sample, "location", 0)
+ elif ev_name == "skb:skb_copy_datagram_iovec":
+ event_data['skbaddr'] = getattr(sample, "skbaddr", 0)
+ event_data['skblen'] = getattr(sample, "len", 0)
+
+ self.handle_single_event(event_data)
+
+
+NetdevTimes = NetDevTimesAnalyzer
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(description="Display a process of packets and processed time.")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ ap.add_argument("--tx", action="store_true", help="show only tx chart")
+ ap.add_argument("--rx", action="store_true", help="show only rx chart")
+ ap.add_argument("--dev", default=None, help="show only specified device")
+ ap.add_argument("--debug", action="store_true",
+ help="work with debug mode. It shows buffer status.")
+ ap.add_argument("positionals", nargs="*",
+ help="optional positional arguments (tx, rx, dev=<name>, debug)")
+ args, unknown_args = ap.parse_known_args()
+
+ parsed_args = argparse.Namespace(
+ tx=args.tx, rx=args.rx, dev=args.dev, debug=args.debug, input=args.input
+ )
+
+ for arg in list(args.positionals) + unknown_args:
+ if not arg or not isinstance(arg, str):
+ continue
+ if arg in ('tx', '--tx'):
+ parsed_args.tx = True
+ elif arg in ('rx', '--rx'):
+ parsed_args.rx = True
+ elif arg.startswith('dev='):
+ parsed_args.dev = arg[4:]
+ elif arg.startswith('--dev='):
+ parsed_args.dev = arg[6:]
+ elif arg in ('debug', '--debug'):
+ parsed_args.debug = True
+
+ analyzer = NetDevTimesAnalyzer(parsed_args)
+
+ try:
+ try:
+ analyzer.session = perf.session(
+ perf.data(parsed_args.input), sample=analyzer.process_event
+ )
+ try:
+ analyzer.session.process_events()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ # Break the reference cycle between analyzer.session and the bound
+ # analyzer.process_event callback so the C perf.session object is freed.
+ analyzer.session = None
+ analyzer.print_summary()
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e:
+ print(f"Error processing events: {e}")
+ sys.exit(1)
diff --git a/tools/perf/python/parallel-perf.py b/tools/perf/python/parallel-perf.py
new file mode 100755
index 000000000000..a43b003634fd
--- /dev/null
+++ b/tools/perf/python/parallel-perf.py
@@ -0,0 +1,1250 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""Parallel perf script."""
+#
+# run a perf script command multiple times in parallel, using perf script
+# options --cpu and --time so that each job processes a different chunk
+# of the data.
+#
+# Copyright (c) 2024, Intel Corporation.
+
+import subprocess
+import argparse
+import pathlib
+import shlex
+import time
+from typing import Any
+import copy
+import sys
+import os
+import re
+
+glb_prog_name = "parallel-perf.py"
+glb_min_interval = 10.0
+glb_min_samples = 64
+
+
+class Verbosity():
+
+ def __init__(self, quiet: bool = False, verbose: bool = False,
+ debug: bool = False) -> None:
+ """__init__."""
+
+ self.normal = True
+ self.verbose = verbose
+ self.debug = debug
+ self.self_test = True
+ if self.debug:
+ self.verbose = True
+ if self.verbose:
+ quiet = False
+ if quiet:
+ self.normal = False
+
+# Manage work (start/wait/kill), as represented by a subprocess.Popen command
+
+
+class Work():
+
+ def __init__(self, cmd: list[str], pipe_to: str,
+ output_dir: str = ".") -> None:
+ """__init__."""
+
+ self.popen: Any = None
+ self.consumer: Any = None
+ self.cmd = cmd
+ self.pipe_to = pipe_to
+ self.output_dir = output_dir
+ self.cmdout_name = f"{output_dir}/cmd.txt"
+ self.stdout_name = f"{output_dir}/out.txt"
+ self.stderr_name = f"{output_dir}/err.txt"
+
+ def command(self):
+ """command."""
+
+ return " ".join(shlex.quote(x) for x in self.cmd)
+
+ def stdout(self):
+ """stdout."""
+
+ return open(self.stdout_name, "w", encoding="utf-8")
+
+ def stderr(self):
+ """stderr."""
+
+ return open(self.stderr_name, "w", encoding="utf-8")
+
+ def create_output_dir(self):
+ """create_output_dir."""
+
+ pathlib.Path(self.output_dir).mkdir(parents=True, exist_ok=True)
+
+ def start(self):
+ """start."""
+
+ if self.popen:
+ return
+ self.create_output_dir()
+ with open(self.cmdout_name, "w", encoding="utf-8") as f:
+ f.write(self.command())
+ f.write("\n")
+ stdout = self.stdout()
+ stderr = self.stderr()
+ if self.pipe_to:
+ self.popen = subprocess.Popen(
+ self.cmd, stdout=subprocess.PIPE, stderr=stderr)
+ args = shlex.split(self.pipe_to)
+ self.consumer = subprocess.Popen(
+ args, stdin=self.popen.stdout, stdout=stdout, stderr=stderr)
+ # The consumer now owns the read end of the pipe. Close this
+ # process's copy so that the producer receives SIGPIPE if the
+ # consumer exits early, and so that the descriptor isn't leaked
+ # for the lifetime of this Work.
+ self.popen.stdout.close()
+ self.popen.stdout = None
+ else:
+ self.popen = subprocess.Popen(
+ self.cmd, stdout=stdout, stderr=stderr)
+
+ def remove_empty_err_file(self):
+ """remove_empty_err_file."""
+
+ if os.path.exists(self.stderr_name):
+ if os.path.getsize(self.stderr_name) == 0:
+ os.unlink(self.stderr_name)
+
+ def errors(self):
+ """errors."""
+
+ if os.path.exists(self.stderr_name):
+ if os.path.getsize(self.stderr_name) != 0:
+ return [f"Non-empty error file {self.stderr_name}"]
+ return []
+
+ def tidy_up(self):
+ """tidy_up."""
+
+ self.remove_empty_err_file()
+
+ def raw_poll_wait(self, p, wait):
+ """raw_poll_wait."""
+
+ if wait:
+ return p.wait()
+ return p.poll()
+
+ def poll(self, wait=False):
+ """poll."""
+
+ if not self.popen:
+ return None
+ result = self.raw_poll_wait(self.popen, wait)
+ if self.consumer:
+ res = result
+ result = self.raw_poll_wait(self.consumer, wait)
+ if result is not None and res is None:
+ self.popen.kill()
+ result = None
+ elif result == 0 and res is not None and res != 0:
+ result = res
+ if result is not None:
+ self.tidy_up()
+ return result
+
+ def wait(self):
+ """wait."""
+
+ return self.poll(wait=True)
+
+ def kill(self):
+ """kill."""
+
+ if not self.popen:
+ return
+ self.popen.kill()
+ if self.consumer:
+ self.consumer.kill()
+
+
+def kill_work(worklist, _verbosity):
+ """kill_work."""
+
+ for w in worklist:
+ w.kill()
+ for w in worklist:
+ w.wait()
+
+
+def number_of_cp_us():
+ """number_of_cp_us."""
+
+ return os.sysconf("SC_NPROCESSORS_ONLN")
+
+
+def nano_secs_to_secs_str(x):
+ """nano_secs_to_secs_str."""
+
+ if x is None:
+ return ""
+ x = str(x)
+ if len(x) < 10:
+ x = "0" * (10 - len(x)) + x
+ return x[:len(x) - 9] + "." + x[-9:]
+
+
+def insert_option_after(cmd, option, after):
+ """insert_option_after."""
+
+ try:
+ pos = cmd.index(after)
+ cmd.insert(pos + 1, option)
+ except (OSError, ValueError, RuntimeError):
+ cmd.append(option)
+
+
+def create_work_list(cmd, pipe_to, output_dir, cpus, time_ranges_by_cpu):
+ """create_work_list."""
+
+ max_len = len(str(cpus[-1]))
+ cpu_dir_fmt = f"cpu-%.{max_len}u"
+ worklist = []
+ pos = 0
+ for cpu in cpus:
+ if cpu >= 0:
+ cpu_dir = os.path.join(output_dir, cpu_dir_fmt % cpu)
+ cpu_option = f"--cpu={cpu}"
+ else:
+ cpu_dir = output_dir
+ cpu_option = None
+
+ tr_dir_fmt = "time-range"
+
+ if len(time_ranges_by_cpu) > 1:
+ time_ranges = time_ranges_by_cpu[pos]
+ tr_dir_fmt += f"-{pos}"
+ pos += 1
+ else:
+ time_ranges = time_ranges_by_cpu[0]
+
+ max_len = len(str(len(time_ranges)))
+ tr_dir_fmt += f"-%.{max_len}u"
+
+ i = 0
+ for r in time_ranges:
+ if r == [None, None]:
+ time_option = None
+ work_output_dir = cpu_dir
+ else:
+ time_option = "--time=" + \
+ nano_secs_to_secs_str(r[0]) + "," + \
+ nano_secs_to_secs_str(r[1])
+ work_output_dir = os.path.join(cpu_dir, tr_dir_fmt % i)
+ i += 1
+ work_cmd = list(cmd)
+ if time_option is not None:
+ insert_option_after(work_cmd, time_option, "script")
+ if cpu_option is not None:
+ insert_option_after(work_cmd, cpu_option, "script")
+ w = Work(work_cmd, pipe_to, work_output_dir)
+ worklist.append(w)
+ return worklist
+
+
+def do_run_work(worklist: list[Work], nr_jobs: int,
+ _verbosity: Verbosity) -> bool:
+ """do_run_work."""
+
+ nr_to_do = len(worklist)
+ not_started = list(worklist)
+ running: list[Work] = []
+ done: list[Work] = []
+ chg = False
+ while True:
+ nr_done = len(done)
+ if chg and _verbosity.normal:
+ nr_run = len(running)
+ print(
+ f"\rThere are {nr_to_do} jobs: {nr_done} completed, {nr_run} running",
+ flush=True, end=" ")
+ if _verbosity.verbose:
+ print()
+ chg = False
+ if nr_done == nr_to_do:
+ break
+ while len(running) < nr_jobs and len(not_started):
+ w = not_started.pop(0)
+ running.append(w)
+ if _verbosity.verbose:
+ print("Starting:", w.command())
+ w.start()
+ chg = True
+ if len(running):
+ time.sleep(0.1)
+ finished: list[Work] = []
+ not_finished: list[Work] = []
+ while len(running):
+ w = running.pop(0)
+ r = w.poll()
+ if r is None:
+ not_finished.append(w)
+ continue
+ if r == 0:
+ if _verbosity.verbose:
+ print("Finished:", w.command())
+ finished.append(w)
+ chg = True
+ continue
+ if _verbosity.normal and not _verbosity.verbose:
+ print()
+ print("Job failed!\n return code:", r,
+ "\n command: ", w.command())
+ if w.pipe_to:
+ print(" piped to: ", w.pipe_to)
+ print("Killing outstanding jobs")
+ kill_work(not_finished, _verbosity)
+ kill_work(running, _verbosity)
+ return False
+ running = not_finished
+ done += finished
+ errorlist: list[str] = []
+ for w in worklist:
+ errorlist += w.errors()
+ if len(errorlist):
+ print("errors:")
+ for e in errorlist:
+ print(e)
+ elif _verbosity.normal:
+ print("\r", " "*50, "\rAll jobs finished successfully", flush=True)
+ return True
+
+
+def run_work(worklist: list[Work], nr_jobs: int = number_of_cp_us(),
+ _verbosity: Verbosity = Verbosity()) -> bool:
+ """run_work."""
+ try:
+ return do_run_work(worklist, nr_jobs, _verbosity)
+ except BaseException:
+ for w in worklist:
+ w.kill()
+ raise
+
+
+def read_header(perf, file_name):
+ """read_header."""
+
+ cmd = [perf, "script", "--header-only", "--input", file_name]
+ with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
+ out = proc.stdout.read() if proc.stdout else b""
+ return out.decode("utf-8")
+
+
+def parse_header(hdr):
+ """parse_header."""
+
+ result = {}
+ lines = hdr.split("\n")
+ for line in lines:
+ if ":" in line and line[0] == "#":
+ pos = line.index(":")
+ name = line[1:pos-1].strip()
+ value = line[pos+1:].strip()
+ if name in result:
+ orig_name = name
+ nr = 2
+ while True:
+ name = f"{orig_name} {nr}"
+ if name not in result:
+ break
+ nr += 1
+ result[name] = value
+ return result
+
+
+def header_field(hdr_dict, hdr_fld):
+ """header_field."""
+
+ if hdr_fld not in hdr_dict:
+ raise RuntimeError(f"'{hdr_fld}' missing from header information")
+ return hdr_dict[hdr_fld]
+
+# Represent the position of an option within a command string
+# and provide the option value and/or remove the option
+
+
+class OptPos():
+
+ def init(self, opt_element=-1, value_element=-1, opt_pos=-1, value_pos=-1, error=None):
+ """init."""
+
+ self.opt_element = opt_element # list element that contains option
+ self.value_element = value_element # list element that contains option value
+ self.opt_pos = opt_pos # string position of option
+ self.value_pos = value_pos # string position of value
+ self.error = error # error message string
+
+ def __init__(self, args, short_name, long_name, default=None):
+ """__init__."""
+
+ self.args = list(args)
+ self.default = default
+ n = 2 + len(long_name)
+ m = len(short_name)
+ pos = -1
+ for opt in args:
+ pos += 1
+ if m and opt[:2] == f"-{short_name}":
+ if len(opt) == 2:
+ if pos + 1 < len(args):
+ self.init(pos, pos + 1, 0, 0)
+ else:
+ self.init(error=f"-{short_name} option missing value")
+ else:
+ self.init(pos, pos, 0, 2)
+ return
+ if opt[:n] == f"--{long_name}":
+ if len(opt) == n:
+ if pos + 1 < len(args):
+ self.init(pos, pos + 1, 0, 0)
+ else:
+ self.init(error=f"--{long_name} option missing value")
+ elif opt[n] == "=":
+ self.init(pos, pos, 0, n + 1)
+ else:
+ self.init(error=f"--{long_name} option expected '='")
+ return
+ if m and opt[:1] == "-" and opt[:2] != "--" and short_name in opt:
+ ipos = opt.index(short_name)
+ if "-" in opt[1:]:
+ hpos = opt[1:].index("-")
+ if hpos < ipos:
+ continue
+ if ipos + 1 == len(opt):
+ if pos + 1 < len(args):
+ self.init(pos, pos + 1, ipos, 0)
+ else:
+ self.init(error=f"-{short_name} option missing value")
+ else:
+ self.init(pos, pos, ipos, ipos + 1)
+ return
+ self.init()
+
+ def value(self):
+ """value."""
+
+ if self.opt_element >= 0:
+ if self.opt_element != self.value_element:
+ return self.args[self.value_element]
+ else:
+ return self.args[self.value_element][self.value_pos:]
+ return self.default
+
+ def remove(self, args):
+ """remove."""
+
+ if self.opt_element == -1:
+ return
+ if self.opt_element != self.value_element:
+ del args[self.value_element]
+ if self.opt_pos:
+ args[self.opt_element] = args[self.opt_element][:self.opt_pos]
+ else:
+ del args[self.opt_element]
+
+
+def determine_input_file_name(cmd):
+ """determine_input_file_name."""
+
+ p = OptPos(cmd, "i", "input", "perf.data")
+ if p.error:
+ raise RuntimeError(f"perf command {p.error}")
+ file_name = p.value()
+ if not os.path.exists(file_name):
+ raise RuntimeError(f"perf command input file '{file_name}' not found")
+ return file_name
+
+
+def read_option(args, short_name, long_name, err_prefix, remove=False):
+ """read_option."""
+
+ p = OptPos(args, short_name, long_name)
+ if p.error:
+ raise RuntimeError(f"{err_prefix}{p.error}")
+ value = p.value()
+ if remove:
+ p.remove(args)
+ return value
+
+
+def extract_option(args, short_name, long_name, err_prefix):
+ """extract_option."""
+
+ return read_option(args, short_name, long_name, err_prefix, True)
+
+
+def read_perf_option(args, short_name, long_name):
+ """read_perf_option."""
+
+ return read_option(args, short_name, long_name, "perf command ")
+
+
+def extract_perf_option(args, short_name, long_name):
+ """extract_perf_option."""
+
+ return extract_option(args, short_name, long_name, "perf command ")
+
+
+def perf_double_quick_commands(cmd, file_name):
+ """perf_double_quick_commands."""
+
+ cpu_str = read_perf_option(cmd, "C", "cpu")
+ time_str = read_perf_option(cmd, "", "time")
+ # Use double-quick sampling to determine trace data density
+ times_cmd = ["perf", "script", "--ns",
+ "--input", file_name, "--itrace=qqi"]
+ if cpu_str is not None and cpu_str != "":
+ times_cmd.append(f"--cpu={cpu_str}")
+ if time_str is not None and time_str != "":
+ times_cmd.append(f"--time={time_str}")
+ cnts_cmd = list(times_cmd)
+ cnts_cmd.append("-Fcpu")
+ times_cmd.append("-Fcpu,time")
+ return cnts_cmd, times_cmd
+
+
+class CPUTimeRange():
+ def __init__(self, cpu):
+ """__init__."""
+
+ self.cpu = cpu
+ self.sample_cnt = 0
+ self.time_ranges = None
+ self.interval = 0
+ self.interval_remaining = 0
+ self.remaining = 0
+ self.tr_pos = 0
+
+
+def calc_time_ranges_by_cpu(line, cpu, cpu_time_ranges, max_time):
+ """calc_time_ranges_by_cpu."""
+
+ cpu_time_range = cpu_time_ranges[cpu]
+ cpu_time_range.remaining -= 1
+ cpu_time_range.interval_remaining -= 1
+ if cpu_time_range.remaining == 0:
+ cpu_time_range.time_ranges[cpu_time_range.tr_pos][1] = max_time
+ return
+ if cpu_time_range.interval_remaining == 0:
+ ts = time_val(line[1][:-1], 0)
+ time_ranges = cpu_time_range.time_ranges
+ time_ranges[cpu_time_range.tr_pos][1] = ts - 1
+ time_ranges.append([ts, max_time])
+ cpu_time_range.tr_pos += 1
+ cpu_time_range.interval_remaining = cpu_time_range.interval
+
+
+def count_samples_by_cpu(_line: list[str], cpu: int,
+ cpu_time_ranges: list[Any]) -> None:
+ """count_samples_by_cpu."""
+
+ try:
+ cpu_time_ranges[cpu].sample_cnt += 1
+ except (OSError, ValueError, RuntimeError, IndexError):
+ print("exception")
+ print("cpu", cpu)
+ print("len(cpu_time_ranges)", len(cpu_time_ranges))
+ raise
+
+
+def process_command_output_lines(cmd, per_cpu, fn, *x):
+ """process_command_output_lines."""
+
+ # Assume CPU number is at beginning of line and enclosed by []
+ pat = re.compile(r"\s*\[[0-9]+\]")
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ while True:
+ line = p.stdout.readline()
+ if line:
+ line = line.decode("utf-8")
+ if pat.match(line):
+ line = line.split()
+ if per_cpu:
+ # Assumes CPU number is enclosed by []
+ cpu = int(line[0][1:-1])
+ else:
+ cpu = 0
+ fn(line, cpu, *x)
+ else:
+ break
+ p.wait()
+
+
+def intersect_time_ranges(new_time_ranges, time_ranges):
+ """intersect_time_ranges."""
+
+ pos = 0
+ new_pos = 0
+ # Can assume len(time_ranges) != 0 and len(new_time_ranges) != 0
+ # Note also, there *must* be at least one intersection.
+ while pos < len(time_ranges) and new_pos < len(new_time_ranges):
+ # new end < old start => no intersection, remove new
+ if new_time_ranges[new_pos][1] < time_ranges[pos][0]:
+ del new_time_ranges[new_pos]
+ continue
+ # new start > old end => no intersection, check next
+ if new_time_ranges[new_pos][0] > time_ranges[pos][1]:
+ pos += 1
+ if pos < len(time_ranges):
+ continue
+ # no next, so remove remaining
+ while new_pos < len(new_time_ranges):
+ del new_time_ranges[new_pos]
+ return
+ # Found an intersection
+ # new start < old start => adjust new start = old start
+ if new_time_ranges[new_pos][0] < time_ranges[pos][0]:
+ new_time_ranges[new_pos][0] = time_ranges[pos][0]
+ # new end > old end => keep the overlap, insert the remainder
+ if new_time_ranges[new_pos][1] > time_ranges[pos][1]:
+ r = [time_ranges[pos][1] + 1, new_time_ranges[new_pos][1]]
+ new_time_ranges[new_pos][1] = time_ranges[pos][1]
+ new_pos += 1
+ new_time_ranges.insert(new_pos, r)
+ continue
+ # new [start, end] is within old [start, end]
+ new_pos += 1
+
+
+def split_time_ranges_by_trace_data_density(
+ time_ranges, cpus, nr, cmd, file_name, per_cpu, min_size, min_interval, _verbosity
+):
+ """split_time_ranges_by_trace_data_density."""
+
+ if _verbosity.normal:
+ print("\rAnalyzing...", flush=True, end=" ")
+ if _verbosity.verbose:
+ print()
+ cnts_cmd, times_cmd = perf_double_quick_commands(cmd, file_name)
+
+ nr_cpus = cpus[-1] + 1 if per_cpu else 1
+ if per_cpu:
+ nr_cpus = cpus[-1] + 1
+ cpu_time_ranges = [CPUTimeRange(cpu) for cpu in range(nr_cpus)]
+ else:
+ nr_cpus = 1
+ cpu_time_ranges = [CPUTimeRange(-1)]
+
+ if _verbosity.debug:
+ print("nr_cpus", nr_cpus)
+ print("cnts_cmd", cnts_cmd)
+ print("times_cmd", times_cmd)
+
+ # Count the number of "double quick" samples per CPU
+ process_command_output_lines(
+ cnts_cmd, per_cpu, count_samples_by_cpu, cpu_time_ranges)
+
+ tot = 0
+ mx = 0
+ for cpu_time_range in cpu_time_ranges:
+ cnt = cpu_time_range.sample_cnt
+ tot += cnt
+ if cnt > mx:
+ mx = cnt
+ if _verbosity.debug:
+ print("cpu:", cpu_time_range.cpu, "sample_cnt", cnt)
+
+ if min_size < 1:
+ min_size = 1
+
+ if mx < min_size:
+ # Too little data to be worth splitting
+ if _verbosity.debug:
+ print("Too little data to split by time")
+ if nr == 0:
+ nr = 1
+ return [split_time_ranges_into_n(time_ranges, nr, min_interval)]
+
+ if nr:
+ divisor = nr
+ min_size = 1
+ else:
+ divisor = number_of_cp_us()
+
+ interval = int(round(tot / divisor, 0))
+ if interval < min_size:
+ interval = min_size
+
+ if _verbosity.debug:
+ print("divisor", divisor)
+ print("min_size", min_size)
+ print("interval", interval)
+
+ min_time = time_ranges[0][0]
+ max_time = time_ranges[-1][1]
+
+ for cpu_time_range in cpu_time_ranges:
+ cnt = cpu_time_range.sample_cnt
+ if cnt == 0:
+ cpu_time_range.time_ranges = copy.deepcopy(time_ranges)
+ continue
+ # Adjust target interval for CPU to give approximately equal interval sizes
+ # Determine number of intervals, rounding to nearest integer
+ n = int(round(cnt / interval, 0))
+ if n < 1:
+ n = 1
+ # Determine interval size, rounding up
+ d, m = divmod(cnt, n)
+ if m:
+ d += 1
+ cpu_time_range.interval = d
+ cpu_time_range.interval_remaining = d
+ cpu_time_range.remaining = cnt
+ # init. time ranges for each CPU with the start time
+ cpu_time_range.time_ranges = [[min_time, max_time]]
+
+ # Set time ranges so that the same number of "double quick" samples
+ # will fall into each time range.
+ process_command_output_lines(
+ times_cmd, per_cpu, calc_time_ranges_by_cpu, cpu_time_ranges, max_time)
+
+ for cpu_time_range in cpu_time_ranges:
+ if cpu_time_range.sample_cnt:
+ intersect_time_ranges(cpu_time_range.time_ranges, time_ranges)
+
+ return [cpu_time_ranges[cpu].time_ranges for cpu in cpus]
+
+
+def split_single_time_range_into_n(time_range, n):
+ """split_single_time_range_into_n."""
+
+ if n <= 1:
+ return [time_range]
+ start = time_range[0]
+ end = time_range[1]
+ duration = int((end - start + 1) / n)
+ if duration < 1:
+ return [time_range]
+ time_ranges = []
+ for _i in range(n):
+ time_ranges.append([start, start + duration - 1])
+ start += duration
+ time_ranges[-1][1] = end
+ return time_ranges
+
+
+def time_range_duration(r):
+ """time_range_duration."""
+
+ return r[1] - r[0] + 1
+
+
+def total_duration(time_ranges):
+ """total_duration."""
+
+ duration = 0
+ for r in time_ranges:
+ duration += time_range_duration(r)
+ return duration
+
+
+def split_time_ranges_by_interval(time_ranges, interval):
+ """split_time_ranges_by_interval."""
+
+ new_ranges = []
+ for r in time_ranges:
+ duration = time_range_duration(r)
+ n = duration / interval
+ n = int(round(n, 0))
+ new_ranges += split_single_time_range_into_n(r, n)
+ return new_ranges
+
+
+def split_time_ranges_into_n(time_ranges, n, min_interval):
+ """split_time_ranges_into_n."""
+
+ if n <= len(time_ranges):
+ return time_ranges
+ duration = total_duration(time_ranges)
+ interval = duration / n
+ if interval < min_interval:
+ interval = min_interval
+ return split_time_ranges_by_interval(time_ranges, interval)
+
+
+def recombine_time_ranges(tr):
+ """recombine_time_ranges."""
+
+ new_tr = copy.deepcopy(tr)
+ i = 1
+ while i < len(new_tr):
+ # if prev end + 1 == cur start, combine them
+ if new_tr[i - 1][1] + 1 == new_tr[i][0]:
+ new_tr[i][0] = new_tr[i - 1][0]
+ del new_tr[i - 1]
+ else:
+ i += 1
+ return new_tr
+
+
+def open_time_range_ends(time_ranges, min_time, max_time):
+ """open_time_range_ends."""
+
+ if time_ranges[0][0] <= min_time:
+ time_ranges[0][0] = None
+ if time_ranges[-1][1] >= max_time:
+ time_ranges[-1][1] = None
+
+
+def bad_time_str(time_str):
+ """bad_time_str."""
+
+ raise RuntimeError(
+ f"perf command bad time option: '{time_str}'\n"
+ "Check also 'time of first sample' and 'time of last sample' "
+ "in perf script --header-only"
+ )
+
+
+def validate_time_ranges(time_ranges, time_str):
+ """validate_time_ranges."""
+
+ n = len(time_ranges)
+ for i in range(n):
+ start = time_ranges[i][0]
+ end = time_ranges[i][1]
+ if i != 0 and start <= time_ranges[i - 1][1]:
+ bad_time_str(time_str)
+ if start > end:
+ bad_time_str(time_str)
+
+
+def time_val(s, dflt):
+ """time_val."""
+
+ s = s.strip()
+ if s == "":
+ return dflt
+ a = s.split(".")
+ if len(a) > 2:
+ raise RuntimeError(f"Bad time value'{s}'")
+ x = int(a[0])
+ if x < 0:
+ raise RuntimeError("Negative time not allowed")
+ x *= 1000000000
+ if len(a) > 1:
+ x += int((a[1] + "000000000")[:9])
+ return x
+
+
+def bad_cpu_str(cpu_str):
+ """bad_cpu_str."""
+
+ raise RuntimeError(
+ f"perf command bad cpu option: '{cpu_str}'\n"
+ "Check also 'nrcpus avail' in perf script --header-only"
+ )
+
+
+def parse_time_str(time_str, min_time, max_time):
+ """parse_time_str."""
+
+ if time_str is None or time_str == "":
+ return [[min_time, max_time]]
+ time_ranges = []
+ for r in time_str.split():
+ a = r.split(",")
+ if len(a) != 2:
+ bad_time_str(time_str)
+ try:
+ start = time_val(a[0], min_time)
+ end = time_val(a[1], max_time)
+ except (OSError, ValueError, RuntimeError):
+ bad_time_str(time_str)
+ time_ranges.append([start, end])
+ validate_time_ranges(time_ranges, time_str)
+ return time_ranges
+
+
+def parse_cpu_str(cpu_str, nr_cpus):
+ """parse_cpu_str."""
+
+ if cpu_str is None or cpu_str == "":
+ return [-1]
+ cpus = []
+ for r in cpu_str.split(","):
+ a = r.split("-")
+ if len(a) < 1 or len(a) > 2:
+ bad_cpu_str(cpu_str)
+ try:
+ start = int(a[0].strip())
+ if len(a) > 1:
+ end = int(a[1].strip())
+ else:
+ end = start
+ except (OSError, ValueError, RuntimeError):
+ bad_cpu_str(cpu_str)
+ if start < 0 or end < 0 or end < start or end >= nr_cpus:
+ bad_cpu_str(cpu_str)
+ cpus.extend(range(start, end + 1))
+ cpus = list(set(cpus)) # remove duplicates
+ cpus.sort()
+ return cpus
+
+
+class ParallelPerf():
+
+ def __init__(self, a):
+ """init."""
+ self.nr = 0
+ self.jobs = 0
+ self.file_name = None
+ self.hdr = None
+ self.hdr_dict = None
+ self.cmd_line = None
+ self.min_time = None
+ self.max_time = None
+ self.time_str = None
+ self.time_ranges = None
+ self.cpu_str = None
+ self.cpus = None
+ self.split_time_ranges_for_each_cpu = None
+ self.worklist = None
+ self.per_cpu = None
+ self.interval = None
+ self.min_size = None
+ self.min_interval = None
+ self.pipe_to = None
+ self.output_dir = None
+ self.cmd = getattr(a, 'cmd', None)
+ self.no_per_cpu = getattr(a, 'no_per_cpu', False)
+ self.dry_run = getattr(a, 'dry_run', False)
+ self._verbosity = getattr(a, "_verbosity", Verbosity())
+
+ for arg_name in vars(a):
+ setattr(self, arg_name, getattr(a, arg_name))
+ self.orig_nr = self.nr
+ self.orig_cmd = list(self.cmd)
+ self.perf = self.cmd[0]
+ if os.path.exists(self.output_dir):
+ raise RuntimeError(f"Output '{self.output_dir}' already exists")
+ if self.jobs < 0 or self.nr < 0 or self.interval < 0:
+ raise RuntimeError(
+ "Bad options (negative values): try -h option for help")
+ if self.nr != 0 and self.interval != 0:
+ raise RuntimeError(
+ "Cannot specify number of time subdivisions and time interval")
+ if self.jobs == 0:
+ self.jobs = number_of_cp_us()
+ if self.nr == 0 and self.interval == 0:
+ if self.per_cpu:
+ self.nr = 1
+ else:
+ self.nr = self.jobs
+
+ def init(self):
+ """init."""
+
+ if self._verbosity.debug:
+ print("cmd", self.cmd)
+ self.file_name = determine_input_file_name(self.cmd)
+ self.hdr = read_header(self.perf, self.file_name)
+ self.hdr_dict = parse_header(self.hdr)
+ self.cmd_line = header_field(self.hdr_dict, "cmdline")
+
+ def extract_time_info(self):
+ """extract_time_info."""
+
+ self.min_time = time_val(header_field(
+ self.hdr_dict, "time of first sample"), 0)
+ self.max_time = time_val(header_field(
+ self.hdr_dict, "time of last sample"), 0)
+ self.time_str = extract_perf_option(self.cmd, "", "time")
+ self.time_ranges = parse_time_str(
+ self.time_str, self.min_time, self.max_time)
+ if self._verbosity.debug:
+ print("time_ranges", self.time_ranges)
+
+ def extract_cpu_info(self):
+ """extract_cpu_info."""
+
+ if self.per_cpu:
+ nr_cpus = int(header_field(self.hdr_dict, "nrcpus avail"))
+ self.cpu_str = extract_perf_option(self.cmd, "C", "cpu")
+ if self.cpu_str is None or self.cpu_str == "":
+ self.cpus = [x for x in range(nr_cpus)]
+ else:
+ self.cpus = parse_cpu_str(self.cpu_str, nr_cpus)
+ else:
+ self.cpu_str = None
+ self.cpus = [-1]
+ if self._verbosity.debug:
+ print("cpus", self.cpus)
+
+ def is_intel_pt(self):
+ """is_intel_pt."""
+
+ return self.cmd_line.find("intel_pt") >= 0
+
+ def split_time_ranges(self):
+ """split_time_ranges."""
+
+ if self.is_intel_pt() and self.interval == 0:
+ self.split_time_ranges_for_each_cpu = (
+ split_time_ranges_by_trace_data_density(
+ self.time_ranges, self.cpus, self.orig_nr,
+ self.orig_cmd, self.file_name, self.per_cpu,
+ self.min_size, self.min_interval, self._verbosity
+ )
+ )
+ elif self.nr:
+ self.split_time_ranges_for_each_cpu = [split_time_ranges_into_n(
+ self.time_ranges, self.nr, self.min_interval)]
+ else:
+ self.split_time_ranges_for_each_cpu = [
+ split_time_ranges_by_interval(self.time_ranges, self.interval)]
+
+ def check_time_ranges(self):
+ """check_time_ranges."""
+
+ for tr in self.split_time_ranges_for_each_cpu:
+ # Re-combined time ranges should be the same
+ new_tr = recombine_time_ranges(tr)
+ if new_tr != self.time_ranges:
+ if self._verbosity.debug:
+ print("tr", tr)
+ print("new_tr", new_tr)
+ raise RuntimeError("Self test failed!")
+
+ def open_time_range_ends(self):
+ """open_time_range_ends."""
+
+ for time_ranges in self.split_time_ranges_for_each_cpu:
+ open_time_range_ends(time_ranges, self.min_time, self.max_time)
+
+ def create_work_list(self):
+ """create_work_list."""
+
+ self.worklist = create_work_list(
+ self.cmd, self.pipe_to, self.output_dir, self.cpus, self.split_time_ranges_for_each_cpu)
+
+ def perf_data_recorded_per_cpu(self):
+ """perf_data_recorded_per_cpu."""
+
+ if "--per-thread" in self.cmd_line.split():
+ return False
+ return True
+
+ def default_to_per_cpu(self):
+ """default_to_per_cpu."""
+
+ # --no-per-cpu option takes precedence
+ if self.no_per_cpu:
+ return False
+ if not self.perf_data_recorded_per_cpu():
+ return False
+ # Default to per-cpu for Intel PT data that was recorded per-cpu,
+ # because decoding can be done for each CPU separately.
+ if self.is_intel_pt():
+ return True
+ return False
+
+ def config(self):
+ """config."""
+
+ self.init()
+ self.extract_time_info()
+ if not self.per_cpu:
+ self.per_cpu = self.default_to_per_cpu()
+ if self._verbosity.debug:
+ print("per_cpu", self.per_cpu)
+ self.extract_cpu_info()
+ self.split_time_ranges()
+ if self._verbosity.self_test:
+ self.check_time_ranges()
+ # Prefer open-ended time range to starting / ending with min_time / max_time resp.
+ self.open_time_range_ends()
+ self.create_work_list()
+
+ def run(self):
+ """run."""
+
+ if self.dry_run:
+ print(len(self.worklist), "jobs:")
+ for w in self.worklist:
+ print(w.command())
+ return True
+ result = run_work(self.worklist, self.jobs, _verbosity=self._verbosity)
+ if self._verbosity.verbose:
+ print(glb_prog_name, "done")
+ return result
+
+
+def run_parallel_perf(a):
+ """run_parallel_perf."""
+
+ pp = ParallelPerf(a)
+ pp.config()
+ return pp.run()
+
+
+def main(args):
+ """main."""
+
+ ap = argparse.ArgumentParser(
+ prog=glb_prog_name, formatter_class=argparse.RawDescriptionHelpFormatter,
+ description="""
+run a perf script command multiple times in parallel, using perf script options
+--cpu and --time so that each job processes a different chunk of the data.
+""",
+ epilog="""
+Follow the options by '--' and then the perf script command e.g.
+
+ $ perf record -a -- sleep 10
+ $ parallel-perf.py --nr=4 -- perf script --ns
+ All jobs finished successfully
+ $ tree parallel-perf-output/
+ parallel-perf-output/
+ ├── time-range-0
+ │   ├── cmd.txt
+ │   └── out.txt
+ ├── time-range-1
+ │   ├── cmd.txt
+ │   └── out.txt
+ ├── time-range-2
+ │   ├── cmd.txt
+ │   └── out.txt
+ └── time-range-3
+ ├── cmd.txt
+ └── out.txt
+ $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H .
+ parallel-perf-output/time-range-0/cmd.txt:perf script --time=,9466.504461499 --ns
+ parallel-perf-output/time-range-1/cmd.txt:perf script --time=9466.504461500,9469.005396999 --ns
+ parallel-perf-output/time-range-2/cmd.txt:perf script --time=9469.005397000,9471.506332499 --ns
+ parallel-perf-output/time-range-3/cmd.txt:perf script --time=9471.506332500, --ns
+
+Any perf script command can be used, including the use of perf script options
+--dlfilter and --script, so that the benefit of running parallel jobs
+naturally extends to them also.
+
+If option --pipe-to is used, standard output is first piped through that
+command. Beware, if the command fails (e.g. grep with no matches), it will be
+considered a fatal error.
+
+Final standard output is redirected to files named out.txt in separate
+subdirectories under the output directory. Similarly, standard error is
+written to files named err.txt. In addition, files named cmd.txt contain the
+corresponding perf script command. After processing, err.txt files are removed
+if they are empty.
+
+If any job exits with a non-zero exit code, then all jobs are killed and no
+more are started. A message is printed if any job results in a non-empty
+err.txt file.
+
+There is a separate output subdirectory for each time range. If the --per-cpu
+option is used, these are further grouped under cpu-n subdirectories, e.g.
+
+ $ parallel-perf.py --per-cpu --nr=2 -- perf script --ns --cpu=0,1
+ All jobs finished successfully
+ $ tree parallel-perf-output
+ parallel-perf-output/
+ ├── cpu-0
+ │   ├── time-range-0
+ │   │   ├── cmd.txt
+ │   │   └── out.txt
+ │   └── time-range-1
+ │   ├── cmd.txt
+ │   └── out.txt
+ └── cpu-1
+ ├── time-range-0
+ │   ├── cmd.txt
+ │   └── out.txt
+ └── time-range-1
+ ├── cmd.txt
+ └── out.txt
+ $ find parallel-perf-output -name cmd.txt | sort | xargs grep -H .
+ parallel-perf-output/cpu-0/time-range-0/cmd.txt:perf script --cpu=0 --time=,9469.005396999 --ns
+ parallel-perf-output/cpu-0/time-range-1/cmd.txt:perf script --cpu=0 --time=9469.005397000, --ns
+ parallel-perf-output/cpu-1/time-range-0/cmd.txt:perf script --cpu=1 --time=,9469.005396999 --ns
+ parallel-perf-output/cpu-1/time-range-1/cmd.txt:perf script --cpu=1 --time=9469.005397000, --ns
+
+Subdivisions of time range, and cpus if the --per-cpu option is used, are
+expressed by the --time and --cpu perf script options respectively. If the
+supplied perf script command has a --time option, then that time range is
+subdivided, otherwise the time range given by 'time of first sample' to
+'time of last sample' is used (refer perf script --header-only). Similarly, the
+supplied perf script command may provide a --cpu option, and only those CPUs
+will be processed.
+
+To prevent time intervals becoming too small, the --min-interval option can
+be used.
+
+Note there is special handling for processing Intel PT traces. If an interval is
+not specified and the perf record command contained the intel_pt event, then the
+time range will be subdivided in order to produce subdivisions that contain
+approximately the same amount of trace data. That is accomplished by counting
+double-quick (--itrace=qqi) samples, and choosing time ranges that encompass
+approximately the same number of samples. In that case, time ranges may not be
+the same for each CPU processed. For Intel PT, --per-cpu is the default, but
+that can be overridden by --no-per-cpu. Note, for Intel PT, double-quick
+decoding produces 1 sample for each PSB synchronization packet, which in turn
+come after a certain number of bytes output, determined by psb_period (refer
+perf Intel PT documentation). The minimum number of double-quick samples that
+will define a time range can be set by the --min_size option, which defaults to
+64.
+""")
+ ap.add_argument("-o", "--output-dir", default="parallel-perf-output",
+ help="output directory (default 'parallel-perf-output')")
+ ap.add_argument("-j", "--jobs", type=int, default=0,
+ help="maximum number of jobs to run in parallel at one time "
+ "(default is the number of CPUs)")
+ ap.add_argument("-n", "--nr", type=int, default=0,
+ help="number of time subdivisions (default is the number of jobs)")
+ ap.add_argument("-i", "--interval", type=float, default=0,
+ help="subdivide the time range using this time interval "
+ "(in seconds e.g. 0.1 for a tenth of a second)")
+ ap.add_argument("-c", "--per-cpu", action="store_true",
+ help="process data for each CPU in parallel")
+ ap.add_argument("-m", "--min-interval", type=float, default=glb_min_interval,
+ help=f"minimum interval (default {glb_min_interval} seconds)")
+ ap.add_argument("-p", "--pipe-to",
+ help="command to pipe output to (optional)")
+ ap.add_argument("-N", "--no-per-cpu", action="store_true",
+ help="do not process data for each CPU in parallel")
+ ap.add_argument("-b", "--min_size", type=int, default=glb_min_samples,
+ help="minimum data size (for Intel PT in PSBs)")
+ ap.add_argument("-D", "--dry-run", action="store_true",
+ help="do not run any jobs, just show the perf script commands")
+ ap.add_argument("-q", "--quiet", action="store_true",
+ help="do not print any messages except errors")
+ ap.add_argument("-v", "--verbose", action="store_true",
+ help="print more messages")
+ ap.add_argument("-d", "--debug", action="store_true",
+ help="print debugging messages")
+ cmd_line = list(args)
+ try:
+ split_pos = cmd_line.index("--")
+ cmd = cmd_line[split_pos + 1:]
+ args = cmd_line[:split_pos]
+ except (OSError, ValueError, RuntimeError):
+ cmd = None
+ args = cmd_line
+ a = ap.parse_args(args=args[1:])
+ a.cmd = cmd
+ setattr(a, "_verbosity", Verbosity(a.quiet, a.verbose, a.debug))
+ try:
+ if not a.cmd:
+ if a.cmd is None and len(args) <= 1:
+ ap.print_help()
+ return True
+ raise RuntimeError(
+ "command line must contain '--' before perf command")
+ return run_parallel_perf(a)
+ except (OSError, ValueError, RuntimeError, KeyboardInterrupt) as e:
+ print("Fatal error: ", str(e))
+ if a.debug:
+ raise
+ return False
+
+
+if __name__ == "__main__":
+ if not main(sys.argv):
+ sys.exit(1)
diff --git a/tools/perf/python/perf.pyi b/tools/perf/python/perf.pyi
index 58b0b3ed819d..087da00728fd 100644
--- a/tools/perf/python/perf.pyi
+++ b/tools/perf/python/perf.pyi
@@ -20,7 +20,7 @@ def metrics() -> List[Dict[str, Union[str, List[str]]]]:
"""
...
-def syscall_name(id: int, *, elf_machine: Optional[int] = None) -> str:
+def syscall_name(id: int, elf_machine: Optional[int] = None) -> Optional[str]:
"""Convert a syscall number to its name.
Args:
@@ -32,7 +32,7 @@ def syscall_name(id: int, *, elf_machine: Optional[int] = None) -> str:
"""
...
-def syscall_id(name: str, *, elf_machine: Optional[int] = None) -> int:
+def syscall_id(name: str, elf_machine: Optional[int] = None) -> int:
"""Convert a syscall name to its number.
Args:
@@ -44,6 +44,18 @@ def syscall_id(name: str, *, elf_machine: Optional[int] = None) -> int:
"""
...
+def arch_strerrno(err: int, elf_machine: Optional[int] = None) -> Optional[str]:
+ """Convert an errno number to its symbolic name.
+
+ Args:
+ err: The errno number (positive or negative).
+ elf_machine: Optional ELF machine type.
+
+ Returns:
+ The symbolic name of the errno, or None if unknown.
+ """
+ ...
+
def parse_events(
event_string: str,
cpus: Optional[cpu_map] = None,
@@ -84,7 +96,7 @@ class data:
class thread:
"""Represents a thread in the system."""
- def comm(self) -> str:
+ def comm(self) -> Optional[str]:
"""Get the command name of the thread."""
...
pid: int
@@ -153,7 +165,13 @@ class evsel:
def __str__(self) -> str:
"""Return string representation of the event."""
...
- def open(self) -> None:
+ def open(
+ self,
+ cpus: Optional['cpu_map'] = None,
+ threads: Optional[thread_map] = None,
+ group: bool = False,
+ inherit: bool = False,
+ ) -> None:
"""Open the event selector file descriptor table."""
...
def read(self, cpu: int, thread: int) -> counts_values:
@@ -182,6 +200,8 @@ class _sample_members:
sample_time: int
sample_id: int
sample_stream_id: int
+ machine_pid: int
+ vcpu: int
sample_period: int
sample_cpu: int
@@ -197,19 +217,27 @@ class sample_event(_sample_members):
sample_cyc_count: int
type: int
raw_buf: bytes
- dso: str
- dso_long_name: str
- dso_bid: Optional[bytes]
- map_start: int
- map_end: int
- map_pgoff: int
- symbol: str
- sym_start: int
- sym_end: int
+ dso: Optional[str]
+ dso_long_name: Optional[str]
+ dso_bid: Optional[str]
+ map_start: Optional[int]
+ map_end: Optional[int]
+ map_pgoff: Optional[int]
+ symbol: Optional[str]
+ sym_start: Optional[int]
+ sym_end: Optional[int]
+ sym_offset: Optional[int]
+ addr_dso: Optional[str]
+ addr_symbol: Optional[str]
+ addr_sym_offset: Optional[int]
+ branch_type: int
+ in_tx: int
+ flags: int
+ transaction: int
brstack: Optional['branch_stack']
callchain: Optional['callchain']
- def srccode(self) -> str: ...
- def insn(self) -> str: ...
+ def srccode(self, addr: int = ..., /) -> Optional[tuple[Optional[str], int, Optional[str]]]: ...
+ def insn(self) -> Optional[bytes]: ...
def __getattr__(self, name: str) -> Any: ...
class mmap_event(_sample_members):
@@ -286,8 +314,9 @@ class read_event(_sample_members):
class switch_event(_sample_members):
"""Represents a SWITCH or SWITCH_CPU_WIDE record."""
type: int
- next_prev_pid: int
- next_prev_tid: int
+ misc: int
+ next_prev_pid: Optional[int]
+ next_prev_tid: Optional[int]
evsel: Optional['evsel']
class branch_entry:
@@ -316,17 +345,20 @@ class branch_stack:
"""Sequence of branch entries in the branch stack."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> branch_entry: ...
+ def __iter__(self) -> Iterator[branch_entry]: ...
class callchain_node:
"""Represents a frame in the callchain."""
ip: int
- symbol: Optional[str]
- dso: Optional[str]
+ symbol: str
+ dso: str
+ sym_offset: Optional[int]
class callchain:
"""Sequence of callchain frames."""
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> callchain_node: ...
+ def __iter__(self) -> Iterator[callchain_node]: ...
class stat_event(_sample_members):
"""Represents a stat event from perf."""
@@ -362,10 +394,10 @@ class evlist:
def close(self) -> None:
"""Close the events in the list."""
...
- def mmap(self) -> None:
+ def mmap(self, pages: int = 128, overwrite: bool = False) -> None:
"""Memory map the event buffers."""
...
- def poll(self, timeout: int) -> int:
+ def poll(self, timeout: int = -1) -> int:
"""Poll for events.
Args:
@@ -375,7 +407,7 @@ class evlist:
Number of events ready.
"""
...
- def read_on_cpu(self, cpu: int) -> Optional[Any]:
+ def read_on_cpu(self, cpu: int, sample_id_all: bool = True) -> Optional[Any]:
"""Read a sample event from a specific CPU.
Args:
@@ -412,23 +444,50 @@ class evlist:
def enable(self) -> None:
"""Enable all events in the list."""
...
- def get_pollfd(self) -> List[int]:
+ def get_pollfd(self) -> List[Any]:
"""Get a list of file descriptors for polling."""
...
- def add(self, evsel: evsel) -> int:
+ def add(self, evsel: evsel, /) -> int:
"""Add an event to the list."""
...
+ def __len__(self) -> int: ...
+ def __getitem__(self, index: int) -> evsel: ...
def __iter__(self) -> Iterator[evsel]:
"""Iterate over the events (evsel) in the list."""
...
+class call_return:
+ """Represents a call/return event from instruction trace decoding."""
+ db_id: int
+ parent_id: int
+ insn_count: int
+ cyc_count: int
+ comm: Optional[str]
+ machine_pid: int
+ pid: Optional[int]
+ tid: Optional[int]
+ call_time: int
+ return_time: int
+ branch_count: int
+ call_ref: int
+ return_ref: int
+ flags: int
+ call_path: Optional[callchain]
+
+
class session:
def __init__(
self,
data: data,
sample: Optional[Callable[[sample_event], None]] = None,
- stat: Optional[Callable[[Any, Optional[str]], None]] = None
+ stat: Optional[Callable[[Any, Optional[str]], None]] = None,
+ context_switch: Optional[Callable[[switch_event], None]] = None,
+ call_return: Optional[Callable[[call_return], None]] = None,
+ itrace: Optional[str] = None,
+ vmlinux: Optional[str] = None,
+ kallsyms: Optional[str] = None,
+ symfs: Optional[str] = None
) -> None:
"""Initialize a perf session.
@@ -438,10 +497,12 @@ class session:
stat: Callback for stat events.
"""
...
+ e_machine: int
+ is_64_bit: bool
def process_events(self) -> None:
"""Process all events in the session."""
...
- def find_thread(self, pid: int) -> thread:
+ def find_thread(self, pid: int, tid: int = -1, /) -> Optional[thread]:
"""Returns the thread associated with a pid."""
...
@@ -670,3 +731,12 @@ RECORD_STAT_ROUND: int
RECORD_MISC_SWITCH_OUT: int
"""MISC_SWITCH_OUT record."""
+
+CALL_RETURN_NO_CALL: int
+"""'return' but no matching 'call'."""
+
+CALL_RETURN_NO_RETURN: int
+"""'call' but no matching 'return'."""
+
+CALL_RETURN_NON_CALL: int
+"""A branch but not a 'call' to the start of a different symbol."""
diff --git a/tools/perf/python/perf_live.py b/tools/perf/python/perf_live.py
index 616b5f657463..99e73d58a96f 100755
--- a/tools/perf/python/perf_live.py
+++ b/tools/perf/python/perf_live.py
@@ -33,10 +33,16 @@ class LiveSession:
# Poll for events with 100ms timeout
try:
self.evlist.poll(100)
+ # CPython's PyErr_SetFromErrno() invokes PyErr_CheckSignals() on
+ # EINTR, converting SIGINT into KeyboardInterrupt, which inherits
+ # from BaseException rather than InterruptedError/OSError, so
+ # Ctrl-C exits the loop and triggers finally: self.evlist.close()
+ # while non-fatal signals retry poll().
except InterruptedError:
continue
for cpu in self.cpus:
- for _ in range(1000): # Limit to 1000 events per CPU per poll to prevent starvation
+ # Limit to 1000 events per CPU per poll to prevent starvation
+ for _ in range(1000):
try:
event = self.evlist.read_on_cpu(cpu)
except TypeError as e:
@@ -53,7 +59,5 @@ class LiveSession:
if event.type == perf.RECORD_SAMPLE:
self.sample_callback(event)
- except KeyboardInterrupt:
- pass
finally:
self.evlist.close()
diff --git a/tools/perf/python/powerpc-hcalls.py b/tools/perf/python/powerpc-hcalls.py
new file mode 100755
index 000000000000..ab5867c05d3e
--- /dev/null
+++ b/tools/perf/python/powerpc-hcalls.py
@@ -0,0 +1,334 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0+
+"""
+Hypervisor call statistics
+
+Copyright (C) 2018 Ravi Bangoria, IBM Corporation
+Ported from tools/perf/scripts/python/powerpc-hcalls.py
+"""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import sys
+import perf
+
+# Hypervisor call table
+HCALL_TABLE = {
+ 4: 'H_REMOVE',
+ 8: 'H_ENTER',
+ 12: 'H_READ',
+ 16: 'H_CLEAR_MOD',
+ 20: 'H_CLEAR_REF',
+ 24: 'H_PROTECT',
+ 28: 'H_GET_TCE',
+ 32: 'H_PUT_TCE',
+ 36: 'H_SET_SPRG0',
+ 40: 'H_SET_DABR',
+ 44: 'H_PAGE_INIT',
+ 48: 'H_SET_ASR',
+ 52: 'H_ASR_ON',
+ 56: 'H_ASR_OFF',
+ 60: 'H_LOGICAL_CI_LOAD',
+ 64: 'H_LOGICAL_CI_STORE',
+ 68: 'H_LOGICAL_CACHE_LOAD',
+ 72: 'H_LOGICAL_CACHE_STORE',
+ 76: 'H_LOGICAL_ICBI',
+ 80: 'H_LOGICAL_DCBF',
+ 84: 'H_GET_TERM_CHAR',
+ 88: 'H_PUT_TERM_CHAR',
+ 92: 'H_REAL_TO_LOGICAL',
+ 96: 'H_HYPERVISOR_DATA',
+ 100: 'H_EOI',
+ 104: 'H_CPPR',
+ 108: 'H_IPI',
+ 112: 'H_IPOLL',
+ 116: 'H_XIRR',
+ 120: 'H_MIGRATE_DMA',
+ 124: 'H_PERFMON',
+ 220: 'H_REGISTER_VPA',
+ 224: 'H_CEDE',
+ 228: 'H_CONFER',
+ 232: 'H_PROD',
+ 236: 'H_GET_PPP',
+ 240: 'H_SET_PPP',
+ 244: 'H_PURR',
+ 248: 'H_PIC',
+ 252: 'H_REG_CRQ',
+ 256: 'H_FREE_CRQ',
+ 260: 'H_VIO_SIGNAL',
+ 264: 'H_SEND_CRQ',
+ 272: 'H_COPY_RDMA',
+ 276: 'H_REGISTER_LOGICAL_LAN',
+ 280: 'H_FREE_LOGICAL_LAN',
+ 284: 'H_ADD_LOGICAL_LAN_BUFFER',
+ 288: 'H_SEND_LOGICAL_LAN',
+ 292: 'H_BULK_REMOVE',
+ 304: 'H_MULTICAST_CTRL',
+ 308: 'H_SET_XDABR',
+ 312: 'H_STUFF_TCE',
+ 316: 'H_PUT_TCE_INDIRECT',
+ 332: 'H_CHANGE_LOGICAL_LAN_MAC',
+ 336: 'H_VTERM_PARTNER_INFO',
+ 340: 'H_REGISTER_VTERM',
+ 344: 'H_FREE_VTERM',
+ 348: 'H_RESET_EVENTS',
+ 352: 'H_ALLOC_RESOURCE',
+ 356: 'H_FREE_RESOURCE',
+ 360: 'H_MODIFY_QP',
+ 364: 'H_QUERY_QP',
+ 368: 'H_REREGISTER_PMR',
+ 372: 'H_REGISTER_SMR',
+ 376: 'H_QUERY_MR',
+ 380: 'H_QUERY_MW',
+ 384: 'H_QUERY_HCA',
+ 388: 'H_QUERY_PORT',
+ 392: 'H_MODIFY_PORT',
+ 396: 'H_DEFINE_AQP1',
+ 400: 'H_GET_TRACE_BUFFER',
+ 404: 'H_DEFINE_AQP0',
+ 408: 'H_RESIZE_MR',
+ 412: 'H_ATTACH_MCQP',
+ 416: 'H_DETACH_MCQP',
+ 420: 'H_CREATE_RPT',
+ 424: 'H_REMOVE_RPT',
+ 428: 'H_REGISTER_RPAGES',
+ 432: 'H_DISABLE_AND_GET',
+ 436: 'H_ERROR_DATA',
+ 440: 'H_GET_HCA_INFO',
+ 444: 'H_GET_PERF_COUNT',
+ 448: 'H_MANAGE_TRACE',
+ 456: 'H_GET_CPU_CHARACTERISTICS',
+ 468: 'H_FREE_LOGICAL_LAN_BUFFER',
+ 472: 'H_POLL_PENDING',
+ 484: 'H_QUERY_INT_STATE',
+ 580: 'H_ILLAN_ATTRIBUTES',
+ 584: 'H_ADD_LOGICAL_LAN_BUFFERS',
+ 592: 'H_MODIFY_HEA_QP',
+ 596: 'H_QUERY_HEA_QP',
+ 600: 'H_QUERY_HEA',
+ 604: 'H_QUERY_HEA_PORT',
+ 608: 'H_MODIFY_HEA_PORT',
+ 612: 'H_REG_BCMC',
+ 616: 'H_DEREG_BCMC',
+ 620: 'H_REGISTER_HEA_RPAGES',
+ 624: 'H_DISABLE_AND_GET_HEA',
+ 628: 'H_GET_HEA_INFO',
+ 632: 'H_ALLOC_HEA_RESOURCE',
+ 644: 'H_ADD_CONN',
+ 648: 'H_DEL_CONN',
+ 664: 'H_JOIN',
+ 672: 'H_VASI_SIGNAL',
+ 676: 'H_VASI_STATE',
+ 680: 'H_VIOCTL',
+ 688: 'H_ENABLE_CRQ',
+ 696: 'H_GET_EM_PARMS',
+ 720: 'H_SET_MPP',
+ 724: 'H_GET_MPP',
+ 732: 'H_REG_SUB_CRQ',
+ 736: 'H_FREE_SUB_CRQ',
+ 740: 'H_SEND_SUB_CRQ',
+ 744: 'H_SEND_SUB_CRQ_INDIRECT',
+ 748: 'H_HOME_NODE_ASSOCIATIVITY',
+ 756: 'H_BEST_ENERGY',
+ 764: 'H_XIRR_X',
+ 768: 'H_RANDOM',
+ 772: 'H_COP',
+ 788: 'H_GET_MPP_X',
+ 796: 'H_SET_MODE',
+ 808: 'H_BLOCK_REMOVE',
+ 856: 'H_CLEAR_HPT',
+ 864: 'H_REQUEST_VMC',
+ 876: 'H_RESIZE_HPT_PREPARE',
+ 880: 'H_RESIZE_HPT_COMMIT',
+ 892: 'H_REGISTER_PROC_TBL',
+ 896: 'H_SIGNAL_SYS_RESET',
+ 904: 'H_ALLOCATE_VAS_WINDOW',
+ 908: 'H_MODIFY_VAS_WINDOW',
+ 912: 'H_DEALLOCATE_VAS_WINDOW',
+ 916: 'H_QUERY_VAS_WINDOW',
+ 920: 'H_QUERY_VAS_CAPABILITIES',
+ 924: 'H_QUERY_NX_CAPABILITIES',
+ 928: 'H_GET_NX_FAULT',
+ 936: 'H_INT_GET_SOURCE_INFO',
+ 940: 'H_INT_SET_SOURCE_CONFIG',
+ 944: 'H_INT_GET_SOURCE_CONFIG',
+ 948: 'H_INT_GET_QUEUE_INFO',
+ 952: 'H_INT_SET_QUEUE_CONFIG',
+ 956: 'H_INT_GET_QUEUE_CONFIG',
+ 960: 'H_INT_SET_OS_REPORTING_LINE',
+ 964: 'H_INT_GET_OS_REPORTING_LINE',
+ 968: 'H_INT_ESB',
+ 972: 'H_INT_SYNC',
+ 976: 'H_INT_RESET',
+ 996: 'H_SCM_READ_METADATA',
+ 1000: 'H_SCM_WRITE_METADATA',
+ 1004: 'H_SCM_BIND_MEM',
+ 1008: 'H_SCM_UNBIND_MEM',
+ 1012: 'H_SCM_QUERY_BLOCK_MEM_BINDING',
+ 1016: 'H_SCM_QUERY_LOGICAL_MEM_BINDING',
+ 1020: 'H_SCM_UNBIND_ALL',
+ 1024: 'H_SCM_HEALTH',
+ 1048: 'H_SCM_PERFORMANCE_STATS',
+ 1052: 'H_PKS_GET_CONFIG',
+ 1056: 'H_PKS_SET_PASSWORD',
+ 1060: 'H_PKS_GEN_PASSWORD',
+ 1068: 'H_PKS_WRITE_OBJECT',
+ 1072: 'H_PKS_GEN_KEY',
+ 1076: 'H_PKS_READ_OBJECT',
+ 1080: 'H_PKS_REMOVE_OBJECT',
+ 1084: 'H_PKS_CONFIRM_OBJECT_FLUSHED',
+ 1096: 'H_RPT_INVALIDATE',
+ 1100: 'H_SCM_FLUSH',
+ 1104: 'H_GET_ENERGY_SCALE_INFO',
+ 1108: 'H_PKS_SIGNED_UPDATE',
+ 1112: 'H_HTM',
+ 1116: 'H_WATCHDOG',
+ # Platform specific hcalls used by KVM on PowerVM
+ 1120: 'H_GUEST_GET_CAPABILITIES',
+ 1124: 'H_GUEST_SET_CAPABILITIES',
+ 1136: 'H_GUEST_CREATE',
+ 1140: 'H_GUEST_CREATE_VCPU',
+ 1144: 'H_GUEST_GET_STATE',
+ 1148: 'H_GUEST_SET_STATE',
+ 1152: 'H_GUEST_RUN_VCPU',
+ 1156: 'H_GUEST_COPY_MEMORY',
+ 1160: 'H_GUEST_DELETE',
+ # Key wrapping hcalls
+ 1168: 'H_PKS_WRAP_OBJECT',
+ 1172: 'H_PKS_UNWRAP_OBJECT',
+ # Platform-specific hcalls used by the Ultravisor
+ 61184: 'H_SVM_PAGE_IN',
+ 61188: 'H_SVM_PAGE_OUT',
+ 61192: 'H_SVM_INIT_START',
+ 61196: 'H_SVM_INIT_DONE',
+ 61204: 'H_SVM_INIT_ABORT',
+ # Platform specific hcalls used by KVM
+ 61440: 'H_RTAS',
+ # Platform specific hcalls used by QEMU/SLOF
+ 61441: 'H_LOGICAL_MEMOP',
+ 61442: 'H_CAS',
+ 61443: 'H_UPDATE_DT',
+ # Platform specific hcalls provided by PHYP
+ 61560: 'H_GET_24X7_CATALOG_PAGE',
+ 61564: 'H_GET_24X7_DATA',
+ 61568: 'H_GET_PERF_COUNTER_INFO',
+ # Platform-specific hcalls used for nested HV KVM
+ 63488: 'H_SET_PARTITION_TABLE',
+ 63492: 'H_ENTER_NESTED',
+ 63496: 'H_TLB_INVALIDATE',
+ 63500: 'H_COPY_TOFROM_GUEST',
+}
+
+
+class HCallAnalyzer:
+ """Analyzes hypervisor calls and aggregates statistics."""
+
+ def __init__(self, cmd_args) -> None:
+ self.args = cmd_args
+ # output: {opcode: {'min': min, 'max': max, 'time': time, 'cnt': cnt}}
+ self.output: dict[int, dict[str, 'int | float']] = \
+ defaultdict(lambda: {'time': 0, 'cnt': 0, 'min': float('inf'), 'max': 0})
+ # d_enter: {cpu: {opcode: nsec}}
+ self.d_enter: dict[int, dict[int, int]] = {}
+ self.print_ptrn = '%-28s%10s%10s%10s%10s'
+
+ def hcall_table_lookup(self, opcode: int) -> str:
+ """Lookup hcall name by opcode."""
+ return HCALL_TABLE.get(opcode, str(opcode))
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process a single sample event."""
+ name = str(sample.evsel)
+ if name not in ("evsel(powerpc:hcall_entry)", "evsel(powerpc:hcall_exit)"):
+ return
+
+ try:
+ cpu = sample.sample_cpu
+ opcode = sample.opcode
+ except AttributeError:
+ print("ERROR: tracepoint fields missed (is libtraceevent enabled?)",
+ file=sys.stderr)
+ sys.exit(1)
+
+ # sample.sample_time represents the nsecs
+ time = sample.sample_time
+
+ if opcode < 0 or cpu < 0:
+ return
+
+ if name == "evsel(powerpc:hcall_entry)":
+ if cpu not in self.d_enter:
+ self.d_enter[cpu] = {}
+ self.d_enter[cpu][opcode] = time
+ elif name == "evsel(powerpc:hcall_exit)":
+ if cpu in self.d_enter and opcode in self.d_enter[cpu]:
+ time_entry = self.d_enter[cpu][opcode]
+ diff = time - time_entry
+ del self.d_enter[cpu][opcode]
+
+ stats = self.output[opcode]
+ stats['time'] += diff
+ stats['cnt'] += 1
+ if diff < stats['min']:
+ stats['min'] = diff
+ if diff > stats['max']:
+ stats['max'] = diff
+
+ def print_summary(self) -> None:
+ """Print aggregated statistics."""
+ print(self.print_ptrn % ('hcall', 'count', 'min(ns)', 'max(ns)', 'avg(ns)'))
+ print('-' * 68)
+
+ def sort_output(opcode):
+ stats = self.output[opcode]
+ sort_by = getattr(self.args, 'sort', None)
+ if sort_by == 'min':
+ return stats['min']
+ if sort_by == 'max':
+ return stats['max']
+ if sort_by == 'avg':
+ return stats['time'] // stats['cnt']
+ return stats['cnt']
+
+ for opcode in sorted(self.output.keys(), key=sort_output, reverse=True):
+ h_name = self.hcall_table_lookup(opcode)
+ stats = self.output[opcode]
+ time = stats['time']
+ cnt = stats['cnt']
+ min_t = stats['min']
+ max_t = stats['max']
+
+ # Avoid float representation for large integers if possible,
+ # or use formatted strings. Legacy used time//cnt.
+ avg_t = time // cnt if cnt > 0 else 0
+
+ # If min was not updated, it remains inf, but cnt should be > 0 if in output
+ if min_t == float('inf'):
+ min_t = 0
+
+ print(self.print_ptrn % (h_name, cnt, int(min_t), int(max_t), avg_t))
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(description="Hypervisor call statistics")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ ap.add_argument("-s", "--sort", default="count",
+ choices=["count", "min", "max", "avg"], help="Sort key")
+ args = ap.parse_args()
+
+ analyzer = HCallAnalyzer(args)
+
+ session = None
+ try:
+ session = perf.session(perf.data(args.input), sample=analyzer.process_event)
+ session.process_events()
+ analyzer.print_summary()
+ except KeyboardInterrupt:
+ analyzer.print_summary()
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError) as e:
+ print(f"Error processing events: {e}")
+ sys.exit(1)
+ finally:
+ # Break the reference cycle between perf.session and analyzer.process_event.
+ session = None
diff --git a/tools/perf/python/rw-by-file.py b/tools/perf/python/rw-by-file.py
new file mode 100755
index 000000000000..b33330261606
--- /dev/null
+++ b/tools/perf/python/rw-by-file.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+"""Display r/w activity for files read/written to for a given program."""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import sys
+from typing import Optional, Dict
+import perf
+
+class RwByFile:
+ """Tracks and displays read/write activity by file descriptor."""
+ def __init__(self, comm: str) -> None:
+ self.for_comm = comm
+ self.reads: Dict[int, Dict[str, int]] = defaultdict(
+ lambda: {"bytes_requested": 0, "total_reads": 0}
+ )
+ self.writes: Dict[int, Dict[str, int]] = defaultdict(
+ lambda: {"bytes_written": 0, "total_writes": 0}
+ )
+ self.unhandled: Dict[str, int] = defaultdict(int)
+ self.session: Optional[perf.session] = None
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process events."""
+ event_name = str(sample.evsel)
+ if event_name.startswith("evsel(") and event_name.endswith(")"):
+ event_name = event_name[6:-1]
+ event_name = "".join(c if c.isprintable() else "?" for c in event_name)
+
+ pid = sample.sample_pid
+ assert self.session is not None
+ try:
+ thread = self.session.find_thread(pid, sample.sample_tid)
+ comm = (thread.comm() if thread else None) or "unknown"
+ except (TypeError, AttributeError):
+ comm = "unknown"
+
+ if comm != self.for_comm:
+ return
+
+ if event_name == "syscalls:sys_enter_read":
+ try:
+ fd = sample.fd
+ count = sample.count
+ self.reads[fd]["bytes_requested"] += count
+ self.reads[fd]["total_reads"] += 1
+ except AttributeError:
+ self.unhandled[event_name] += 1
+ elif event_name == "syscalls:sys_enter_write":
+ try:
+ fd = sample.fd
+ count = sample.count
+ self.writes[fd]["bytes_written"] += count
+ self.writes[fd]["total_writes"] += 1
+ except AttributeError:
+ self.unhandled[event_name] += 1
+ else:
+ self.unhandled[event_name] += 1
+
+ def print_totals(self) -> None:
+ """Print summary tables."""
+ print(f"file read counts for {self.for_comm}:\n")
+ print(f"{'fd':>6s} {'# reads':>10s} {'bytes_requested':>15s}")
+ print(f"{'-'*6} {'-'*10} {'-'*15}")
+
+ for fd, data in sorted(self.reads.items(),
+ key=lambda kv: kv[1]["bytes_requested"], reverse=True):
+ print(f"{fd:6d} {data['total_reads']:10d} {data['bytes_requested']:15d}")
+
+ print(f"\nfile write counts for {self.for_comm}:\n")
+ print(f"{'fd':>6s} {'# writes':>10s} {'bytes_written':>15s}")
+ print(f"{'-'*6} {'-'*10} {'-'*15}")
+
+ for fd, data in sorted(self.writes.items(),
+ key=lambda kv: kv[1]["bytes_written"], reverse=True):
+ print(f"{fd:6d} {data['total_writes']:10d} {data['bytes_written']:15d}")
+
+ if self.unhandled:
+ print("\nunhandled events:\n")
+ print(f"{'event':<40s} {'count':>10s}")
+ print(f"{'-'*40} {'-'*10}")
+ for event_name, count in self.unhandled.items():
+ print(f"{event_name:<40s} {count:10d}")
+
+ def run(self, input_file: str) -> None:
+ """Run the session."""
+ self.session = perf.session(perf.data(input_file), sample=self.process_event)
+ try:
+ self.session.process_events()
+ finally:
+ # Break the reference cycle between perf.session and self.process_event
+ # because perf.session lacks cyclic GC support (tp_traverse).
+ self.session = None
+ self.print_totals()
+
+def main() -> None:
+ """Main function."""
+ parser = argparse.ArgumentParser(description="Trace r/w activity by file")
+ parser.add_argument("comm", help="Filter by command name")
+ parser.add_argument("-i", "--input", default="perf.data", help="Input file")
+ args = parser.parse_args()
+
+ analyzer = RwByFile(args.comm)
+ try:
+ analyzer.run(args.input)
+ except IOError as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/rw-by-pid.py b/tools/perf/python/rw-by-pid.py
new file mode 100755
index 000000000000..8e2d1dbd9a08
--- /dev/null
+++ b/tools/perf/python/rw-by-pid.py
@@ -0,0 +1,205 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+"""Display r/w activity for all processes."""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import sys
+from typing import Optional, Dict, List, Tuple, Any
+import perf
+
+class RwByPid:
+ """Tracks and displays read/write activity by PID."""
+ def __init__(self) -> None:
+ self.reads: Dict[int, Dict[str, Any]] = defaultdict(
+ lambda: {
+ "bytes_requested": 0,
+ "bytes_read": 0,
+ "total_reads": 0,
+ "comm": "",
+ "errors": defaultdict(int),
+ }
+ )
+ self.writes: Dict[int, Dict[str, Any]] = defaultdict(
+ lambda: {
+ "bytes_requested": 0,
+ "bytes_written": 0,
+ "total_writes": 0,
+ "comm": "",
+ "errors": defaultdict(int),
+ }
+ )
+ self.unhandled: Dict[str, int] = defaultdict(int)
+ self.session: Optional[perf.session] = None
+
+ def _handle_sys_enter_read(self, pid: int, comm: str, sample: perf.sample_event) -> None:
+ try:
+ count = sample.count
+ self.reads[pid]["bytes_requested"] += count
+ self.reads[pid]["total_reads"] += 1
+ self.reads[pid]["comm"] = comm
+ except AttributeError:
+ self.unhandled["sys_enter_read_attr_err"] += 1
+
+ def _handle_sys_exit_read(self, pid: int, sample: perf.sample_event, comm: str = "") -> None:
+ try:
+ ret = sample.ret
+ # Convert unsigned 64-bit or 32-bit tracepoint return values to signed integers.
+ # Because kernel read/write transfers are bounded by MAX_RW_COUNT (0x7ffff000 < 2 GiB),
+ # 0xfffff000..0xffffffff is always a 32-bit negative errno even on 64-bit hosts.
+ if ret >= 0x8000000000000000:
+ ret -= 0x10000000000000000
+ elif 0xfffff000 <= ret <= 0xffffffff:
+ ret -= 0x100000000
+ if not self.reads[pid]["comm"] and comm:
+ self.reads[pid]["comm"] = comm
+ if ret >= 0:
+ self.reads[pid]["bytes_read"] += ret
+ else:
+ self.reads[pid]["errors"][ret] += 1
+ except AttributeError:
+ self.unhandled["sys_exit_read_attr_err"] += 1
+
+ def _handle_sys_enter_write(self, pid: int, comm: str, sample: perf.sample_event) -> None:
+ try:
+ count = sample.count
+ self.writes[pid]["bytes_requested"] += count
+ self.writes[pid]["total_writes"] += 1
+ self.writes[pid]["comm"] = comm
+ except AttributeError:
+ self.unhandled["sys_enter_write_attr_err"] += 1
+
+ def _handle_sys_exit_write(self, pid: int, sample: perf.sample_event, comm: str = "") -> None:
+ try:
+ ret = sample.ret
+ # Convert unsigned 64-bit or 32-bit tracepoint return values to signed integers.
+ # Because kernel read/write transfers are bounded by MAX_RW_COUNT (0x7ffff000 < 2 GiB),
+ # 0xfffff000..0xffffffff is always a 32-bit negative errno even on 64-bit hosts.
+ if ret >= 0x8000000000000000:
+ ret -= 0x10000000000000000
+ elif 0xfffff000 <= ret <= 0xffffffff:
+ ret -= 0x100000000
+ if not self.writes[pid]["comm"] and comm:
+ self.writes[pid]["comm"] = comm
+ if ret >= 0:
+ self.writes[pid]["bytes_written"] += ret
+ else:
+ self.writes[pid]["errors"][ret] += 1
+ except AttributeError:
+ self.unhandled["sys_exit_write_attr_err"] += 1
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process events."""
+ event_name = str(sample.evsel)
+ if event_name.startswith("evsel(") and event_name.endswith(")"):
+ event_name = event_name[6:-1]
+ event_name = "".join(c if c.isprintable() else "?" for c in event_name)
+ pid = sample.sample_pid
+
+ assert self.session is not None
+ try:
+ thread = self.session.find_thread(pid, sample.sample_tid)
+ comm = (thread.comm() if thread else None) or "unknown"
+ except (TypeError, AttributeError):
+ comm = "unknown"
+ comm = "".join(c if c.isprintable() else "?" for c in comm)
+
+ if event_name == "syscalls:sys_enter_read":
+ self._handle_sys_enter_read(pid, comm, sample)
+ elif event_name == "syscalls:sys_exit_read":
+ self._handle_sys_exit_read(pid, sample, comm)
+ elif event_name == "syscalls:sys_enter_write":
+ self._handle_sys_enter_write(pid, comm, sample)
+ elif event_name == "syscalls:sys_exit_write":
+ self._handle_sys_exit_write(pid, sample, comm)
+ else:
+ self.unhandled[event_name] += 1
+
+ def print_totals(self) -> None:
+ """Print summary tables."""
+ print("read counts by pid:\n")
+ print(
+ f"{'pid':>6s} {'comm':<20s} {'# reads':>10s} "
+ f"{'bytes_requested':>15s} {'bytes_read':>10s}"
+ )
+ print(f"{'-'*6} {'-'*20} {'-'*10} {'-'*15} {'-'*10}")
+
+ for pid, data in sorted(self.reads.items(),
+ key=lambda kv: kv[1]["bytes_read"], reverse=True):
+ print(
+ f"{pid:6d} {data['comm']:<20s} {data['total_reads']:10d} "
+ f"{data['bytes_requested']:15d} {data['bytes_read']:10d}"
+ )
+
+ print("\nfailed reads by pid:\n")
+ print(f"{'pid':>6s} {'comm':<20s} {'error #':>7s} {'# errors':>10s}")
+ print(f"{'-'*6} {'-'*20} {'-'*7} {'-'*10}")
+
+ errcounts: List[Tuple[int, str, int, int]] = []
+ for pid, data in self.reads.items():
+ for error, count in data["errors"].items():
+ errcounts.append((pid, data["comm"], error, count))
+
+ for pid, comm, error, count in sorted(errcounts, key=lambda x: x[3], reverse=True):
+ print(f"{pid:6d} {comm:<20s} {error:7d} {count:10d}")
+
+ print("\nwrite counts by pid:\n")
+ print(
+ f"{'pid':>6s} {'comm':<20s} {'# writes':>10s} "
+ f"{'bytes_requested':>15s} {'bytes_written':>15s}"
+ )
+ print(f"{'-'*6} {'-'*20} {'-'*10} {'-'*15} {'-'*15}")
+
+ for pid, data in sorted(self.writes.items(),
+ key=lambda kv: kv[1]["bytes_written"], reverse=True):
+ print(
+ f"{pid:6d} {data['comm']:<20s} {data['total_writes']:10d} "
+ f"{data['bytes_requested']:15d} {data['bytes_written']:15d}"
+ )
+
+ print("\nfailed writes by pid:\n")
+ print(f"{'pid':>6s} {'comm':<20s} {'error #':>7s} {'# errors':>10s}")
+ print(f"{'-'*6} {'-'*20} {'-'*7} {'-'*10}")
+
+ errcounts = []
+ for pid, data in self.writes.items():
+ for error, count in data["errors"].items():
+ errcounts.append((pid, data["comm"], error, count))
+
+ for pid, comm, error, count in sorted(errcounts, key=lambda x: x[3], reverse=True):
+ print(f"{pid:6d} {comm:<20s} {error:7d} {count:10d}")
+
+ if self.unhandled:
+ print("\nunhandled events:\n")
+ print(f"{'event':<40s} {'count':>10s}")
+ print(f"{'-'*40} {'-'*10}")
+ for event_name, count in self.unhandled.items():
+ print(f"{event_name:<40s} {count:10d}")
+
+ def run(self, input_file: str) -> None:
+ """Run the session."""
+ self.session = perf.session(perf.data(input_file), sample=self.process_event)
+ try:
+ self.session.process_events()
+ finally:
+ # Break the reference cycle between perf.session and self.process_event
+ # because perf.session lacks cyclic GC support (tp_traverse).
+ self.session = None
+ self.print_totals()
+
+def main() -> None:
+ """Main function."""
+ parser = argparse.ArgumentParser(description="Trace r/w activity by PID")
+ parser.add_argument("-i", "--input", default="perf.data", help="Input file")
+ args = parser.parse_args()
+
+ analyzer = RwByPid()
+ try:
+ analyzer.run(args.input)
+ except IOError as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/rwtop.py b/tools/perf/python/rwtop.py
new file mode 100755
index 000000000000..86674d7c8f6f
--- /dev/null
+++ b/tools/perf/python/rwtop.py
@@ -0,0 +1,256 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+"""Periodically displays system-wide r/w call activity, broken down by pid."""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import os
+import sys
+from typing import Optional, Dict, Any
+import perf
+from perf_live import LiveSession
+
+class RwTop:
+ """Periodically displays system-wide r/w call activity."""
+ def __init__(self, interval: int = 3, nlines: int = 20) -> None:
+ self.offline = False
+ self.interval_ns = interval * 1000000000
+ self.nlines = nlines
+ self.reads: Dict[int, Dict[str, Any]] = defaultdict(
+ lambda: {
+ "bytes_requested": 0,
+ "bytes_read": 0,
+ "total_reads": 0,
+ "comm": "",
+ "errors": defaultdict(int),
+ }
+ )
+ self.writes: Dict[int, Dict[str, Any]] = defaultdict(
+ lambda: {
+ "bytes_requested": 0,
+ "bytes_written": 0,
+ "total_writes": 0,
+ "comm": "",
+ "errors": defaultdict(int),
+ }
+ )
+ self.unhandled: Dict[str, int] = defaultdict(int)
+ self.comm_cache: Dict[int, str] = {}
+ self.session: Optional[perf.session] = None
+ self.last_print_time: int = 0
+
+ def get_comm(self, pid: int, tid: Optional[int] = None) -> str:
+ """Resolve and cache the comm(and) of a pid."""
+ comm = None
+ if self.session:
+ # In offline mode, query session.find_thread() directly so PERF_RECORD_COMM
+ # updates after execve are reflected immediately instead of returning a
+ # stale pre-exec comm from comm_cache.
+ try:
+ thread = (self.session.find_thread(pid, tid)
+ if tid is not None else self.session.find_thread(pid))
+ comm = thread.comm() if thread else None
+ except (TypeError, AttributeError):
+ pass
+ if not comm:
+ comm = self.comm_cache.get(pid)
+ else:
+ comm = self.comm_cache.get(pid)
+ if comm:
+ return comm
+ try:
+ with open(f"/proc/{pid}/comm", "r", encoding="utf-8", errors="replace") as f:
+ comm = f.read().strip()
+ except OSError:
+ # The thread may have exited before /proc could be read.
+ pass
+ if not comm:
+ comm = f"PID({pid})"
+ comm = ''.join(c if c.isprintable() else '?' for c in comm)
+ self.comm_cache[pid] = comm
+ return comm
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process events."""
+ event_name = str(sample.evsel)
+ if event_name.startswith("evsel(") and event_name.endswith(")"):
+ event_name = event_name[6:-1]
+ event_name = "".join(c if c.isprintable() else "?" for c in event_name)
+ pid = sample.sample_pid
+ sample_time = getattr(sample, "sample_time", 0) or 0
+
+ if sample_time > 0:
+ if self.last_print_time == 0:
+ self.last_print_time = sample_time
+ elif (sample_time > self.last_print_time and
+ sample_time - self.last_print_time >= self.interval_ns):
+ self.print_totals()
+ self.last_print_time = sample_time
+
+ # Map each event onto the totals it updates. "enter" events count the
+ # requested bytes, "exit" events the transferred bytes or the error.
+ handlers = {
+ "syscalls:sys_enter_read": (self.reads, "total_reads", None),
+ "syscalls:sys_exit_read": (self.reads, None, "bytes_read"),
+ "syscalls:sys_enter_write": (self.writes, "total_writes", None),
+ "syscalls:sys_exit_write": (self.writes, None, "bytes_written"),
+ }
+ handler = handlers.get(event_name)
+ if not handler:
+ self.unhandled[event_name] += 1
+ return
+
+ totals, count_key, bytes_key = handler
+ try:
+ value = sample.count if count_key else sample.ret
+ except AttributeError:
+ self.unhandled[event_name] += 1
+ return
+
+ data = totals[pid]
+ data["comm"] = self.get_comm(pid, getattr(sample, "sample_tid", None))
+ if count_key:
+ data["bytes_requested"] += value
+ data[count_key] += 1
+ elif bytes_key:
+ # Convert unsigned 32-bit or 64-bit kernel error return values to signed integers.
+ # Because kernel read/write transfers are bounded by MAX_RW_COUNT (0x7ffff000 < 2 GiB),
+ # 0xfffff000..0xffffffff is always a 32-bit negative errno even on 64-bit hosts.
+ if value > 0:
+ if 0xfffff000 <= value <= 0xffffffff:
+ value -= 0x100000000
+ elif value >= 0x8000000000000000:
+ value -= 0x10000000000000000
+ if value >= 0:
+ data[bytes_key] += value
+ else:
+ data["errors"][value] += 1
+
+ def print_totals(self) -> None:
+ """Print summary tables."""
+ if not self.offline:
+ print('\x1b[H\x1b[2J', end='')
+ print("read counts by pid:\n")
+ print(
+ f"{'pid':>6s} {'comm':<20s} {'# reads':>10s} "
+ f"{'bytes_req':>10s} {'bytes_read':>10s}"
+ )
+ print(f"{'-'*6} {'-'*20} {'-'*10} {'-'*10} {'-'*10}")
+
+ count = 0
+ for pid, data in sorted(self.reads.items(),
+ key=lambda kv: kv[1]["bytes_read"], reverse=True):
+ print(
+ f"{pid:6d} {data['comm']:<20s} {data['total_reads']:10d} "
+ f"{data['bytes_requested']:10d} {data['bytes_read']:10d}"
+ )
+ count += 1
+ if count >= self.nlines:
+ break
+
+ print("\nfailed reads by pid:\n")
+ print(f"{'pid':>6s} {'comm':<20s} {'error #':>7s} {'# errors':>10s}")
+ print(f"{'-'*6} {'-'*20} {'-'*7} {'-'*10}")
+
+ errcounts = []
+ for pid, data in self.reads.items():
+ for error, cnt in data["errors"].items():
+ errcounts.append((pid, data["comm"], error, cnt))
+
+ sorted_errcounts = sorted(errcounts, key=lambda x: x[3], reverse=True)
+ for pid, comm, error, cnt in sorted_errcounts[:self.nlines]:
+ print(f"{pid:6d} {comm:<20s} {error:7d} {cnt:10d}")
+
+ print("\nwrite counts by pid:\n")
+ print(
+ f"{'pid':>6s} {'comm':<20s} {'# writes':>10s} "
+ f"{'bytes_req':>10s} {'bytes_written':>13s}"
+ )
+ print(f"{'-'*6} {'-'*20} {'-'*10} {'-'*10} {'-'*13}")
+
+ count = 0
+ for pid, data in sorted(self.writes.items(),
+ key=lambda kv: kv[1]["bytes_written"], reverse=True):
+ print(
+ f"{pid:6d} {data['comm']:<20s} {data['total_writes']:10d} "
+ f"{data['bytes_requested']:10d} {data['bytes_written']:13d}"
+ )
+ count += 1
+ if count >= self.nlines:
+ break
+
+ print("\nfailed writes by pid:\n")
+ print(f"{'pid':>6s} {'comm':<20s} {'error #':>7s} {'# errors':>10s}")
+ print(f"{'-'*6} {'-'*20} {'-'*7} {'-'*10}")
+
+ errcounts = []
+ for pid, data in self.writes.items():
+ for error, cnt in data["errors"].items():
+ errcounts.append((pid, data["comm"], error, cnt))
+
+ sorted_errcounts = sorted(errcounts, key=lambda x: x[3], reverse=True)
+ for pid, comm, error, cnt in sorted_errcounts[:self.nlines]:
+ print(f"{pid:6d} {comm:<20s} {error:7d} {cnt:10d}")
+
+ # Reset counts
+ self.reads.clear()
+ self.writes.clear()
+ self.comm_cache.clear()
+
+ def run(self, input_file: str) -> None:
+ """Run the session."""
+ self.session = perf.session(perf.data(input_file), sample=self.process_event)
+ try:
+ self.session.process_events()
+ finally:
+ # Break the reference cycle between perf.session and self.process_event
+ # because perf.session lacks cyclic GC support (tp_traverse).
+ self.session = None
+
+ # Print final totals if there are any left
+ if self.reads or self.writes:
+ self.print_totals()
+
+ if self.unhandled:
+ print("\nunhandled events:\n")
+ print(f"{'event':<40s} {'count':>10s}")
+ print(f"{'-'*40} {'-'*10}")
+ for event_name, count in self.unhandled.items():
+ print(f"{event_name:<40s} {count:10d}")
+
+def main() -> None:
+ """Main function."""
+ parser = argparse.ArgumentParser(description="Trace r/w activity by PID")
+ parser.add_argument(
+ "interval", type=int, nargs="?", default=3, help="Refresh interval in seconds"
+ )
+ parser.add_argument("-i", "--input", default="perf.data", help="Input file")
+ parser.add_argument("-l", "--live", action="store_true", help="Run in live mode")
+ args = parser.parse_args()
+
+ analyzer = RwTop(args.interval)
+ try:
+ if args.live or (not os.path.exists(args.input) and args.input == "perf.data"):
+ # Live mode
+ events = (
+ "syscalls:sys_enter_read,syscalls:sys_exit_read,"
+ "syscalls:sys_enter_write,syscalls:sys_exit_write"
+ )
+ live_session = LiveSession(events, sample_callback=analyzer.process_event)
+ print("Live mode started. Press Ctrl+C to stop.", file=sys.stderr)
+ live_session.run()
+ else:
+ analyzer.offline = True
+ analyzer.run(args.input)
+ except IOError as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+ except KeyboardInterrupt:
+ if not analyzer.offline:
+ print("\nStopping live mode...", file=sys.stderr)
+ if analyzer.reads or analyzer.writes:
+ analyzer.print_totals()
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/sched-migration.py b/tools/perf/python/sched-migration.py
new file mode 100755
index 000000000000..d92c1a80d42f
--- /dev/null
+++ b/tools/perf/python/sched-migration.py
@@ -0,0 +1,496 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Cpu task migration overview toy
+
+Copyright (C) 2010 Frederic Weisbecker <fweisbec@gmail.com>
+Ported to modern directory structure and refactored to use class.
+"""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict, UserList
+import importlib
+import sys
+from typing import Any
+import perf
+
+
+# Global threads dictionary
+threads = defaultdict(lambda: "unknown")
+threads[0] = "idle"
+
+
+def thread_name(pid: int) -> str:
+ """Return thread name formatted with pid."""
+ return f"{threads[pid]}:{pid}"
+
+
+def task_state(state: int) -> str:
+ """Map task state integer to string."""
+ states = {
+ 0: "R",
+ 1: "S",
+ 2: "D",
+ 64: "DEAD"
+ }
+ return states.get(state, "Unknown")
+
+
+class RunqueueEventUnknown:
+ """Unknown runqueue event."""
+ @staticmethod
+ def color():
+ """Return color for event."""
+ return None
+
+ def __repr__(self):
+ return "unknown"
+
+
+class RunqueueEventSleep:
+ """Sleep runqueue event."""
+ @staticmethod
+ def color():
+ """Return color for event."""
+ return 0, 0, 0xff
+
+ def __init__(self, sleeper: int):
+ self.sleeper = sleeper
+
+ def __repr__(self):
+ return f"{thread_name(self.sleeper)} gone to sleep"
+
+
+class RunqueueEventWakeup:
+ """Wakeup runqueue event."""
+ @staticmethod
+ def color():
+ """Return color for event."""
+ return 0xff, 0xff, 0
+
+ def __init__(self, wakee: int):
+ self.wakee = wakee
+
+ def __repr__(self):
+ return f"{thread_name(self.wakee)} woke up"
+
+
+class RunqueueEventFork:
+ """Fork runqueue event."""
+ @staticmethod
+ def color():
+ """Return color for event."""
+ return 0, 0xff, 0
+
+ def __init__(self, child: int):
+ self.child = child
+
+ def __repr__(self):
+ return f"new forked task {thread_name(self.child)}"
+
+
+class RunqueueMigrateIn:
+ """Migrate in runqueue event."""
+ @staticmethod
+ def color():
+ """Return color for event."""
+ return 0, 0xf0, 0xff
+
+ def __init__(self, new: int):
+ self.new = new
+
+ def __repr__(self):
+ return f"task migrated in {thread_name(self.new)}"
+
+
+class RunqueueMigrateOut:
+ """Migrate out runqueue event."""
+ @staticmethod
+ def color():
+ """Return color for event."""
+ return 0xff, 0, 0xff
+
+ def __init__(self, old: int):
+ self.old = old
+
+ def __repr__(self):
+ return f"task migrated out {thread_name(self.old)}"
+
+
+class RunqueueSnapshot:
+ """Snapshot of runqueue state."""
+
+ def __init__(self, tasks=None, event=None):
+ if tasks is None:
+ tasks = (0,)
+ if event is None:
+ event = RunqueueEventUnknown()
+ self.tasks = tuple(tasks)
+ self.event = event
+
+ def sched_switch(self, prev: int, prev_state: int, next_pid: int):
+ """Handle sched switch in snapshot."""
+ if task_state(prev_state) == "R" and next_pid in self.tasks \
+ and prev in self.tasks:
+ return self
+
+ event = (
+ RunqueueEventSleep(prev)
+ if task_state(prev_state) != "R"
+ else RunqueueEventUnknown()
+ )
+
+ next_tasks = list(self.tasks[:])
+ if prev in self.tasks:
+ if task_state(prev_state) != "R":
+ next_tasks.remove(prev)
+ elif task_state(prev_state) == "R":
+ next_tasks.append(prev)
+
+ if next_pid not in next_tasks:
+ next_tasks.append(next_pid)
+
+ return RunqueueSnapshot(next_tasks, event)
+
+ def migrate_out(self, old: int):
+ """Handle task migrate out in snapshot."""
+ if old not in self.tasks:
+ return self
+ next_tasks = [task for task in self.tasks if task != old]
+
+ return RunqueueSnapshot(next_tasks, RunqueueMigrateOut(old))
+
+ def __migrate_in(self, new: int, event):
+ if new in self.tasks:
+ return RunqueueSnapshot(self.tasks, event)
+ next_tasks = self.tasks + tuple([new])
+
+ return RunqueueSnapshot(next_tasks, event)
+
+ def migrate_in(self, new: int):
+ """Handle task migrate in snapshot."""
+ return self.__migrate_in(new, RunqueueMigrateIn(new))
+
+ def wake_up(self, new: int):
+ """Handle task wakeup in snapshot."""
+ return self.__migrate_in(new, RunqueueEventWakeup(new))
+
+ def wake_up_new(self, new: int):
+ """Handle task fork in snapshot."""
+ return self.__migrate_in(new, RunqueueEventFork(new))
+
+ def load(self) -> int:
+ """Provide the number of tasks on the runqueue. Don't count idle"""
+ return len(self.tasks) - 1
+
+ def __repr__(self):
+ return self.tasks.__repr__()
+
+
+class TimeSlice:
+ """Represents a time slice of execution."""
+
+ def __init__(self, start: int, prev):
+ self.start = start
+ self.prev = prev
+ self.end = start
+ # cpus that triggered the event
+ self.event_cpus: list[int] = []
+ if prev is not None:
+ self.total_load = prev.total_load
+ self.rqs = prev.rqs.copy()
+ else:
+ self.rqs = defaultdict(RunqueueSnapshot)
+ self.total_load = 0
+
+ def __update_total_load(self, old_rq: RunqueueSnapshot, new_rq: RunqueueSnapshot):
+ diff = new_rq.load() - old_rq.load()
+ self.total_load += diff
+
+ def sched_switch(self, ts_list, prev: int, prev_state: int, next_pid: int, cpu: int):
+ """Process sched_switch in time slice."""
+ old_rq = self.prev.rqs[cpu]
+ new_rq = old_rq.sched_switch(prev, prev_state, next_pid)
+
+ if old_rq is new_rq:
+ return
+
+ self.rqs[cpu] = new_rq
+ self.__update_total_load(old_rq, new_rq)
+ ts_list.append(self)
+ self.event_cpus = [cpu]
+
+ def migrate(self, ts_list, new: int, old_cpu: int, new_cpu: int):
+ """Process task migration in time slice."""
+ if old_cpu == new_cpu:
+ return
+ old_rq = self.prev.rqs[old_cpu]
+ out_rq = old_rq.migrate_out(new)
+ self.rqs[old_cpu] = out_rq
+ self.__update_total_load(old_rq, out_rq)
+
+ new_rq = self.prev.rqs[new_cpu]
+ in_rq = new_rq.migrate_in(new)
+ self.rqs[new_cpu] = in_rq
+ self.__update_total_load(new_rq, in_rq)
+
+ ts_list.append(self)
+
+ if old_rq is not out_rq:
+ self.event_cpus.append(old_cpu)
+ self.event_cpus.append(new_cpu)
+
+ def wake_up(self, ts_list, pid: int, cpu: int, fork: bool):
+ """Process wakeup in time slice."""
+ old_rq = self.prev.rqs[cpu]
+ if fork:
+ new_rq = old_rq.wake_up_new(pid)
+ else:
+ new_rq = old_rq.wake_up(pid)
+
+ if new_rq is old_rq:
+ return
+ self.rqs[cpu] = new_rq
+ self.__update_total_load(old_rq, new_rq)
+ ts_list.append(self)
+ self.event_cpus = [cpu]
+
+ def next(self, t: int):
+ """Create next time slice."""
+ self.end = t
+ return TimeSlice(t, self)
+
+
+class TimeSliceList(UserList):
+ """List of time slices with search capabilities."""
+
+ def __init__(self, arg=None):
+ super().__init__(arg if arg is not None else [])
+ self.root_win = None
+
+ def get_time_slice(self, ts: int) -> TimeSlice:
+ """Get or create time slice for timestamp."""
+ if len(self.data) == 0:
+ ts_slice = TimeSlice(ts, TimeSlice(-1, None))
+ else:
+ ts_slice = self.data[-1].next(ts)
+ return ts_slice
+
+ def find_time_slice(self, ts: int) -> int:
+ """Binary search for time slice containing timestamp."""
+ if not self.data:
+ return -1
+ start = 0
+ end = len(self.data)
+ found = -1
+ searching = True
+ while searching:
+ if start in (end, end - 1):
+ searching = False
+
+ i = (end + start) // 2
+ if self.data[i].start <= ts <= self.data[i].end:
+ found = i
+ break
+
+ if self.data[i].end < ts:
+ start = i
+ elif self.data[i].start > ts:
+ end = i
+
+ return found
+
+ def set_root_win(self, win):
+ """Set root window for GUI."""
+ self.root_win = win
+
+ def mouse_down(self, cpu: int, t: int):
+ """Handle mouse down event from GUI."""
+ idx = self.find_time_slice(t)
+ if idx == -1:
+ return
+
+ ts = self[idx]
+ rq = ts.rqs[cpu]
+ raw = f"CPU: {cpu}\n"
+ raw += f"Last event : {repr(rq.event)}\n"
+ raw += f"Timestamp : {ts.start // (10 ** 9)}.{ts.start % (10 ** 9) // 1000:06d}\n"
+ raw += f"Duration : {(ts.end - ts.start) // (10 ** 3):6d} us\n"
+ raw += f"Load = {rq.load()}\n"
+ for task in rq.tasks:
+ raw += f"{thread_name(task)} \n"
+
+ if self.root_win:
+ self.root_win.update_summary(raw)
+
+ def update_rectangle_cpu(self, slice_obj: TimeSlice, cpu: int):
+ """Update rectangle for CPU in GUI."""
+ rq = slice_obj.rqs[cpu]
+
+ if slice_obj.total_load != 0:
+ load_rate = rq.load() / float(slice_obj.total_load)
+ else:
+ load_rate = 0
+
+ red_power = int(0xff - (0xff * load_rate))
+ color = (0xff, red_power, red_power)
+
+ top_color = None
+ if cpu in slice_obj.event_cpus:
+ top_color = rq.event.color()
+
+ if self.root_win:
+ self.root_win.paint_rectangle_zone(cpu, color, top_color,
+ slice_obj.start, slice_obj.end)
+
+ def fill_zone(self, start: int, end: int):
+ """Fill zone in GUI."""
+ i = self.find_time_slice(start)
+ if i == -1:
+ return
+
+ for idx in range(i, len(self.data)):
+ timeslice = self.data[idx]
+ if timeslice.start > end:
+ return
+
+ for cpu in timeslice.rqs:
+ self.update_rectangle_cpu(timeslice, cpu)
+
+ def interval(self) -> tuple[int, int]:
+ """Return start and end timestamps."""
+ if len(self.data) == 0:
+ return 0, 0
+ return self.data[0].start, self.data[-1].end
+
+ def nr_rectangles(self) -> int:
+ """Return maximum CPU number."""
+ if not self.data:
+ return 0
+ last_ts = self.data[-1]
+ max_cpu = 0
+ for cpu in last_ts.rqs:
+ max_cpu = max(max_cpu, cpu)
+ return max_cpu
+
+
+class SchedMigrationAnalyzer:
+ """Analyzes task migrations and manages time slices."""
+
+ def __init__(self):
+ self.current_tsk = defaultdict(lambda: -1)
+ self.timeslices = TimeSliceList()
+
+ def sched_switch(self, time: int, cpu: int, prev_comm: str, prev_pid: int, prev_state: int,
+ next_comm: str, next_pid: int):
+ """Handle sched_switch event."""
+ on_cpu_task = self.current_tsk[cpu]
+
+ if on_cpu_task not in (-1, prev_pid):
+ print(f"Sched switch event rejected ts: {time} cpu: {cpu} "
+ f"prev: {prev_comm}({prev_pid}) next: {next_comm}({next_pid})")
+
+ threads[prev_pid] = prev_comm
+ threads[next_pid] = next_comm
+ self.current_tsk[cpu] = next_pid
+
+ ts = self.timeslices.get_time_slice(time)
+ ts.sched_switch(self.timeslices, prev_pid, prev_state, next_pid, cpu)
+
+ def migrate(self, time: int, pid: int, orig_cpu: int, dest_cpu: int):
+ """Handle sched_migrate_task event."""
+ ts = self.timeslices.get_time_slice(time)
+ ts.migrate(self.timeslices, pid, orig_cpu, dest_cpu)
+
+ def wake_up(self, time: int, pid: int, success: int, target_cpu: int, fork: bool):
+ """Handle wakeup event."""
+ if success == 0:
+ return
+ ts = self.timeslices.get_time_slice(time)
+ ts.wake_up(self.timeslices, pid, target_cpu, fork)
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Collect events and pass to analyzer."""
+ name = str(sample.evsel)
+ if name.startswith("evsel(") and name.endswith(")"):
+ name = name[6:-1]
+ if name.count(":") >= 2:
+ name = ":".join(name.split(":")[:2])
+ time = sample.sample_time
+ cpu = sample.sample_cpu
+ _pid = sample.sample_pid
+ _comm = "Unknown"
+
+ if name.startswith("sched:sched_switch"):
+ prev_comm = getattr(sample, "prev_comm", "Unknown")
+ prev_pid = getattr(sample, "prev_pid", -1)
+ prev_state = getattr(sample, "prev_state", 0)
+ next_comm = getattr(sample, "next_comm", "Unknown")
+ next_pid = getattr(sample, "next_pid", -1)
+ self.sched_switch(time, cpu, prev_comm, prev_pid, prev_state, next_comm, next_pid)
+ elif name.startswith("sched:sched_migrate_task"):
+ task_pid = getattr(sample, "pid", -1)
+ orig_cpu = getattr(sample, "orig_cpu", -1)
+ dest_cpu = getattr(sample, "dest_cpu", -1)
+ self.migrate(time, task_pid, orig_cpu, dest_cpu)
+ elif name.startswith("sched:sched_wakeup_new"):
+ task_pid = getattr(sample, "pid", -1)
+ success = getattr(sample, "success", 1)
+ target_cpu = getattr(sample, "target_cpu", -1)
+ self.wake_up(time, task_pid, success, target_cpu, True)
+ elif name.startswith("sched:sched_wakeup"):
+ task_pid = getattr(sample, "pid", -1)
+ success = getattr(sample, "success", 1)
+ target_cpu = getattr(sample, "target_cpu", -1)
+ self.wake_up(time, task_pid, success, target_cpu, False)
+
+ def run_gui(self):
+ """Start wxPython GUI."""
+ try:
+ wx_mod: Any = importlib.import_module("wx")
+ sched_gui: Any = importlib.import_module("SchedGui")
+ except ImportError:
+ print("wxPython is not available. Cannot start GUI.")
+ return
+ app = wx_mod.App(False)
+ _frame = sched_gui.RootFrame(self.timeslices, "Migration")
+ app.MainLoop()
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(description="Cpu task migration overview toy")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ ap.add_argument("-v", "--verbose", action="store_true",
+ help="Print parsed migration summary")
+ ap.add_argument("--gui", action="store_true", default=True,
+ help="Start the GUI (default)")
+ ap.add_argument("--no-gui", action="store_true", help="Do not start the GUI")
+ args = ap.parse_args()
+
+ analyzer = SchedMigrationAnalyzer()
+ session = None
+
+ try:
+ try:
+ session = perf.session(perf.data(args.input), sample=analyzer.process_event)
+ try:
+ session.process_events()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ # Release the perf.session reference before launching the long-running
+ # wxPython GUI event loop so the underlying C session and callback
+ # reference cycle are freed immediately.
+ session = None
+ if args.verbose:
+ start_ts, end_ts = analyzer.timeslices.interval()
+ print(f"Timeslices: {len(analyzer.timeslices.data)} "
+ f"(interval: {start_ts}..{end_ts})")
+ if args.gui and not args.no_gui:
+ analyzer.run_gui()
+ except KeyboardInterrupt:
+ pass
+ except OSError as e:
+ print(f"Error processing events: {e}")
+ sys.exit(1)
diff --git a/tools/perf/python/sctop.py b/tools/perf/python/sctop.py
new file mode 100755
index 000000000000..48320af8b755
--- /dev/null
+++ b/tools/perf/python/sctop.py
@@ -0,0 +1,253 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+System call top
+
+Periodically displays system-wide system call totals, broken down by
+syscall. If a [comm] arg is specified, only syscalls called by
+[comm] are displayed. If an [interval] arg is specified, the display
+will be refreshed every [interval] seconds. The default interval is
+3 seconds.
+
+Ported from tools/perf/scripts/python/sctop.py
+"""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import os
+import sys
+import threading
+from typing import Optional
+import perf
+from perf_live import LiveSession
+
+
+class SCTopAnalyzer:
+ """Periodically displays system-wide system call totals."""
+
+ def __init__(self, for_comm: Optional[str], interval: int, offline: bool = False):
+ self.for_comm = for_comm
+ self.interval = interval
+ self.syscalls: dict[int, int] = defaultdict(int)
+ self.comm_cache: dict[int, str] = {}
+ self.lock = threading.Lock()
+ self.stop_event = threading.Event()
+ self.thread = threading.Thread(target=self.print_syscall_totals)
+ self.offline = offline
+ self.own_pid = os.getpid()
+ self.last_print_time: Optional[int] = None
+ self.printed = False
+ self.session: Optional[perf.session] = None
+ self.e_machine: Optional[int] = None
+
+ def syscall_name(self, syscall_id: int) -> str:
+ """Lookup syscall name by ID."""
+ # Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000) before
+ # resolving the syscall number in the architecture's syscall table.
+ raw_sc_id = syscall_id & ~0x40000000
+ try:
+ e_machine = getattr(self.session, "e_machine", self.e_machine)
+ if e_machine is not None:
+ name = perf.syscall_name(raw_sc_id, e_machine)
+ else:
+ name = perf.syscall_name(raw_sc_id)
+ if name is not None:
+ return name
+ except (TypeError, OverflowError):
+ pass
+ return str(syscall_id)
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Collect syscall events."""
+ if not self.offline and sample.sample_pid == self.own_pid:
+ return
+
+ name = str(sample.evsel)
+ # Per-syscall syscalls:sys_enter_* tracepoints expose '__syscall_nr' (or 'nr')
+ # and may have an unrelated syscall argument named 'id', whereas
+ # raw_syscalls:sys_enter (and legacy pre-2.6.35 syscalls:sys_enter) expose 'id'.
+ if name.startswith("evsel(syscalls:sys_enter_"):
+ syscall_id = getattr(sample, "__syscall_nr", -1)
+ if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
+ syscall_id = getattr(sample, "nr", -1)
+ elif name.startswith(("evsel(raw_syscalls:sys_enter", "evsel(syscalls:sys_enter")):
+ syscall_id = getattr(sample, "id", -1)
+ if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
+ syscall_id = getattr(sample, "__syscall_nr", -1)
+ if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
+ syscall_id = getattr(sample, "nr", -1)
+ else:
+ syscall_id = -1
+
+ skip = False
+ with self.lock:
+ if self.for_comm is not None:
+ is_execve = (0 <= (syscall_id & ~0x40000000) <= 0xffff and
+ self.syscall_name(syscall_id) in ("execve", "execveat"))
+ if is_execve:
+ self.comm_cache.pop(sample.sample_pid, None)
+
+ comm = "Unknown"
+ if hasattr(self, 'session') and self.session:
+ # In offline perf.data mode, query session.find_thread() directly
+ # so PERF_RECORD_COMM updates after execve (e.g. perf -> sleep)
+ # are reflected immediately rather than returning a stale cached comm.
+ try:
+ proc = self.session.find_thread(sample.sample_pid, sample.sample_tid)
+ if proc:
+ comm = proc.comm() or "Unknown"
+ except TypeError:
+ pass
+ if comm != "Unknown" and not is_execve:
+ self.comm_cache[sample.sample_pid] = comm
+ elif sample.sample_pid in self.comm_cache:
+ comm = self.comm_cache[sample.sample_pid]
+ elif sample.sample_pid in self.comm_cache:
+ comm = self.comm_cache[sample.sample_pid]
+ else:
+ try:
+ with open(f"/proc/{sample.sample_pid}/comm", "r",
+ encoding="utf-8", errors="replace") as f:
+ comm = f.read().strip()
+ except OSError:
+ comm = "Unknown"
+ # Cache both matching and non-matching comms (including "Unknown"
+ # when a PID has exited or is inaccessible) so live system-wide
+ # tracing does not re-open /proc/<pid>/comm on every syscall.
+ # Do not cache during sys_enter(execve/execveat) since /proc/<pid>/comm
+ # still holds the pre-exec command name until the syscall completes.
+ if not is_execve:
+ self.comm_cache[sample.sample_pid] = comm
+
+ if comm != self.for_comm:
+ skip = True
+
+ is_enter = (name.startswith("evsel(raw_syscalls:sys_enter") or
+ name.startswith("evsel(syscalls:sys_enter"))
+ if not skip and is_enter and 0 <= (syscall_id & ~0x40000000) <= 0xffff:
+ self.syscalls[syscall_id] += 1
+
+ if self.offline and hasattr(sample, "sample_time"):
+ interval_ns = self.interval * (10 ** 9)
+ if self.last_print_time is None:
+ self.last_print_time = sample.sample_time
+ elif sample.sample_time - self.last_print_time >= interval_ns:
+ self.print_current_totals()
+ self.last_print_time = sample.sample_time
+
+ def print_current_totals(self):
+ """Print current syscall totals."""
+ self.printed = True
+ # Clear terminal
+ if not self.offline:
+ print("\x1b[2J\x1b[H", end="")
+ else:
+ print()
+
+ with self.lock:
+ for_comm = self.for_comm
+ if for_comm is not None:
+ print(f"\nsyscall events for {for_comm}:\n")
+ else:
+ print("\nsyscall events:\n")
+
+ print(f"{'event':40s} {'count':10s}")
+ print(f"{'-' * 40:40s} {'-' * 10:10s}")
+
+ with self.lock:
+ current_syscalls = list(self.syscalls.items())
+ self.syscalls.clear()
+ self.comm_cache.clear()
+
+ current_syscalls.sort(key=lambda kv: (-kv[1], kv[0]))
+
+ for syscall_id, val in current_syscalls:
+ print(f"{self.syscall_name(syscall_id):<40s} {val:10d}")
+
+ def print_syscall_totals(self):
+ """Periodically print syscall totals."""
+ while not self.stop_event.is_set():
+ self.print_current_totals()
+ self.stop_event.wait(self.interval)
+ # Print final batch
+ self.print_current_totals()
+
+ def start(self):
+ """Start the background thread."""
+ self.thread.start()
+
+ def stop(self):
+ """Stop the background thread."""
+ self.stop_event.set()
+ self.thread.join()
+
+
+def main():
+ """Main function."""
+ ap = argparse.ArgumentParser(description="System call top")
+ ap.add_argument("args", nargs="*", help="[comm] [interval] or [interval]")
+ ap.add_argument("-i", "--input", help="Input file name")
+ args = ap.parse_args()
+
+ for_comm = None
+ default_interval = 3
+ interval = default_interval
+
+ if len(args.args) > 2:
+ print("Usage: python sctop.py [comm] [interval]")
+ sys.exit(1)
+
+ if len(args.args) > 1:
+ for_comm = args.args[0]
+ try:
+ interval = int(args.args[1])
+ except ValueError:
+ print(f"Invalid interval: {args.args[1]}")
+ sys.exit(1)
+ elif len(args.args) > 0:
+ try:
+ interval = int(args.args[0])
+ except ValueError:
+ for_comm = args.args[0]
+ interval = default_interval
+
+ analyzer = SCTopAnalyzer(for_comm, interval, offline=bool(args.input))
+ session = None
+
+ try:
+ if args.input:
+ session = perf.session(perf.data(args.input), sample=analyzer.process_event)
+ analyzer.session = session
+ analyzer.e_machine = getattr(session, "e_machine", None)
+ session.process_events()
+ else:
+ try:
+ live_session = LiveSession(
+ "raw_syscalls:sys_enter", sample_callback=analyzer.process_event
+ )
+ except OSError:
+ live_session = LiveSession(
+ "syscalls:sys_enter_*", sample_callback=analyzer.process_event
+ )
+ analyzer.start()
+ live_session.run()
+ except KeyboardInterrupt:
+ pass
+ except (OSError, IOError) as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+ finally:
+ if args.input:
+ if not analyzer.printed or analyzer.syscalls:
+ analyzer.print_current_totals()
+ # Break the reference cycle between perf.session and analyzer.process_event
+ # because perf.session lacks cyclic GC support (tp_traverse).
+ analyzer.session = None
+ session = None
+ elif analyzer.thread.is_alive():
+ analyzer.stop()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/stackcollapse.py b/tools/perf/python/stackcollapse.py
new file mode 100755
index 000000000000..0e8a65969db3
--- /dev/null
+++ b/tools/perf/python/stackcollapse.py
@@ -0,0 +1,145 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+stackcollapse.py - format perf samples with one line per distinct call stack
+
+This script's output has two space-separated fields. The first is a semicolon
+separated stack including the program name (from the "comm" field) and the
+function names from the call stack. The second is a count:
+
+ swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 2
+
+The file is sorted according to the first field.
+
+Ported from tools/perf/scripts/python/stackcollapse.py
+"""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import os
+import sys
+import perf
+
+
+class StackCollapseAnalyzer:
+ """Accumulates call stacks and prints them collapsed."""
+
+ def __init__(self, args: argparse.Namespace) -> None:
+ self.args = args
+ self.lines: dict[str, int] = defaultdict(int)
+ self.session: perf.session | None = None
+
+ def tidy_function_name(self, sym: str, dso: str) -> str:
+ """Beautify function names based on options."""
+ if sym is None:
+ sym = "[unknown]"
+
+ sym = sym.replace(";", ":")
+ if self.args.tidy_java:
+ # Beautify Java signatures
+ sym = sym.replace("<", "")
+ sym = sym.replace(">", "")
+ if sym.startswith("L") and "/" in sym:
+ sym = sym[1:]
+ try:
+ sym = sym[:sym.index("(")]
+ except ValueError:
+ pass
+
+ if self.args.annotate_kernel and dso == "[kernel.kallsyms]":
+ return sym + "_[k]"
+ return sym
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Collect call stack for each sample."""
+ stack = []
+ callchain = sample.callchain
+ if callchain is not None:
+ for node in callchain:
+ stack.append(self.tidy_function_name(node.symbol, node.dso))
+ else:
+ # Fallback if no callchain
+ sym = (sample.symbol or '[unknown]')
+ dso = (sample.dso or '[unknown]')
+ stack.append(self.tidy_function_name(sym, dso))
+
+ if self.args.include_comm:
+ comm = "Unknown"
+ if self.session is not None:
+ try:
+ proc = self.session.find_thread(
+ sample.sample_pid, sample.sample_tid
+ )
+ if proc:
+ proc_comm = proc.comm()
+ if proc_comm is not None:
+ comm = proc_comm
+ except TypeError:
+ pass
+ comm = str(comm).replace(" ", "_")
+ sep = "-"
+ if self.args.include_pid:
+ comm = f"{comm}{sep}{(sample.sample_pid or 0)}"
+ sep = "/"
+ if self.args.include_tid:
+ comm = f"{comm}{sep}{(sample.sample_tid or 0)}"
+ stack.append(comm)
+
+ stack_string = ";".join(reversed(stack))
+ self.lines[stack_string] += 1
+
+ def print_totals(self) -> None:
+ """Print sorted collapsed stacks."""
+ try:
+ for stack in sorted(self.lines):
+ print(f"{stack} {self.lines[stack]}")
+ sys.stdout.flush()
+ except BrokenPipeError:
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ os.dup2(devnull, sys.stdout.fileno())
+ os.close(devnull)
+
+
+def main():
+ """Main function."""
+ ap = argparse.ArgumentParser(
+ description="Format perf samples with one line per distinct call stack"
+ )
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ ap.add_argument("--include-tid", action="store_true", help="include thread id in stack")
+ ap.add_argument("--include-pid", action="store_true", help="include process id in stack")
+ ap.add_argument("--no-comm", dest="include_comm", action="store_false", default=True,
+ help="do not separate stacks according to comm")
+ ap.add_argument("--tidy-java", action="store_true", help="beautify Java signatures")
+ ap.add_argument("--kernel", dest="annotate_kernel", action="store_true",
+ help="annotate kernel functions with _[k]")
+
+ args = ap.parse_args()
+
+ if args.include_tid and not args.include_comm:
+ print("requesting tid but not comm is invalid", file=sys.stderr)
+ sys.exit(1)
+ if args.include_pid and not args.include_comm:
+ print("requesting pid but not comm is invalid", file=sys.stderr)
+ sys.exit(1)
+
+ analyzer = StackCollapseAnalyzer(args)
+
+ try:
+ session = perf.session(perf.data(args.input), sample=analyzer.process_event)
+ analyzer.session = session
+ session.process_events()
+ except IOError as e:
+ print(f"Error: {e}", file=sys.stderr)
+ sys.exit(1)
+ except KeyboardInterrupt:
+ pass
+ finally:
+ analyzer.session = None
+
+ analyzer.print_totals()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/stat-cpi.py b/tools/perf/python/stat-cpi.py
new file mode 100755
index 000000000000..b4ec87b07938
--- /dev/null
+++ b/tools/perf/python/stat-cpi.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""Calculate CPI from perf stat data or live."""
+from __future__ import annotations
+
+import argparse
+import os
+import signal
+import sys
+import time
+from typing import Any, Optional
+import perf
+
+class StatCpiAnalyzer:
+ """Accumulates cycles and instructions and calculates CPI."""
+
+ def __init__(self, args: argparse.Namespace) -> None:
+ self.args = args
+ self.data: dict[str, float] = {}
+ self.prev_data: dict[str, tuple[int, int, int]] = {}
+ self.recorded_pairs: set[tuple[int, int]] = set()
+
+ def get_key(self, event: str, cpu: int, thread: int) -> str:
+ """Get key for data dictionary."""
+ return f"{event}-{cpu}-{thread}"
+
+ def store_key(self, cpu: int, thread: int) -> None:
+ """Store CPU and thread IDs."""
+ self.recorded_pairs.add((cpu, thread))
+
+ def store(self, event: str, cpu: int, thread: int,
+ counts: tuple[int, int, int], is_delta: bool = False,
+ raw_name: Optional[str] = None) -> None:
+ """Store counter values, computing difference from previous
+ absolute values if not already deltas."""
+ self.store_key(cpu, thread)
+ key = self.get_key(event, cpu, thread)
+ prev_key = self.get_key(raw_name or event, cpu, thread)
+
+ val, ena, run = counts
+ if is_delta:
+ # counts are already deltas
+ cur_val = val
+ cur_ena = ena
+ cur_run = run
+ else:
+ if prev_key in self.prev_data:
+ prev_val, prev_ena, prev_run = self.prev_data[prev_key]
+ cur_val = val - prev_val
+ cur_ena = ena - prev_ena
+ cur_run = run - prev_run
+ else:
+ cur_val = val
+ cur_ena = ena
+ cur_run = run
+ self.prev_data[prev_key] = counts # Store absolute value for next time
+
+ # Scale each raw event's delta by its own multiplexing ratio before
+ # summing across PMUs (e.g. cpu_core and cpu_atom on hybrid systems) so
+ # enabled time from an idle PMU does not inflate the multiplier of an
+ # active PMU.
+ scaled_val = cur_val * (cur_ena / float(cur_run)) if cur_run > 0 else float(cur_val)
+ self.data[key] = self.data.get(key, 0.0) + scaled_val
+
+ def get(self, event: str, cpu: int, thread: int) -> float:
+ """Get scaled counter value."""
+ key = self.get_key(event, cpu, thread)
+ return self.data.get(key, 0.0)
+
+ @staticmethod
+ def _classify_event(name: str) -> Optional[str]:
+ """Classify an event name as 'cycles' or 'instructions'."""
+ ev = name[6:-1] if name.startswith("evsel(") and name.endswith(")") else name
+ ev = ev.split(":", 1)[0]
+ if "/" in ev:
+ parts = [p for p in ev.split("/") if p]
+ if len(parts) >= 2:
+ ev = parts[1]
+ if ev in ("cycles", "cpu-cycles"):
+ return "cycles"
+ if ev == "instructions":
+ return "instructions"
+ return None
+
+ def process_stat_event(self, event: Any, name: Optional[str] = None) -> None:
+ """Process PERF_RECORD_STAT and PERF_RECORD_STAT_ROUND events."""
+ if event.type == perf.RECORD_STAT:
+ if name:
+ event_name = self._classify_event(name)
+ if not event_name:
+ return
+ self.store(event_name, event.cpu, event.thread,
+ (event.val, event.ena, event.run), raw_name=name)
+ elif event.type == perf.RECORD_STAT_ROUND:
+ timestamp = getattr(event, "time", 0)
+ self.print_interval(timestamp)
+ self.data.clear()
+ self.recorded_pairs.clear()
+
+ def print_interval(self, timestamp: int) -> None:
+ """Print CPI for the current interval."""
+ for cpu, thread in sorted(self.recorded_pairs):
+ cyc = self.get("cycles", cpu, thread)
+ ins = self.get("instructions", cpu, thread)
+ cpi = 0.0
+ if ins != 0:
+ cpi = cyc / float(ins)
+ t_sec = timestamp / 1000000000.0
+ print(f"{t_sec:15f}: cpu {cpu}, thread {thread} -> cpi {cpi:f} ({cyc:.0f}/{ins:.0f})",
+ flush=True)
+
+ def read_counters(self, evlist: Any) -> None:
+ """Read counters live."""
+ for evsel in evlist:
+ name = str(evsel)
+ event_name = self._classify_event(name)
+ if not event_name:
+ continue
+
+ for cpu in evsel.cpus():
+ for thread in evsel.threads():
+ try:
+ counts = evsel.read(cpu, thread)
+ self.store(event_name, cpu, thread,
+ (counts.val, counts.ena, counts.run),
+ is_delta=True, raw_name=name)
+ except OSError:
+ pass
+
+ def run_file(self) -> None:
+ """Process events from file."""
+ session: Optional[perf.session] = perf.session(
+ perf.data(self.args.input), stat=self.process_stat_event
+ )
+ try:
+ assert session is not None
+ session.process_events()
+ finally:
+ session = None
+
+ def _open_live_evlist(self) -> Any:
+ """Open evlist for live mode, falling back to user-space or process scope on EACCES."""
+ threads = perf.thread_map(self.args.pid) if self.args.pid else None
+ candidates = [
+ ("cycles,instructions", threads),
+ ("cycles:u,instructions:u", threads),
+ ]
+ if threads is None:
+ self_threads = perf.thread_map(os.getpid())
+ candidates.append(("cycles,instructions", self_threads))
+ candidates.append(("cycles:u,instructions:u", self_threads))
+
+ last_err: Optional[OSError] = None
+ for events, tmap in candidates:
+ evlist = None
+ try:
+ evlist = perf.parse_events(events, None, tmap)
+ for evsel in evlist:
+ evsel.read_format |= (
+ perf.FORMAT_TOTAL_TIME_ENABLED | perf.FORMAT_TOTAL_TIME_RUNNING
+ )
+ evlist.open()
+ evlist.enable()
+ return evlist
+ except PermissionError as e:
+ if evlist is not None:
+ evlist.close()
+ last_err = e
+ except OSError as e:
+ if evlist is not None:
+ evlist.close()
+ if e.errno == 13:
+ last_err = e
+ else:
+ raise
+ except BaseException:
+ if evlist is not None:
+ evlist.close()
+ raise
+ if last_err is not None:
+ raise last_err
+ raise RuntimeError("Failed to open events")
+
+ def run_live(self) -> None:
+ """Read counters live."""
+ def handle_signal(_signum: int, _frame: Any) -> None:
+ raise KeyboardInterrupt
+
+ signal.signal(signal.SIGINT, signal.default_int_handler)
+ signal.signal(signal.SIGTERM, handle_signal)
+
+ evlist = None
+ try:
+ try:
+ evlist = self._open_live_evlist()
+ except OSError as e:
+ print(f"Failed to open events: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ print("Live mode started. Press Ctrl+C to stop.", flush=True)
+ while True:
+ time.sleep(self.args.interval)
+ timestamp = time.time_ns()
+ self.read_counters(evlist)
+ self.print_interval(timestamp)
+ self.data.clear()
+ self.recorded_pairs.clear()
+ except KeyboardInterrupt:
+ print("\nStopped.", flush=True)
+ finally:
+ if evlist is not None:
+ evlist.close()
+
+def main() -> None:
+ """Main function."""
+ ap = argparse.ArgumentParser(description="Calculate CPI from perf stat data or live")
+ ap.add_argument("-i", "--input", help="Input file name (enables file mode)")
+ ap.add_argument("-I", "--interval", type=float, default=1.0,
+ help="Interval in seconds for live mode")
+ ap.add_argument("-p", "--pid", type=int,
+ help="Monitor specific process ID in live mode")
+ args = ap.parse_args()
+
+ analyzer = StatCpiAnalyzer(args)
+ if args.input:
+ analyzer.run_file()
+ else:
+ analyzer.run_live()
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/syscall-counts-by-pid.py b/tools/perf/python/syscall-counts-by-pid.py
new file mode 100755
index 000000000000..fc0ec1bf7ca1
--- /dev/null
+++ b/tools/perf/python/syscall-counts-by-pid.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Displays system-wide system call totals, broken down by syscall.
+If a [comm] arg is specified, only syscalls called by [comm] are displayed.
+"""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+from typing import (Dict, Tuple)
+import perf
+
+syscalls: Dict[Tuple[str, int, int], int] = defaultdict(int)
+for_comm = None
+for_pid = None
+session = None
+
+
+def print_syscall_totals():
+ """Print aggregated statistics."""
+ if for_comm is not None:
+ print(f"\nsyscall events for {for_comm}:\n")
+ elif for_pid is not None:
+ print(f"\nsyscall events for PID {for_pid}:\n")
+ else:
+ print("\nsyscall events:\n")
+
+ print(f"{'comm [pid]/syscalls':<40} {'count':>10}")
+ print("---------------------------------------- -----------")
+
+ sorted_keys = sorted(syscalls.keys(), key=lambda k: (k[0], k[1], -syscalls[k], k[2]))
+ current_comm_pid = None
+ for comm, pid, sc_id in sorted_keys:
+ if current_comm_pid != (comm, pid):
+ print(f"\n{comm} [{pid}]")
+ current_comm_pid = (comm, pid)
+ e_machine = getattr(session, "e_machine", 0) or 0
+ # Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000) before
+ # resolving the syscall number in the architecture's syscall table.
+ raw_sc_id = sc_id & ~0x40000000
+ if e_machine:
+ name = perf.syscall_name(raw_sc_id, e_machine) or str(sc_id)
+ else:
+ name = perf.syscall_name(raw_sc_id) or str(sc_id)
+ print(f" {name:<38} {syscalls[(comm, pid, sc_id)]:>10}")
+
+
+def process_event(sample):
+ """Process a single sample event."""
+ event_name = str(sample.evsel)
+ # Per-syscall syscalls:sys_enter_* tracepoints expose '__syscall_nr' (or 'nr')
+ # and may have an unrelated syscall argument named 'id', whereas
+ # raw_syscalls:sys_enter (and legacy pre-2.6.35 syscalls:sys_enter) expose 'id'.
+ if event_name.startswith("evsel(syscalls:sys_enter_"):
+ sc_id = getattr(sample, "__syscall_nr", None)
+ if sc_id is not None and not (0 <= (sc_id & ~0x40000000) <= 0xffff):
+ sc_id = None
+ if sc_id is None:
+ sc_id = getattr(sample, "nr", -1)
+ elif event_name.startswith(("evsel(raw_syscalls:sys_enter", "evsel(syscalls:sys_enter")):
+ sc_id = getattr(sample, "id", -1)
+ if not (0 <= (sc_id & ~0x40000000) <= 0xffff):
+ sc_id = getattr(sample, "__syscall_nr", -1)
+ if not (0 <= (sc_id & ~0x40000000) <= 0xffff):
+ sc_id = getattr(sample, "nr", -1)
+ else:
+ return
+
+ # Mask out __X32_SYSCALL_BIT (0x40000000) when validating the syscall ID range.
+ if not (0 <= (sc_id & ~0x40000000) <= 0xffff):
+ return
+
+ pid = sample.sample_pid
+
+ if for_pid is not None and pid != for_pid:
+ return
+
+ comm = "unknown"
+ try:
+ if session:
+ proc = session.find_thread(sample.sample_pid, sample.sample_tid)
+ if proc:
+ comm = proc.comm() or "unknown"
+ except (TypeError, AttributeError):
+ pass
+
+ if for_comm and comm != for_comm:
+ return
+ syscalls[(comm, pid, sc_id)] += 1
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser()
+ ap.add_argument("filter", nargs="?", help="COMM or PID to filter by")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ args = ap.parse_args()
+
+ if args.filter:
+ try:
+ for_pid = int(args.filter)
+ except ValueError:
+ for_comm = args.filter
+
+ try:
+ session = perf.session(perf.data(args.input), sample=process_event)
+ session.process_events()
+ print_syscall_totals()
+ finally:
+ # Break the reference cycle between session and process_event (whose module
+ # globals reference session) since perf.session lacks cyclic GC (tp_traverse).
+ session = None
diff --git a/tools/perf/python/syscall-counts.py b/tools/perf/python/syscall-counts.py
new file mode 100755
index 000000000000..3ddb1bcc27e0
--- /dev/null
+++ b/tools/perf/python/syscall-counts.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+Displays system-wide system call totals, broken down by syscall.
+
+If a [comm] arg is specified, only syscalls called by [comm] are displayed.
+"""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+from typing import DefaultDict
+import perf
+
+syscalls: DefaultDict[int, int] = defaultdict(int)
+for_comm = None
+session = None
+
+
+def print_syscall_totals():
+ """Print aggregated statistics."""
+ if for_comm is not None:
+ print(f"\nsyscall events for {for_comm}:\n")
+ else:
+ print("\nsyscall events:\n")
+
+ print(f"{'event':<40} {'count':>10}")
+ print("---------------------------------------- -----------")
+
+ for sc_id, val in sorted(syscalls.items(),
+ key=lambda kv: (-kv[1], kv[0])):
+ e_machine = getattr(session, "e_machine", 0) or 0
+ # Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000) before
+ # resolving the syscall number in the architecture's syscall table.
+ raw_sc_id = sc_id & ~0x40000000
+ if e_machine:
+ name = perf.syscall_name(raw_sc_id, e_machine) or str(sc_id)
+ else:
+ name = perf.syscall_name(raw_sc_id) or str(sc_id)
+ print(f"{name:<40} {val:>10}")
+
+
+def process_event(sample):
+ """Process a single sample event."""
+ event_name = str(sample.evsel)
+ # Per-syscall syscalls:sys_enter_* tracepoints expose '__syscall_nr' (or 'nr')
+ # and may have an unrelated syscall argument named 'id' (e.g. sys_enter_clock_gettime),
+ # whereas raw_syscalls:sys_enter (and legacy pre-2.6.35 syscalls:sys_enter) expose 'id'.
+ if event_name.startswith("evsel(syscalls:sys_enter_"):
+ sc_id = getattr(sample, "__syscall_nr", None)
+ if sc_id is not None and not (0 <= (sc_id & ~0x40000000) <= 0xffff):
+ sc_id = None
+ if sc_id is None:
+ sc_id = getattr(sample, "nr", -1)
+ elif event_name.startswith(("evsel(raw_syscalls:sys_enter", "evsel(syscalls:sys_enter")):
+ sc_id = getattr(sample, "id", -1)
+ if not (0 <= (sc_id & ~0x40000000) <= 0xffff):
+ sc_id = getattr(sample, "__syscall_nr", -1)
+ if not (0 <= (sc_id & ~0x40000000) <= 0xffff):
+ sc_id = getattr(sample, "nr", -1)
+ else:
+ return
+
+ # Mask out __X32_SYSCALL_BIT (0x40000000) when validating the syscall ID range.
+ if not (0 <= (sc_id & ~0x40000000) <= 0xffff):
+ return
+
+ comm = "unknown"
+ try:
+ if session:
+ proc = session.find_thread(sample.sample_pid, sample.sample_tid)
+ if proc:
+ comm = proc.comm() or "unknown"
+ except (TypeError, AttributeError):
+ pass
+
+ if for_comm and comm != for_comm:
+ return
+ syscalls[sc_id] += 1
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser()
+ ap.add_argument("comm", nargs="?", help="Only report syscalls for comm")
+ ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ args = ap.parse_args()
+ for_comm = args.comm
+ try:
+ session = perf.session(perf.data(args.input), sample=process_event)
+ session.process_events()
+ print_syscall_totals()
+ finally:
+ # Break the reference cycle between session and process_event (whose module
+ # globals reference session) since perf.session lacks cyclic GC (tp_traverse).
+ session = None
diff --git a/tools/perf/python/task-analyzer.py b/tools/perf/python/task-analyzer.py
new file mode 100755
index 000000000000..fedba36b5ced
--- /dev/null
+++ b/tools/perf/python/task-analyzer.py
@@ -0,0 +1,908 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+# task-analyzer.py - comprehensive perf tasks analysis
+# Copyright (c) 2022, Hagen Paul Pfeifer <hagen@jauu.net>
+# Licensed under the terms of the GNU GPL License version 2
+#
+# Usage:
+#
+# perf record -e sched:sched_switch -a -- sleep 10
+# ./task-analyzer.py
+#
+"""Comprehensive perf tasks analysis."""
+from __future__ import annotations
+
+import argparse
+from contextlib import contextmanager
+import decimal
+
+from typing import List, Dict, Union
+
+def _median(numbers: List[decimal.Decimal]) -> decimal.Decimal:
+ """phython3 hat statistics module - we have nothing"""
+ n = len(numbers)
+ index = n // 2
+ if n % 2:
+ return sorted(numbers)[index]
+ return sum(sorted(numbers)[index - 1 : index + 1]) / decimal.Decimal(2)
+
+def _mean(numbers: List[decimal.Decimal]) -> decimal.Decimal:
+ return sum(numbers) / decimal.Decimal(len(numbers))
+
+import os
+import string
+import sys
+from typing import Any, Optional
+import perf
+
+
+# Columns will have a static size to align everything properly
+# Support of 116 days of active update with nano precision
+LEN_SWITCHED_IN = len("9999999.999999999")
+LEN_SWITCHED_OUT = len("9999999.999999999")
+LEN_CPU = len("000")
+LEN_PID = len("maxvalue")
+LEN_TID = len("maxvalue")
+LEN_COMM = len("max-comms-length")
+LEN_RUNTIME = len("999999.999")
+# Support of 3.45 hours of timespans
+LEN_OUT_IN = len("99999999999.999")
+LEN_OUT_OUT = len("99999999999.999")
+LEN_IN_IN = len("99999999999.999")
+LEN_IN_OUT = len("99999999999.999")
+
+class Timespans:
+ """Tracks elapsed time between occurrences of the same task."""
+ def __init__(self, args: argparse.Namespace, time_unit: str) -> None:
+ self.args = args
+ self.time_unit = time_unit
+ self._last_start: Optional[decimal.Decimal] = None
+ self._last_finish: Optional[decimal.Decimal] = None
+ self.current = {
+ 'out_out': decimal.Decimal(-1),
+ 'in_out': decimal.Decimal(-1),
+ 'out_in': decimal.Decimal(-1),
+ 'in_in': decimal.Decimal(-1)
+ }
+ if args.summary_extended:
+ self._time_in: decimal.Decimal = decimal.Decimal(-1)
+ self.max_vals = {
+ 'out_in': decimal.Decimal(-1),
+ 'at': decimal.Decimal(-1),
+ 'in_out': decimal.Decimal(-1),
+ 'in_in': decimal.Decimal(-1),
+ 'out_out': decimal.Decimal(-1)
+ }
+
+ def feed(self, task: 'Task') -> None:
+ """Calculate timespans from chronological task occurrences."""
+ if not self._last_finish:
+ self._last_start = task.time_in(self.time_unit)
+ self._last_finish = task.time_out(self.time_unit)
+ return
+ assert self._last_start is not None
+ assert self._last_finish is not None
+ self._time_in = task.time_in()
+ time_in = task.time_in(self.time_unit)
+ time_out = task.time_out(self.time_unit)
+ self.current['in_in'] = time_in - self._last_start
+ self.current['out_in'] = time_in - self._last_finish
+ self.current['in_out'] = time_out - self._last_start
+ self.current['out_out'] = time_out - self._last_finish
+ if self.args.summary_extended:
+ self.update_max_entries()
+ self._last_finish = task.time_out(self.time_unit)
+ self._last_start = task.time_in(self.time_unit)
+
+ def update_max_entries(self) -> None:
+ """Update maximum timespans."""
+ self.max_vals['in_in'] = max(self.max_vals['in_in'], self.current['in_in'])
+ self.max_vals['out_out'] = max(self.max_vals['out_out'], self.current['out_out'])
+ self.max_vals['in_out'] = max(self.max_vals['in_out'], self.current['in_out'])
+ if self.current['out_in'] > self.max_vals['out_in']:
+ self.max_vals['out_in'] = self.current['out_in']
+ self.max_vals['at'] = self._time_in
+
+class Task:
+ """Handles information of a given task."""
+ def __init__(self, task_id: str, tid: int, cpu: int, comm: str) -> None:
+ self.id = task_id
+ self.tid = tid
+ self.cpu = cpu
+ self.comm = comm
+ self.pid: Optional[int] = None
+ self._time_in: Optional[decimal.Decimal] = None
+ self._time_out: Optional[decimal.Decimal] = None
+
+ def schedule_in_at(self, time_ns: int) -> None:
+ """Set schedule in time."""
+ self._time_in = decimal.Decimal(time_ns) / decimal.Decimal(1e9)
+
+ def schedule_out_at(self, time_ns: int) -> None:
+ """Set schedule out time."""
+ self._time_out = decimal.Decimal(time_ns) / decimal.Decimal(1e9)
+
+ def time_out(self, unit: str = "s") -> decimal.Decimal:
+ """Return schedule out time."""
+ factor = TaskAnalyzer.time_uniter(unit)
+ return self._time_out * decimal.Decimal(factor) if self._time_out else decimal.Decimal(0)
+
+ def time_in(self, unit: str = "s") -> decimal.Decimal:
+ """Return schedule in time."""
+ factor = TaskAnalyzer.time_uniter(unit)
+ return self._time_in * decimal.Decimal(factor) if self._time_in else decimal.Decimal(0)
+
+ def runtime(self, unit: str = "us") -> decimal.Decimal:
+ """Return runtime."""
+ factor = TaskAnalyzer.time_uniter(unit)
+ if self._time_out is not None and self._time_in is not None:
+ return (self._time_out - self._time_in) * decimal.Decimal(factor)
+ return decimal.Decimal(0)
+
+ def update_pid(self, pid: int) -> None:
+ """Update PID."""
+ self.pid = pid
+
+class Summary:
+ """
+ Primary instance for calculating the summary output. Processes the whole trace to
+ find and memorize relevant data such as mean, max et cetera. This instance handles
+ dynamic alignment aspects for summary output.
+ """
+
+ def __init__(self, analyzer):
+ self.analyzer = analyzer
+ self.args = analyzer.args
+ self.db = analyzer.db
+ self.time_unit = analyzer.time_unit
+ self.fd_sum = analyzer.fd_sum
+ self._body = []
+
+ class AlignmentHelper:
+ """
+ Used to calculated the alignment for the output of the summary.
+ """
+ def __init__(self, pid, tid, comm, runs, acc, mean,
+ median, min_val, max_val, max_at):
+ self.pid = pid
+ self.tid = tid
+ self.comm = comm
+ self.runs = runs
+ self.acc = acc
+ self.mean = mean
+ self.median = median
+ self.min = min_val
+ self.max = max_val
+ self.max_at = max_at
+ self.out_in = None
+ self.inter_at = None
+ self.out_out = None
+ self.in_in = None
+ self.in_out = None
+
+ def _print_header(self):
+ '''
+ Output is aligned using the formatted column widths in self.db.
+ '''
+ len_tasks = max(
+ len("Task Information"),
+ sum(self.db["task_info"].values()) + len(self.db["task_info"]) - 1,
+ )
+ fmt = "{{:^{}}}".format(len_tasks)
+ fmt += " {{:^{}}}".format(
+ sum(self.db["runtime_info"].values()) + len(self.db["runtime_info"]) - 1
+ )
+ _header = ("Task Information", "Runtime Information")
+
+ if self.args.summary_extended:
+ fmt += " {{:^{}}}".format(
+ sum(self.db["inter_times"].values()) + len(self.db["inter_times"]) - 1
+ )
+ _header += ("Max Inter Task Times",)
+ self.fd_sum.write(fmt.format(*_header) + "\n")
+
+ def _column_titles(self):
+ """
+ Cells are being processed and displayed in different way so an alignment adjust
+ is implemented depeding on the choice of the timeunit. The positions of the max
+ values are being displayed in grey. Thus in their format two additional {},
+ are placed for color set and reset.
+ """
+ separator, fix_csv_align = self.analyzer.prepare_fmt_sep(is_summary=True)
+ fmt = "{{:>{}}}".format(self.db["task_info"]["pid"] * fix_csv_align)
+ fmt += "{}{{:>{}}}".format(separator, self.db["task_info"]["tid"] * fix_csv_align)
+ fmt += "{}{{:>{}}}".format(separator, self.db["task_info"]["comm"] * fix_csv_align)
+ fmt += "{}{{:>{}}}".format(separator, self.db["runtime_info"]["runs"] * fix_csv_align)
+ fmt += "{}{{:>{}}}".format(separator, self.db["runtime_info"]["acc"] * fix_csv_align)
+ fmt += "{}{{:>{}}}".format(separator, self.db["runtime_info"]["mean"] * fix_csv_align)
+ fmt += "{}{{:>{}}}".format(
+ separator, self.db["runtime_info"]["median"] * fix_csv_align
+ )
+ fmt += "{}{{:>{}}}".format(
+ separator, self.db["runtime_info"]["min"] * fix_csv_align
+ )
+ fmt += "{}{{:>{}}}".format(
+ separator, self.db["runtime_info"]["max"] * fix_csv_align
+ )
+ fmt += "{}{{}}{{:>{}}}{{}}".format(
+ separator, self.db["runtime_info"]["max_at"] * fix_csv_align
+ )
+
+ grey = "" if self.args.csv_summary else TaskAnalyzer.COLORS["grey"]
+ reset = "" if self.args.csv_summary else TaskAnalyzer.COLORS["reset"]
+ column_titles = ("PID", "TID", "Comm")
+ column_titles += ("Runs", "Accumulated", "Mean", "Median", "Min", "Max")
+ column_titles += (grey, "Max At", reset)
+
+ if self.args.summary_extended:
+ fmt += "{}{{:>{}}}".format(
+ separator,
+ self.db["inter_times"]["out_in"] * fix_csv_align
+ )
+ fmt += "{}{{}}{{:>{}}}{{}}".format(
+ separator,
+ self.db["inter_times"]["inter_at"] * fix_csv_align
+ )
+ fmt += "{}{{:>{}}}".format(
+ separator,
+ self.db["inter_times"]["out_out"] * fix_csv_align
+ )
+ fmt += "{}{{:>{}}}".format(
+ separator,
+ self.db["inter_times"]["in_in"] * fix_csv_align
+ )
+ fmt += "{}{{:>{}}}".format(
+ separator,
+ self.db["inter_times"]["in_out"] * fix_csv_align
+ )
+
+ column_titles += (
+ "Out-In", grey, "Max At",
+ reset, "Out-Out", "In-In", "In-Out"
+ )
+
+ self.fd_sum.write(fmt.format(*column_titles) + "\n")
+
+ def _task_stats(self):
+ """calculates the stats of every task and constructs the printable summary"""
+ grey = "" if self.args.csv_summary else TaskAnalyzer.COLORS["grey"]
+ reset = "" if self.args.csv_summary else TaskAnalyzer.COLORS["reset"]
+ for tid in sorted(self.db["tid"]):
+ color_one_sample = grey
+ color_reset = reset
+ no_executed = 0
+ runtimes = []
+ time_in = []
+ timespans = Timespans(self.args, self.time_unit)
+ for task in self.db["tid"][tid]:
+ pid = task.pid
+ comm = task.comm
+ no_executed += 1
+ runtimes.append(task.runtime(self.time_unit))
+ time_in.append(task.time_in())
+ timespans.feed(task)
+ if len(runtimes) > 1:
+ color_one_sample = ""
+ color_reset = ""
+ time_max = max(runtimes)
+ time_min = min(runtimes)
+ max_at = time_in[runtimes.index(max(runtimes))]
+
+ # The size of the decimal after sum,mean and median varies, thus we cut
+ # the decimal number, by rounding it. It has no impact on the output,
+ # because we have a precision of the decimal points at the output.
+ time_sum = round(sum(runtimes), 3)
+ time_mean = round(_mean(runtimes), 3)
+ time_median = round(_median(runtimes), 3)
+
+ align_helper = self.AlignmentHelper(pid, tid, comm, no_executed, time_sum,
+ time_mean, time_median, time_min, time_max, max_at)
+ self._body.append([
+ pid, tid, comm, no_executed, time_sum, color_one_sample,
+ time_mean, time_median, time_min, time_max,
+ grey, max_at,
+ reset, color_reset
+ ])
+ if self.args.summary_extended:
+ self._body[-1].extend([timespans.max_vals['out_in'],
+ grey, timespans.max_vals['at'],
+ reset, timespans.max_vals['out_out'],
+ timespans.max_vals['in_in'],
+ timespans.max_vals['in_out']])
+ align_helper.out_in = timespans.max_vals['out_in']
+ align_helper.inter_at = timespans.max_vals['at']
+ align_helper.out_out = timespans.max_vals['out_out']
+ align_helper.in_in = timespans.max_vals['in_in']
+ align_helper.in_out = timespans.max_vals['in_out']
+ self._calc_alignments_summary(align_helper)
+
+ def _format_stats(self):
+ separator, fix_csv_align = self.analyzer.prepare_fmt_sep(is_summary=True)
+ decimal_precision, time_precision = self.analyzer.prepare_fmt_precision()
+ len_pid = self.db["task_info"]["pid"] * fix_csv_align
+ len_tid = self.db["task_info"]["tid"] * fix_csv_align
+ len_comm = self.db["task_info"]["comm"] * fix_csv_align
+ len_runs = self.db["runtime_info"]["runs"] * fix_csv_align
+ len_acc = self.db["runtime_info"]["acc"] * fix_csv_align
+ len_mean = self.db["runtime_info"]["mean"] * fix_csv_align
+ len_median = self.db["runtime_info"]["median"] * fix_csv_align
+ len_min = self.db["runtime_info"]["min"] * fix_csv_align
+ len_max = self.db["runtime_info"]["max"] * fix_csv_align
+ len_max_at = self.db["runtime_info"]["max_at"] * fix_csv_align
+ if self.args.summary_extended:
+ len_out_in = self.db["inter_times"]["out_in"] * fix_csv_align
+ len_inter_at = self.db["inter_times"]["inter_at"] * fix_csv_align
+ len_out_out = self.db["inter_times"]["out_out"] * fix_csv_align
+ len_in_in = self.db["inter_times"]["in_in"] * fix_csv_align
+ len_in_out = self.db["inter_times"]["in_out"] * fix_csv_align
+
+ fmt = "{{:{}d}}".format(len_pid)
+ fmt += "{}{{:{}d}}".format(separator, len_tid)
+ fmt += "{}{{:>{}}}".format(separator, len_comm)
+ fmt += "{}{{:{}d}}".format(separator, len_runs)
+ fmt += "{}{{:{}.{}f}}".format(separator, len_acc, time_precision)
+ fmt += "{}{{}}{{:{}.{}f}}".format(separator, len_mean, time_precision)
+ fmt += "{}{{:{}.{}f}}".format(separator, len_median, time_precision)
+ fmt += "{}{{:{}.{}f}}".format(separator, len_min, time_precision)
+ fmt += "{}{{:{}.{}f}}".format(separator, len_max, time_precision)
+ fmt += "{}{{}}{{:{}.{}f}}{{}}{{}}".format(
+ separator, len_max_at, decimal_precision
+ )
+ if self.args.summary_extended:
+ fmt += "{}{{:{}.{}f}}".format(separator, len_out_in, time_precision)
+ fmt += "{}{{}}{{:{}.{}f}}{{}}".format(
+ separator, len_inter_at, decimal_precision
+ )
+ fmt += "{}{{:{}.{}f}}".format(separator, len_out_out, time_precision)
+ fmt += "{}{{:{}.{}f}}".format(separator, len_in_in, time_precision)
+ fmt += "{}{{:{}.{}f}}".format(separator, len_in_out, time_precision)
+ return fmt
+
+ def _calc_alignments_summary(self, align_helper):
+ # Measure the formatted string widths (e.g. f"{val:.{decimal_precision}f}"
+ # and f"{val:.{time_precision}f}") rather than str(val), because Decimal's
+ # default string representation may include unrounded fractional digits or
+ # omit trailing zeros that _format_stats() renders with fixed precision.
+ decimal_precision, time_precision = self.analyzer.prepare_fmt_precision()
+ for key in self.db["task_info"]:
+ val_len = len(str(getattr(align_helper, key)))
+ if val_len > self.db["task_info"][key]:
+ self.db["task_info"][key] = val_len
+ for key in self.db["runtime_info"]:
+ val = getattr(align_helper, key)
+ if key == "runs":
+ val_len = len(str(val))
+ elif key == "max_at":
+ val_len = len(f"{val:.{decimal_precision}f}")
+ else:
+ val_len = len(f"{val:.{time_precision}f}")
+ if val_len > self.db["runtime_info"][key]:
+ self.db["runtime_info"][key] = val_len
+ if self.args.summary_extended:
+ for key in self.db["inter_times"]:
+ val = getattr(align_helper, key)
+ if key == "inter_at":
+ val_len = len(f"{val:.{decimal_precision}f}")
+ else:
+ val_len = len(f"{val:.{time_precision}f}")
+ if val_len > self.db["inter_times"][key]:
+ self.db["inter_times"][key] = val_len
+
+ def print(self):
+ self._task_stats()
+ fmt = self._format_stats()
+
+ if not self.args.csv_summary:
+ print("\nSummary", file=self.fd_sum)
+ self._print_header()
+ self._column_titles()
+ for i in range(len(self._body)):
+ self.fd_sum.write(fmt.format(*tuple(self._body[i])) + "\n")
+
+
+class TaskAnalyzer:
+
+ """Main class for task analysis."""
+
+ COLORS = {
+ "grey": "\033[90m",
+ "red": "\033[91m",
+ "green": "\033[92m",
+ "yellow": "\033[93m",
+ "blue": "\033[94m",
+ "violet": "\033[95m",
+ "reset": "\033[0m",
+ }
+
+ def __init__(self, args: argparse.Namespace) -> None:
+ self.args = args
+ self.db: Dict[str, Any] = {}
+ self.session: Optional[perf.session] = None
+ self._tgid_cache: Dict[int, int] = {}
+ self.time_unit = "us"
+ if args.ns:
+ self.time_unit = "ns"
+ elif args.ms:
+ self.time_unit = "ms"
+ self._init_db()
+ self._check_color()
+ self.fd_task = sys.stdout
+ self.fd_sum = sys.stdout
+
+ @contextmanager
+ def open_output(self, filename: str, default: Any):
+ """Context manager for file or stdout."""
+ if filename:
+ with open(filename, "w", encoding="utf-8") as f:
+ yield f
+ else:
+ yield default
+
+ def _init_db(self) -> None:
+ self.db["running"] = {}
+ self.db["tid"] = {}
+ self.db["global"] = []
+ if (self.args.summary or self.args.summary_extended or
+ self.args.summary_only or self.args.csv_summary):
+ self.db["task_info"] = {}
+ self.db["runtime_info"] = {}
+ self.db["task_info"]["pid"] = len("PID")
+ self.db["task_info"]["tid"] = len("TID")
+ self.db["task_info"]["comm"] = len("Comm")
+ self.db["runtime_info"]["runs"] = len("Runs")
+ self.db["runtime_info"]["acc"] = len("Accumulated")
+ self.db["runtime_info"]["max"] = len("Max")
+ self.db["runtime_info"]["max_at"] = len("Max At")
+ self.db["runtime_info"]["min"] = len("Min")
+ self.db["runtime_info"]["mean"] = len("Mean")
+ self.db["runtime_info"]["median"] = len("Median")
+ if self.args.summary_extended:
+ self.db["inter_times"] = {}
+ self.db["inter_times"]["out_in"] = len("Out-In")
+ self.db["inter_times"]["inter_at"] = len("Max At")
+ self.db["inter_times"]["out_out"] = len("Out-Out")
+ self.db["inter_times"]["in_in"] = len("In-In")
+ self.db["inter_times"]["in_out"] = len("In-Out")
+
+ def _check_color(self) -> None:
+ """Check if color should be enabled."""
+ if self.args.csv:
+ TaskAnalyzer.COLORS = {k: "" for k in TaskAnalyzer.COLORS}
+ return
+ if sys.stdout.isatty() and self.args.stdio_color != "never":
+ return
+ if self.args.stdio_color == "always":
+ return
+ TaskAnalyzer.COLORS = {k: "" for k in TaskAnalyzer.COLORS}
+
+ @staticmethod
+ def time_uniter(unit: str) -> float:
+ """Return time unit factor."""
+ picker = {"s": 1, "ms": 1e3, "us": 1e6, "ns": 1e9}
+ return picker[unit]
+
+ def _task_id(self, pid: int, cpu: int) -> str:
+ return f"{pid}-{cpu}"
+
+ def _filter_non_printable(self, unfiltered: Union[str, bytearray]) -> str:
+ # Strip non-printable characters, whitespace control characters
+ # (\r\n\t\x0b\x0c), quotes (", '), and ';' because --csv and
+ # --csv-summary emit unquoted ';'-delimited columns. Also replace a
+ # leading '=', '+', '-', or '@' with '_' to mitigate CSV formula / DDE
+ # injection attacks if an attacker-controlled process comm is opened in
+ # a spreadsheet program.
+ if isinstance(unfiltered, (bytearray, bytes)):
+ unfiltered = unfiltered.decode('utf-8', 'ignore')
+ filtered = ""
+ for char in unfiltered:
+ if char in string.printable and char not in "\r\n\t\x0b\x0c;\"'":
+ filtered += char
+ stripped = filtered.lstrip()
+ if stripped and stripped[0] in "=+-@":
+ filtered = "_" + stripped[1:]
+ return filtered
+
+ def prepare_fmt_precision(self) -> tuple[int, int]:
+ if self.args.ns:
+ return 9, 0
+ return 6, 3
+
+ def prepare_fmt_sep(self, is_summary: bool = False) -> tuple[str, int]:
+ # fix_csv_align multiplies format field widths: 0 in CSV mode collapses
+ # {:>N} padding to {:>0} (no extra spaces around ';'), while 1 in
+ # standard text mode preserves fixed-width column alignment.
+ if (is_summary and self.args.csv_summary) or (not is_summary and self.args.csv):
+ return ";", 0
+ return " ", 1
+
+ def _fmt_header(self) -> str:
+ separator, fix_csv_align = self.prepare_fmt_sep()
+ fmt = f"{{:>{LEN_SWITCHED_IN*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_SWITCHED_OUT*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_CPU*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_PID*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_TID*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_COMM*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_RUNTIME*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_OUT_IN*fix_csv_align}}}"
+ if self.args.extended_times:
+ fmt += f"{separator}{{:>{LEN_OUT_OUT*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_IN_IN*fix_csv_align}}}"
+ fmt += f"{separator}{{:>{LEN_IN_OUT*fix_csv_align}}}"
+ return fmt
+
+ def _fmt_body(self) -> str:
+ separator, fix_csv_align = self.prepare_fmt_sep()
+ decimal_precision, time_precision = self.prepare_fmt_precision()
+ fmt = f"{{}}{{:{LEN_SWITCHED_IN*fix_csv_align}.{decimal_precision}f}}"
+ fmt += f"{separator}{{:{LEN_SWITCHED_OUT*fix_csv_align}.{decimal_precision}f}}"
+ fmt += f"{separator}{{:{LEN_CPU*fix_csv_align}d}}"
+ fmt += f"{separator}{{:{LEN_PID*fix_csv_align}d}}"
+ fmt += f"{separator}{{}}{{:{LEN_TID*fix_csv_align}d}}{{}}"
+ fmt += f"{separator}{{}}{{:>{LEN_COMM*fix_csv_align}}}"
+ fmt += f"{separator}{{:{LEN_RUNTIME*fix_csv_align}.{time_precision}f}}"
+ if self.args.extended_times:
+ fmt += f"{separator}{{:{LEN_OUT_IN*fix_csv_align}.{time_precision}f}}"
+ fmt += f"{separator}{{:{LEN_OUT_OUT*fix_csv_align}.{time_precision}f}}"
+ fmt += f"{separator}{{:{LEN_IN_IN*fix_csv_align}.{time_precision}f}}"
+ fmt += f"{separator}{{:{LEN_IN_OUT*fix_csv_align}.{time_precision}f}}{{}}"
+ else:
+ fmt += f"{separator}{{:{LEN_OUT_IN*fix_csv_align}.{time_precision}f}}{{}}"
+ return fmt
+
+ def _print_header(self) -> None:
+ fmt = self._fmt_header()
+ header = ["Switched-In", "Switched-Out", "CPU", "PID", "TID", "Comm",
+ "Runtime", "Time Out-In"]
+ if self.args.extended_times:
+ header += ["Time Out-Out", "Time In-In", "Time In-Out"]
+ self.fd_task.write(fmt.format(*header) + "\n")
+
+ def _print_task_finish(self, task: Task) -> None:
+ c_row_set = ""
+ c_row_reset = ""
+ out_in: Any = -1
+ out_out: Any = -1
+ in_in: Any = -1
+ in_out: Any = -1
+ fmt = self._fmt_body()
+
+ if str(task.tid) in self.args.highlight_tasks_map:
+ c_row_set = TaskAnalyzer.COLORS.get(self.args.highlight_tasks_map[str(task.tid)], '')
+ c_row_reset = TaskAnalyzer.COLORS["reset"]
+ if task.comm in self.args.highlight_tasks_map:
+ c_row_set = TaskAnalyzer.COLORS.get(self.args.highlight_tasks_map[task.comm], '')
+ c_row_reset = TaskAnalyzer.COLORS["reset"]
+
+ c_tid_set = ""
+ c_tid_reset = ""
+ if task.pid == task.tid:
+ c_tid_set = TaskAnalyzer.COLORS["grey"]
+ c_tid_reset = TaskAnalyzer.COLORS["reset"]
+
+ if task.tid in self.db["tid"]:
+ last_tid_task = self.db["tid"][task.tid][-1]
+ timespan_gap_tid = Timespans(self.args, self.time_unit)
+ timespan_gap_tid.feed(last_tid_task)
+ timespan_gap_tid.feed(task)
+ out_in = timespan_gap_tid.current['out_in']
+ out_out = timespan_gap_tid.current['out_out']
+ in_in = timespan_gap_tid.current['in_in']
+ in_out = timespan_gap_tid.current['in_out']
+
+ if self.args.extended_times:
+ line_out = fmt.format(c_row_set, task.time_in(), task.time_out(), task.cpu,
+ task.pid, c_tid_set, task.tid, c_tid_reset, c_row_set, task.comm,
+ task.runtime(self.time_unit), out_in, out_out, in_in, in_out,
+ c_row_reset) + "\n"
+ else:
+ line_out = fmt.format(c_row_set, task.time_in(), task.time_out(), task.cpu,
+ task.pid, c_tid_set, task.tid, c_tid_reset, c_row_set, task.comm,
+ task.runtime(self.time_unit), out_in, c_row_reset) + "\n"
+ self.fd_task.write(line_out)
+
+ def _record_cleanup(self, _list: list[Any]) -> list[Any]:
+ need_summary = (self.args.summary or self.args.summary_extended or
+ self.args.summary_only or self.args.csv_summary)
+ if not need_summary and len(_list) > 1:
+ return _list[len(_list) - 1:]
+ return _list
+
+ def _record_by_tid(self, task: Task) -> None:
+ tid = task.tid
+ if tid not in self.db["tid"]:
+ self.db["tid"][tid] = []
+ self.db["tid"][tid].append(task)
+ self.db["tid"][tid] = self._record_cleanup(self.db["tid"][tid])
+
+ def _record_global(self, task: Task) -> None:
+ self.db["global"].append(task)
+ self.db["global"] = self._record_cleanup(self.db["global"])
+
+ def _handle_task_finish(self, tid: int, cpu: int, time_ns: int, pid: int) -> None:
+ if tid == 0 and (not self.args.tid_renames or 0 not in self.args.tid_renames):
+ return
+ _id = self._task_id(tid, cpu)
+ if _id not in self.db["running"]:
+ return
+ task = self.db["running"][_id]
+ task.schedule_out_at(time_ns)
+ task.update_pid(pid)
+ del self.db["running"][_id]
+
+ if not self._limit_filtered(tid, pid, task.comm):
+ if not self.args.summary_only:
+ self._print_task_finish(task)
+ self._record_by_tid(task)
+ self._record_global(task)
+
+ def _handle_task_start(self, tid: int, cpu: int, comm: str, time_ns: int) -> None:
+ if tid == 0 and (not self.args.tid_renames or 0 not in self.args.tid_renames):
+ return
+ if tid in self.args.tid_renames:
+ comm = self._filter_non_printable(self.args.tid_renames[tid])
+ _id = self._task_id(tid, cpu)
+ if _id in self.db["running"]:
+ return
+ task = Task(_id, tid, cpu, comm)
+ task.schedule_in_at(time_ns)
+ self.db["running"][_id] = task
+
+ def _limit_filtered(self, tid: int, pid: int, comm: str) -> bool:
+ """Filter tasks based on CLI arguments."""
+ match_filter = False
+ if self.args.filter_tasks:
+ if (str(tid) in self.args.filter_tasks or
+ str(pid) in self.args.filter_tasks or
+ comm in self.args.filter_tasks):
+ match_filter = True
+
+ match_limit = False
+ if self.args.limit_to_tasks:
+ if (str(tid) in self.args.limit_to_tasks or
+ str(pid) in self.args.limit_to_tasks or
+ comm in self.args.limit_to_tasks):
+ match_limit = True
+
+ if self.args.filter_tasks and match_filter:
+ return True
+ if self.args.limit_to_tasks and not match_limit:
+ return True
+ return False
+
+ def _is_within_timelimit(self, time_ns: int) -> bool:
+ if not self.args.time_limit:
+ return True
+ time_s = decimal.Decimal(time_ns) / decimal.Decimal(1e9)
+ bounds = self.args.time_limit.split(":")
+ lower_bound = bounds[0] if len(bounds) > 0 else ""
+ upper_bound = bounds[1] if len(bounds) > 1 else ""
+ if lower_bound and time_s < decimal.Decimal(lower_bound):
+ return False
+ if upper_bound and time_s > decimal.Decimal(upper_bound):
+ return False
+ return True
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process sched:sched_switch events."""
+ if "sched:sched_switch" not in str(sample.evsel):
+ return
+
+ time_ns = sample.sample_time
+ if not self._is_within_timelimit(time_ns):
+ return
+
+ # Access tracepoint fields directly from sample object
+ try:
+ prev_pid = sample.prev_pid
+ next_pid = sample.next_pid
+ next_comm = sample.next_comm
+ common_cpu = sample.sample_cpu
+ except AttributeError:
+ if not self.db.get("warned_missing_fields"):
+ self.db["warned_missing_fields"] = True
+ print("Warning: sched:sched_switch sample missing tracepoint fields "
+ "(is libtraceevent support enabled?).", file=sys.stderr)
+ return
+
+ next_comm = self._filter_non_printable(next_comm)
+
+ # Task finish for previous task. Resolve TGID (process ID) from TID:
+ # in file mode we query perf.session.find_thread() (checking thread.pid > 0
+ # so an unresolved -1 does not overwrite the prev_pid fallback), whereas in
+ # live mode (self.session is None) we read /proc/<tid>/status and cache
+ # the lookup in _tgid_cache.
+ prev_tgid = prev_pid # Fallback
+ if prev_pid != 0:
+ if self.session:
+ try:
+ thread = self.session.find_thread(-1, prev_pid)
+ if thread and thread.pid > 0:
+ prev_tgid = thread.pid
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError):
+ pass
+ elif prev_pid in self._tgid_cache:
+ prev_tgid = self._tgid_cache[prev_pid]
+ else:
+ if len(self._tgid_cache) >= 4096:
+ self._tgid_cache.pop(next(iter(self._tgid_cache)))
+ self._tgid_cache[prev_pid] = prev_tgid
+ try:
+ with open(f"/proc/{prev_pid}/status", encoding="utf-8") as f:
+ for line in f:
+ if line.startswith("Tgid:"):
+ prev_tgid = int(line.split()[1])
+ self._tgid_cache[prev_pid] = prev_tgid
+ break
+ except (OSError, ValueError, KeyError, RuntimeError):
+ pass
+ self._handle_task_finish(prev_pid, common_cpu, time_ns, prev_tgid)
+ # Task start for next task
+ self._handle_task_start(next_pid, common_cpu, next_comm, time_ns)
+
+ def print_summary(self) -> None:
+ """Calculate and print summary."""
+ need_summary = (self.args.summary or self.args.summary_extended or
+ self.args.summary_only or self.args.csv_summary)
+ if not need_summary:
+ return
+
+ Summary(self).print()
+
+ def _run_file(self) -> None:
+ if not self.args.summary_only:
+ self._print_header()
+
+ session = perf.session(perf.data(self.args.input), sample=self.process_event)
+ self.session = session
+ try:
+ session.process_events()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ # Break the reference cycle between self.session and the bound
+ # self.process_event callback so the C perf.session object is freed.
+ self.session = None
+
+ if not self.db["global"]:
+ print(f"Warning: No sched:sched_switch trace events found in '{self.args.input}'.",
+ file=sys.stderr)
+
+ self.print_summary()
+
+ def _run_live(self) -> None:
+ if not self.args.summary_only:
+ self._print_header()
+
+ cpus = perf.cpu_map()
+ threads = perf.thread_map(-1)
+ evlist = perf.parse_events("sched:sched_switch", cpus, threads)
+ evlist.config()
+
+ evlist.open()
+ evlist.mmap()
+ evlist.enable()
+
+ # Per-CPU perf ring buffers are drained sequentially in a loop, so events
+ # read from CPU N may have timestamps slightly earlier than events just
+ # read from CPU 0. Buffer events in pending_events and only dispatch those
+ # older than a 50 ms watermark behind max_ts so cross-CPU migrations and
+ # switches are processed in global timestamp order.
+ pending_events: list[perf.sample_event] = []
+ print("Live mode started. Press Ctrl+C to stop.", file=sys.stderr)
+ try:
+ while True:
+ try:
+ evlist.poll(timeout=100)
+ except InterruptedError:
+ continue
+ for cpu in cpus:
+ while True:
+ event = evlist.read_on_cpu(cpu)
+ if not event:
+ break
+ if not isinstance(event, perf.sample_event):
+ continue
+ pending_events.append(event)
+ if pending_events:
+ pending_events.sort(
+ key=lambda e: getattr(e, 'sample_time', getattr(e, 'time', 0))
+ )
+ max_ts = getattr(
+ pending_events[-1], 'sample_time', getattr(pending_events[-1], 'time', 0)
+ )
+ cutoff_ts = max_ts - 50_000_000
+ ready_idx = 0
+ for event in pending_events:
+ ev_ts = getattr(event, 'sample_time', getattr(event, 'time', 0))
+ if ev_ts <= cutoff_ts:
+ self.process_event(event)
+ ready_idx += 1
+ else:
+ break
+ del pending_events[:ready_idx]
+ except KeyboardInterrupt:
+ print("\nStopping live mode...", file=sys.stderr)
+ finally:
+ pending_events.sort(key=lambda e: getattr(e, 'sample_time', getattr(e, 'time', 0)))
+ for event in pending_events:
+ self.process_event(event)
+ evlist.close()
+ self.print_summary()
+
+ def run(self) -> None:
+ """Run the session."""
+ is_live = (self.args.live or
+ (not os.path.exists(self.args.input) and self.args.input == "perf.data"))
+ with self.open_output(self.args.csv, sys.stdout) as fd_task:
+ if self.args.csv and self.args.csv == self.args.csv_summary:
+ self.fd_task = fd_task
+ self.fd_sum = fd_task
+ if is_live:
+ self._run_live()
+ else:
+ self._run_file()
+ else:
+ with self.open_output(self.args.csv_summary, sys.stdout) as fd_sum:
+ self.fd_task = fd_task
+ self.fd_sum = fd_sum
+ if is_live:
+ self._run_live()
+ else:
+ self._run_file()
+
+def main() -> None:
+ """Main function."""
+ parser = argparse.ArgumentParser(description="Analyze tasks behavior")
+ parser.add_argument("-i", "--input", default="perf.data", help="Input file name")
+ parser.add_argument("--time-limit", default="", help="print tasks only in time window")
+ parser.add_argument("--summary", action="store_true",
+ help="print additional runtime information")
+ parser.add_argument("--summary-only", action="store_true",
+ help="print only summary without traces")
+ parser.add_argument("--summary-extended", action="store_true",
+ help="print extended summary")
+ parser.add_argument("--ns", action="store_true", help="show timestamps in nanoseconds")
+ parser.add_argument("--ms", action="store_true", help="show timestamps in milliseconds")
+ parser.add_argument("--live", action="store_true",
+ help="force live mode (ignores -i/perf.data)")
+ parser.add_argument("--extended-times", action="store_true",
+ help="Show elapsed times between schedule in/out")
+ parser.add_argument("--filter-tasks", default="", help="filter tasks by tid, pid or comm")
+ parser.add_argument("--limit-to-tasks", default="", help="limit output to selected tasks")
+ parser.add_argument("--highlight-tasks", default="", help="colorize special tasks")
+ parser.add_argument("--rename-comms-by-tids", default="", help="rename task names by using tid")
+ parser.add_argument("--stdio-color", default="auto", choices=["always", "never", "auto"],
+ help="configure color output")
+ parser.add_argument("--csv", default="", help="Write trace to file")
+ parser.add_argument("--csv-summary", default="", help="Write summary to file")
+
+ args = parser.parse_args()
+ args.tid_renames = {}
+ args.highlight_tasks_map = {}
+ args.filter_tasks = args.filter_tasks.split(",") if args.filter_tasks else []
+ args.limit_to_tasks = args.limit_to_tasks.split(",") if args.limit_to_tasks else []
+
+ if args.rename_comms_by_tids:
+ for item in args.rename_comms_by_tids.split(","):
+ try:
+ tid, name = item.split(":", 1)
+ args.tid_renames[int(tid)] = name
+ except ValueError:
+ print(f"Error: Invalid format for --rename-comms-by-tids '{item}', "
+ "expected tid:name", file=sys.stderr)
+ sys.exit(1)
+
+ if args.highlight_tasks:
+ for item in args.highlight_tasks.split(","):
+ parts = item.split(":")
+ if len(parts) == 1:
+ parts.append("red")
+ key, color = parts[0], parts[1]
+ args.highlight_tasks_map[key] = color
+
+ analyzer = TaskAnalyzer(args)
+ analyzer.run()
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/tracepoint.py b/tools/perf/python/tracepoint.py
index 15b0c8268996..faa5311b8418 100755
--- a/tools/perf/python/tracepoint.py
+++ b/tools/perf/python/tracepoint.py
@@ -1,5 +1,6 @@
#! /usr/bin/env python
# SPDX-License-Identifier: GPL-2.0
+"""Example showing how to enable a tracepoint and access its fields."""
# -*- python -*-
# -*- coding: utf-8 -*-
@@ -9,7 +10,7 @@ def change_proctitle():
try:
import setproctitle
setproctitle.setproctitle("tracepoint.py")
- except:
+ except ImportError:
print("Install the setproctitle python package to help with top and friends")
def main():
@@ -29,7 +30,7 @@ def main():
evlist.open()
evlist.mmap()
- evlist.enable();
+ evlist.enable()
while True:
evlist.poll(timeout = -1)
diff --git a/tools/perf/python/treport.py b/tools/perf/python/treport.py
new file mode 100755
index 000000000000..fb583786b17a
--- /dev/null
+++ b/tools/perf/python/treport.py
@@ -0,0 +1,807 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: MIT
+"""treport.py - perf report like tool written using textual."""
+from abc import ABC, abstractmethod
+from typing import Callable, Dict, List, Optional, Set
+import argparse
+import os
+import sys
+import threading
+import time
+import perf
+from rich.markup import escape
+from rich.segment import Segment
+from rich.style import Style
+from textual import events, work
+from textual.app import App, ComposeResult
+from textual.binding import Binding
+from textual.color import Color
+from textual.scroll_view import ScrollView
+from textual.strip import Strip
+from textual.widgets import Footer, Header, TabbedContent, TabPane, Tree
+from textual.widgets.tree import TreeNode
+
+def make_fixed_length_string(s: str, length: int, pad_char=' '):
+ """Make the string s a fixed length.
+
+ Increases or decreases the length of s to be length. If the length is
+ increased then pad_char is inserted on the right.
+ """
+ return s[:length] if len(s) > length else s.ljust(length, pad_char)
+
+
+class FlameVisitor(ABC):
+ """Parent for visitor used by ProfileNode.flame_walk"""
+ @abstractmethod
+ def visit(self, node: Optional["ProfileNode"], width: int) -> None:
+ """Visit a profile node width the specified flame graph width.
+
+ Args:
+ node: The `ProfileNode` for the current segment. This may be `None`
+ to represent a gap or an unknown portion of the stack.
+ width: The calculated width of the flame graph rectangle for this
+ node, which is proportional to its sample count.
+ """
+
+
+class ProfileNode:
+ """Represents a single node in a call stack tree.
+
+ Generally a ProfileNode corresponds to a symbol in a call stack.
+ The root is special, its children are events and the events
+ children are process names. After the process name come the
+ samples.
+
+ Attributes:
+ name (str): The name of the function, process or event.
+ value (int): The sample count for this node including counts from its
+ children.
+ parent (ProfileNode): The parent of this node, this node belongs to its
+ children.
+ children (Dict[str, ProfileNode]): A dictionary of child nodes, keyed by
+ their names.
+ """
+ def __init__(self, name: str, parent: Optional["ProfileNode"]):
+ """Initializes a ProfileNode."""
+ self.name = name
+ self.value: int = 0
+ self.parent = parent if parent else self
+ self.children: Dict[str, ProfileNode] = {}
+
+ def find_or_create_node(self, name: str) -> "ProfileNode":
+ """Finds a child node by name or creates it if it doesn't exist."""
+ if name in self.children:
+ return self.children[name]
+ child = ProfileNode(name, self)
+ self.children[name] = child
+ return child
+
+ def depth(self) -> int:
+ """The maximum depth of the call stack tree from this node down."""
+ if not self.children:
+ return 1
+ return max(child.depth() for child in self.children.values()) + 1
+
+ def process_event(self, sample, comm: str) -> None:
+ """Processes a single profiling event to update the call stack tree.
+
+ Args:
+ sample: a single profiling sample.
+ comm: the command name of the sampled thread.
+ """
+ period = sample.sample_period
+ self.value += period
+
+ node = self.find_or_create_node(comm)
+ node.value += period
+
+ if sample.callchain:
+ for entry in reversed(sample.callchain):
+ name = entry.symbol
+ if not name or name == "[unknown]":
+ name = entry.dso or "unknown"
+ if entry.ip:
+ name += f" 0x{entry.ip:x}"
+ node = node.find_or_create_node(name)
+ node.value += period
+ else:
+ name = sample.symbol
+ if not name or name == "[unknown]":
+ name = sample.dso or "unknown"
+ if sample.sample_ip:
+ name += f" 0x{sample.sample_ip:x}"
+ node = node.find_or_create_node(name)
+ node.value += period
+
+ def sorted_children(self) -> List["ProfileNode"]:
+ """The children, largest value first.
+
+ The root's children are events whose values aren't comparable, each is
+ 100% of itself, so they are kept in the order they were first seen
+ rather than reordering as they load.
+ """
+ if self.parent is self:
+ return list(self.children.values())
+ return sorted(self.children.values(), key=lambda pnode: pnode.value, reverse=True)
+
+ def tree_label(self, root_value: int) -> str:
+ """Label for the node in a Tree, with a percentage of root_value.
+
+ The percentage is highlighted with reversed colors.
+ """
+ if root_value == 0:
+ return escape(self.name)
+ return f"{escape(self.name)} [r]{self.value / root_value * 100:.3g}%[/]"
+
+ def largest_child(self) -> "ProfileNode":
+ """Finds the child with the highest value (sample count)."""
+ if self.children:
+ return self.sorted_children()[0]
+ return self
+
+ def child_after(self, sought: "ProfileNode") -> "ProfileNode":
+ """Finds the next sibling after the given node, sorted by value."""
+ found = False
+ for child in self.sorted_children():
+ if child == sought:
+ found = True
+ elif found:
+ return child
+ return sought
+
+ def child_before(self, sought: "ProfileNode") -> "ProfileNode":
+ """Finds the previous sibling before the given node, sorted by value."""
+ last = None
+ for child in self.sorted_children():
+ if child == sought:
+ return last if last else sought
+ last = child
+ return sought
+
+ def has_parent(self, parent: "ProfileNode") -> bool:
+ """Checks if the parent node is an ancestor of this node."""
+ p = self.parent
+ while True:
+ if p == parent:
+ return True
+ new_p = p.parent
+ if new_p == p:
+ break
+ p = new_p
+ return False
+
+ def has_child(self, sought: "ProfileNode") -> bool:
+ """Checks if the sought node is a descendant of this node."""
+ return sought.has_parent(self)
+
+ def flame_walk(self, wanted_strip: int, cur_strip: int, parent_width: int,
+ selected: "ProfileNode", visitor: FlameVisitor) -> None:
+ """Recursively walks the tree to visit a single flame graph row.
+
+ This method calculates the proportional width for each child
+ based on its value (sample count) relative to its parent. It
+ then invokes a `visitor` to process each segment of the flame
+ graph row.
+
+ Args:
+ wanted_strip (int): The target depth (Y-axis) of the flame graph row
+ to generate.
+ cur_strip (int): The current depth of the traversal.
+ parent_width (int): The width of the parent of this node.
+ selected (ProfileNode): The currently selected node in the UI, used
+ to adjust rendering to highlight the
+ selected path.
+ visitor (FlameVisitor): A visitor object whose `visit` method is
+ called for each segment of the flame graph
+ row.
+ """
+ if parent_width == 0:
+ return
+
+ parent_selected = selected == self or self.has_parent(selected)
+ child_selected = not parent_selected and self.has_child(selected)
+ if not parent_selected and not child_selected:
+ # Branches of the tree with no node selected aren't drawn.
+ return
+
+ # left_over is used to check for a gap after the children due
+ # to samples being in the parent.
+ left_over = parent_width
+ for child in self.sorted_children():
+ if parent_selected:
+ if self.value:
+ desired_width = int((parent_width * child.value) / self.value)
+ else:
+ desired_width = parent_width // len(self.children)
+ if desired_width == 0:
+ # Nothing can be drawn for this node or later smaller children.
+ break
+ elif child == selected or child.has_child(selected):
+ desired_width = parent_width
+ else:
+ # A sibling or its child are selected, but not this branch.
+ continue
+
+ # Either visit the wanted_strip or recurse to the next level.
+ if wanted_strip == cur_strip:
+ visitor.visit(child, desired_width)
+ else:
+ child.flame_walk(wanted_strip, cur_strip + 1, desired_width,
+ selected, visitor)
+ left_over -= desired_width
+ if left_over == 0:
+ # No space left to draw in.
+ break
+
+ # Always visit the left_over regardless of the wanted_strip as there
+ # may be additional gap added to a line by a parent.
+ if left_over:
+ visitor.visit(None, left_over)
+
+ def make_flame_strip(self, wanted_strip: int, parent_width: int,
+ cursor: "ProfileNode", selected: "ProfileNode",
+ theme_variables: Dict[str, str]) -> Strip:
+ """Creates a renderable 'Strip' for a single row of a flame graph.
+
+ This method orchestrates the `flame_walk` traversal with a specialized
+ visitor to generate a list of segments. The segments are used by a`Strip`
+ object for rendering in the terminal.
+
+ Args:
+ wanted_strip (int): The target depth (Y-axis) of the flame graph row.
+ parent_width (int): The total width (in characters) of the display
+ area.
+ cursor (ProfileNode): The node currently under the cursor, for
+ highlighting.
+ selected (ProfileNode): The node that is actively selected.
+ theme_variables(Dict): Values of colors for the textual theme.
+
+ Returns:
+ Strip: A renderable strip of segments for the specified row.
+ """
+ primary = Color.parse(theme_variables["primary"])
+ secondary = Color.parse(theme_variables["secondary"])
+ surface = Color.parse(theme_variables["surface"])
+ def luminance(color: Color) -> float:
+ """Computes the luminance of a color from the rgb"""
+ return color.r * 0.299 + color.g * 0.587 + color.b * 0.114
+
+ # Set of styles for different flamegraph segments, the styles are
+ # cycled through to provide contrast.
+ normal_styles = []
+ for x in range(0, 125, 25):
+ fgcolor = secondary.blend(primary, x/100)
+ if luminance(fgcolor) > luminance(surface):
+ bgcolor = surface.lighten(0.05+x/500)
+ else:
+ bgcolor = surface.darken(0.05+x/500)
+ normal_styles.append(Style(color=fgcolor.rich_color,
+ bgcolor=bgcolor.rich_color))
+
+ # Style for the selected flame graph node.
+ accent = Color.parse(theme_variables["accent"])
+ accent_muted = Color.parse(theme_variables["accent-muted"])
+ cursor_style = Style(color=accent.rich_color, bgcolor=accent_muted.rich_color)
+
+ class StripVisitor(FlameVisitor):
+ """Visitor creating textual flame graph segments.
+
+ Attributes:
+ segments (list): The textual segments that will be placed in a
+ `Strip`.
+ gap_width (int): The width of any outstanding gap between the
+ last and next node.
+ ctr (int): Used to adjust the flame graph segment's color.
+ """
+ def __init__(self):
+ self.segments = []
+ self.gap_width = 0
+ self.ctr = wanted_strip
+
+ def visit(self, node: Optional[ProfileNode], width: int) -> None:
+ if node:
+ if self.gap_width > 0:
+ self.segments.append(Segment(
+ make_fixed_length_string(" ", self.gap_width)))
+ self.gap_width = 0
+ style = cursor_style
+ if node != cursor:
+ style = normal_styles[self.ctr % len(normal_styles)]
+ self.segments.append(Segment(
+ make_fixed_length_string(node.name, width), style))
+ else:
+ self.gap_width += width
+ self.ctr += 1
+
+ visitor = StripVisitor()
+ self.flame_walk(wanted_strip, 0, parent_width, selected, visitor)
+ return Strip(visitor.segments) if visitor.segments else Strip.blank(parent_width)
+
+ def find_node(self, sought_x: int, sought_y: int, parent_width: int,
+ selected: "ProfileNode") -> "ProfileNode":
+ """Finds the ProfileNode corresponding to specific X, Y coordinates.
+
+ This translates a mouse click on a flame graph back to the
+ `ProfileNode` that it represents.
+
+ Args:
+ sought_x (int): The X coordinate (character column).
+ sought_y (int): The Y coordinate (row or depth).
+ parent_width (int): The total width of the display area.
+ selected (ProfileNode): The currently selected node, which affects
+ layout.
+
+ Returns:
+ Optional[ProfileNode]: The node found at the coordinates, or None.
+
+ """
+ class FindVisitor(FlameVisitor):
+ """Visitor locating a `ProfileNode`.
+
+ Attributes:
+ x (int): offset within line.
+ found (Optional[ProfileNode]): located node
+ gap_width (int): The width of any outstanding gap between the
+ last and next node.
+ ctr (int): Used to adjust the flame graph segment's color.
+ """
+ def __init__(self):
+ self.x = 0
+ self.found = None
+
+ def visit(self, node: Optional[ProfileNode], width: int) -> None:
+ if self.x <= sought_x < self.x + width:
+ self.found = node
+ self.x += width
+
+ visitor = FindVisitor()
+ self.flame_walk(sought_y, 0, parent_width, selected, visitor)
+ return visitor.found
+
+
+class FlameGraph(ScrollView):
+ """A scrollable widget to display a flame graph from a profile.
+
+ Attributes:
+ root (ProfileNode): Root of the profile tree.
+ cursor (ProfileNode): Currently highlighted cursor node.
+ selected (ProfileNode): The currently selected node for zooming.
+ """
+
+ # Define key bindings for navigating the flame graph.
+ # Allows movement with vim-style keys (h,j,k,l) and arrow keys.
+ BINDINGS = [
+ Binding("j,down", "move_down", "Down", key_display="↓",
+ tooltip="Move cursor down to largest child"),
+ Binding("k,up", "move_up", "Up", key_display="↑",
+ tooltip="Move cursor up to parent"),
+ Binding("l,right", "move_right", "Right", key_display="→",
+ tooltip="Move cursor to the right sibling"),
+ Binding("h,left", "move_left", "Left", key_display="←",
+ tooltip="Move cursor to the left sibling"),
+ Binding("enter", "zoom_in", "Zoom In",
+ tooltip="Expand the cursor's node to be screen width"),
+ Binding("escape", "zoom_out", "Zoom Out",
+ tooltip="Zoom out to initial view."),
+ ]
+
+ # Default CSS for the widget to ensure it fills its container's width.
+ DEFAULT_CSS = """
+ FlameGraph {
+ width: 100%;
+ }
+ """
+
+ def __init__(self, root: ProfileNode, lock: threading.Lock, *pos_args, **kwargs):
+ """Initialize the FlameGraph widget.
+
+ The lock must be held when reading the profile as it may be being built
+ in another thread.
+ """
+ super().__init__(*pos_args, **kwargs)
+ self.root = root
+ self.profile_lock = lock
+ self.cursor = root
+ self.selected = root
+
+ def action_move_down(self) -> None:
+ """Handle key press down."""
+ with self.profile_lock:
+ self.cursor = self.cursor.largest_child()
+ self.refresh()
+
+ def action_move_up(self) -> None:
+ """Handle key press up."""
+ if self.cursor.parent != self.cursor.parent.parent:
+ self.cursor = self.cursor.parent
+ self.refresh()
+
+ def action_move_right(self) -> None:
+ """Handle key press right."""
+ with self.profile_lock:
+ self.cursor = self.cursor.parent.child_after(self.cursor)
+ self.refresh()
+
+ def action_move_left(self) -> None:
+ """Handle key press left."""
+ with self.profile_lock:
+ self.cursor = self.cursor.parent.child_before(self.cursor)
+ self.refresh()
+
+ def action_zoom_in(self) -> None:
+ """Handle key press zoom in."""
+ self.selected = self.cursor
+ self.refresh()
+
+ def action_zoom_out(self) -> None:
+ """Handle key press zoom out."""
+ self.selected = self.root
+ self.refresh()
+
+ def render_line(self, y: int) -> Strip:
+ """Render a single line (row) of the flame graph."""
+ _, scroll_y = self.scroll_offset
+ y += scroll_y
+ with self.profile_lock:
+ return self.root.make_flame_strip(y, self.size.width, self.cursor,
+ self.selected, self.app.theme_variables)
+
+ def profile_changed(self) -> None:
+ """Resize and redraw after the profile changed."""
+ with self.profile_lock:
+ self.styles.height = self.root.depth()
+ self.refresh()
+
+ def on_mount(self) -> None:
+ """Set the height of the widget when it is displayed."""
+ self.profile_changed()
+
+ def on_click(self, click: events.Click) -> None:
+ """Handles a mouse click and update the cursor position."""
+ _, scroll_y = self.scroll_offset
+ y = scroll_y + click.y
+ with self.profile_lock:
+ clicked_node = self.root.find_node(click.x, y, self.size.width,
+ self.selected)
+ if clicked_node:
+ self.cursor = clicked_node
+ self.refresh()
+
+
+class ProfileTree(Tree):
+ """A tree view of the profile that can be updated while it is built.
+
+ Tree nodes are created lazily when expanded, the data of each tree node is
+ its ProfileNode. The lock must be held when reading the profile.
+ """
+
+ def __init__(self, root: ProfileNode, lock: threading.Lock, *pos_args, **kwargs):
+ super().__init__("Profile", *pos_args, **kwargs)
+ self.profile = root
+ self.profile_lock = lock
+ # Events whose initial expansion has been done.
+ self.seen_events: Set[str] = set()
+
+ def pnode_of(self, tnode: TreeNode) -> ProfileNode:
+ """The ProfileNode shown by tnode, the tree's root shows the profile's root."""
+ return tnode.data if tnode.data is not None else self.profile
+
+ def event_value(self, pnode: ProfileNode) -> int:
+ """Value of the event containing pnode, percentages are relative to it."""
+ while pnode.parent is not self.profile and pnode.parent is not pnode:
+ pnode = pnode.parent
+ return pnode.value
+
+ @staticmethod
+ def expanded_descendants(tnode: TreeNode, expanded: Set[ProfileNode]) -> None:
+ """Add the profile nodes of the expanded descendants of tnode to expanded."""
+ for tchild in tnode.children:
+ if tchild.is_expanded and tchild.data is not None:
+ expanded.add(tchild.data)
+ if tchild.children:
+ ProfileTree.expanded_descendants(tchild, expanded)
+
+ def sync_node(self, tnode: TreeNode, expanded: Optional[Set[ProfileNode]] = None) -> None:
+ """Update the children of tnode, and expanded descendants, from the profile.
+
+ expanded holds the profile nodes of descendants that were expanded
+ before an ancestor's children were recreated. The lock must be held.
+ """
+ children = self.pnode_of(tnode).sorted_children()
+ if expanded is not None or [tchild.data for tchild in tnode.children] != children:
+ # Recreate the children in the new order keeping the expanded
+ # ones, and their expanded descendants.
+ if expanded is None:
+ expanded = set()
+ self.expanded_descendants(tnode, expanded)
+ tnode.remove_children()
+ ancestors: Set[ProfileNode] = set()
+ for pnode in expanded:
+ parent = pnode.parent
+ while parent is not None and parent not in ancestors:
+ ancestors.add(parent)
+ parent = parent.parent
+ for child in children:
+ label = child.tree_label(self.event_value(child))
+ new = tnode.add(label, child, allow_expand=bool(child.children))
+ if child in expanded:
+ new.expand()
+ if child in expanded or child in ancestors:
+ self.sync_node(new, expanded)
+ return
+ for tchild in tnode.children:
+ child = self.pnode_of(tchild)
+ tchild.set_label(child.tree_label(self.event_value(child)))
+ tchild.allow_expand = bool(child.children)
+ if tchild.is_expanded or tchild.children:
+ self.sync_node(tchild)
+
+ def expand_largest(self, tnode: TreeNode) -> None:
+ """Expand the chain of largest children below tnode.
+
+ The lock must be held.
+ """
+ while self.pnode_of(tnode).children:
+ self.sync_node(tnode)
+ tnode = tnode.children[0]
+ tnode.expand()
+
+ def find(self, tnode: TreeNode, pnode: ProfileNode) -> Optional[TreeNode]:
+ """Find the visible tree node for pnode."""
+ for tchild in tnode.children:
+ if tchild.data is pnode:
+ return tchild
+ if tchild.is_expanded:
+ found = self.find(tchild, pnode)
+ if found:
+ return found
+ return None
+
+ def sync(self) -> None:
+ """Update the tree from the profile keeping the expanded nodes and cursor."""
+ cursor = self.cursor_node.data if self.cursor_node else None
+ with self.profile_lock:
+ self.root.expand()
+ self.sync_node(self.root)
+ for tnode in self.root.children:
+ name = self.pnode_of(tnode).name
+ if name in self.seen_events:
+ continue
+ self.seen_events.add(name)
+ self.expand_largest(tnode)
+ # If there is only one event, expand it also.
+ if len(self.root.children) == 1:
+ tnode.expand()
+ if cursor is not None:
+ found = self.find(self.root, cursor)
+ if found:
+ self.move_cursor(found)
+
+ def on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None:
+ """Create the children of a node when it is expanded."""
+ if event.node is not self.root:
+ with self.profile_lock:
+ self.sync_node(event.node)
+
+
+class ReportApp(App):
+ """A Textual application to display profiling data."""
+
+ # The ^q binding is implied but having it here adds it in the Footer.
+ BINDINGS = [
+ Binding(key="^q", action="quit", description="Quit",
+ tooltip="Quit the app"),
+ ]
+
+ def __init__(self, input_file: str, root: Optional[ProfileNode] = None):
+ """Initialize the application.
+
+ If root isn't given the profile is loaded from input_file in the
+ background after the application starts, the views show the profile
+ as it is built.
+ """
+ super().__init__()
+ self.input_file = input_file
+ self.root = root if root else ProfileNode("root", parent=None)
+ self.loaded = root is not None
+ # Held when reading or changing the profile, which is built in a
+ # background thread.
+ self.profile_lock = threading.Lock()
+ # The profile being built in a background thread.
+ self.loading: Optional[ProfileBuilder] = None
+
+ def compose(self) -> ComposeResult:
+ """Composes the user interface of the application."""
+ yield Header()
+ with TabbedContent(initial="report"):
+ with TabPane("Report", id="report"):
+ yield ProfileTree(self.root, self.profile_lock)
+ with TabPane("Flame Graph", id="flame"):
+ yield FlameGraph(self.root, self.profile_lock)
+ yield Footer()
+
+ def on_mount(self) -> None:
+ """Start loading the profile unless it was given."""
+ self.sub_title = self.input_file
+ if self.loaded:
+ self.update_views()
+ else:
+ self.sub_title = f"Loading {self.input_file}"
+ self.loading = ProfileBuilder(self.root, self.profile_lock)
+ self.load_profile()
+
+ def update_views(self) -> None:
+ """Show the latest state of the profile."""
+ self.query_one(ProfileTree).sync()
+ self.query_one(FlameGraph).profile_changed()
+
+ @work(thread=True, exclusive=True)
+ def load_profile(self) -> None:
+ """Build the profile in a thread so the UI stays responsive."""
+ loading = self.loading
+ assert loading is not None
+ profile = loading
+
+ def progress() -> None:
+ try:
+ self.call_from_thread(self.update_progress, profile)
+ except RuntimeError:
+ # The app is no longer running.
+ profile.cancelled = True
+
+ profile.progress = progress
+ try:
+ profile.read(self.input_file)
+ except LoadCancelled:
+ return
+ except (OSError, ValueError, RuntimeError) as e:
+ self.call_from_thread(self.exit, None, 1,
+ f"Error processing {self.input_file}: {e}")
+ return
+ finally:
+ profile.progress = None
+ self.loading = None
+ if not profile.cancelled:
+ self.call_from_thread(self.profile_loaded, profile)
+
+ def update_progress(self, profile: "ProfileBuilder") -> None:
+ """Show how much of the file has been processed and the profile so far."""
+ self.sub_title = (f"Loading {self.input_file}: {profile.nr_samples:,} samples"
+ f"{profile.duration_str()}")
+ self.update_views()
+
+ def profile_loaded(self, profile: "ProfileBuilder") -> None:
+ """Called on the UI thread when loading completes."""
+ self.loaded = True
+ self.sub_title = (f"{self.input_file}: {profile.nr_samples:,} samples"
+ f"{profile.duration_str()}")
+ self.update_views()
+
+ def cancel_loading(self) -> None:
+ """Stop a background load, noticed at the next progress interval."""
+ loading = self.loading
+ if loading is not None:
+ loading.cancelled = True
+
+ async def action_quit(self) -> None:
+ """Quit, stopping any background load."""
+ self.cancel_loading()
+ await super().action_quit()
+
+ def on_unmount(self) -> None:
+ """Stop any background load when the app exits."""
+ self.cancel_loading()
+
+
+class LoadCancelled(Exception):
+ """Raised from the sample callback to stop processing events early."""
+
+
+class ProfileBuilder:
+ """Constructs a profile tree from a stream of events."""
+ # Number of samples between checks for cancellation and progress.
+ PROGRESS_INTERVAL = 1000
+ # Minimum and maximum time between calls to the progress callback.
+ PROGRESS_SECONDS = 1.0
+ PROGRESS_MAX_SECONDS = 10.0
+ # Updating the views costs more as the profile grows, so the time between
+ # progress calls grows as this fraction of the time spent loading. This
+ # bounds the fraction of the load time spent updating the views.
+ PROGRESS_FRACTION = 0.25
+
+ def __init__(self, root: Optional[ProfileNode] = None,
+ lock: Optional[threading.Lock] = None) -> None:
+ """Build into root, holding lock when changing it."""
+ self.root = root if root else ProfileNode("root", parent=None)
+ self.profile_lock = lock if lock else threading.Lock()
+ self.session: Optional[perf.session] = None
+ self.nr_samples = 0
+ self.first_time = 0
+ self.last_time = 0
+ # Set, possibly from another thread, to stop processing events.
+ self.cancelled = False
+ # Called periodically, see PROGRESS_FRACTION, while processing events.
+ self.progress: Optional[Callable[[], None]] = None
+ self.start_progress = time.monotonic()
+ self.last_progress = self.start_progress
+
+ def duration_str(self) -> str:
+ """Description of the time covered by the processed samples."""
+ if not self.last_time:
+ return ""
+ return f", {(self.last_time - self.first_time) / 1e9:.3f}s of trace"
+
+ def comm(self, sample) -> str:
+ """The command name of the thread of the sample."""
+ pid = sample.sample_pid
+ try:
+ assert self.session
+ thread = self.session.find_thread(pid, sample.sample_tid)
+ return (thread.comm() if thread else None) or f"unknown ({pid})"
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError,
+ AssertionError):
+ return f"unknown ({pid})"
+
+ def process_event(self, sample) -> None:
+ """Called by session.process_events to update the profile tree."""
+ self.nr_samples += 1
+ if self.nr_samples % self.PROGRESS_INTERVAL == 0:
+ if self.cancelled:
+ raise LoadCancelled()
+ now = time.monotonic()
+ interval = min(max(self.PROGRESS_SECONDS,
+ (now - self.start_progress) * self.PROGRESS_FRACTION),
+ self.PROGRESS_MAX_SECONDS)
+ if self.progress and now - self.last_progress >= interval:
+ # Must not hold the lock as the callback may read the profile.
+ self.progress()
+ # Time from when the callback, that may block, returns.
+ self.last_progress = time.monotonic()
+ sample_time = sample.sample_time
+ if sample_time:
+ if not self.first_time or sample_time < self.first_time:
+ self.first_time = sample_time
+ self.last_time = max(self.last_time, sample_time)
+ ev_name = str(sample.evsel)[6:-1]
+ comm = self.comm(sample)
+ with self.profile_lock:
+ ev_root = self.root.find_or_create_node(ev_name)
+ ev_root.process_event(sample, comm)
+
+ def read(self, input_file: str) -> None:
+ """Process the events in input_file, raising on errors."""
+ if self.cancelled:
+ raise LoadCancelled()
+ self.start_progress = self.last_progress = time.monotonic()
+ try:
+ self.session = perf.session(perf.data(input_file), sample=self.process_event)
+ self.session.process_events()
+ finally:
+ # Break the reference cycle between the session and the callback.
+ self.session = None
+
+
+def main() -> None:
+ """Parse arguments and run the app."""
+ parser = argparse.ArgumentParser(
+ description="TUI report and flame graph using perf python module.")
+ parser.add_argument("-i", "--input", help="input perf.data file")
+ args = parser.parse_args()
+
+ input_file = args.input or "perf.data"
+ if input_file == "-":
+ # The interactive UI reads the keyboard from stdin.
+ print("Error: reading perf.data from stdin isn't supported.", file=sys.stderr)
+ sys.exit(1)
+ if not os.path.exists(input_file):
+ print(f"Error: {input_file} not found. (try 'perf record' first)", file=sys.stderr)
+ sys.exit(1)
+
+ # The app starts immediately and builds the profile in the background.
+ app = ReportApp(input_file)
+ app.run()
+ sys.exit(app.return_code or 0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/ttimechart.py b/tools/perf/python/ttimechart.py
new file mode 100755
index 000000000000..d7ba0276a9fb
--- /dev/null
+++ b/tools/perf/python/ttimechart.py
@@ -0,0 +1,1806 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""ttimechart.py - interactive perf timechart written using textual.
+
+Reads a perf.data file, typically created with 'perf timechart record', and
+displays per-CPU and per-task timelines in the terminal. Scheduler
+(sched:sched_switch, sched:sched_wakeup), power (power:cpu_idle,
+power:cpu_frequency) and I/O syscall (perf timechart record -I) tracepoints
+are understood. Unlike 'perf timechart', which writes an SVG file, the
+timeline can be zoomed, panned and queried interactively.
+
+Usage:
+ perf timechart record -- <workload>
+ perf timechart --tui
+or:
+ perf script ttimechart [-i perf.data]
+"""
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+import argparse
+import bisect
+from collections import defaultdict
+from dataclasses import dataclass, replace
+import math
+import os
+import sys
+import threading
+from time import monotonic
+from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
+
+import perf
+from rich.segment import Segment
+from rich.style import Style
+from textual import events, on, work
+from textual.app import App, ComposeResult
+from textual.binding import Binding
+from textual.color import Color
+from textual.geometry import Size
+from textual.message import Message
+from textual.reactive import reactive
+from textual.scroll_view import ScrollView
+from textual.strip import Strip
+from textual.widgets import DataTable, Footer, Header, Static, TabbedContent, TabPane
+from textual.widgets.data_table import CellDoesNotExist, RowDoesNotExist
+
+# Task states, SLEEPING and UNKNOWN aren't drawn.
+STATE_UNKNOWN = -1
+STATE_SLEEPING = 0
+STATE_RUNNING = 1
+STATE_WAITING = 2
+STATE_BLOCKED = 3
+NUM_STATES = 4
+STATE_NAMES = {
+ STATE_UNKNOWN: "unknown",
+ STATE_SLEEPING: "sleeping",
+ STATE_RUNNING: "running",
+ STATE_WAITING: "runnable (waiting for a CPU)",
+ STATE_BLOCKED: "blocked (uninterruptible)",
+}
+
+# I/O types, matching builtin-timechart.c.
+IOTYPE_READ = 0
+IOTYPE_WRITE = 1
+IOTYPE_SYNC = 2
+IOTYPE_TX = 3
+IOTYPE_RX = 4
+IOTYPE_POLL = 5
+NUM_IOTYPES = 6
+IOTYPE_NAMES = ["read", "write", "sync", "tx", "rx", "poll"]
+
+IO_SYSCALLS = {
+ "read": IOTYPE_READ, "pread64": IOTYPE_READ, "readv": IOTYPE_READ,
+ "preadv": IOTYPE_READ,
+ "write": IOTYPE_WRITE, "pwrite64": IOTYPE_WRITE, "writev": IOTYPE_WRITE,
+ "pwritev": IOTYPE_WRITE,
+ "sync": IOTYPE_SYNC, "sync_file_range": IOTYPE_SYNC, "fsync": IOTYPE_SYNC,
+ "msync": IOTYPE_SYNC,
+ "recvfrom": IOTYPE_RX, "recvmmsg": IOTYPE_RX, "recvmsg": IOTYPE_RX,
+ "sendto": IOTYPE_TX, "sendmsg": IOTYPE_TX, "sendmmsg": IOTYPE_TX,
+ "epoll_pwait": IOTYPE_POLL, "epoll_wait": IOTYPE_POLL, "poll": IOTYPE_POLL,
+ "ppoll": IOTYPE_POLL, "pselect6": IOTYPE_POLL, "select": IOTYPE_POLL,
+}
+
+# Trace flags for interrupt context in the common_flags tracepoint field.
+TRACE_FLAG_HARDIRQ = 0x08
+TRACE_FLAG_SOFTIRQ = 0x10
+# Value of state in power:cpu_idle when leaving idle.
+PWR_EVENT_EXIT = 0xffffffff
+# Width of the row label column.
+LABEL_WIDTH = 28
+# Characters for drawing fractional bars.
+BARS = " ▁▂▃▄▅▆▇█"
+NSEC_PER_SEC = 1_000_000_000
+
+
+def fmt_duration(nsecs: float) -> str:
+ """Format a duration in nanoseconds with an appropriate unit."""
+ if nsecs >= NSEC_PER_SEC:
+ return f"{nsecs / NSEC_PER_SEC:.3f}s"
+ if nsecs >= 1_000_000:
+ return f"{nsecs / 1_000_000:.3f}ms"
+ if nsecs >= 1_000:
+ return f"{nsecs / 1_000:.3f}us"
+ return f"{nsecs:.0f}ns"
+
+
+def escape(text: str) -> str:
+ """Escape text, such as a task name, for use in textual markup.
+
+ rich.markup.escape doesn't escape tags like "[1]", the name given to tasks
+ with an unknown command, but textual fails to parse them.
+ """
+ return text.replace("[", "\\[")
+
+
+def make_fixed_length_string(s: str, length: int, pad_char: str = ' ') -> str:
+ """Truncate or right pad s so that it is length characters long."""
+ return s[:length] if len(s) > length else s.ljust(length, pad_char)
+
+
+def bar_char(frac: float) -> str:
+ """A block character whose height represents frac, in the range [0, 1]."""
+ if frac <= 0:
+ return BARS[0]
+ return BARS[max(1, min(8, round(frac * 8)))]
+
+
+class SegmentList:
+ """A time ordered list of non-overlapping [start, end) segments.
+
+ Each segment has a small integer key, used for computing coverage, and
+ arbitrary associated data.
+ """
+ def __init__(self) -> None:
+ self.starts: List[int] = []
+ self.ends: List[int] = []
+ self.keys: List[int] = []
+ self.data: List[Any] = []
+
+ def __len__(self) -> int:
+ return len(self.starts)
+
+ def add(self, start: int, end: int, key: int, data: Any = None) -> None:
+ """Append a segment, segments must be added in time order."""
+ if end <= start:
+ return
+ if self.ends and start < self.ends[-1]:
+ # Clip overlaps caused by inconsistent data.
+ start = self.ends[-1]
+ if end <= start:
+ return
+ self.starts.append(start)
+ self.ends.append(end)
+ self.keys.append(key)
+ self.data.append(data)
+
+ def find(self, time: float) -> int:
+ """Index of the segment containing time or -1."""
+ i = bisect.bisect_right(self.starts, time) - 1
+ if i >= 0 and time < self.ends[i]:
+ return i
+ return -1
+
+ def last_before(self, time: float) -> int:
+ """Index of the last segment starting at or before time or -1."""
+ return bisect.bisect_right(self.starts, time) - 1
+
+ def next_change(self, time: float) -> Optional[int]:
+ """The first segment start or end after time."""
+ candidates = []
+ i = bisect.bisect_right(self.starts, time)
+ if i < len(self.starts):
+ candidates.append(self.starts[i])
+ i = bisect.bisect_right(self.ends, time)
+ if i < len(self.ends):
+ candidates.append(self.ends[i])
+ return min(candidates) if candidates else None
+
+ def prev_change(self, time: float) -> Optional[int]:
+ """The last segment start or end before time."""
+ candidates = []
+ i = bisect.bisect_left(self.starts, time) - 1
+ if i >= 0:
+ candidates.append(self.starts[i])
+ i = bisect.bisect_left(self.ends, time) - 1
+ if i >= 0:
+ candidates.append(self.ends[i])
+ return max(candidates) if candidates else None
+
+ def coverage(self, t0: float, dt: float, width: int, nkeys: int,
+ weight_fn=None) -> List[List[float]]:
+ """Time covered by each key in each of width columns of size dt.
+
+ If weight_fn is given then, rather than the time covered, the time
+ covered multiplied by weight_fn(data) is accumulated.
+ """
+ cols = [[0.0] * nkeys for _ in range(width)]
+ t1 = t0 + dt * width
+ i = bisect.bisect_right(self.ends, t0)
+ num = len(self.starts)
+ while i < num and self.starts[i] < t1:
+ start = max(self.starts[i], t0)
+ end = min(self.ends[i], t1)
+ key = self.keys[i]
+ weight = weight_fn(self.data[i]) if weight_fn else 1.0
+ x0 = min(int((start - t0) / dt), width - 1)
+ x1 = min(int((end - t0) / dt), width - 1)
+ if x0 == x1:
+ cols[x0][key] += (end - start) * weight
+ else:
+ cols[x0][key] += (t0 + (x0 + 1) * dt - start) * weight
+ for x in range(x0 + 1, x1):
+ cols[x][key] += dt * weight
+ cols[x1][key] += (end - (t0 + x1 * dt)) * weight
+ i += 1
+ return cols
+
+
+class Task:
+ """Scheduling and I/O history of a single thread."""
+ def __init__(self, tid: int, comm: str) -> None:
+ self.tid = tid
+ self.comm = comm
+ self.comms: List[str] = [comm]
+ self.state = STATE_UNKNOWN
+ self.since = 0
+ self.cpu = -1
+ # Segments with a state key and the CPU as data.
+ self.segs = SegmentList()
+ # Segments with an I/O type key and (fd, ret) as data.
+ self.io = SegmentList()
+ self.io_pending: Optional[Tuple[int, int, int]] = None
+ # Wakeups of this task as (time, waker tid) pairs.
+ self.wakeups: List[Tuple[int, int]] = []
+ self.totals = [0] * NUM_STATES
+ self.switches = 0
+ self.io_bytes = 0
+
+ def name(self) -> str:
+ """Name for the task used in labels."""
+ return f"{self.comm} ({self.tid})"
+
+ def set_comm(self, comm: Optional[str]) -> None:
+ """Update the task's command name."""
+ if not comm or comm == self.comm:
+ return
+ self.comm = comm
+ if comm not in self.comms:
+ self.comms.append(comm)
+
+ def change_state(self, time: int, state: int, cpu: int = -1) -> None:
+ """Record the current state as a segment and switch to a new state."""
+ if self.state in (STATE_RUNNING, STATE_WAITING, STATE_BLOCKED) and time > self.since:
+ self.segs.add(self.since, time, self.state, self.cpu)
+ self.totals[self.state] += time - self.since
+ self.state = state
+ self.since = time
+ if cpu >= 0:
+ self.cpu = cpu
+
+ def passes_filter(self, filters: Sequence[str]) -> bool:
+ """Does the task match one of the process filters (pid or name)?"""
+ if not filters:
+ return True
+ return any(f == str(self.tid) or f in self.comms for f in filters)
+
+
+class Cpu:
+ """Activity on a single CPU."""
+ def __init__(self, cpu: int) -> None:
+ self.cpu = cpu
+ self.cur_tid = -1
+ self.since = 0
+ # Busy segments, key 1, with the running tid as data.
+ self.run = SegmentList()
+ # Idle state segments, key 1, with the C-state as data.
+ self.cstate = SegmentList()
+ self.cstate_cur: Optional[Tuple[int, int]] = None
+ # Frequency segments, key 1, with the frequency in kHz as data.
+ self.pstate = SegmentList()
+ self.pstate_cur: Optional[Tuple[int, int]] = None
+
+
+class LoadCancelled(Exception):
+ """Raised from the sample callback to stop processing events early."""
+
+
+class TimechartData:
+ """Builds per-task and per-CPU timelines from perf events.
+
+ The data may be displayed while it is loaded in another thread, lock must
+ be held when modifying it or when reading it from another thread.
+ """
+ # Number of samples between checks for cancellation and progress.
+ PROGRESS_INTERVAL = 1000
+ # Minimum and maximum time between calls to the progress callback.
+ PROGRESS_SECONDS = 1.0
+ PROGRESS_MAX_SECONDS = 10.0
+ # Updating the views costs more as more data is loaded, so the time between
+ # progress calls grows as this fraction of the time spent loading. This
+ # bounds the fraction of the load time spent updating the views.
+ PROGRESS_FRACTION = 0.25
+
+ def __init__(self) -> None:
+ self.tasks: Dict[int, Task] = {}
+ self.cpus: Dict[int, Cpu] = {}
+ self.first_time = 0
+ self.last_time = 0
+ self.min_freq = 0
+ self.max_freq = 0
+ self.max_cstate = 0
+ self.sched_events = 0
+ self.power_events = 0
+ self.io_events = 0
+ self.nr_samples = 0
+ self.unhandled: Dict[str, int] = defaultdict(int)
+ self.session: Optional[perf.session] = None
+ # Evsel name and event handler, keyed by sample ID.
+ self._handlers: Dict[int, Tuple[str, Optional[Callable[[int, perf.sample_event],
+ None]]]] = {}
+ self.lock = threading.Lock()
+ # Set, possibly from another thread, to stop processing events.
+ self.cancelled = False
+ # Called periodically, see PROGRESS_FRACTION, while processing events.
+ self.progress: Optional[Callable[[], None]] = None
+ self.start_progress = monotonic()
+ self.last_progress = self.start_progress
+
+ def has_events(self) -> bool:
+ """Were any events that can be displayed processed?"""
+ return bool(self.sched_events or self.power_events or self.io_events)
+
+ def task(self, tid: int, comm: Optional[str] = None) -> Task:
+ """Find or create a task."""
+ task = self.tasks.get(tid)
+ if task is None:
+ if not comm and self.session:
+ try:
+ thread = self.session.find_thread(tid, tid)
+ comm = thread.comm() if thread else None
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError):
+ comm = None
+ task = Task(tid, comm or f"[{tid}]")
+ self.tasks[tid] = task
+ else:
+ task.set_comm(comm)
+ return task
+
+ def cpu(self, cpu: int) -> Cpu:
+ """Find or create a CPU."""
+ c = self.cpus.get(cpu)
+ if c is None:
+ c = Cpu(cpu)
+ self.cpus[cpu] = c
+ return c
+
+ def sched_switch(self, time: int, cpu: int, prev_tid: int, prev_comm: Optional[str],
+ prev_state: int, next_tid: int, next_comm: Optional[str]) -> None:
+ """Process a sched:sched_switch event."""
+ self.sched_events += 1
+ c = self.cpu(cpu)
+ if c.cur_tid == -1 and prev_tid != 0:
+ # Assume the task was running from the start of the trace.
+ c.cur_tid = prev_tid
+ c.since = self.first_time
+ if c.cur_tid > 0:
+ c.run.add(c.since, time, 1, c.cur_tid)
+ c.cur_tid = next_tid
+ c.since = time
+
+ if prev_tid != 0:
+ prev = self.task(prev_tid, prev_comm)
+ if prev.state == STATE_UNKNOWN:
+ prev.state = STATE_RUNNING
+ prev.since = self.first_time
+ prev.cpu = cpu
+ # Ignore bits like TASK_REPORT_MAX used to report preemption.
+ state = prev_state & 0xff
+ if state == 0:
+ new_state = STATE_WAITING
+ elif state & 2:
+ new_state = STATE_BLOCKED
+ else:
+ new_state = STATE_SLEEPING
+ prev.change_state(time, new_state)
+ prev.switches += 1
+
+ if next_tid != 0:
+ nxt = self.task(next_tid, next_comm)
+ nxt.change_state(time, STATE_RUNNING, cpu)
+
+ def sched_wakeup(self, time: int, wakee: int, comm: Optional[str], waker: int) -> None:
+ """Process a sched:sched_wakeup or sched:sched_wakeup_new event."""
+ self.sched_events += 1
+ if wakee == 0:
+ return
+ task = self.task(wakee, comm)
+ task.wakeups.append((time, waker))
+ if task.state in (STATE_UNKNOWN, STATE_SLEEPING, STATE_BLOCKED):
+ task.change_state(time, STATE_WAITING)
+
+ def cstate_start(self, time: int, cpu: int, state: int) -> None:
+ """Enter an idle state."""
+ self.power_events += 1
+ c = self.cpu(cpu)
+ if c.cstate_cur:
+ c.cstate.add(c.cstate_cur[0], time, 1, c.cstate_cur[1])
+ c.cstate_cur = (time, state)
+ self.max_cstate = max(self.max_cstate, state)
+
+ def cstate_end(self, time: int, cpu: int) -> None:
+ """Leave an idle state."""
+ self.power_events += 1
+ c = self.cpu(cpu)
+ if c.cstate_cur:
+ c.cstate.add(c.cstate_cur[0], time, 1, c.cstate_cur[1])
+ c.cstate_cur = None
+
+ def pstate_change(self, time: int, cpu: int, freq: int) -> None:
+ """Change of CPU frequency, freq is in kHz."""
+ if freq <= 0 or freq > 8000000:
+ return
+ self.power_events += 1
+ c = self.cpu(cpu)
+ if c.pstate_cur:
+ c.pstate.add(c.pstate_cur[0], time, 1, c.pstate_cur[1])
+ c.pstate_cur = (time, freq)
+ self.max_freq = max(self.max_freq, freq)
+ self.min_freq = freq if not self.min_freq else min(self.min_freq, freq)
+
+ def io_enter(self, time: int, tid: int, iotype: int, fd: int) -> None:
+ """Entry to an I/O syscall."""
+ self.io_events += 1
+ self.task(tid).io_pending = (time, iotype, fd)
+
+ def io_exit(self, time: int, tid: int, iotype: int, ret: int) -> None:
+ """Exit from an I/O syscall."""
+ self.io_events += 1
+ task = self.task(tid)
+ pending = task.io_pending
+ task.io_pending = None
+ if not pending or pending[1] != iotype:
+ return
+ start = pending[0]
+ task.io.add(start, max(time, start + 1), iotype, (pending[2], ret))
+ if ret > 0 and iotype in (IOTYPE_READ, IOTYPE_WRITE, IOTYPE_TX, IOTYPE_RX):
+ task.io_bytes += ret
+
+ def _on_sched_switch(self, time: int, sample: perf.sample_event) -> None:
+ self.sched_switch(time, sample.sample_cpu, sample.prev_pid,
+ getattr(sample, "prev_comm", None), sample.prev_state,
+ sample.next_pid, getattr(sample, "next_comm", None))
+
+ def _on_sched_wakeup(self, time: int, sample: perf.sample_event) -> None:
+ waker = getattr(sample, "common_pid", sample.sample_tid)
+ flags = getattr(sample, "common_flags", 0)
+ if flags & (TRACE_FLAG_HARDIRQ | TRACE_FLAG_SOFTIRQ):
+ waker = -1
+ self.sched_wakeup(time, sample.pid, getattr(sample, "comm", None), waker)
+
+ def _cstate(self, time: int, cpu: int, state: int) -> None:
+ if (state & 0xffffffff) == PWR_EVENT_EXIT:
+ self.cstate_end(time, cpu)
+ else:
+ self.cstate_start(time, cpu, state)
+
+ def _on_cpu_idle(self, time: int, sample: perf.sample_event) -> None:
+ self._cstate(time, sample.cpu_id, sample.state)
+
+ def _on_power_start(self, time: int, sample: perf.sample_event) -> None:
+ self._cstate(time, sample.cpu_id, sample.value)
+
+ def _on_power_end(self, time: int, sample: perf.sample_event) -> None:
+ self.cstate_end(time, sample.sample_cpu)
+
+ def _on_cpu_frequency(self, time: int, sample: perf.sample_event) -> None:
+ self.pstate_change(time, sample.cpu_id, sample.state)
+
+ def _on_power_frequency(self, time: int, sample: perf.sample_event) -> None:
+ self.pstate_change(time, sample.cpu_id, sample.value)
+
+ def _handler_for(self, name: str) -> Optional[Callable[[int, perf.sample_event], None]]:
+ """Find the handler for events with the given evsel name."""
+ handlers: Dict[str, Callable[[int, perf.sample_event], None]] = {
+ "sched:sched_switch": self._on_sched_switch,
+ "sched:sched_wakeup": self._on_sched_wakeup,
+ "sched:sched_wakeup_new": self._on_sched_wakeup,
+ "power:cpu_idle": self._on_cpu_idle,
+ "power:power_start": self._on_power_start,
+ "power:power_end": self._on_power_end,
+ "power:cpu_frequency": self._on_cpu_frequency,
+ "power:power_frequency": self._on_power_frequency,
+ }
+ if name in handlers:
+ return handlers[name]
+ if name.startswith("syscalls:sys_enter_") and name[19:] in IO_SYSCALLS:
+ iotype = IO_SYSCALLS[name[19:]]
+ return lambda time, sample: self.io_enter(time, sample.sample_tid, iotype,
+ getattr(sample, "fd", -1))
+ if name.startswith("syscalls:sys_exit_") and name[18:] in IO_SYSCALLS:
+ iotype = IO_SYSCALLS[name[18:]]
+ return lambda time, sample: self.io_exit(time, sample.sample_tid, iotype,
+ sample.ret)
+ return None
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Callback from perf.session for each sample."""
+ self.nr_samples += 1
+ if self.nr_samples % self.PROGRESS_INTERVAL == 0:
+ if self.cancelled:
+ raise LoadCancelled()
+ now = monotonic()
+ interval = min(max(self.PROGRESS_SECONDS,
+ (now - self.start_progress) * self.PROGRESS_FRACTION),
+ self.PROGRESS_MAX_SECONDS)
+ if self.progress and now - self.last_progress >= interval:
+ with self.lock:
+ self.update_comms()
+ # Must not hold the lock as the callback may read the data.
+ self.progress()
+ # Time from when the callback, that may block, returns.
+ self.last_progress = monotonic()
+ with self.lock:
+ self._process_event(sample)
+
+ def _process_event(self, sample: perf.sample_event) -> None:
+ """Update the data from a sample, the lock must be held."""
+ time = sample.sample_time
+ if not self.first_time or time < self.first_time:
+ self.first_time = time
+ self.last_time = max(self.last_time, time)
+
+ # Computing the evsel name and matching it is relatively expensive,
+ # so cache the result by sample ID. Each ID belongs to a single evsel.
+ sample_id = sample.sample_id
+ cached = self._handlers.get(sample_id)
+ if cached is None:
+ name = str(sample.evsel)
+ if name.startswith("evsel(") and name.endswith(")"):
+ name = name[6:-1]
+ cached = (name, self._handler_for(name))
+ self._handlers[sample_id] = cached
+ name, handler = cached
+ if handler is None:
+ self.unhandled[name] += 1
+ return
+ try:
+ handler(time, sample)
+ except AttributeError:
+ self.unhandled[name] += 1
+
+ def update_comms(self) -> None:
+ """Refresh task command names from the session, the lock must be held."""
+ if not self.session:
+ return
+ for tid, task in self.tasks.items():
+ try:
+ thread = self.session.find_thread(tid, tid)
+ if thread:
+ task.set_comm(thread.comm())
+ except (OSError, ValueError, KeyError, RuntimeError, TypeError, AttributeError):
+ pass
+
+ def finish(self) -> None:
+ """Close open segments at the end of the trace."""
+ with self.lock:
+ self.update_comms()
+ end = self.last_time
+ for task in self.tasks.values():
+ task.change_state(end, STATE_UNKNOWN)
+ for c in self.cpus.values():
+ if c.cur_tid > 0:
+ c.run.add(c.since, end, 1, c.cur_tid)
+ if c.cstate_cur:
+ c.cstate.add(c.cstate_cur[0], end, 1, c.cstate_cur[1])
+ c.cstate_cur = None
+ if c.pstate_cur:
+ c.pstate.add(c.pstate_cur[0], end, 1, c.pstate_cur[1])
+ c.pstate_cur = None
+
+ def fmt_time(self, time: float) -> str:
+ """Format an absolute timestamp relative to the trace start."""
+ return f"{(time - self.first_time) / NSEC_PER_SEC:.6f}s"
+
+ def task_name(self, tid: int) -> str:
+ """Name of a task, or a description for special tids."""
+ if tid == 0:
+ return "idle"
+ if tid < 0:
+ return "interrupt"
+ task = self.tasks.get(tid)
+ return task.name() if task else f"[{tid}]"
+
+ def sched_tasks(self, filters: Sequence[str]) -> List[Task]:
+ """Tasks with scheduling history passing the filters, sorted by tid."""
+ return sorted((t for t in self.tasks.values()
+ if len(t.segs) and t.passes_filter(filters)),
+ key=lambda t: t.tid)
+
+ def io_tasks(self, filters: Sequence[str]) -> List[Task]:
+ """Tasks with I/O passing the filters, most I/O first."""
+ return sorted((t for t in self.tasks.values()
+ if len(t.io) and t.passes_filter(filters)),
+ key=lambda t: -len(t.io))
+
+ def dump(self, power_only: bool, tasks_only: bool, filters: Sequence[str]) -> None:
+ """Print a plain text summary, for use without a terminal UI."""
+ duration = self.last_time - self.first_time
+ span = max(duration, 1)
+ tasks = self.sched_tasks(filters)
+ io_tasks = self.io_tasks(filters)
+ print(f"Duration: {fmt_duration(duration)}, CPUs: {len(self.cpus)}, "
+ f"tasks: {len(tasks) or len(io_tasks)}, sched events: {self.sched_events}, "
+ f"power events: {self.power_events}, I/O events: {self.io_events}")
+ if not tasks_only:
+ for c in sorted(self.cpus.values(), key=lambda c: c.cpu):
+ busy = sum(e - s for s, e in zip(c.run.starts, c.run.ends))
+ print(f"CPU {c.cpu}: busy {busy * 100 / span:.1f}%, "
+ f"{len(c.run)} runs, {len(c.cstate)} idle periods, "
+ f"{len(c.pstate)} frequency periods")
+ if power_only:
+ return
+ print(f"{'Task':<24} {'TID':>8} {'Running':>12} {'Waiting':>12} {'Blocked':>12} "
+ f"{'Switches':>9} {'Wakeups':>8}")
+ for task in sorted(tasks, key=lambda t: -t.totals[STATE_RUNNING]):
+ print(f"{task.comm[:24]:<24} {task.tid:>8} "
+ f"{fmt_duration(task.totals[STATE_RUNNING]):>12} "
+ f"{fmt_duration(task.totals[STATE_WAITING]):>12} "
+ f"{fmt_duration(task.totals[STATE_BLOCKED]):>12} "
+ f"{task.switches:>9} {len(task.wakeups):>8}")
+ for task in io_tasks:
+ print(f"I/O {task.name()}: {len(task.io)} syscalls, {task.io_bytes} bytes")
+
+
+Cell = Tuple[str, Style]
+
+
+class ThemeColors:
+ """Colors and Rich styles derived from the active Textual theme."""
+ def __init__(self, theme_variables: Mapping[str, str]) -> None:
+ self.running = theme_variables["primary"]
+ self.waiting = theme_variables["error"]
+ self.blocked = theme_variables["warning"]
+ self.idle_light = theme_variables["secondary-lighten-2"]
+ self.idle_dark = theme_variables["secondary-darken-2"]
+ self.freq_low = theme_variables["success"]
+ self.freq_high = theme_variables["error"]
+ self.io = [
+ theme_variables["success"],
+ theme_variables["error"],
+ theme_variables["warning"],
+ theme_variables["accent"],
+ theme_variables["primary"],
+ theme_variables["secondary-lighten-2"],
+ ]
+ run_color = Color.parse(self.running).rich_color
+ wait_color = Color.parse(self.waiting).rich_color
+ block_color = Color.parse(self.blocked).rich_color
+ self.run_style = Style(color=run_color)
+ self.wait_style = Style(color=run_color, bgcolor=wait_color)
+ self.block_style = Style(color=run_color, bgcolor=block_color)
+ low = Color.parse(self.freq_low)
+ high = Color.parse(self.freq_high)
+ self.freq_styles = [Style(color=low.blend(high, x / 8).rich_color) for x in range(9)]
+ light = Color.parse(self.idle_light)
+ dark = Color.parse(self.idle_dark)
+ self.idle_styles = [Style(color=light.blend(dark, x / 8).rich_color) for x in range(9)]
+ self.io_styles = [Style(color=Color.parse(c).rich_color) for c in self.io]
+ self.io_err_styles = [Style(color=Color.parse(c).rich_color, underline=True)
+ for c in self.io]
+ accent = Color.parse(theme_variables["accent"]).rich_color
+ accent_muted = Color.parse(theme_variables["accent-muted"]).rich_color
+ self.selected_style = Style(color=accent, bgcolor=accent_muted, bold=True)
+
+
+class Row(ABC):
+ """A row within a timeline view."""
+ def __init__(self, label: str) -> None:
+ self._label = label
+
+ def label(self) -> str:
+ """Label shown to the left of the row."""
+ return self._label
+
+ @abstractmethod
+ def segments(self) -> SegmentList:
+ """Segments used for navigating between changes."""
+
+ @abstractmethod
+ def cells(self, t0: float, dt: float, width: int, colors: ThemeColors) -> List[Cell]:
+ """Cells for the columns starting at time t0 and dt wide."""
+
+ @abstractmethod
+ def describe(self, time: float, t0: float, t1: float) -> str:
+ """Rich markup describing the row at time within the window [t0, t1)."""
+
+
+class TaskRow(Row):
+ """Row showing the running, waiting and blocked states of a task.
+
+ The height of the bar shows the fraction of the time running, the
+ background shows waiting for a CPU or being blocked.
+ """
+ def __init__(self, data: TimechartData, task: Task) -> None:
+ super().__init__(task.name())
+ self.data = data
+ self.task = task
+
+ def label(self) -> str:
+ return self.task.name()
+
+ def segments(self) -> SegmentList:
+ return self.task.segs
+
+ def cells(self, t0: float, dt: float, width: int, colors: ThemeColors) -> List[Cell]:
+ result = []
+ for col in self.task.segs.coverage(t0, dt, width, NUM_STATES):
+ run = col[STATE_RUNNING] / dt
+ wait = col[STATE_WAITING]
+ block = col[STATE_BLOCKED]
+ if wait > 0 and wait >= block:
+ style = colors.wait_style
+ elif block > 0:
+ style = colors.block_style
+ else:
+ style = colors.run_style
+ result.append((bar_char(run), style))
+ return result
+
+ def describe(self, time: float, t0: float, t1: float) -> str:
+ task = self.task
+ i = task.segs.find(time)
+ if i >= 0:
+ state = task.segs.keys[i]
+ desc = STATE_NAMES[state]
+ if state == STATE_RUNNING:
+ desc += f" on CPU {task.segs.data[i]}"
+ elif state == STATE_WAITING and task.segs.data[i] >= 0:
+ desc += f", last ran on CPU {task.segs.data[i]}"
+ start = task.segs.starts[i]
+ end = task.segs.ends[i]
+ desc += (f" from {self.data.fmt_time(start)} for "
+ f"{fmt_duration(end - start)}")
+ else:
+ desc = "sleeping or not traced"
+ window = task.segs.coverage(t0, max(t1 - t0, 1), 1, NUM_STATES)[0]
+ span = max(t1 - t0, 1)
+ lines = [
+ f"[b]{escape(task.name())}[/b]: {desc}",
+ f"In view: running {window[STATE_RUNNING] * 100 / span:.1f}%, "
+ f"waiting {window[STATE_WAITING] * 100 / span:.1f}%, "
+ f"blocked {window[STATE_BLOCKED] * 100 / span:.1f}%",
+ f"Total: running {fmt_duration(task.totals[STATE_RUNNING])}, "
+ f"waiting {fmt_duration(task.totals[STATE_WAITING])}, "
+ f"blocked {fmt_duration(task.totals[STATE_BLOCKED])}, "
+ f"{task.switches} switches, {len(task.wakeups)} wakeups",
+ ]
+ idx = bisect.bisect_right(task.wakeups, (time, sys.maxsize)) - 1
+ if idx >= 0:
+ wake_time, waker = task.wakeups[idx]
+ lines.append(f"Last woken at {self.data.fmt_time(wake_time)} by "
+ f"{escape(self.data.task_name(waker))} (press 'w' to go to waker)")
+ if len(task.comms) > 1:
+ lines.append(f"Names: {escape(', '.join(task.comms))}")
+ return "\n".join(lines)
+
+
+class CpuRow(Row):
+ """Row showing how busy a CPU is, colored by frequency if known."""
+ def __init__(self, data: TimechartData, cpu: Cpu) -> None:
+ super().__init__(f"CPU {cpu.cpu}")
+ self.data = data
+ self.cpu = cpu
+
+ def segments(self) -> SegmentList:
+ return self.cpu.run
+
+ def cells(self, t0: float, dt: float, width: int, colors: ThemeColors) -> List[Cell]:
+ busy = self.cpu.run.coverage(t0, dt, width, 2)
+ freqs: Optional[List[List[float]]] = None
+ weighted: Optional[List[List[float]]] = None
+ if len(self.cpu.pstate) and self.data.max_freq > self.data.min_freq:
+ freqs = self.cpu.pstate.coverage(t0, dt, width, 2)
+ weighted = self.cpu.pstate.coverage(t0, dt, width, 2, weight_fn=float)
+ result = []
+ for x in range(width):
+ style = colors.run_style
+ if freqs and weighted and freqs[x][1] > 0:
+ freq = weighted[x][1] / freqs[x][1]
+ frac = (freq - self.data.min_freq) / (self.data.max_freq - self.data.min_freq)
+ style = colors.freq_styles[max(0, min(8, round(frac * 8)))]
+ result.append((bar_char(busy[x][1] / dt), style))
+ return result
+
+ def describe(self, time: float, t0: float, t1: float) -> str:
+ c = self.cpu
+ i = c.run.find(time)
+ if i >= 0:
+ desc = (f"running {escape(self.data.task_name(c.run.data[i]))} from "
+ f"{self.data.fmt_time(c.run.starts[i])} for "
+ f"{fmt_duration(c.run.ends[i] - c.run.starts[i])}")
+ else:
+ desc = "idle"
+ i = c.cstate.find(time)
+ if i >= 0:
+ desc += f", C-state C{c.cstate.data[i]}"
+ i = c.pstate.find(time)
+ if i >= 0:
+ desc += f", {c.pstate.data[i] / 1000:.0f} MHz"
+ span = max(t1 - t0, 1)
+ window = c.run.coverage(t0, span, 1, 2)[0]
+ return "\n".join([
+ f"[b]CPU {c.cpu}[/b]: {desc}",
+ f"In view: busy {window[1] * 100 / span:.1f}%",
+ "Bar height is the fraction of time busy" +
+ (", color is the frequency from low to high"
+ if len(c.pstate) else ""),
+ ])
+
+
+class CStateRow(Row):
+ """Row showing the idle states of a CPU."""
+ def __init__(self, data: TimechartData, cpu: Cpu) -> None:
+ super().__init__(f" CPU {cpu.cpu} idle")
+ self.data = data
+ self.cpu = cpu
+
+ def segments(self) -> SegmentList:
+ return self.cpu.cstate
+
+ def cells(self, t0: float, dt: float, width: int, colors: ThemeColors) -> List[Cell]:
+ cov = self.cpu.cstate.coverage(t0, dt, width, 2)
+ depth = self.cpu.cstate.coverage(t0, dt, width, 2, weight_fn=float)
+ max_cstate = max(self.data.max_cstate, 1)
+ result = []
+ for x in range(width):
+ frac = cov[x][1] / dt
+ style = colors.idle_styles[0]
+ if cov[x][1] > 0:
+ avg = depth[x][1] / cov[x][1]
+ style = colors.idle_styles[max(0, min(8, round(avg * 8 / max_cstate)))]
+ result.append((bar_char(frac), style))
+ return result
+
+ def describe(self, time: float, t0: float, t1: float) -> str:
+ c = self.cpu
+ i = c.cstate.find(time)
+ if i >= 0:
+ desc = (f"C{c.cstate.data[i]} from {self.data.fmt_time(c.cstate.starts[i])} for "
+ f"{fmt_duration(c.cstate.ends[i] - c.cstate.starts[i])}")
+ else:
+ desc = "not idle"
+ span = max(t1 - t0, 1)
+ window = c.cstate.coverage(t0, span, 1, 2)[0]
+ return "\n".join([
+ f"[b]CPU {c.cpu} idle state[/b]: {desc}",
+ f"In view: idle {window[1] * 100 / span:.1f}%",
+ "Bar height is the fraction of time idle, darker colors are deeper C-states",
+ ])
+
+
+class FreqRow(Row):
+ """Row showing the frequency of a CPU."""
+ def __init__(self, data: TimechartData, cpu: Cpu) -> None:
+ super().__init__(f" CPU {cpu.cpu} freq")
+ self.data = data
+ self.cpu = cpu
+
+ def segments(self) -> SegmentList:
+ return self.cpu.pstate
+
+ def cells(self, t0: float, dt: float, width: int, colors: ThemeColors) -> List[Cell]:
+ cov = self.cpu.pstate.coverage(t0, dt, width, 2)
+ weighted = self.cpu.pstate.coverage(t0, dt, width, 2, weight_fn=float)
+ max_freq = max(self.data.max_freq, 1)
+ min_freq = self.data.min_freq
+ result = []
+ for x in range(width):
+ if cov[x][1] <= 0:
+ result.append((" ", colors.freq_styles[0]))
+ continue
+ freq = weighted[x][1] / cov[x][1]
+ frac = (freq - min_freq) / (max_freq - min_freq) if max_freq > min_freq else 1.0
+ result.append((bar_char(max(freq / max_freq, 1 / 8)),
+ colors.freq_styles[max(0, min(8, round(frac * 8)))]))
+ return result
+
+ def describe(self, time: float, t0: float, t1: float) -> str:
+ c = self.cpu
+ i = c.pstate.find(time)
+ if i >= 0:
+ desc = (f"{c.pstate.data[i] / 1000:.0f} MHz from "
+ f"{self.data.fmt_time(c.pstate.starts[i])} for "
+ f"{fmt_duration(c.pstate.ends[i] - c.pstate.starts[i])}")
+ else:
+ desc = "unknown"
+ return "\n".join([
+ f"[b]CPU {c.cpu} frequency[/b]: {desc}",
+ f"Range: {self.data.min_freq / 1000:.0f} - {self.data.max_freq / 1000:.0f} MHz",
+ "Bar height is the frequency relative to the maximum",
+ ])
+
+
+class IoRow(Row):
+ """Row showing the I/O syscalls of a task."""
+ def __init__(self, data: TimechartData, task: Task) -> None:
+ super().__init__(task.name())
+ self.data = data
+ self.task = task
+
+ def label(self) -> str:
+ return self.task.name()
+
+ def segments(self) -> SegmentList:
+ return self.task.io
+
+ def cells(self, t0: float, dt: float, width: int, colors: ThemeColors) -> List[Cell]:
+ errs = self.task.io.coverage(t0, dt, width, NUM_IOTYPES,
+ weight_fn=lambda d: 1.0 if d[1] < 0 else 0.0)
+ result = []
+ for x, col in enumerate(self.task.io.coverage(t0, dt, width, NUM_IOTYPES)):
+ total = sum(col)
+ if total <= 0:
+ result.append((" ", colors.io_styles[0]))
+ continue
+ iotype = col.index(max(col))
+ styles = colors.io_err_styles if sum(errs[x]) > 0 else colors.io_styles
+ result.append((bar_char(max(total / dt, 1 / 8)), styles[iotype]))
+ return result
+
+ def describe(self, time: float, t0: float, t1: float) -> str:
+ task = self.task
+ i = task.io.find(time)
+ if i < 0:
+ i = task.io.last_before(time)
+ prefix = "last I/O"
+ else:
+ prefix = "in"
+ if i >= 0:
+ fd, ret = task.io.data[i]
+ result = f"returned {ret}" if ret >= 0 else f"failed with error {-ret}"
+ desc = (f"{prefix} {IOTYPE_NAMES[task.io.keys[i]]} fd={fd} at "
+ f"{self.data.fmt_time(task.io.starts[i])} for "
+ f"{fmt_duration(task.io.ends[i] - task.io.starts[i])}, {result}")
+ else:
+ desc = "no I/O"
+ span = max(t1 - t0, 1)
+ window = task.io.coverage(t0, span, 1, NUM_IOTYPES)[0]
+ in_view = ", ".join(f"{IOTYPE_NAMES[t]} {window[t] * 100 / span:.1f}%"
+ for t in range(NUM_IOTYPES) if window[t] > 0)
+ return "\n".join([
+ f"[b]{escape(task.name())}[/b]: {desc}",
+ f"In view: {in_view or 'no I/O'}",
+ f"Total: {len(task.io)} I/O syscalls, {task.io_bytes} bytes",
+ ])
+
+
+@dataclass(frozen=True)
+class TimeWindow:
+ """The visible time range and the cursor, shared between views.
+
+ Immutable so that it can be a reactive value, the methods return new windows.
+ """
+ first: int
+ last: int
+ start: float
+ end: float
+ cursor: float
+
+ @staticmethod
+ def whole(first: int, last: int) -> "TimeWindow":
+ """A window showing the whole trace with the cursor at the start."""
+ last = max(last, first + 1)
+ return TimeWindow(first, last, first, last, first)
+
+ def span(self) -> float:
+ """Length of the visible time range."""
+ return self.end - self.start
+
+ def reset(self) -> "TimeWindow":
+ """Show the whole trace."""
+ return replace(self, start=self.first, end=self.last)
+
+ def with_range(self, start: float, span: float) -> "TimeWindow":
+ """Set the visible range clamped to the trace."""
+ span = min(max(span, 100.0), self.last - self.first)
+ start = min(max(start, self.first), self.last - span)
+ return replace(self, start=start, end=start + span)
+
+ def zoom(self, factor: float) -> "TimeWindow":
+ """Zoom by factor keeping the cursor at the same position."""
+ cursor = self.cursor
+ if not self.start <= cursor <= self.end:
+ cursor = (self.start + self.end) / 2
+ rel = (cursor - self.start) / self.span()
+ span = self.span() * factor
+ return replace(self, cursor=cursor).with_range(cursor - rel * span, span)
+
+ def pan(self, frac: float) -> "TimeWindow":
+ """Pan the view by the fraction of the visible span."""
+ delta = self.span() * frac
+ win = self.with_range(self.start + delta, self.span())
+ return replace(win, cursor=min(max(self.cursor + delta, win.start), win.end))
+
+ def with_cursor(self, time: float) -> "TimeWindow":
+ """Move the cursor, scrolling to keep it visible."""
+ win = replace(self, cursor=min(max(time, self.first), self.last))
+ if win.cursor < win.start:
+ return win.with_range(win.cursor, win.span())
+ if win.cursor >= win.end:
+ return win.with_range(win.cursor - win.span() * 0.9, win.span())
+ return win
+
+ def extend(self, first: int, last: int) -> "TimeWindow":
+ """Change the bounds of the trace as more of it is loaded.
+
+ If the whole trace was visible then it still is, otherwise the visible
+ range is kept.
+ """
+ last = max(last, first + 1)
+ whole = self.start == self.first and self.end == self.last
+ win = replace(self, first=first, last=last, cursor=min(max(self.cursor, first), last))
+ return win.reset() if whole else win.with_range(self.start, self.span())
+
+
+class TimelineView(ScrollView):
+ """A scrollable view of rows against a time axis.
+
+ Line 0 is a time ruler that stays at the top, the other lines are rows.
+ """
+ BINDINGS = [
+ Binding("up,k", "row_up", "Up", show=False),
+ Binding("down,j", "row_down", "Down", show=False),
+ Binding("left,h", "cursor_left", "Cursor ←", key_display="←"),
+ Binding("right,l", "cursor_right", "Cursor →", key_display="→"),
+ Binding("shift+left,H", "pan_left", "Pan left", show=False),
+ Binding("shift+right,L", "pan_right", "Pan right", show=False),
+ Binding("plus,equals_sign", "zoom_in", "Zoom in", key_display="+"),
+ Binding("minus", "zoom_out", "Zoom out", key_display="-"),
+ Binding("0,escape", "zoom_reset", "Reset zoom", key_display="0"),
+ Binding("n", "next_change", "Next change"),
+ Binding("p", "prev_change", "Prev change"),
+ Binding("pageup", "page_up", "Page up", show=False),
+ Binding("pagedown", "page_down", "Page down", show=False),
+ ]
+
+ DEFAULT_CSS = """
+ TimelineView {
+ width: 100%;
+ height: 1fr;
+ }
+ """
+
+ class SelectionChanged(Message):
+ """Posted when the selected row changes."""
+
+ # Bound to TimechartApp.window, changing it repaints the view.
+ window: reactive[TimeWindow] = reactive(TimeWindow.whole(0, 1))
+ # Index of the selected row.
+ selected: reactive[int] = reactive(0)
+
+ def __init__(self, data: TimechartData, rows: List[Row], *pos_args, **kwargs) -> None:
+ super().__init__(*pos_args, **kwargs)
+ self.can_focus = True
+ self.data = data
+ self.rows = rows
+ self.cache: Dict[int, List[Cell]] = {}
+ self.cache_key: Tuple[float, float, int, Optional[ThemeColors]] = (0.0, 0.0, 0, None)
+ self.label_style = Style()
+ self.ruler_style = Style(dim=True)
+ self.cursor_style = Style(reverse=True)
+
+ def selected_row(self) -> Optional[Row]:
+ """The selected row, if any."""
+ if 0 <= self.selected < len(self.rows):
+ return self.rows[self.selected]
+ return None
+
+ def timeline_width(self) -> int:
+ """Number of columns used for the timeline."""
+ return max(self.scrollable_content_region.width - LABEL_WIDTH, 1)
+
+ def update_size(self) -> None:
+ """Update the virtual size after rows change or the widget resizes."""
+ self.virtual_size = Size(self.scrollable_content_region.width, len(self.rows) + 1)
+
+ def on_mount(self) -> None:
+ """Size the view when mounted."""
+ self.update_size()
+
+ def on_resize(self) -> None:
+ """Size the view when resized."""
+ self.update_size()
+ self.refresh()
+
+ def set_rows(self, rows: List[Row]) -> None:
+ """Replace the rows, after sorting or as more data is loaded.
+
+ The selected row is kept, if it moves watch_selected scrolls to it.
+ """
+ selected = self.selected_row()
+ self.rows = rows
+ self.cache.clear()
+ self.update_size()
+ self.selected = rows.index(selected) if selected in rows else 0
+ self.refresh()
+
+ def validate_selected(self, idx: int) -> int:
+ """Keep the selection within the rows."""
+ return max(0, min(idx, len(self.rows) - 1))
+
+ def watch_selected(self) -> None:
+ """Keep the selection visible and tell the app."""
+ self.scroll_to_selected()
+ self.post_message(self.SelectionChanged())
+
+ def scroll_to_selected(self) -> None:
+ """Scroll so that the selected row is visible below the ruler."""
+ _, scroll_y = self.scroll_offset
+ visible = max(self.scrollable_content_region.height - 1, 1)
+ if self.selected < scroll_y:
+ self.scroll_to(y=self.selected, animate=False)
+ elif self.selected >= scroll_y + visible:
+ self.scroll_to(y=self.selected - visible + 1, animate=False)
+
+ def set_window(self, window: TimeWindow) -> None:
+ """Change the window shared by all views, owned by the app."""
+ app = self.app
+ if isinstance(app, TimechartApp):
+ app.window = window
+
+ def ruler(self, width: int) -> Strip:
+ """The time axis, labeled in seconds relative to the trace start."""
+ win = self.window
+ dt = win.span() / width
+ tick = 14
+ decimals = max(0, min(9, 1 - math.floor(math.log10(max(dt * tick, 1) / NSEC_PER_SEC))))
+ chars = [" "] * width
+ x = 0
+ while x < width:
+ label = f"|{(win.start + x * dt - self.data.first_time) / NSEC_PER_SEC:.{decimals}f}"
+ if x + len(label) > width:
+ break
+ chars[x:x + len(label)] = list(label)
+ x += max(tick, len(label) + 2)
+ segments = [Segment(make_fixed_length_string("Time (s)", LABEL_WIDTH), self.ruler_style)]
+ cursor_x = self.cursor_column(width)
+ if 0 <= cursor_x < width:
+ segments.append(Segment("".join(chars[:cursor_x]), self.ruler_style))
+ segments.append(Segment("▼"))
+ segments.append(Segment("".join(chars[cursor_x + 1:]), self.ruler_style))
+ else:
+ segments.append(Segment("".join(chars), self.ruler_style))
+ return Strip(segments)
+
+ def cursor_column(self, width: int) -> int:
+ """Column of the cursor or -1 if not visible."""
+ win = self.window
+ if not win.start <= win.cursor < win.end:
+ return -1
+ return min(int((win.cursor - win.start) * width / win.span()), width - 1)
+
+ def render_line(self, y: int) -> Strip:
+ """Render the ruler or a row."""
+ width = self.timeline_width()
+ if y == 0:
+ return self.ruler(width)
+ _, scroll_y = self.scroll_offset
+ idx = scroll_y + y - 1
+ if idx >= len(self.rows):
+ return Strip.blank(self.scrollable_content_region.width)
+
+ colors = self.app.theme_colors if isinstance(self.app, TimechartApp) else \
+ ThemeColors(self.app.theme_variables)
+ win = self.window
+ key = (win.start, win.end, width, colors)
+ if key != self.cache_key:
+ self.cache.clear()
+ self.cache_key = key
+ row = self.rows[idx]
+ with self.data.lock:
+ cells = self.cache.get(idx)
+ if cells is None:
+ cells = row.cells(win.start, win.span() / width, width, colors)
+ self.cache[idx] = cells
+ label = row.label()
+
+ label_style = colors.selected_style if idx == self.selected else self.label_style
+ segments = [Segment(make_fixed_length_string(label, LABEL_WIDTH - 1) + " ",
+ label_style)]
+ cursor_x = self.cursor_column(width)
+ # Merge runs of cells with the same style into one segment.
+ text = ""
+ style: Optional[Style] = None
+ for x, (char, cell_style) in enumerate(cells):
+ if x == cursor_x:
+ cell_style = cell_style + self.cursor_style
+ if cell_style is not style and text:
+ segments.append(Segment(text, style))
+ text = ""
+ text += char
+ style = cell_style
+ if text:
+ segments.append(Segment(text, style))
+ return Strip(segments)
+
+ def column_time(self, x: int) -> float:
+ """Time at the center of the column containing screen offset x."""
+ width = self.timeline_width()
+ return self.window.start + (x - LABEL_WIDTH + 0.5) * self.window.span() / width
+
+ def on_click(self, click: events.Click) -> None:
+ """Select the clicked row and move the cursor to the clicked time."""
+ if click.x >= LABEL_WIDTH:
+ self.set_window(self.window.with_cursor(self.column_time(click.x)))
+ if click.y > 0:
+ _, scroll_y = self.scroll_offset
+ self.selected = scroll_y + click.y - 1
+
+ def action_row_up(self) -> None:
+ """Select the previous row."""
+ self.selected -= 1
+
+ def action_row_down(self) -> None:
+ """Select the next row."""
+ self.selected += 1
+
+ def action_page_up(self) -> None:
+ """Select a row a page up."""
+ self.selected -= max(self.scrollable_content_region.height - 2, 1)
+
+ def action_page_down(self) -> None:
+ """Select a row a page down."""
+ self.selected += max(self.scrollable_content_region.height - 2, 1)
+
+ def action_cursor_left(self) -> None:
+ """Move the cursor one column left."""
+ win = self.window
+ self.set_window(win.with_cursor(win.cursor - win.span() / self.timeline_width()))
+
+ def action_cursor_right(self) -> None:
+ """Move the cursor one column right."""
+ win = self.window
+ self.set_window(win.with_cursor(win.cursor + win.span() / self.timeline_width()))
+
+ def action_pan_left(self) -> None:
+ """Pan a quarter of the view left."""
+ self.set_window(self.window.pan(-0.25))
+
+ def action_pan_right(self) -> None:
+ """Pan a quarter of the view right."""
+ self.set_window(self.window.pan(0.25))
+
+ def action_zoom_in(self) -> None:
+ """Halve the visible time range around the cursor."""
+ self.set_window(self.window.zoom(0.5))
+
+ def action_zoom_out(self) -> None:
+ """Double the visible time range around the cursor."""
+ self.set_window(self.window.zoom(2))
+
+ def action_zoom_reset(self) -> None:
+ """Show the whole trace."""
+ self.set_window(self.window.reset())
+
+ def action_next_change(self) -> None:
+ """Move the cursor to the next change in the selected row."""
+ row = self.selected_row()
+ if row:
+ # Skip changes within the cursor's column.
+ win = self.window
+ with self.data.lock:
+ time = row.segments().next_change(
+ win.cursor + win.span() / self.timeline_width() / 2)
+ if time is not None:
+ self.set_window(win.with_cursor(time))
+
+ def action_prev_change(self) -> None:
+ """Move the cursor to the previous change in the selected row."""
+ row = self.selected_row()
+ if row:
+ win = self.window
+ with self.data.lock:
+ time = row.segments().prev_change(
+ win.cursor - win.span() / self.timeline_width() / 2)
+ if time is not None:
+ self.set_window(win.with_cursor(time))
+
+
+class TimechartApp(App):
+ """A Textual application to display a timechart."""
+ TITLE = "perf timechart"
+
+ BINDINGS = [
+ Binding("s", "sort", "Sort tasks", tooltip="Cycle task sort order"),
+ Binding("w", "goto_waker", "Go to waker",
+ tooltip="Select the task that last woke the selected task"),
+ Binding(key="^q", action="quit", description="Quit", tooltip="Quit the app"),
+ ]
+
+ CSS = """
+ TabbedContent, TabbedContent > ContentSwitcher, TabPane {
+ height: 1fr;
+ }
+ TabPane {
+ padding: 0;
+ }
+ .legend {
+ height: 1;
+ padding: 0 1;
+ }
+ #details {
+ height: 8;
+ border: round $primary;
+ padding: 0 1;
+ }
+ """
+
+ SORT_ORDERS = ["run time", "tid", "name", "first run"]
+
+ # The visible time range and cursor, bound to each TimelineView's window.
+ window: reactive[TimeWindow] = reactive(TimeWindow.whole(0, 1), init=False)
+ # Index into SORT_ORDERS for the tasks view.
+ sort_order: reactive[int] = reactive(0, init=False)
+
+ def __init__(self, input_name: str, power_only: bool, tasks_only: bool,
+ filters: Sequence[str], data: Optional[TimechartData] = None) -> None:
+ """Create the app, if data isn't given it is loaded from input_name.
+
+ While the data is loading the views show what has been read so far.
+ """
+ super().__init__()
+ self.input_name = input_name
+ self.power_only = power_only
+ self.tasks_only = tasks_only
+ self.filters = filters
+ # The data being displayed, possibly still being loaded.
+ self.data = data if data else TimechartData()
+ self.loaded = data is not None
+ # The data being loaded in a background thread.
+ self.loading: Optional[TimechartData] = None
+ self.tasks: List[Task] = []
+ self.io_tasks: List[Task] = []
+ # Rows keyed by type and CPU or tid. They are reused as the data loads
+ # so that the views can keep the selected row.
+ self.row_cache: Dict[Tuple[str, int], Row] = {}
+ self.io_tab_shown = True
+ # Does the summary table need updating before it is next shown?
+ self.summary_stale = True
+ self.theme_colors = ThemeColors(self.theme_variables)
+
+ def cached_row(self, cls: Callable[[TimechartData, Any], Row], num: int, obj: Any) -> Row:
+ """Find or create the row of type cls for the CPU or task obj numbered num."""
+ key = (getattr(cls, "__name__", ""), num)
+ row = self.row_cache.get(key)
+ if row is None:
+ row = cls(self.data, obj)
+ self.row_cache[key] = row
+ return row
+
+ def cpu_rows(self) -> List[Row]:
+ """Rows for the CPUs tab, the data's lock must be held."""
+ rows: List[Row] = []
+ for cpu in sorted(self.data.cpus.values(), key=lambda c: c.cpu):
+ if len(cpu.run) or len(cpu.cstate) or len(cpu.pstate):
+ rows.append(self.cached_row(CpuRow, cpu.cpu, cpu))
+ if len(cpu.cstate):
+ rows.append(self.cached_row(CStateRow, cpu.cpu, cpu))
+ if len(cpu.pstate):
+ rows.append(self.cached_row(FreqRow, cpu.cpu, cpu))
+ return rows
+
+ def task_rows(self) -> List[Row]:
+ """Rows for the tasks tab in the current sort order, the data's lock must be held."""
+ order = self.SORT_ORDERS[self.sort_order]
+ if order == "run time":
+ tasks = sorted(self.tasks, key=lambda t: -t.totals[STATE_RUNNING])
+ elif order == "name":
+ tasks = sorted(self.tasks, key=lambda t: (t.comm, t.tid))
+ elif order == "first run":
+ tasks = sorted(self.tasks, key=lambda t: t.segs.starts[0])
+ else:
+ tasks = self.tasks
+ return [self.cached_row(TaskRow, t.tid, t) for t in tasks]
+
+ def compose(self) -> ComposeResult:
+ """Composes the user interface of the application."""
+ yield Header()
+ with TabbedContent():
+ if not self.tasks_only:
+ with TabPane("CPUs", id="cpus"):
+ yield Static(id="cpus_legend", classes="legend")
+ yield TimelineView(self.data, [],
+ id="cpus_view").data_bind(TimechartApp.window)
+ if not self.power_only:
+ with TabPane("Tasks", id="tasks"):
+ yield Static(id="tasks_legend", classes="legend")
+ yield TimelineView(self.data, [],
+ id="tasks_view").data_bind(TimechartApp.window)
+ # Hidden until there are tasks doing I/O.
+ with TabPane("I/O", id="io"):
+ yield Static(id="io_legend", classes="legend")
+ yield TimelineView(self.data, [],
+ id="io_view").data_bind(TimechartApp.window)
+ with TabPane("Summary", id="summary"):
+ yield DataTable(id="summary_table", cursor_type="row")
+ yield Static(id="details")
+ yield Footer()
+
+ def on_mount(self) -> None:
+ """Show the data, loading it in the background if it wasn't given."""
+ self.theme_changed_signal.subscribe(self, self.on_theme_changed)
+ self.update_legends()
+ self.update_views()
+ view = self.active_view()
+ if view:
+ view.focus()
+ if self.loaded:
+ self.finish_loading()
+ else:
+ self.sub_title = f"Loading {self.input_name}"
+ self.loading = self.data
+ self.load_events()
+
+ def on_theme_changed(self, _theme: Any) -> None:
+ """Update colors, legends and timeline views when the theme changes."""
+ self.theme_colors = ThemeColors(self.theme_variables)
+ self.update_legends()
+ for view in self.query(TimelineView):
+ view.refresh()
+
+ def update_legends(self) -> None:
+ """Update the legend strings using the active theme's colors."""
+ c = self.theme_colors
+ with self.data.lock:
+ cpus_legend = self.cpu_legend()
+ for static in self.query("#cpus_legend").results(Static):
+ static.update(cpus_legend)
+ tasks_legend = (f"[{c.running}]█[/] running "
+ f"[on {c.waiting}] [/] runnable "
+ f"[on {c.blocked}] [/] blocked "
+ "(bar height is the fraction of time running)")
+ for static in self.query("#tasks_legend").results(Static):
+ static.update(tasks_legend)
+ io_legend = (" ".join(f"[{c.io[t]}]█[/] {IOTYPE_NAMES[t]}"
+ for t in range(NUM_IOTYPES)) +
+ " (underline is an error)")
+ for static in self.query("#io_legend").results(Static):
+ static.update(io_legend)
+
+ @work(thread=True, exclusive=True)
+ def load_events(self) -> None:
+ """Read the perf.data file in a thread so the UI stays responsive."""
+ data = self.loading or self.data
+ self.loading = data
+
+ def progress() -> None:
+ try:
+ # Blocks until the UI has shown the data read so far.
+ self.call_from_thread(self.update_progress, data)
+ except RuntimeError:
+ # The app is no longer running.
+ data.cancelled = True
+
+ data.progress = progress
+ data.start_progress = data.last_progress = monotonic()
+ try:
+ read_events(data, self.input_name)
+ except LoadCancelled:
+ return
+ except (OSError, ValueError, RuntimeError) as e:
+ self.call_from_thread(self.exit, None, 1,
+ f"Error processing {self.input_name}: {e}")
+ return
+ finally:
+ data.progress = None
+ self.loading = None
+ if data.cancelled:
+ return
+ if not data.has_events():
+ self.call_from_thread(self.exit, None, 1,
+ f"Error: no scheduler, power or I/O events found in "
+ f"{self.input_name}.\n"
+ "Record them with 'perf timechart record'.")
+ return
+ self.call_from_thread(self.data_loaded)
+
+ def data_loaded(self) -> None:
+ """Called on the UI thread when loading completes."""
+ self.loaded = True
+ self.finish_loading()
+
+ def update_progress(self, data: TimechartData) -> None:
+ """Show how much of the file has been processed and the data so far."""
+ self.sub_title = (f"Loading {self.input_name}: {data.nr_samples:,} samples, "
+ f"{fmt_duration(data.last_time - data.first_time)} of trace")
+ self.update_views()
+
+ def finish_loading(self) -> None:
+ """Show all of the data and a summary of it."""
+ self.update_views()
+ views = self.query("#cpus_view")
+ if views and not views.first(TimelineView).rows:
+ self.query_one(TabbedContent).hide_tab("cpus")
+ task_views = self.query("#tasks_view")
+ if task_views and not task_views.first(TimelineView).rows and self.io_tasks:
+ self.query_one(TabbedContent).hide_tab("tasks")
+ nr_tasks = len(self.tasks) or len(self.io_tasks)
+ self.sub_title = (f"{self.input_name}: "
+ f"{fmt_duration(self.data.last_time - self.data.first_time)}, "
+ f"{len(self.data.cpus)} CPUs, {nr_tasks} tasks")
+
+ def cancel_loading(self) -> None:
+ """Stop a background load, the worker notices at the next progress interval."""
+ loading = self.loading
+ if loading is not None:
+ loading.cancelled = True
+
+ async def action_quit(self) -> None:
+ """Quit, stopping any background load."""
+ self.cancel_loading()
+ await super().action_quit()
+
+ def on_unmount(self) -> None:
+ """Stop any background load when the app exits."""
+ self.cancel_loading()
+
+ def cpu_legend(self) -> str:
+ """Legend for the CPUs tab, saying which power events are missing.
+
+ The data's lock must be held.
+ """
+ c = self.theme_colors
+ data = self.data
+ has_pstate = any(len(cpu.pstate) for cpu in data.cpus.values())
+ has_cstate = any(len(cpu.cstate) for cpu in data.cpus.values())
+ parts = [f"[{c.running}]█[/] busy"]
+ if has_pstate and data.max_freq > data.min_freq:
+ parts.append(f"[{c.freq_low}]█[/]→[{c.freq_high}]█[/] frequency")
+ elif has_pstate:
+ parts.append(f"frequency constant at {data.max_freq / 1000:.0f} MHz")
+ if has_cstate:
+ parts.append(f"[{c.idle_light}]█[/]→[{c.idle_dark}]█[/] idle state")
+ missing = []
+ if not has_pstate:
+ missing.append("power:cpu_frequency")
+ if not has_cstate:
+ missing.append("power:cpu_idle")
+ if missing:
+ parts.append(f"[dim](no {' or '.join(missing)} events)[/dim]")
+ return " ".join(parts)
+
+ def update_views(self) -> None:
+ """Update the views from the data, which may still be loading."""
+ data = self.data
+ with data.lock:
+ first, last = data.first_time, data.last_time
+ self.tasks = data.sched_tasks(self.filters)
+ self.io_tasks = data.io_tasks(self.filters)
+ all_rows = {
+ "cpus_view": self.cpu_rows(),
+ "tasks_view": self.task_rows(),
+ "io_view": [self.cached_row(IoRow, t.tid, t) for t in self.io_tasks],
+ }
+ legend = self.cpu_legend()
+ for view_id, rows in all_rows.items():
+ for view in self.query(f"#{view_id}").results(TimelineView):
+ view.set_rows(rows)
+ for static in self.query("#cpus_legend").results(Static):
+ static.update(legend)
+ if self.query("#io") and bool(self.io_tasks) != self.io_tab_shown:
+ self.io_tab_shown = bool(self.io_tasks)
+ tabbed = self.query_one(TabbedContent)
+ if self.io_tab_shown:
+ tabbed.show_tab("io")
+ else:
+ tabbed.hide_tab("io")
+ if first or last:
+ self.window = self.window.extend(first, last)
+ self.summary_stale = True
+ tabs = self.query(TabbedContent)
+ if tabs and tabs.first(TabbedContent).active == "summary":
+ self.update_summary()
+ self.update_details()
+
+ def update_summary(self) -> None:
+ """Fill in the summary table keeping the selected task."""
+ tables = self.query("#summary_table")
+ if not tables:
+ return
+ self.summary_stale = False
+ table = tables.first(DataTable)
+ if not table.columns:
+ table.add_columns("Task", "TID", "Running", "Waiting", "Blocked",
+ "Switches", "Wakeups", "I/O bytes")
+ selected = None
+ if table.row_count:
+ try:
+ selected = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
+ except CellDoesNotExist:
+ pass
+ summary_tasks = self.tasks if self.tasks else self.io_tasks
+ with self.data.lock:
+ rows = [(task.comm, task.tid,
+ fmt_duration(task.totals[STATE_RUNNING]),
+ fmt_duration(task.totals[STATE_WAITING]),
+ fmt_duration(task.totals[STATE_BLOCKED]),
+ task.switches, len(task.wakeups), task.io_bytes)
+ for task in sorted(summary_tasks,
+ key=lambda t: (-t.totals[STATE_RUNNING], -t.io_bytes))]
+ table.clear()
+ for row in rows:
+ table.add_row(*row, key=str(row[1]))
+ if selected is not None:
+ try:
+ table.move_cursor(row=table.get_row_index(selected), animate=False)
+ except RowDoesNotExist:
+ pass
+
+ def active_view(self) -> Optional[TimelineView]:
+ """The timeline view in the active tab, if any."""
+ tabs = self.query(TabbedContent)
+ if not tabs:
+ return None
+ active = tabs.first(TabbedContent).active
+ views = self.query(f"#{active}_view") if active else None
+ return views.first(TimelineView) if views else None
+
+ def watch_window(self) -> None:
+ """The bound views repaint themselves, update the details."""
+ self.update_details()
+
+ @on(TimelineView.SelectionChanged)
+ def on_selection_changed(self) -> None:
+ """Describe the newly selected row."""
+ self.update_details()
+
+ def update_details(self) -> None:
+ """Describe the selected row at the cursor."""
+ details = self.query("#details")
+ if not details:
+ return
+ win = self.window
+ text = (f"Cursor [b]{self.data.fmt_time(win.cursor)}[/b] "
+ f"View {self.data.fmt_time(win.start)} - {self.data.fmt_time(win.end)} "
+ f"({fmt_duration(win.span())})\n")
+ view = self.active_view()
+ row = view.selected_row() if view else None
+ if row:
+ with self.data.lock:
+ text += row.describe(win.cursor, win.start, win.end)
+ elif view is None:
+ text += "Select a task and press enter to show it in the Tasks timeline"
+ details.first(Static).update(text)
+
+ @on(TabbedContent.TabActivated)
+ def on_tab_activated(self) -> None:
+ """Focus the view in the newly active tab."""
+ view = self.active_view()
+ if view:
+ view.focus()
+ else:
+ if self.summary_stale:
+ self.update_summary()
+ tables = self.query("#summary_table")
+ if tables:
+ tables.first(DataTable).focus()
+ self.update_details()
+
+ @on(DataTable.RowSelected)
+ def on_row_selected(self, event: DataTable.RowSelected) -> None:
+ """Show the selected summary task in the tasks timeline."""
+ if event.row_key.value is not None:
+ self.goto_task(int(event.row_key.value), None)
+
+ def goto_task(self, tid: int, time: Optional[float]) -> None:
+ """Select the task in the tasks or I/O view, optionally moving the cursor."""
+ for tab_id, view_id in (("tasks", "#tasks_view"), ("io", "#io_view")):
+ views = self.query(view_id)
+ if not views:
+ continue
+ view = views.first(TimelineView)
+ for idx, row in enumerate(view.rows):
+ if isinstance(row, (TaskRow, IoRow)) and row.task.tid == tid:
+ self.query_one(TabbedContent).active = tab_id
+ if time is not None:
+ self.window = self.window.with_cursor(time)
+ view.selected = idx
+ view.focus()
+ return
+ self.notify(f"Task {tid} isn't shown in the tasks view", severity="warning")
+
+ def action_sort(self) -> None:
+ """Cycle the sort order of the tasks view."""
+ self.sort_order = (self.sort_order + 1) % len(self.SORT_ORDERS)
+
+ def watch_sort_order(self) -> None:
+ """Re-sort the tasks view."""
+ views = self.query("#tasks_view")
+ if not views:
+ return
+ with self.data.lock:
+ rows = self.task_rows()
+ views.first(TimelineView).set_rows(rows)
+ self.notify(f"Tasks sorted by {self.SORT_ORDERS[self.sort_order]}")
+ self.update_details()
+
+ def action_goto_waker(self) -> None:
+ """Select the task that woke the selected task before the cursor."""
+ view = self.active_view()
+ row = view.selected_row() if view else None
+ if not isinstance(row, (TaskRow, IoRow)):
+ return
+ with self.data.lock:
+ wakeups = row.task.wakeups
+ idx = bisect.bisect_right(wakeups, (self.window.cursor, sys.maxsize)) - 1
+ wake_time, waker = wakeups[idx] if idx >= 0 else (0, 0)
+ if idx < 0:
+ self.notify("No wakeup before the cursor", severity="warning")
+ return
+ if waker <= 0:
+ self.notify(f"Woken by {self.data.task_name(waker)} at "
+ f"{self.data.fmt_time(wake_time)}")
+ return
+ self.goto_task(waker, wake_time)
+
+
+def main() -> None:
+ """Parse arguments, read the perf.data file and run the app."""
+ parser = argparse.ArgumentParser(
+ description="Interactive timechart of CPU, task and I/O activity.")
+ parser.add_argument("-i", "--input", default="perf.data", help="input perf.data file")
+ parser.add_argument("-P", "--power-only", action="store_true",
+ help="only show CPU power information")
+ parser.add_argument("-T", "--tasks-only", action="store_true",
+ help="only show task information")
+ parser.add_argument("-p", "--process", action="append", default=[],
+ help="only show processes with the given name or PID, may be repeated")
+ parser.add_argument("--dump", action="store_true",
+ help="print a text summary rather than running the interactive UI")
+ args = parser.parse_args()
+
+ if args.power_only and args.tasks_only:
+ print("Error: -P and -T options cannot be used at the same time.", file=sys.stderr)
+ sys.exit(1)
+
+ if args.input == "-":
+ if not args.dump:
+ # The interactive UI reads the keyboard from stdin.
+ print("Error: reading perf.data from stdin requires --dump.", file=sys.stderr)
+ sys.exit(1)
+ elif not os.path.exists(args.input):
+ print(f"Error: {args.input} not found. (try 'perf timechart record' first)",
+ file=sys.stderr)
+ sys.exit(1)
+
+ if args.dump:
+ data = load_data(args.input)
+ data.dump(args.power_only, args.tasks_only, args.process)
+ return
+
+ # The app starts immediately and loads the data in the background.
+ app = TimechartApp(args.input, args.power_only, args.tasks_only, args.process)
+ app.run()
+ sys.exit(app.return_code or 0)
+
+
+def read_events(data: TimechartData, input_name: str) -> None:
+ """Process the events in input_name into data, raising on errors."""
+ if data.cancelled:
+ raise LoadCancelled()
+ try:
+ data.session = perf.session(perf.data(input_name), sample=data.process_event)
+ data.session.process_events()
+ data.finish()
+ finally:
+ # Break the reference cycle between the session and the callback.
+ data.session = None
+
+
+def load_data(input_name: str) -> TimechartData:
+ """Read the perf.data file exiting on errors or if there's nothing to show."""
+ if input_name != "-" and not os.path.exists(input_name):
+ print(f"Error: {input_name} not found. (try 'perf timechart record' first)",
+ file=sys.stderr)
+ sys.exit(1)
+
+ data = TimechartData()
+ try:
+ read_events(data, input_name)
+ except (OSError, ValueError, RuntimeError) as e:
+ print(f"Error processing {input_name}: {e}", file=sys.stderr)
+ sys.exit(1)
+ except KeyboardInterrupt:
+ data.finish()
+
+ if not data.has_events():
+ print(f"Error: no scheduler, power or I/O events found in {input_name}.\n"
+ "Record them with 'perf timechart record'.", file=sys.stderr)
+ sys.exit(1)
+ return data
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/perf/python/twatch.py b/tools/perf/python/twatch.py
index 04f3db29b9bc..b0f1bf113162 100755
--- a/tools/perf/python/twatch.py
+++ b/tools/perf/python/twatch.py
@@ -1,5 +1,6 @@
#! /usr/bin/env python
# SPDX-License-Identifier: GPL-2.0-only
+"""Example to show how to enable a software event and track context switches."""
# -*- python -*-
# -*- coding: utf-8 -*-
# twatch - Experimental use of the perf python interface
@@ -9,53 +10,51 @@
import perf
def main(context_switch = 0, thread = -1):
- cpus = perf.cpu_map()
- threads = perf.thread_map(thread)
- evsel = perf.evsel(type = perf.TYPE_SOFTWARE,
- config = perf.COUNT_SW_DUMMY,
- task = 1, comm = 1, mmap = 0, freq = 0,
- wakeup_events = 1, watermark = 1,
- sample_id_all = 1, context_switch = context_switch,
- sample_type = perf.SAMPLE_PERIOD | perf.SAMPLE_TID | perf.SAMPLE_CPU)
-
- """What we want are just the PERF_RECORD_ lifetime events for threads,
- using the default, PERF_TYPE_HARDWARE + PERF_COUNT_HW_CYCLES & freq=1
- (the default), makes perf reenable irq_vectors:local_timer_entry, when
- disabling nohz, not good for some use cases where all we want is to get
- threads comes and goes... So use (perf.TYPE_SOFTWARE, perf_COUNT_SW_DUMMY,
- freq=0) instead."""
-
- evsel.open(cpus = cpus, threads = threads);
- evlist = perf.evlist(cpus, threads)
- evlist.add(evsel)
- evlist.mmap()
- while True:
- evlist.poll(timeout = -1)
- for cpu in cpus:
- event = evlist.read_on_cpu(cpu)
- if not event:
- continue
- print("cpu: {0}, pid: {1}, tid: {2} {3}".format(event.sample_cpu,
+ cpus = perf.cpu_map()
+ threads = perf.thread_map(thread)
+ evsel = perf.evsel(type = perf.TYPE_SOFTWARE,
+ config = perf.COUNT_SW_DUMMY,
+ task = 1, comm = 1, mmap = 0, freq = 0,
+ wakeup_events = 1, watermark = 1,
+ sample_id_all = 1, context_switch = context_switch,
+ sample_type = perf.SAMPLE_PERIOD | perf.SAMPLE_TID | perf.SAMPLE_CPU)
+
+ # What we want are just the PERF_RECORD_ lifetime events for threads,
+ # using the default, PERF_TYPE_HARDWARE + PERF_COUNT_HW_CYCLES & freq=1
+ # (the default), makes perf reenable irq_vectors:local_timer_entry, when
+ # disabling nohz, not good for some use cases where all we want is to get
+ # threads comes and goes... So use (perf.TYPE_SOFTWARE, perf_COUNT_SW_DUMMY,
+ # freq=0) instead.
+
+ evsel.open(cpus = cpus, threads = threads)
+ evlist = perf.evlist(cpus, threads)
+ evlist.add(evsel)
+ evlist.mmap()
+ while True:
+ evlist.poll(timeout = -1)
+ for cpu in cpus:
+ event = evlist.read_on_cpu(cpu)
+ if not event:
+ continue
+ print("cpu: {0}, pid: {1}, tid: {2} {3}".format(event.sample_cpu,
event.sample_pid,
event.sample_tid,
event))
if __name__ == '__main__':
- """
- To test the PERF_RECORD_SWITCH record, pick a pid and replace
- in the following line.
+ # To test the PERF_RECORD_SWITCH record, pick a pid and replace
+ # in the following line.
- Example output:
+ # Example output:
-cpu: 3, pid: 31463, tid: 31593 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31593, switch_out: 1 }
-cpu: 1, pid: 31463, tid: 31489 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31489, switch_out: 1 }
-cpu: 2, pid: 31463, tid: 31496 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31496, switch_out: 1 }
-cpu: 3, pid: 31463, tid: 31491 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31491, switch_out: 0 }
+ # cpu: 3, pid: 31463, tid: 31593 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31593, switch_out: 1 }
+ # cpu: 1, pid: 31463, tid: 31489 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31489, switch_out: 1 }
+ # cpu: 2, pid: 31463, tid: 31496 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31496, switch_out: 1 }
+ # cpu: 3, pid: 31463, tid: 31491 { type: context_switch, next_prev_pid: 31463, next_prev_tid: 31491, switch_out: 0 }
+ #
+ # It is possible as well to use event.misc & perf.PERF_RECORD_MISC_SWITCH_OUT
+ # to figure out if this is a context switch in or out of the monitored threads.
- It is possible as well to use event.misc & perf.PERF_RECORD_MISC_SWITCH_OUT
- to figure out if this is a context switch in or out of the monitored threads.
-
- If bored, please add command line option parsing support for these options :-)
- """
- # main(context_switch = 1, thread = 31463)
+ # If bored, please add command line option parsing support for these options :-)
+ # # main(context_switch = 1, thread = 31463)
main()
diff --git a/tools/perf/python/wakeup-latency.py b/tools/perf/python/wakeup-latency.py
new file mode 100755
index 000000000000..f77045d02734
--- /dev/null
+++ b/tools/perf/python/wakeup-latency.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+"""Display avg/min/max wakeup latency."""
+from __future__ import annotations
+
+import argparse
+from collections import defaultdict
+import sys
+from typing import Optional, Dict
+import perf
+
+class WakeupLatency:
+ """Tracks and displays wakeup latency statistics."""
+ def __init__(self) -> None:
+ self.last_wakeup: Dict[int, int] = defaultdict(int)
+ self.max_wakeup_latency: int = 0
+ self.min_wakeup_latency: Optional[int] = None
+ self.total_wakeup_latency = 0
+ self.total_wakeups = 0
+ self.unhandled: Dict[str, int] = defaultdict(int)
+ self.session: Optional[perf.session] = None
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process events."""
+ event_name = str(sample.evsel)
+ if event_name.startswith("evsel(") and event_name.endswith(")"):
+ event_name = event_name[6:-1]
+ sample_time = sample.sample_time
+
+ if "sched:sched_wakeup" in event_name:
+ try:
+ pid = sample.pid
+ self.last_wakeup[pid] = sample_time
+ except AttributeError:
+ self.unhandled[event_name] += 1
+ elif "sched:sched_switch" in event_name:
+ try:
+ next_pid = sample.next_pid
+ wakeup_ts = self.last_wakeup.get(next_pid, 0)
+ if wakeup_ts:
+ latency = sample_time - wakeup_ts
+ self.max_wakeup_latency = max(self.max_wakeup_latency, latency)
+ if self.min_wakeup_latency is None:
+ self.min_wakeup_latency = latency
+ else:
+ self.min_wakeup_latency = min(self.min_wakeup_latency, latency)
+ self.total_wakeup_latency += latency
+ self.total_wakeups += 1
+ del self.last_wakeup[next_pid]
+ except AttributeError:
+ self.unhandled[event_name] += 1
+ else:
+ self.unhandled[event_name] += 1
+
+ def print_totals(self) -> None:
+ """Print summary statistics."""
+ print("wakeup_latency stats:\n")
+ print(f"total_wakeups: {self.total_wakeups}")
+ if self.total_wakeups:
+ avg = self.total_wakeup_latency // self.total_wakeups
+ print(f"avg_wakeup_latency (ns): {avg}")
+ print(f"min_wakeup_latency (ns): {self.min_wakeup_latency}")
+ print(f"max_wakeup_latency (ns): {self.max_wakeup_latency}")
+ else:
+ print("avg_wakeup_latency (ns): N/A")
+ print("min_wakeup_latency (ns): N/A")
+ print("max_wakeup_latency (ns): N/A")
+
+ if self.unhandled:
+ print("\nunhandled events:\n")
+ print(f"{'event':<40s} {'count':>10s}")
+ print(f"{'-'*40} {'-'*10}")
+ for event_name, count in self.unhandled.items():
+ print(f"{event_name:<40s} {count:10d}")
+
+ def run(self, input_file: str) -> None:
+ """Run the session."""
+ self.session = perf.session(perf.data(input_file), sample=self.process_event)
+ try:
+ self.session.process_events()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ # Break the reference cycle between self.session and the bound
+ # self.process_event callback so the C perf.session object is freed.
+ self.session = None
+ self.print_totals()
+
+def main() -> None:
+ """Main function."""
+ parser = argparse.ArgumentParser(description="Trace wakeup latency")
+ parser.add_argument("-i", "--input", default="perf.data", help="Input file")
+ args = parser.parse_args()
+
+ analyzer = WakeupLatency()
+ try:
+ analyzer.run(args.input)
+ except IOError as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()