perf python: Port event_analyzing_sample to perf module

Port event_analyzing_sample.py to a standalone script in
tools/perf/python/ using the perf module and standard library sqlite3
module.

Improvements compared to the legacy script:
- Encapsulate database state in a _DB container instead of mutating
  module-level globals, and ensure temporary SQLite database files are
  cleaned up on exit.
- Add argparse CLI options (-i/--input and -d/--db) while preserving
  PerfEvent, PebsEvent, and PebsNHM binary raw_buf unpacking and
  symbol/DSO histogram reporting.
- Remove Python 2 compatibility code and add type annotations.

Add a shell test (test_event_analyzing_sample_python.sh) to verify the
standalone script.

Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Ian Rogers <irogers@google.com>
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
This commit is contained in:
Ian Rogers 2026-09-25 23:19:48 -07:00 committed by Arnaldo Carvalho de Melo
parent e1e4bd6f8b
commit eeb70645a8
2 changed files with 394 additions and 0 deletions

View File

@ -0,0 +1,332 @@
#!/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 os
import sqlite3
import struct
import tempfile
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
temp_path: str | None = None
def trace_begin(db_path: str | None = None) -> None:
"""Initialize database tables."""
print("In trace_begin:\n")
if not db_path:
fd, db_path = tempfile.mkstemp(prefix="perf_events_", suffix=".db")
os.close(fd)
_DB.temp_path = db_path
con = sqlite3.connect(db_path)
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")
try:
if _DB.con:
try:
_DB.con.commit()
show_general_events()
show_pebs_ll()
finally:
_DB.con.close()
_DB.con = None
finally:
if _DB.temp_path and os.path.exists(_DB.temp_path):
try:
os.remove(_DB.temp_path)
except OSError:
pass
_DB.temp_path = 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 a temporary file cleaned up on exit)")
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()

View File

@ -0,0 +1,62 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# event_analyzing_sample python test
set -e
shelldir=$(dirname "$0")
# shellcheck source=lib/setup_python.sh
. "${shelldir}"/lib/setup_python.sh
# If we don't have the perf python module, we can't test
if ! "$PYTHON" -c 'import perf' > /dev/null 2>&1; then
echo "Skipping test, perf python module not found"
exit 2
fi
script_dir="$(dirname "$0")/../../python"
script_path="${script_dir}/event_analyzing_sample.py"
if [ ! -f "$script_path" ]; then
echo "Skipping test, event_analyzing_sample.py not found at $script_path"
exit 2
fi
err=0
temp_dir=""
cleanup() {
rm -rf "${temp_dir}"
}
trap 'cleanup' EXIT TERM INT
temp_dir=$(mktemp -d /tmp/perf.event_analyzing.XXXXXX)
temp_data="${temp_dir}/perf.data"
temp_db="${temp_dir}/perf.db"
test_file_mode() {
echo "Testing event_analyzing_sample.py..."
# Generate some events
if ! perf record -o "${temp_data}" -- perf test -w noploop >/dev/null 2>&1; then
echo "Skipping test, perf record failed"
exit 2
fi
# Run the script
if ! "$PYTHON" "$script_path" -i "${temp_data}" -d "${temp_db}" > "${temp_dir}/perf.out" 2>&1; then
echo "File mode test failed."
err=1
elif ! grep -q "Statistics about the general events" "${temp_dir}/perf.out" || \
grep -q "Error creating/inserting event" "${temp_dir}/perf.out"; then
echo "Event analysis output validation failed."
err=1
else
echo "File mode test passed."
fi
}
test_file_mode
exit $err