From nobody Tue Sep 29 10:31:59 2026 Received: from mail-pf1-f199.google.com (mail-pf1-f199.google.com [209.85.210.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id BE6ED3A2E0A for ; Sun, 9 Aug 2026 04:16:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.199 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786249006; cv=none; b=UsJ9PE4xjEDJ/Kjpax0xfy+avbxBixblM6mLCZH1yKLw2mEbmZpcjZKdDm4EN0CypHtfINKqcGYkQ79zH+xwgnVHd7nshpFHDZQwm7Tae31GUvIuxokfu3tivMstuhS+NvzFlPinQh33y5ISgZRtQqZXRoEblAk3StSR2LJ+aNk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786249006; c=relaxed/simple; bh=mEH//4qopPTMwiG6eZxQiEqMyVZ4g8WiMJvhJ/Rq2bM=; h=Date:In-Reply-To:Mime-Version:References:Message-ID:Subject:From: To:Cc:Content-Type; b=mlTlf2xlM0hWFeVWpzJYaOlqCac2cbRye/GNeTpXQjUXldVBAQdQTHk7JBbczrswPZn6YChuNhcQkRQMxZFAlk7/mDWjADLXwI3Av+XALCJIgMPXhmj+UHeBTRGl4U9xkMkM7FUNIzINKTxYIr/0I+U4P//QYaSIUsbcQkIyWWo= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b=VcUkLo6E; arc=none smtp.client-ip=209.85.210.199 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="VcUkLo6E" Received: by mail-pf1-f199.google.com with SMTP id d2e1a72fcca58-848662cd2a1so1028441b3a.2 for ; Sat, 08 Aug 2026 21:16:43 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20251104; t=1786249003; x=1786853803; darn=vger.kernel.org; h=content-transfer-encoding:content-type:cc:to:from:subject :message-id:references:mime-version:in-reply-to:date:from:to:cc :subject:date:message-id:reply-to:content-type; bh=QGZXWoGsm0gG8UEqRPRw6ZGccLULCKbSmtN9bcKEN5M=; b=VcUkLo6EZSm4yOSntlfx4dc1gTu1/2NdQmFocXY4/mvlzTTCUyOKM1bdF1nWFbeA2l oakQpvKQj3bGvfBzePPbmnVfuWTxtCSYucJ03RpP7nmTDarTG0qXF1LCOe4LOv7z9Uvl JNAf04JVbNsr9ZjC34pAu/cQhoSBknDjrH9Jt/PkARmcdaB56G+FizDKENY0P9/72Ytp al3pVWp4me0jGNBgCCqOc82VN+vexQedYPOVhaPKXvh3Aw346fX+mge6ARK7/HqOEJtG gI/aCSSnCfl6R4YgR+9+b77pEF88jpFb1UlLJbHj0jU7taYQprQKqtm2DZRodCA9ZmFX XHYQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786249003; x=1786853803; h=content-transfer-encoding:content-type:cc:to:from:subject :message-id:references:mime-version:in-reply-to:date :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=QGZXWoGsm0gG8UEqRPRw6ZGccLULCKbSmtN9bcKEN5M=; b=M9V1B42C7TgZq7YkXfX8+/Gm4QYwJcdMsllrt9R7SpF79r7nt+gJgu37H1DZB6zHuo rvpSMXlDw4FlW+ksT5s3iqY9h5qdET8rvKRFiWtEhvJrQ5xDXDcp00FkW/H/XrLu1Zfi 45+n574s43jruzXznVyNIqlGyt/upsBY8mEXYK5c7FXiezvRBaPA5kM1x/lwddLIpTng TJImJjqnaQxZ+9uSuFKcMWoWfSa4KplfZUxxoRbCtg19nXbGy9SW46aOO6u4FhzV0Drr GUbUgFgQagyt20YEV4whmqBhkYtI2qUH538tI2ra+uLVnkpsNtlRaR4TkAkQEMrkf8xi 8zGQ== X-Forwarded-Encrypted: i=1; AHgh+RrK1kZHK6H6Ork5WZnIR1PBtTk9FfQRroceYN1LKxXVCEyl27huy9KbLA28wRDKx+56krMxtJc8/1hIvbw=@vger.kernel.org X-Gm-Message-State: AOJu0Ywmj4MjMJ18NTE/mbeo5b96f1/TRjtgsGeaHnE2Nsc2fWZygHMe w9ex3tI9zA5/rhzfskU6JWkVuibSJwO57yHrepjvkD7n0/bmk+X9vEXqx7Q4KV93olCY1htOuFa ngVKGE5/DIw== X-Received: from dlaj10.prod.google.com ([2002:a05:701b:280a:b0:13c:eacb:77ba]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a05:6a20:d495:b0:3b4:6026:6c5d with SMTP id adf61e73a8af0-3cbce6f18e2mr13556476637.5.1786249002866; Sat, 08 Aug 2026 21:16:42 -0700 (PDT) Date: Sat, 8 Aug 2026 21:16:32 -0700 In-Reply-To: <20260809041638.2402705-1-irogers@google.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: Mime-Version: 1.0 References: <20260808065743.2328212-1-irogers@google.com> <20260809041638.2402705-1-irogers@google.com> X-Mailer: git-send-email 2.55.0.654.g21b8a5bc05-goog Message-ID: <20260809041638.2402705-2-irogers@google.com> Subject: [PATCH v3 1/2] perf python: New treport script From: Ian Rogers To: irogers@google.com, acme@kernel.org, alice.mei.rogers@gmail.com, namhyung@kernel.org Cc: adrian.hunter@intel.com, jolsa@kernel.org, laixintaoo@gmail.com, linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org, mingo@redhat.com, peterz@infradead.org Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" From: Alice Rogers A textual app that displays the results of processing samples similar to perf report. The app displays a tree of first processes and then functions which drop down to show more detail on the functions they call. The functions with the largest number of samples are sorted first, after each function the percentage of time spent within it is highlighted. If more than one event is recorded then each event appears first, with the processes and functions sorted for that event beneath it. The app is written in python and requires the textual framework. Using the textual framework means that in most modern terminals the mouse is supported, the color scheme can be configured, and screenshots made. With perf report: ``` $ perf record -e cycles:u -g perf test -w brstack [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.008 MB perf.data (57 samples) ] $ perf report ... Samples: 57 of event 'cycles:u', Event count (approx.): 15907831 Children Self Command Shared Object Symbol - 88.86% 0.00% perf libc.so.6 [.] 0x000079b17df69ca8 0x79b17df69ca8 main handle_internal_command cmd_test - brstack - 84.27% brstack_bench - 40.69% brstack_foo brstack_bar 3.32% brstack_bar 3.31% brstack_bar 1.27% brstack_foo + 88.86% 0.00% perf perf [.] main + 88.86% 0.00% perf perf [.] handle_internal_co= mmand + 88.86% 0.00% perf perf [.] cmd_test + 88.86% 0.00% perf perf [.] brstack + 84.27% 40.26% perf perf [.] brstack_bench + 41.96% 30.71% perf perf [.] brstack_foo + 17.89% 17.89% perf perf [.] brstack_bar ``` With the treport script: ``` $ perf record -e cycles:u -g perf test -w brstack [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.008 MB perf.data (57 samples) ] $ tools/perf/python/treport.py O ReportApp Report =E2=95=B8=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=95=BA=E2= =94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94= =81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81= =E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2= =94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94= =81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81= =E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2=94=81=E2= =94=81=E2=94=81=E2=94=81=E2=94=81 =E2=96=BC Profile =E2=94=94=E2=94=80=E2=94=80 =E2=96=BC cycles:u 100% =E2=94=94=E2=94=80=E2=94=80 =E2=96=BC perf (32963) 100% =E2=94=9C=E2=94=80=E2=94=80 =E2=96=BC /usr/lib/x86_64-linux-gnu/lib= c.so.6 0x79b17df6 =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 =E2=96=BC main 56.1% =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 =E2=96=BC handle_intern= al_command 56.1% =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 =E2=96=BC cmd_test = 56.1% =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 =E2=96=BC brsta= ck 56.1% =E2=96=81=E2=96=81 =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 =E2=96=BC b= rstack_bench 52.6% =E2=94=82 =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80= =E2=96=B6 brstack_foo 24.6% =E2=94=82 =E2=94=82 =E2=94=94=E2=94=80=E2=94=80= brstack_bar 1.75% =E2=94=82 =E2=94=9C=E2=94=80=E2=94=80 brstack_foo= 1.75% =E2=94=82 =E2=94=94=E2=94=80=E2=94=80 brstack_bar= 1.75% =E2=94=9C=E2=94=80=E2=94=80 =E2=96=B6 /usr/lib/x86_64-linux-gnu/ld-= linux-x86-64.so.2 =E2=96=8C ^q Quit =E2=96=8F^p palette ``` Co-developed-by: Ian Rogers Signed-off-by: Ian Rogers Signed-off-by: Alice Rogers --- tools/perf/python/treport.py | 207 +++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100755 tools/perf/python/treport.py diff --git a/tools/perf/python/treport.py b/tools/perf/python/treport.py new file mode 100755 index 000000000000..528a640e4d35 --- /dev/null +++ b/tools/perf/python/treport.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""treport.py - perf report like tool written using textual.""" +from typing import Dict, Optional +import argparse +import os +import sys +import perf +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.widgets import Footer, Header, TabbedContent, TabPane, Tree +from textual.widgets.tree import TreeNode + +# Global session. +session :Optional[perf.session] =3D None + +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 t= o its + children. + children (Dict[str, ProfileNode]): A dictionary of child nodes, ke= yed by + their names. + """ + def __init__(self, name: str, parent: "ProfileNode"): + """Initializes a ProfileNode.""" + self.name =3D name + self.value: int =3D 0 + self.parent =3D parent if parent else self + self.children: Dict[str, ProfileNode] =3D {} + + 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 =3D ProfileNode(name, self) + self.children[name] =3D 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) -> None: + """Processes a single profiling event to update the call stack tre= e. + + Args: + sample: a single profiling sample. + """ + pid =3D sample.sample_pid + try: + assert session + thread =3D session.find_thread(sample.sample_tid) + comm =3D thread.comm() + except Exception: + comm =3D f"unknown ({pid})" + + period =3D sample.sample_period + self.value +=3D period + + node =3D self.find_or_create_node(comm) + node.value +=3D period + + if sample.callchain: + for entry in reversed(sample.callchain): + name =3D entry.symbol + if not name or name =3D=3D "[unknown]": + name =3D entry.dso or "unknown" + if entry.ip: + name +=3D f" 0x{entry.ip:x}" + node =3D node.find_or_create_node(name) + node.value +=3D period + else: + name =3D sample.symbol + if not name or name =3D=3D "[unknown]": + name =3D sample.dso or "unknown" + if sample.sample_ip: + name +=3D f" 0x{sample.sample_ip:x}" + node =3D node.find_or_create_node(name) + node.value +=3D period + + def add_to_tree(self, node: TreeNode, root_value: int) -> None: + """Recursively adds this node and its children to a textual TreeNo= de. + + Args: + node (TreeNode): The textual `TreeNode` object to which this + ProfileNode should be added. + root_value (int): Value at the root of the tree. + """ + if root_value =3D=3D 0: + root_value =3D self.value + + # Calculate the percentage for the node, highlighting the + # percentage with reversed colors. + if root_value !=3D 0: + percent =3D self.value / root_value * 100 + label =3D f"{self.name} [r]{percent:.3g}%[/]" + else: + label =3D self.name + + # Add a standalone leaf. + if not self.children: + node.add_leaf(label) + return + + # Recursively add children. + new_node =3D node.add(label) + for pnode in sorted(self.children.values(), + key=3Dlambda pnode: pnode.value, reverse=3DTru= e): + pnode.add_to_tree(new_node, root_value) + + +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 =3D [ + Binding(key=3D"^q", action=3D"quit", description=3D"Quit", + tooltip=3D"Quit the app"), + ] + + def __init__(self, root: ProfileNode): + """Initialize the application.""" + super().__init__() + self.root =3D root + + def make_report_tree(self) -> Tree: + """Make a Tree widget from the profile data.""" + tree: Tree[None] =3D Tree("Profile") + # Add events to tree skipping the root. + for pnode in sorted(self.root.children.values(), + key=3Dlambda node: node.value, reverse=3DTrue): + pnode.add_to_tree(tree.root, root_value=3D0) + + # Expand the root tree (shows all events) and the largest of the c= hildren + # for each event. + def expand_first_child(tnode: TreeNode) -> None: + """Recursively expand the first child node""" + if not tnode.children: + return + first =3D tnode.children[0] + first.expand() + expand_first_child(first) + tree.root.expand() + for tnode in tree.root.children: + expand_first_child(tnode) + + # If there is only one event, expand it also. + if len(tree.root.children) =3D=3D 1: + tree.root.children[0].expand() + + return tree + + def compose(self) -> ComposeResult: + """Composes the user interface of the application.""" + yield Header() + with TabbedContent(initial=3D"report"): + with TabPane("Report", id=3D"report"): + yield self.make_report_tree() + yield Footer() + + +class ProfileBuilder: + """Constructs a profile tree from a stream of events.""" + def __init__(self): + self.root =3D ProfileNode("root", parent=3DNone) + + def process_event(self, sample) -> None: + """Called by session.process_events to update the profile tree.""" + ev_name =3D str(sample.evsel)[6:-1] + ev_root =3D self.root.find_or_create_node(ev_name) + ev_root.process_event(sample) + +if __name__ =3D=3D "__main__": + parser =3D argparse.ArgumentParser(description=3D"Create flame graphs = using perf python module.") + parser.add_argument("-i", "--input", help=3D"input perf.data file") + args =3D parser.parse_args() + + input_file =3D args.input or "perf.data" + if not os.path.exists(input_file): + print(f"Error: {input_file} not found. (try 'perf record' first)",= file=3Dsys.stderr) + sys.exit(1) + + profile =3D ProfileBuilder() + try: + session =3D perf.session(perf.data(input_file), sample=3Dprofile.p= rocess_event) + except Exception as e: + print(f"Error opening session: {e}", file=3Dsys.stderr) + sys.exit(1) + + # profle.process_event is called for each perf event to build the prof= ile. + session.process_events() + + # Visualize data. + app =3D ReportApp(profile.root) + app.run() --=20 2.55.0.654.g21b8a5bc05-goog From nobody Tue Sep 29 10:31:59 2026 Received: from mail-pj1-f72.google.com (mail-pj1-f72.google.com [209.85.216.72]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D155223E330 for ; Sun, 9 Aug 2026 04:16:48 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.216.72 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786249012; cv=none; b=VHMPmpSBSBpWPDVDwhSVduGWasY1oFwHEDU4IkQDFw5RiPQee5B3mFmBWo2adyvGRHb4+1RUdNL8Fs5PogPV+jIDsjeyTvwkHULC4TajzJdgWIhEQMVh610tfca8ru46uNBsq7SNUFP2zyAJpohhgLyrYeEzJ/QeqZ3IdG5AGFw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786249012; c=relaxed/simple; bh=fNTrAs4+/oo8Kp7OYudlynLf+IYmVqR79aK873HDRQY=; h=Date:In-Reply-To:Mime-Version:References:Message-ID:Subject:From: To:Cc:Content-Type; b=DP+e6DSt6xNC9UEZ3Ww331bbLuMBFjVXzVO7CZ9SwZhjTjGjYg0+aoR9doa6W/LbLjT/lM39s6IdtChig2sRjoJPUqMAuiYIrHn+xLP3TyCsPVJp+aSQHsh50tDVZP2FS9a0I640NFhW8yjnQrOQM95CyCZrRbRwmUupU69NQxs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b=ovciKGJl; arc=none smtp.client-ip=209.85.216.72 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="ovciKGJl" Received: by mail-pj1-f72.google.com with SMTP id 98e67ed59e1d1-38ec0f510a9so2360078a91.2 for ; Sat, 08 Aug 2026 21:16:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20251104; t=1786249007; x=1786853807; darn=vger.kernel.org; h=content-transfer-encoding:content-type:cc:to:from:subject :message-id:references:mime-version:in-reply-to:date:from:to:cc :subject:date:message-id:reply-to:content-type; bh=bFzlV6LSM12WHSmY/3yZakBMLS5lk8VEiNydzLCcax4=; b=ovciKGJllpTO6fXQr0W25+yNdkqi7HsGqCskd9RA9BVnAOGzbZIbJSkv9BWEGNp6T9 pmwyRXUvSlq3jnRgYYEq7WshqseHmqHgh2y1MmbDwY7YEn4oecVfXyGNF30sklc3ERe7 1iocsR9wn4LVGw79vsrXc6mw6f6ArF4eVkKBpsSXDWFCP44qdQQ/Ls2kL5kW19aNop+s 8F7XNHaEkwTkybh5mdTxoWduy09oUI7khbCx6GsY2qSq9F6F6UwvbX4ARPuXWxbYSDgb kbUFPfVqNan6JgwbSgmJt9/4FMAfdpUsH9zfYj6ZlMurQlTeob4wn93Bf/EiL7zQ0E7u 2/yQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786249007; x=1786853807; h=content-transfer-encoding:content-type:cc:to:from:subject :message-id:references:mime-version:in-reply-to:date :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=bFzlV6LSM12WHSmY/3yZakBMLS5lk8VEiNydzLCcax4=; b=dBoVrEVPPsOO5s6qN7QcOfx4ThAjdkclmVyMj+e5ZkVM3ZGyp2i1sgmpBC+wCfgZLZ SfZSv8JWlUjZCfWsSL4Jl0h2EZgpNpbG9oD0bub9baoJzqpfkISPHgBfzz8wRddolqVM 8cFlDsbaLabBGpYJWkaYXrCU9yU+hTVBHfQxEvUeozQCZQyZ/d4t22tJO/a7wpwCbH8h R49GGYEWArZm5FuWtR/YlxkJjpEgTGelpPawuos+icRDWwx2QK/b3Dn8NgMwySybHXvB DZ8UnQtWnE3XLO13yHCNctb+98e3KczcpzaWd4zP9NPCdYmTwEvs/xOmJiF03hlPRRv+ TdXA== X-Forwarded-Encrypted: i=1; AHgh+Rq9+MymjG7vLd11CJ52Nt8n8Z+XtrGQWutIsOmRTSQDm30Wgni9WbwhjpyNpBgrCZlxN67mlK6QwQgJZ9w=@vger.kernel.org X-Gm-Message-State: AOJu0YxFoGRvCWPXKNPddxqlggwlDdC81YXSiDbjGlhZ/aSoEe7AjDnF 0bIVoFqwYKfXAQuLaHt5ivZ7oALbpIO2M8gOODvl45prB3VLVtZMviKplHOGed8vgZK6tFooUIq kXyMH1LYfdA== X-Received: from dleb11-n1.prod.google.com ([2002:a05:701b:424b:10b0:13d:6291:7191]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a17:90b:3b45:b0:38e:4f41:83df with SMTP id 98e67ed59e1d1-392621444f0mr14532294a91.15.1786249007264; Sat, 08 Aug 2026 21:16:47 -0700 (PDT) Date: Sat, 8 Aug 2026 21:16:34 -0700 In-Reply-To: <20260809041638.2402705-1-irogers@google.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: Mime-Version: 1.0 References: <20260808065743.2328212-1-irogers@google.com> <20260809041638.2402705-1-irogers@google.com> X-Mailer: git-send-email 2.55.0.654.g21b8a5bc05-goog Message-ID: <20260809041638.2402705-4-irogers@google.com> Subject: [PATCH v3 2/2] perf python: treport add flamegraph support From: Ian Rogers To: irogers@google.com, acme@kernel.org, alice.mei.rogers@gmail.com, namhyung@kernel.org Cc: adrian.hunter@intel.com, jolsa@kernel.org, laixintaoo@gmail.com, linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org, mingo@redhat.com, peterz@infradead.org Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" From: Alice Rogers Implement a flamegraph widget that recursively walks down a tree splitting line segments based on their value (summed up periods across call chains). A visitor pattern is used so that the same logic can both draw the line segments and locate which segment had a mouse click. Add a tab for the flame graph widget. Co-developed-by: Ian Rogers Signed-off-by: Ian Rogers Signed-off-by: Alice Rogers --- tools/perf/python/treport.py | 353 +++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) diff --git a/tools/perf/python/treport.py b/tools/perf/python/treport.py index 528a640e4d35..ec3263a6b625 100755 --- a/tools/perf/python/treport.py +++ b/tools/perf/python/treport.py @@ -1,19 +1,49 @@ #!/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 Dict, Optional import argparse import os import sys import perf +from rich.segment import Segment +from rich.style import Style +from textual import events 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 =20 # Global session. session :Optional[perf.session] =3D None =20 +def make_fixed_length_string(s: str, length: int, pad_char=3D' '): + """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 t= his + node, which is proportional to its sample count. + """ + + class ProfileNode: """Represents a single node in a call stack tree. =20 @@ -120,6 +150,327 @@ class ProfileNode: key=3Dlambda pnode: pnode.value, reverse=3DTru= e): pnode.add_to_tree(new_node, root_value) =20 + def largest_child(self) -> "ProfileNode": + """Finds the child with the highest value (sample count).""" + if self.children: + return max(self.children.values(), key=3Dlambda node: node.val= ue) + return self + + def child_after(self, sought: "ProfileNode") -> "ProfileNode": + """Finds the next sibling after the given node, sorted by value.""" + found =3D False + for child in sorted(self.children.values(), key=3Dlambda node: nod= e.value, + reverse=3DTrue): + if child =3D=3D sought: + found =3D True + elif found: + return child + return sought + + def child_before(self, sought: "ProfileNode") -> "ProfileNode": + """Finds the previous sibling before the given node, sorted by val= ue.""" + last =3D None + for child in sorted(self.children.values(), key=3Dlambda node: nod= e.value, + reverse=3DTrue): + if child =3D=3D sought: + return last if last else sought + last =3D child + return sought + + def has_parent(self, parent: "ProfileNode") -> bool: + """Checks if the parent node is an ancestor of this node.""" + p =3D self.parent + while True: + if p =3D=3D parent: + return True + new_p =3D p.parent + if new_p =3D=3D p: + break + p =3D 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 gra= ph 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 g= raph + row. + """ + if parent_width =3D=3D 0: + return + + parent_selected =3D selected =3D=3D self or self.has_parent(select= ed) + child_selected =3D 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 =3D parent_width + for child in sorted(self.children.values(), key=3Dlambda node: nod= e.value, + reverse=3DTrue): + if parent_selected: + if self.value: + desired_width =3D int((parent_width * child.value) / s= elf.value) + else: + desired_width =3D parent_width // len(self.children) + if desired_width =3D=3D 0: + # Nothing can be drawn for this node or later smaller = children. + break + elif child =3D=3D selected or child.has_child(selected): + desired_width =3D 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 =3D=3D cur_strip: + visitor.visit(child, desired_width) + else: + child.flame_walk(wanted_strip, cur_strip + 1, desired_widt= h, + selected, visitor) + left_over -=3D desired_width + if left_over =3D=3D 0: + # No space left to draw in. + break + + # Always visit the left_over regardless of the wanted_strip as the= re + # 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 special= ized + 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 gra= ph row. + parent_width (int): The total width (in characters) of the dis= play + 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 =3D Color.parse(theme_variables["primary"]) + secondary =3D Color.parse(theme_variables["secondary"]) + surface =3D 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 =3D [] + for x in range(0, 125, 25): + fgcolor =3D secondary.blend(primary, x/100) + if luminance(fgcolor) > luminance(surface): + bgcolor =3D surface.lighten(0.05+x/500) + else: + bgcolor =3D surface.darken(0.05+x/500) + normal_styles.append(Style(color=3Dfgcolor.rich_color, + bgcolor=3Dbgcolor.rich_color)) + + # Style for the selected flame graph node. + accent =3D Color.parse(theme_variables["accent"]) + accent_muted =3D Color.parse(theme_variables["accent-muted"]) + cursor_style =3D Style(color=3Daccent.rich_color, bgcolor=3Daccent= _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 =3D [] + self.gap_width =3D 0 + self.ctr =3D wanted_strip + + def visit(self, node: Optional[ProfileNode], width: int) -> No= ne: + if node: + if self.gap_width > 0: + self.segments.append(Segment( + make_fixed_length_string(" ", self.gap_width))) + self.gap_width =3D 0 + style =3D cursor_style + if node !=3D cursor: + style =3D normal_styles[self.ctr % len(normal_styl= es)] + self.segments.append(Segment( + make_fixed_length_string(node.name, width), style)) + else: + self.gap_width +=3D width + self.ctr +=3D 1 + + visitor =3D StripVisitor() + self.flame_walk(wanted_strip, 0, parent_width, selected, visitor) + return Strip(visitor.segments) if visitor.segments else Strip.blan= k(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 coordinate= s. + + 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 aff= ects + layout. + + Returns: + Optional[ProfileNode]: The node found at the coordinates, or N= one. + + """ + 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 =3D 0 + self.found =3D None + + def visit(self, node: Optional[ProfileNode], width: int) -> No= ne: + if self.x <=3D sought_x < self.x + width: + self.found =3D node + self.x +=3D width + + visitor =3D 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 =3D [ + Binding("j,down", "move_down", "Down", key_display=3D"=E2=86=93", + tooltip=3D"Move cursor down to largest child"), + Binding("k,up", "move_up", "Up", key_display=3D"=E2=86=91", + tooltip=3D"Move cursor up to parent"), + Binding("l,right", "move_right", "Right", key_display=3D"=E2=86=92= ", + tooltip=3D"Move cursor to the right sibling"), + Binding("h,left", "move_left", "Left", key_display=3D"=E2=86=90", + tooltip=3D"Move cursor to the left sibling"), + Binding("enter", "zoom_in", "Zoom In", + tooltip=3D"Expand the cursor's node to be screen width"), + Binding("escape", "zoom_out", "Zoom Out", + tooltip=3D"Zoom out to initial view."), + ] + + # Default CSS for the widget to ensure it fills its container's width. + DEFAULT_CSS =3D """ + FlameGraph { + width: 100%; + } + """ + + def __init__(self, root: ProfileNode, *args, **kwargs): + """Initialize the FlameGraph widget.""" + super().__init__(*args, **kwargs) + self.root =3D root + self.cursor =3D root + self.selected =3D root + + def action_move_down(self) -> None: + """Handle key press down.""" + self.cursor =3D self.cursor.largest_child() + self.refresh() + + def action_move_up(self) -> None: + """Handle key press up.""" + if self.cursor.parent !=3D self.cursor.parent.parent: + self.cursor =3D self.cursor.parent + self.refresh() + + def action_move_right(self) -> None: + """Handle key press right.""" + self.cursor =3D self.cursor.parent.child_after(self.cursor) + self.refresh() + + def action_move_left(self) -> None: + """Handle key press left.""" + self.cursor =3D self.cursor.parent.child_before(self.cursor) + self.refresh() + + def action_zoom_in(self) -> None: + """Handle key press zoom in.""" + self.selected =3D self.cursor + self.refresh() + + def action_zoom_out(self) -> None: + """Handle key press zoom out.""" + self.selected =3D self.root + self.refresh() + + def render_line(self, y: int) -> Strip: + """Render a single line (row) of the flame graph.""" + _, scroll_y =3D self.scroll_offset + y +=3D scroll_y + return self.root.make_flame_strip(y, self.size.width, self.cursor, + self.selected, self.app.theme_va= riables) + + def on_mount(self) -> None: + """Set the height of the widget when it is displayed.""" + self.styles.height =3D self.root.depth() + + def on_click(self, click: events.Click) -> None: + """Handles a mouse click and update the cursor position.""" + _, scroll_y =3D self.scroll_offset + y =3D scroll_y + click.y + clicked_node =3D self.root.find_node(click.x, y, self.size.width, + self.selected) + if clicked_node: + self.cursor =3D clicked_node + self.refresh() + =20 class ReportApp(App): """A Textual application to display profiling data.""" @@ -168,6 +519,8 @@ class ReportApp(App): with TabbedContent(initial=3D"report"): with TabPane("Report", id=3D"report"): yield self.make_report_tree() + with TabPane("Flame Graph", id=3D"flame"): + yield FlameGraph(self.root) yield Footer() =20 =20 --=20 2.55.0.654.g21b8a5bc05-goog From nobody Tue Sep 29 10:31:59 2026 Received: from mail-pl1-f198.google.com (mail-pl1-f198.google.com [209.85.214.198]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3D70A3A0E88 for ; Sun, 9 Aug 2026 04:16:45 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.214.198 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786249009; cv=none; b=SpgKdSuctrDiTC4UF5icU9G6GpgmosQOku5PFpfcBb6u1jbD69k2KBGrbXBo3SGPs72LiciSgZbCJLHQ/vRAW+pioCdemKkfXiRhp5UHi5IxMc6GhGXhos4qVJLfJB2FvEegtAmfZjpG7p/CPdCKwh6UUx+1yfs5WWLugWk9G/8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786249009; c=relaxed/simple; bh=Nn0miRX6O32JQOtPVCr92wiRbgVDw32950L1rvjPbWw=; h=Date:In-Reply-To:Mime-Version:References:Message-ID:Subject:From: To:Cc:Content-Type; b=iCmMHQgvvOH9zI0VpCKJ9pQfXHaeeIDhSJfQG47ilQI7iMwIEmjKd8+OdWNPwEzI60HAg2K5032rSlM23fU83l6bGhBPdnfGbrXf0GV/nN93YD2pEDRcqz0QfV8jzKs+qOl6zK/kp2nAo8Hm3OVcQ0wECGI/Ov00TDk68aOqcgQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b=K+hGkN6p; arc=none smtp.client-ip=209.85.214.198 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="K+hGkN6p" Received: by mail-pl1-f198.google.com with SMTP id d9443c01a7336-2ce8a76df2dso15745565ad.2 for ; Sat, 08 Aug 2026 21:16:45 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20251104; t=1786249005; x=1786853805; darn=vger.kernel.org; h=content-type:cc:to:from:subject:message-id:references:mime-version :in-reply-to:date:from:to:cc:subject:date:message-id:reply-to :content-type; bh=fLtFtC/oi2eHemCIy4gg+BvRUETIN+tHfDnRPBDklJg=; b=K+hGkN6pNfA4r7zjktVNCLQZdnx25fqYlcxz9V6jYm+21+HCT1LK8mWM7WnrZlQxwV XsWUAB9g/6G7129Pyrch6ziQt+AiUMXZ1X8O81bwkcBzRJC/ZP50PQsg8qvsTDEEE1mF ACunME8tMhdFDJNyZRQj09hT3QJfFuFwLNu0lMiXVjfttkLq5oYqhvw6kvzpMDAaeiTU QIgpFcY0UHcoAYQzTPa2XCRCbI1qKTwPq0KP1aby1qqhzwE+nOFBHiDU04IGs1Is8K3+ Lt+1Ui6WMh7/wqe1QVi+327yv1D+ugvL4R3b8Pi+G3kc0pJKHFo/kD2eeaHDkIivgRty EGbg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1786249005; x=1786853805; h=content-type:cc:to:from:subject:message-id:references:mime-version :in-reply-to:date:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to:content-type; bh=fLtFtC/oi2eHemCIy4gg+BvRUETIN+tHfDnRPBDklJg=; b=fbNW1ZY9w0VmZB3xHsSqTD0z+O3wOSErOdg6riFn8dN4hDEOM3nm5JDoFHM9rf3UpU WjXiEeUdJcfG0XLB9Jfwq+KOi7BpTWu6qzJWlFjXuWMnMqB6UXRcWFBF2f6qcg2wpLR2 v96huPU/m840Ld8uPHyH3Jd7LPt06lZeBOg1em511xMASxve1JvRA2Um443TqqQiYZ+3 VM97LjazMPDCbfX2FSJj83jZ9hxlbqhqb5a2iGSNng6hzHFrfwCcCN5hPVkeQFGdXIBs gFB50vgJD/s+883aRwN1995ZxVuAu9oPiCfEBVzlo3fjl0tzMv+1iadbbE+cHwDSRV3E yaRg== X-Forwarded-Encrypted: i=1; AHgh+RrXWFkqhpoBtX9wUWcfwgdJ/xXbBZvQQyjuvbBSUmySetLnh22cuH5I+7dY3cNCkf8zc2P7vWHFDNNQEfQ=@vger.kernel.org X-Gm-Message-State: AOJu0YwvbBGcBz90vgdTO8ZsaVSZxa2xdtxCBORarO9d5HJUJliS6urT LnNtbH5K+kO2Pd6CagYc2e1F9/Tj1UlNQZ1JNKwxb/c6/lejByDDWSeqetF6Ab4SzDTKzZE7MpK eVRYdS40JJQ== X-Received: from dyah27-n2.prod.google.com ([2002:a05:693c:83db:20b0:30f:3f53:35d3]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a17:902:ea01:b0:2c9:bf82:dd11 with SMTP id d9443c01a7336-2d0ca71b505mr359738125ad.7.1786249004857; Sat, 08 Aug 2026 21:16:44 -0700 (PDT) Date: Sat, 8 Aug 2026 21:16:33 -0700 In-Reply-To: <20260809041638.2402705-1-irogers@google.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: Mime-Version: 1.0 References: <20260808065743.2328212-1-irogers@google.com> <20260809041638.2402705-1-irogers@google.com> X-Mailer: git-send-email 2.55.0.654.g21b8a5bc05-goog Message-ID: <20260809041638.2402705-3-irogers@google.com> Subject: [PATCH v3] perf test: Fixes for check branch stack sampling From: Ian Rogers To: irogers@google.com, acme@kernel.org, alice.mei.rogers@gmail.com, namhyung@kernel.org Cc: adrian.hunter@intel.com, jolsa@kernel.org, laixintaoo@gmail.com, linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org, mingo@redhat.com, peterz@infradead.org Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" When filtering branch stack samples on user events they sample in user land but may have come from the kernel. Avoid the target address being a kernel address but allow the source to be the kernel. When filtering branch stack samples on kernel events they sample in kernel land but may have come from user land. Avoid the target being a user address but allow the source to be in user land. Increase the duration of the system call sampling test to make the likelihood of sampling a system call higher (increased from 1000 to 8000 loops - a number found through experimentation on an Intel Tigerlake laptop), also make the period of the event a prime number. Put unneeded perf record output into a temporary file so that the test output isn't cluttered. More clearly state which test is running and the pass, fail or skipped result of the test. These changes make the test on an Intel tigerlake laptop reliably pass rather than reliably fail. Signed-off-by: Ian Rogers --- tools/perf/tests/shell/test_brstack.sh | 134 ++++++++++++++++--------- 1 file changed, 84 insertions(+), 50 deletions(-) diff --git a/tools/perf/tests/shell/test_brstack.sh b/tools/perf/tests/shel= l/test_brstack.sh index 85233d435be6..025ed9d3d110 100755 --- a/tools/perf/tests/shell/test_brstack.sh +++ b/tools/perf/tests/shell/test_brstack.sh @@ -40,7 +40,7 @@ is_arm64() { =20 check_branches() { if ! tr -s ' ' '\n' < "$TMPDIR/perf.script" | grep -E -m1 -q "$1"; then - echo "Branches missing $1" + echo "ERROR: Branches missing $1" err=3D1 fi } @@ -48,6 +48,8 @@ check_branches() { test_user_branches() { echo "Testing user branch stack sampling" =20 + start_err=3D$err + err=3D0 perf record -o "$TMPDIR/perf.data" --branch-filter any,save_type,u -- ${T= ESTPROG} > "$TMPDIR/record.txt" 2>&1 perf script -i "$TMPDIR/perf.data" --fields brstacksym > "$TMPDIR/perf.sc= ript" =20 @@ -73,59 +75,80 @@ test_user_branches() { perf script -i "$TMPDIR/perf.data" --fields brstack | \ tr ' ' '\n' > "$TMPDIR/perf.script" =20 - # There should be no kernel addresses with the u option, in either - # source or target addresses. - if grep -E -m1 "0x[89a-f][0-9a-f]{15}" $TMPDIR/perf.script; then - echo "ERROR: Kernel address found in user mode" + # There should be no kernel addresses in the target with the u option. + if grep -q -E -m1 "^0x[0-9a-f]{0,16}/0x[89a-f][0-9a-f]{15}/" $TMPDIR/perf= .script; then + echo "Testing user branch stack sampling [Failed kernel address found in= user mode]" err=3D1 fi # some branch types are still not being tested: # IND COND_CALL COND_RET SYSRET SERROR NO_TX + if [ $err -eq 0 ]; then + echo "Testing user branch stack sampling [Passed]" + err=3D$start_err + else + echo "Testing user branch stack sampling [Failed]" + fi } =20 test_trap_eret_branches() { echo "Testing trap & eret branches" + if ! is_arm64; then - echo "skip: not arm64" + echo "Testing trap & eret branches [Skipped not arm64]" + return + fi + start_err=3D$err + err=3D0 + perf record -o $TMPDIR/perf.data --branch-filter any,save_type,u,k -- \ + perf test -w traploop 1000 > "$TMPDIR/record.txt" 2>&1 + perf script -i $TMPDIR/perf.data --fields brstacksym | \ + tr ' ' '\n' > $TMPDIR/perf.script + + # BRBINF.TYPE =3D=3D TRAP are mapped to PERF_BR_IRQ by the BRBE driver + check_branches "^trap_bench\+[^ ]+/[^ ]/IRQ/" + check_branches "^[^ ]+/trap_bench\+[^ ]+/ERET/" + if [ $err -eq 0 ]; then + echo "Testing trap & eret branches [Passed]" + err=3D$start_err else - perf record -o $TMPDIR/perf.data --branch-filter any,save_type,u,k -- \ - perf test -w traploop 1000 - perf script -i $TMPDIR/perf.data --fields brstacksym | \ - tr ' ' '\n' > $TMPDIR/perf.script - - # BRBINF.TYPE =3D=3D TRAP are mapped to PERF_BR_IRQ by the BRBE driver - check_branches "^trap_bench\+[^ ]+/[^ ]/IRQ/" - check_branches "^[^ ]+/trap_bench\+[^ ]+/ERET/" + echo "Testing trap & eret branches [Failed]" fi } =20 test_kernel_branches() { - echo "Testing that k option only includes kernel source addresses" + echo "Testing kernel branch sampling" =20 - if ! perf record --branch-filter any,k -o- -- true > /dev/null; then - echo "skip: not enough privileges" + if ! perf record --branch-filter any,k -o- -- true > "$TMPDIR/record.txt"= 2>&1; then + echo "Testing that k option [Skipped not enough privileges]" + return + fi + start_err=3D$err + err=3D0 + perf record -o $TMPDIR/perf.data --branch-filter any,k -- \ + perf bench syscall basic --loop 1000 > "$TMPDIR/record.txt" 2>&1 + perf script -i $TMPDIR/perf.data --fields brstack | \ + tr ' ' '\n' > $TMPDIR/perf.script + + # Example of branch entries: + # "0xffffffff93bda241/0xffffffff93bda20f/M/-/-/..." + # Source addresses come first in user or kernel code. Next is the target + # address that must be in the kernel. + + # Look for source addresses with top bit set + if ! grep -q -E -m1 "^0x[89a-f][0-9a-f]{15}" $TMPDIR/perf.script; then + echo "Testing kernel branch sampling [Failed kernel branches missing]" + err=3D1 + fi + # Look for no target addresses without top bit set + if grep -q -E -m1 "^0x[0-9a-f]{0,16}/0x[0-7][0-9a-f]{0,15}/" $TMPDIR/perf= .script; then + echo "Testing kernel branch sampling [Failed user branches found]" + err=3D1 + fi + if [ $err -eq 0 ]; then + echo "Testing kernel branch sampling [Passed]" + err=3D$start_err else - perf record -o $TMPDIR/perf.data --branch-filter any,k -- \ - perf bench syscall basic --loop 1000 - perf script -i $TMPDIR/perf.data --fields brstack | \ - tr ' ' '\n' > $TMPDIR/perf.script - - # Example of branch entries: - # "0xffffffff93bda241/0xffffffff93bda20f/M/-/-/..." - # Source addresses come first and target address can be either - # userspace or kernel even with k option, as long as the source - # is in kernel. - - #Look for source addresses with top bit set - if ! grep -E -m1 "^0x[89a-f][0-9a-f]{15}" $TMPDIR/perf.script; then - echo "ERROR: Kernel branches missing" - err=3D1 - fi - # Look for no source addresses without top bit set - if grep -E -m1 "^0x[0-7][0-9a-f]{0,15}" $TMPDIR/perf.script; then - echo "ERROR: User branches found with kernel filter" - err=3D1 - fi + echo "Testing kernel branch sampling [Failed]" fi } =20 @@ -136,14 +159,15 @@ test_filter() { test_filter_expect=3D$2 =20 echo "Testing branch stack filtering permutation ($test_filter_filter,$te= st_filter_expect)" - perf record -o "$TMPDIR/perf.data" --branch-filter "$test_filter_filter,s= ave_type,u" -- ${TESTPROG} > "$TMPDIR/record.txt" 2>&1 + perf record -o "$TMPDIR/perf.data" --branch-filter "$test_filter_filter,s= ave_type,u" -- \ + ${TESTPROG} > "$TMPDIR/record.txt" 2>&1 perf script -i "$TMPDIR/perf.data" --fields brstack > "$TMPDIR/perf.scrip= t" =20 # fail if we find any branch type that doesn't match any of the expected = ones # also consider UNKNOWN branch types (-) if [ ! -s "$TMPDIR/perf.script" ] then - echo "Empty script output" + echo "Testing branch stack filtering [Failed empty script output]" err=3D1 return fi @@ -154,26 +178,36 @@ test_filter() { > "$TMPDIR/perf.script-filtered" || true if [ -s "$TMPDIR/perf.script-filtered" ] then - echo "Unexpected branch filter in script output" + echo "Testing branch stack filtering [Failed unexpected branch filter]" cat "$TMPDIR/perf.script" err=3D1 return fi + echo "Testing branch stack filtering [Passed]" } =20 test_syscall() { echo "Testing syscalls" # skip if perf doesn't have enough privileges - if ! perf record --branch-filter any,k -o- -- true > /dev/null; then - echo "skip: not enough privileges" + if ! perf record --branch-filter any,k -o- -- true > "$TMPDIR/record.txt"= 2>&1; then + echo "Testing syscalls [Skipped: not enough privileges]" + return + fi + start_err=3D$err + err=3D0 + perf record -o $TMPDIR/perf.data --branch-filter \ + any_call,save_type,u,k -c 10007 -- \ + perf bench syscall basic --loop 8000 > "$TMPDIR/record.txt" 2>&1 + perf script -i $TMPDIR/perf.data --fields brstacksym | \ + tr ' ' '\n' > $TMPDIR/perf.script + + check_branches "getppid[^ ]*/SYSCALL/" + + if [ $err -eq 0 ]; then + echo "Testing syscalls [Passed]" + err=3D$start_err else - perf record -o $TMPDIR/perf.data --branch-filter \ - any_call,save_type,u,k -c 10000 -- \ - perf bench syscall basic --loop 1000 - perf script -i $TMPDIR/perf.data --fields brstacksym | \ - tr ' ' '\n' > $TMPDIR/perf.script - - check_branches "getppid[^ ]*/SYSCALL/" + echo "Testing syscalls [Failed]" fi } set -e --=20 2.53.0.1213.gd9a14994de-goog