Linux Perf Users
 help / color / mirror / Atom feed
* [PATCH v1 1/2] perf script: New treport script
@ 2025-07-25  8:24 Ian Rogers
  2025-07-25  8:24 ` [PATCH v1 2/2] perf script: treport add flamegraph support Ian Rogers
  2025-07-26  6:39 ` [PATCH v1 1/2] perf script: New treport script Namhyung Kim
  0 siblings, 2 replies; 24+ messages in thread
From: Ian Rogers @ 2025-07-25  8:24 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
	Namhyung Kim, Mark Rutland, Alexander Shishkin, Jiri Olsa,
	Ian Rogers, Adrian Hunter, Kan Liang, Alice Rogers, linux-kernel,
	linux-perf-users

From: Alice Rogers <alice.mei.rogers@gmail.com>

A textual app that displays the results of processing samples similar
to perf report. The app displays a tree of first processed 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.

Signed-off-by: Alice Rogers <alice.mei.rogers@gmail.com>
Co-developed-by: Ian Rogers <irogers@google.com>
Signed-off-by: Ian Rogers <irogers@google.com>
---
 tools/perf/scripts/python/treport.py | 177 +++++++++++++++++++++++++++
 1 file changed, 177 insertions(+)
 create mode 100644 tools/perf/scripts/python/treport.py

