From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from one.firstfloor.org (one.firstfloor.org [65.21.254.221]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6B2D0521215; Mon, 31 Aug 2026 15:07:16 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=65.21.254.221 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788188839; cv=none; b=cB3Ie0Hyav/1MYs5xRQ1ME9QOF4M7Vz6EHNzT5eUlf2xQcsex1Dh+6tPb8sX7sm5xAcVYNwvqPxhqFBbADPJccvAz22cOUuepaXT/8A4UPa66JKGxumeCK8L8fJeUZtieMvUmWXmVoAE4hx2y8ahuvQbs6qaPWC21BKO3GkkgCk= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788188839; c=relaxed/simple; bh=7eeRvEZ02mBYd+sFixqSgRe8hkrKOm3sjEc8EDKR7Kc=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=e6WJxVkUzLT+MSL+TyGu3UQllXfIUPUBkOyqmS584hstbAngJ6Sw01+ut7oaIO1E1Ibdg+bDJStFcCuDI8uWq/Ru4r8wdHIsPhkAtCyGDcms4Tv00RKdv5leg4/9GtxRuQRUDj2+VjHf82VuVylfg2koodCDStiw+wbEx50IfK8= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=kernel.org; spf=pass smtp.mailfrom=firstfloor.org; arc=none smtp.client-ip=65.21.254.221 Authentication-Results: smtp.subspace.kernel.org; dmarc=fail (p=quarantine dis=none) header.from=kernel.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=firstfloor.org Received: from firstfloor.org (c-73-11-123-161.hsd1.or.comcast.net [73.11.123.161]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by one.firstfloor.org (Postfix) with ESMTPSA id 31F9D63F9B; Mon, 31 Aug 2026 17:07:11 +0200 (CEST) Received: by firstfloor.org (Postfix, from userid 1000) id 361E11622BA; Mon, 31 Aug 2026 08:07:03 -0700 (PDT) From: Andi Kleen To: linux-kernel@vger.kernel.org Cc: mhiramat@kernel.org, oleg@redhat.com, peterz@infradead.org, tglx@kernel.org, x86@kernel.org, jolsa@kernel.org, linux-perf-users@vger.kernel.org, adrian.hunter@intel.com, Andi Kleen Subject: [RFC v1 18/19] ptwrite uprobes / perf tools script: Add ptwrite uprobes decoder Date: Mon, 31 Aug 2026 08:04:54 -0700 Message-ID: <20260831150651.1134594-19-ak@kernel.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260831150651.1134594-1-ak@kernel.org> References: <20260831150651.1134594-1-ak@kernel.org> Precedence: bulk X-Mailing-List: linux-perf-users@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Add a decoder for the PTWRITE records generated by ptwrite uprobes. This runs as a python script in perf script. The main tricky part is how to detect and recover data loss or interleaving with other ptwrites. The decoder uses a magic value in the header to synchronize, and also the IP from the FUP when available (fup_on_ptw=1) It can display other events (including branches) when they are available. Assisted-by: omp:gpt-5.6-luna Signed-off-by: Andi Kleen --- .../scripts/python/uprobe-ptwrite-decode.py | 416 ++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100755 tools/perf/scripts/python/uprobe-ptwrite-decode.py diff --git a/tools/perf/scripts/python/uprobe-ptwrite-decode.py b/tools/perf/scripts/python/uprobe-ptwrite-decode.py new file mode 100755 index 000000000000..22f21cb168e1 --- /dev/null +++ b/tools/perf/scripts/python/uprobe-ptwrite-decode.py @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +""" +uprobe-ptwrite-decode.py - decode ptwrite-uprobe trace events out of +PT logs. + +Usage (perf script): + perf script --itrace=qwe -s uprobe-ptwrite-decode.py -i perf.data +""" +import os +import re +import struct +import sys + +TRACEFS = "/sys/kernel/tracing" + +# type name -> (size bytes, signed, hex, char) +_TYPES = { + "u8": (1, False, False, False), + "u16": (2, False, False, False), + "u32": (4, False, False, False), + "u64": (8, False, False, False), + "s8": (1, True, False, False), + "s16": (2, True, False, False), + "s32": (4, True, False, False), + "s64": (8, True, False, False), + "x8": (1, False, True, False), + "x16": (2, False, True, False), + "x32": (4, False, True, False), + "x64": (8, False, True, False), + "char": (1, False, False, True), +} + +_FIELD_RE = re.compile(r'^\s*field:(\S+)\s+(arg\d+);') + +HDR_MAGIC = 0x5054525731 # "PTRW1" +HDR_MASK = (1 << 40) - 1 + +def load_events(root=TRACEFS): + """Scan tracefs for (event_id -> (name, [(arg name, type)])).""" + events = {} + try: + groups = os.listdir(root + "/events") + except OSError: + return events + for g in groups: + gdir = root + "/events/" + g + if not os.path.isdir(gdir): + continue + for ev in os.listdir(gdir): + edir = gdir + "/" + ev + if not os.path.isdir(edir): + continue + try: + with open(edir + "/id") as f: + eid = int(f.read().strip()) + with open(edir + "/format") as f: + fields = [] + for line in f: + m = _FIELD_RE.match(line) + if m: + fields.append((m.group(2), m.group(1))) + events[eid] = (g + "/" + ev, fields) + except (OSError, ValueError): + continue + return events + +def type_info(t): + """('u64', size, signed, hex, char) for a format-file type name.""" + if t in _TYPES: + return (t,) + _TYPES[t] + return (t, 8, False, False, False) + +def fmt_value(v, t): + name, size, signed, ishex, ischar = type_info(t) + mask = (1 << (8 * size)) - 1 + v &= mask + if ischar: + return repr(chr(v)) + if signed: + sign = 1 << (8 * size - 1) + if v & sign: + v -= 1 << (8 * size) + if ishex: + return "0x%x" % v + return str(v) + +def is_header(word, events): + if (word & HDR_MASK) != HDR_MAGIC: + return None + eid = (word >> 48) & 0xffff + nargs = (word >> 40) & 0xff + if not (1 <= nargs <= 8): + return None + if events and eid not in events: + return None + return (eid, nargs) + +def decode_words(words, events): + """Walk the ptwrite stream.""" + records = dropped = stray = unknown = 0 + cur = None # (event_id, nargs, [args]) + rec_drop = False + learned = {} + arg_off = {} + lines = [] + for w in words: + if isinstance(w, tuple): + word, ip, key = (w + (0, 0))[:3] if len(w) < 3 else (w[0], w[1], w[2]) + else: + word, ip, key = w, 0, 0 + off = (ip & 0xfff) if ip else 0 + hdr = is_header(word, events) + if hdr is not None: + if cur is not None: + dropped += cur[1] - len(cur[2]) + cur = (hdr[0], hdr[1], []) + rec_drop = False + continue + if cur is None: + # no record open: this is a raw ptwrite from the program + lines.append((key, "manual ptwrite: payload=0x%x ip=0x%x" + % (word, ip))) + stray += 1 + continue + eid, nargs, args = cur + if len(args) >= nargs: + cur = None + lines.append((key, "manual ptwrite: payload=0x%x ip=0x%x" + % (word, ip))) + stray += 1 + continue + if ip and learned.get(eid): + offs = arg_off[eid] + expect = offs[len(args)] + if off != expect: + j = next((k for k in range(len(args) + 1, nargs) + if off == offs[k]), None) + if j is not None: + dropped += j - len(args) + args.extend([None] * (j - len(args))) + rec_drop = True + else: + unknown += 1 + continue + if ip and not learned.get(eid): + offsets = arg_off.setdefault(eid, set()) + if len(offsets) < nargs: + offsets.add(off) + args.append(word) + if len(args) == nargs: + if ip and not learned.get(eid): + offs = sorted(set(arg_off.get(eid, []))) + if len(offs) == nargs: + arg_off[eid] = offs + learned[eid] = True + if not rec_drop: + ev = (events or {}).get(eid) + name = ev[0] if ev else "?" + fields = ev[1] if ev else [] + parts = [] + for i, value in enumerate(args[:nargs]): + field_name = None + field_type = "u64" + if i < len(fields): + field_name, field_type = fields[i] + text = fmt_value(value, field_type) + parts.append("%s=%s" % (field_name, text) + if field_name else text) + fmt = ", ".join(parts) + lines.append((key, "record %d: event=%s id=0x%x args=[%s]" + % (records + 1, name, eid, fmt))) + records += 1 + cur = None + if cur is not None: + dropped += cur[1] - len(cur[2]) + + return records, dropped, stray, unknown, lines + +def _emit_record(eid, nargs, args, events, record_drop): + global _records + if record_drop: + return + ev = (events or {}).get(eid) + name = ev[0] if ev else "?" + fields = ev[1] if ev else [] + parts = [] + for i, value in enumerate(args[:nargs]): + field_name = None + field_type = "u64" + if i < len(fields): + field_name, field_type = fields[i] + text = fmt_value(value, field_type) + parts.append("%s=%s" % (field_name, text) + if field_name else text) + fmt = ", ".join(parts) + print("record %d: event=%s id=0x%x args=[%s]" % + (_records + 1, name, eid, fmt)) + +def _decode_stream_word(word, ip, events, stream_key): + global _records, _dropped, _stray, _unknown + state = _streams.setdefault(stream_key, {"cur": None, "drop": False}) + off = (ip & 0xfff) if ip else 0 + hdr = is_header(word, events) + if hdr is not None: + if state["cur"] is not None: + _dropped += state["cur"][1] - len(state["cur"][2]) + state["cur"] = (hdr[0], hdr[1], []) + state["drop"] = False + return + cur = state["cur"] + if cur is None: + print("manual ptwrite: payload=0x%x ip=0x%x" % (word, ip)) + _stray += 1 + return + eid, nargs, args = cur + if len(args) >= nargs: + state["cur"] = None + print("manual ptwrite: payload=0x%x ip=0x%x" % (word, ip)) + _stray += 1 + return + if ip and _learned.get(eid): + offs = _arg_off[eid] + expect = offs[len(args)] + if off != expect: + j = next((k for k in range(len(args) + 1, nargs) + if off == offs[k]), None) + if j is not None: + _dropped += j - len(args) + args.extend([None] * (j - len(args))) + state["drop"] = True + else: + _unknown += 1 + return + if ip and not _learned.get(eid): + offsets = _arg_off.setdefault(eid, set()) + if len(offsets) < nargs: + offsets.add(off) + args.append(word) + if len(args) == nargs: + if ip and not _learned.get(eid): + offs = sorted(set(_arg_off.get(eid, set()))) + if len(offs) == nargs: + _arg_off[eid] = offs + _learned[eid] = True + _emit_record(eid, nargs, args, events, state["drop"]) + _records += 1 + state["cur"] = None + +def decode_buf(raw_buf, events, ip=0, key=0, stream_key=None): + """Decode one 12-byte perf raw sample without retaining prior samples.""" + if len(raw_buf) < 12: + return + flags = struct.unpack_from("= 8: + _errors += 1 + print("ptwrite-decode: error type=%d code=%d cpu=%d pid=%d tid=%d" + " ip=0x%x msg=%s" % (args[0], args[1], args[2], args[3], + args[4], args[5], args[7])) + +def trace_begin(): + global _events, _records, _dropped, _stray, _unknown, _errors, _other_count + if not _events: + _events = load_events() + _streams.clear() + _learned.clear() + _arg_off.clear() + _records = _dropped = _stray = _unknown = 0 + _errors = _other_count = 0 + +def branch_line(param_dict): + sample = param_dict.get("sample") or {} + frm = sample.get("ip") or 0 + to = sample.get("addr") or 0 + frm_sym = param_dict.get("symbol") or "[unknown]" + to_sym = sample.get("addr_symbol") or "[unknown]" + frm_off = param_dict.get("symoff") or 0 + to_off = sample.get("addr_symoff") or 0 + frm_dso = param_dict.get("dso") or "[unknown]" + to_dso = sample.get("addr_dso") or "[unknown]" + fs = "+0x%x" % frm_off if frm_off else "" + ts_ = "+0x%x" % to_off if to_off else "" + return ("branch: 0x%x %s%s (%s) => 0x%x %s%s (%s)" + % (frm, frm_sym, fs, frm_dso, to, to_sym, ts_, to_dso)) + +def other_line(name, sample, comm, sym=None, off=0, dso=None): + """Format a non-ptwrite, non-branch event (classic uprobe, + tracepoint, sample) for interleaved printing.""" + pid = sample.get("pid") + tid = sample.get("tid") + ip = sample.get("ip") or 0 + loc = " %s+0x%x (%s)" % (sym, off, dso) if sym else "" + return ("event: %s comm=%s pid=%s tid=%s ip=0x%x%s" + % (name, comm, pid, tid, ip, loc)) + +def trace_unhandled(handler_name, context, fields, sample=None): + """Print tracepoint-class events immediately in delivery order.""" + global _other_count + fields = fields or {} + name = handler_name.replace("__", ":") + comm = fields.get("common_comm") or "" + pid = fields.get("common_pid") + cpu = fields.get("common_cpu") + print("event: %s comm=%s pid=%s cpu=%s" % + (name, comm, pid, cpu)) + _other_count += 1 + +def process_event(param_dict): + global _other_count + sample = param_dict.get("sample") or {} + name = param_dict.get("ev_name") or "" + if name == "ptwrite": + raw = param_dict.get("raw_buf") + if raw: + decode_buf(raw, _events, sample.get("ip") or 0, + stream_key=_sample_stream_key(sample)) + elif name.startswith("branches"): + if _show_branches: + print(branch_line(param_dict)) + else: + print(other_line(name, sample, + param_dict.get("comm") or "", + param_dict.get("symbol"), + param_dict.get("symoff") or 0, + param_dict.get("dso"))) + _other_count += 1 + +def trace_end(): + global _dropped + for state in _streams.values(): + cur = state["cur"] + if cur is not None: + _dropped += cur[1] - len(cur[2]) + print("summary: records=%d dropped=%d stray=%d unknown=%d errors=%d other=%d" + % (_records, _dropped, _stray, _unknown, _errors, _other_count)) + _streams.clear() + +def main(): + args = sys.argv[1:] + wordfile = None + eid_override = None + types_override = None + i = 0 + while i < len(args): + a = args[i] + if a == "--words": + if i + 1 < len(args) and not args[i + 1].startswith("--"): + wordfile = args[i + 1] + i += 1 + elif a == "--event-id": + if i + 1 >= len(args) or args[i + 1].startswith("--"): + print("--event-id requires a value", file=sys.stderr) + return 2 + try: + eid_override = int(args[i + 1], 0) + except ValueError: + print("--event-id requires an integer", file=sys.stderr) + return 2 + i += 1 + elif a == "--types": + if i + 1 >= len(args) or args[i + 1].startswith("--"): + print("--types requires a value", file=sys.stderr) + return 2 + types_override = args[i + 1].split(",") + i += 1 + i += 1 + + events = dict(_events) + if eid_override is not None and types_override is not None: + events[eid_override] = ("override", + [(None, t) for t in types_override]) + + words = [] + if wordfile: + with open(wordfile) as f: + for line in f: + line = line.strip() + if line.startswith(("0x", "0X")) or line.isdigit(): + words.append(int(line, 16)) + else: + for line in sys.stdin: + line = line.strip() + if line.startswith(("0x", "0X")) or line.isdigit(): + words.append(int(line, 16)) + + records, dropped, stray, unknown, lines = decode_words(words, events) + for _, text in lines: + print(text) + print("summary: records=%d dropped=%d stray=%d unknown=%d" + % (records, dropped, stray, unknown)) + return 0 if (records > 0 and dropped == 0 and unknown == 0) else 1 + +if "--words" in sys.argv: + sys.exit(main()) -- 2.54.0