diff --git a/tools/perf/scripts/python/treport.py b/tools/perf/scripts/python/treport.py
new file mode 100644
index 000000000000..fd1ca79efdad
--- /dev/null
+++ b/tools/perf/scripts/python/treport.py
@@ -0,0 +1,177 @@
+# treport.py - perf report like tool written using textual
+# SPDX-License-Identifier: MIT
+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
+from typing import Dict
+
+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: "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, event: Dict) -> None:
+        """Processes a single profiling event to update the call stack tree.
+
+        Args:
+            event (Dict): A dictionary representing a single profiling sample,
+                          expected to contain keys like 'comm', 'pid', 'period',
+                          and 'callchain'.
+        """
+        pid = 0
+        if "sample" in event and "pid" in event["sample"]:
+            pid = event["sample"]["pid"]
+
+        if pid == 0:
+            comm = event.get("comm", "kernel")
+        else:
+            comm = f"{event.get('comm', 'unknown')} ({pid})"
+
+        period = int(event["period"]) if 'period' in event else 1
+        self.value += period
+
+        node = self.find_or_create_node(comm)
+        node.value += period
+
+        if "callchain" in event:
+            for entry in reversed(event["callchain"]):
+                sym = entry.get("sym")
+                name = None
+                if sym:
+                    name = sym.get("name")
+                if not name:
+                    name = entry.get("dso", "unknown")
+                    if "ip" in entry:
+                        name += f" 0x{entry['ip']:x}"
+                node = node.find_or_create_node(name)
+                node.value += period
+        else:
+            name = event.get("symbol")
+            if not name:
+                name = event.get("dso", "unknown")
+                if "ip" in event:
+                    name += f" 0x{event['ip']:x}"
+            node = node.find_or_create_node(name)
+            node.value += period
+
+    def add_to_tree(self, node: TreeNode, root_value: int) -> None:
+        """Recursively adds this node and its children to a textual TreeNode.
+
+        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 == 0:
+            root_value = self.value
+
+        # Calculate the percentage for the node, highlighting the
+        # percentage with reversed colors.
+        if root_value != 0:
+            percent = self.value / root_value * 100
+            label = f"{self.name} [r]{percent:.3g}%[/]"
+        else:
+            label = self.name
+
+        # Add a standalone leaf.
+        if not self.children:
+            node.add_leaf(label)
+            return
+
+        # Recursively add children.
+        new_node = node.add(label)
+        for pnode in sorted(self.children.values(),
+                            key=lambda pnode: pnode.value, reverse=True):
+            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 = [
+        Binding(key="^q", action="quit", description="Quit",
+                tooltip="Quit the app"),
+    ]
+
+    def __init__(self, root: ProfileNode):
+        """Initialize the application."""
+        super().__init__()
+        self.root = root
+
+    def make_report_tree(self) -> Tree:
+        """Make a Tree widget from the profile data."""
+        tree: Tree[None] = Tree("Profile")
+        # Add events to tree skipping the root.
+        for pnode in sorted(self.root.children.values(),
+                            key=lambda node: node.value, reverse=True):
+            pnode.add_to_tree(tree.root, root_value=0)
+        # Expand the first 2 levels of the tree.
+        tree.root.expand()
+        for tnode in tree.root.children:
+            tnode.expand()
+        return tree
+
+    def compose(self) -> ComposeResult:
+        """Composes the user interface of the application."""
+        yield Header()
+        with TabbedContent(initial="report"):
+            with TabPane("Report", id="report"):
+                yield self.make_report_tree()
+        yield Footer()
+
+
+class ProfileBuilder:
+    """Constructs a profile tree from a stream of events."""
+    def __init__(self):
+        self.root = ProfileNode("root", parent=None)
+
+    def process_event(self, event) -> None:
+        """Called by `perf script` to update the profile tree."""
+        ev_name = event.get("ev_name", "default")
+        ev_root = self.root.find_or_create_node(ev_name)
+        ev_root.process_event(event)
+
+if __name__ == "__main__":
+    # process_event is called for each perf event to build the profile.
+    profile = ProfileBuilder()
+    process_event = profile.process_event
+    # trace_end will run the application, this can't be done
+    # concurrently as perf expects to be the main thread as does
+    # Textual.
+    app = ReportApp(profile.root)
+    trace_end = app.run
-- 
2.50.1.552.g942d659e1b-goog


^ permalink raw reply related	[flat|nested] 24+ messages in thread
* [PATCH v2] perf test: Fixes for check branch stack sampling
@ 2026-04-08  6:20 Ian Rogers
  2026-04-08  6:58 ` [PATCH v3] " Ian Rogers
  0 siblings, 1 reply; 24+ messages in thread
From: Ian Rogers @ 2026-04-08  6:20 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
	Namhyung Kim, Alexander Shishkin, Jiri Olsa, Ian Rogers,
	Adrian Hunter, James Clark, German Gomez, linux-perf-users,
	linux-kernel

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 <irogers@google.com>
---
 tools/perf/tests/shell/test_brstack.sh | 133 ++++++++++++++++---------
 1 file changed, 84 insertions(+), 49 deletions(-)

diff --git a/tools/perf/tests/shell/test_brstack.sh b/tools/perf/tests/shell/test_brstack.sh
index 85233d435be6..51cd1dcd1855 100755
--- a/tools/perf/tests/shell/test_brstack.sh
+++ b/tools/perf/tests/shell/test_brstack.sh
@@ -40,7 +40,7 @@ is_arm64() {
 
 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=1
 	fi
 }
@@ -48,6 +48,8 @@ check_branches() {
 test_user_branches() {
 	echo "Testing user branch stack sampling"
 
+	start_err=$err
+	err=0
 	perf record -o "$TMPDIR/perf.data" --branch-filter any,save_type,u -- ${TESTPROG} > "$TMPDIR/record.txt" 2>&1
 	perf script -i "$TMPDIR/perf.data" --fields brstacksym > "$TMPDIR/perf.script"
 
@@ -73,59 +75,81 @@ test_user_branches() {
 	perf script -i "$TMPDIR/perf.data" --fields brstack | \
 		tr ' ' '\n' > "$TMPDIR/perf.script"
 
-	# 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=1
 	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=$start_err
+	else
+		echo "Testing user branch stack sampling [Failed]"
+	fi
 }
 
 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=$err
+	err=0
+	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<n>.TYPE == 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=$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<n>.TYPE == 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
 }
 
 test_kernel_branches() {
 	echo "Testing that k option only includes kernel source addresses"
 
-	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=$err
+	err=0
+	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 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 -q -E -m1 "^0x[89a-f][0-9a-f]{15}" $TMPDIR/perf.script; then
+		echo "Testing that k option [Failed kernel branches missing]"
+		err=1
+	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 that k option [Failed user branches found with kernel filter]"
+		err=1
+	fi
+	if [ $err -eq 0 ]; then
+		echo "Testing that k option [Passed]"
+		err=$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=1
-		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=1
-		fi
+		echo "Testing that k option [Failed]"
 	fi
 }
 
@@ -136,14 +160,15 @@ test_filter() {
 	test_filter_expect=$2
 
 	echo "Testing branch stack filtering permutation ($test_filter_filter,$test_filter_expect)"
-	perf record -o "$TMPDIR/perf.data" --branch-filter "$test_filter_filter,save_type,u" -- ${TESTPROG}  > "$TMPDIR/record.txt" 2>&1
+	perf record -o "$TMPDIR/perf.data" --branch-filter "$test_filter_filter,save_type,u" -- \
+		${TESTPROG}  > "$TMPDIR/record.txt" 2>&1
 	perf script -i "$TMPDIR/perf.data" --fields brstack > "$TMPDIR/perf.script"
 
 	# 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=1
 		return
 	fi
@@ -154,26 +179,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=1
 		return
 	fi
+	echo "Testing branch stack filtering [Passed]"
 }
 
 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=$err
+	err=0
+	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=$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
-- 
2.53.0.1213.gd9a14994de-goog


^ permalink raw reply related	[flat|nested] 24+ messages in thread

end of thread, other threads:[~2026-08-09  5:21 UTC | newest]

Thread overview: 24+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-07-25  8:24 [PATCH v1 1/2] perf script: New treport script Ian Rogers
2025-07-25  8:24 ` [PATCH v1 2/2] perf script: treport add flamegraph support Ian Rogers
2025-07-25  8:38   ` Ian Rogers
2026-08-08  6:57     ` [PATCH v2 0/2] perf python TUI report and flamegraph Ian Rogers
2026-08-08  6:57       ` [PATCH v2 1/2] perf python: New treport script Ian Rogers
2026-08-08  7:13         ` sashiko-bot
2026-08-08  6:57       ` [PATCH v2 2/2] perf python: treport add flamegraph support Ian Rogers
2026-08-09  4:16       ` [PATCH v3 0/2] perf python TUI report and flamegraph Ian Rogers
2026-08-09  4:16         ` [PATCH v3 1/2] perf python: New treport script Ian Rogers
2026-08-09  4:29           ` sashiko-bot
2026-08-09  4:16         ` [PATCH v3] perf test: Fixes for check branch stack sampling Ian Rogers
2026-08-09  5:21           ` Ian Rogers
2026-08-09  4:16         ` [PATCH v3 2/2] perf python: treport add flamegraph support Ian Rogers
2026-08-09  4:36           ` sashiko-bot
2026-08-09  5:18         ` [PATCH v4 0/2] perf python TUI report and flamegraph Ian Rogers
2026-08-09  5:18           ` [PATCH v4 1/2] perf python: New treport script Ian Rogers
2026-08-09  5:18           ` [PATCH v4 2/2] perf python: treport add flamegraph support Ian Rogers
2025-07-26  6:43   ` [PATCH v1 2/2] perf script: " Namhyung Kim
2025-07-26  6:39 ` [PATCH v1 1/2] perf script: New treport script Namhyung Kim
  -- strict thread matches above, loose matches on Subject: below --
2026-04-08  6:20 [PATCH v2] perf test: Fixes for check branch stack sampling Ian Rogers
2026-04-08  6:58 ` [PATCH v3] " Ian Rogers
2026-04-08  7:16   ` sashiko-bot
2026-04-08  7:40     ` Ian Rogers
2026-04-08 12:37   ` James Clark
2026-04-08 22:02     ` Ian Rogers

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox