[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20250710235735.1089240-15-irogers@google.com>
Date: Thu, 10 Jul 2025 16:57:35 -0700
From: Ian Rogers <irogers@...gle.com>
To: Peter Zijlstra <peterz@...radead.org>, Ingo Molnar <mingo@...hat.com>,
Arnaldo Carvalho de Melo <acme@...nel.org>, Namhyung Kim <namhyung@...nel.org>,
Mark Rutland <mark.rutland@....com>,
Alexander Shishkin <alexander.shishkin@...ux.intel.com>, Jiri Olsa <jolsa@...nel.org>,
Ian Rogers <irogers@...gle.com>, Adrian Hunter <adrian.hunter@...el.com>,
Kan Liang <kan.liang@...ux.intel.com>, James Clark <james.clark@...aro.org>,
Xu Yang <xu.yang_2@....com>, "Masami Hiramatsu (Google)" <mhiramat@...nel.org>,
Collin Funk <collin.funk1@...il.com>, Howard Chu <howardchu95@...il.com>,
Weilin Wang <weilin.wang@...el.com>, Andi Kleen <ak@...ux.intel.com>,
"Dr. David Alan Gilbert" <linux@...blig.org>, Thomas Richter <tmricht@...ux.ibm.com>,
Tiezhu Yang <yangtiezhu@...ngson.cn>, Gautam Menghani <gautam@...ux.ibm.com>,
Thomas Falcon <thomas.falcon@...el.com>, Chun-Tse Shao <ctshao@...gle.com>,
linux-kernel@...r.kernel.org, linux-perf-users@...r.kernel.org
Subject: [PATCH v6 14/14] perf ilist: Add support for metrics
Change tree nodes to having a value of either Metric or PmuEvent,
these values have the ability to match searches, be parsed to create
evlists and to give a value per CPU and per thread to display. Use
perf.metrics to generate a tree of metrics. Most metrics are placed
under their metric group, if the metric group name ends with '_group'
then the metric group is placed next to the associated metric.
Signed-off-by: Ian Rogers <irogers@...gle.com>
---
tools/perf/python/ilist.py | 207 +++++++++++++++++++++++++++----------
1 file changed, 153 insertions(+), 54 deletions(-)
diff --git a/tools/perf/python/ilist.py b/tools/perf/python/ilist.py
index 3e2fae1db738..0d46b44d2961 100755
--- a/tools/perf/python/ilist.py
+++ b/tools/perf/python/ilist.py
@@ -2,8 +2,11 @@
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
"""Interactive perf list."""
+from abc import ABC, abstractmethod
import argparse
-from typing import Any, Dict, Tuple
+from dataclasses import dataclass
+from typing import Any, Dict, Optional, Tuple
+import math
import perf
from textual import on
from textual.app import App, ComposeResult
@@ -14,6 +17,103 @@ from textual.screen import ModalScreen
from textual.widgets import Button, Footer, Header, Input, Label, Sparkline, Static, Tree
from textual.widgets.tree import TreeNode
+def get_info(info: Dict[str, str], key: str):
+ return (info[key] + "\n") if key in info else ""
+
+class TreeValue(ABC):
+ """Abstraction for the data of value in the tree."""
+
+ @abstractmethod
+ def name(self) -> str:
+ pass
+
+ @abstractmethod
+ def description(self) -> str:
+ pass
+
+ @abstractmethod
+ def matches(self, query: str) -> bool:
+ pass
+
+ @abstractmethod
+ def parse(self) -> perf.evlist:
+ pass
+
+ @abstractmethod
+ def value(self, evlist: perf.evlist, evsel: perf.evsel, cpu: int, thread: int) -> float:
+ pass
+
+
+@...aclass
+class Metric(TreeValue):
+ """A metric in the tree."""
+ metric_name: str
+
+ def name(self) -> str:
+ return self.metric_name
+
+ def description(self) -> str:
+ """Find and format metric description."""
+ for metric in perf.metrics():
+ if metric["MetricName"] != self.metric_name:
+ continue
+ desc = get_info(metric, "BriefDescription")
+ desc += get_info(metric, "PublicDescription")
+ desc += get_info(metric, "MetricExpr")
+ desc += get_info(metric, "MetricThreshold")
+ return desc
+ return "description"
+
+ def matches(self, query: str) -> bool:
+ return query in self.metric_name
+
+ def parse(self) -> perf.evlist:
+ return perf.parse_metrics(self.metric_name)
+
+ def value(self, evlist: perf.evlist, evsel: perf.evsel, cpu: int, thread: int) -> float:
+ val = evlist.compute_metric(self.metric_name, cpu, thread)
+ return 0 if math.isnan(val) else val
+
+
+@...aclass
+class PmuEvent(TreeValue):
+ """A PMU and event within the tree."""
+ pmu: str
+ event: str
+
+ def name(self) -> str:
+ if self.event.startswith(self.pmu) or ':' in self.event:
+ return self.event
+ else:
+ return f"{self.pmu}/{self.event}/"
+
+ def description(self) -> str:
+ """Find and format event description for {pmu}/{event}/."""
+ for p in perf.pmus():
+ if p.name() != self.pmu:
+ continue
+ for info in p.events():
+ if "name" not in info or info["name"] != self.event:
+ continue
+
+ desc = get_info(info, "topic")
+ desc += get_info(info, "event_type_desc")
+ desc += get_info(info, "desc")
+ desc += get_info(info, "long_desc")
+ desc += get_info(info, "encoding_desc")
+ return desc
+ return "description"
+
+ def matches(self, query: str) -> bool:
+ return query in self.pmu or query in self.event
+
+ def parse(self) -> perf.evlist:
+ return perf.parse_events(self.name())
+
+ def value(self, evlist: perf.evlist, evsel: perf.evsel, cpu: int, thread: int) -> float:
+ return evsel.read(cpu, thread).val
+
+
class ErrorScreen(ModalScreen[bool]):
"""Pop up dialog for errors."""
@@ -123,8 +223,9 @@ class IListApp(App):
def __init__(self, interval: float) -> None:
self.interval = interval
self.evlist = None
- self.search_results: list[TreeNode[str]] = []
- self.cur_search_result: TreeNode[str] | None = None
+ self.selected: Optional[TreeValue] = None
+ self.search_results: list[TreeNode[TreeValue]] = []
+ self.cur_search_result: TreeNode[TreeValue] | None = None
super().__init__()
@@ -180,15 +281,15 @@ class IListApp(App):
event = event.lower()
search_label.update(f'Searching for events matching "{event}"')
- tree: Tree[str] = self.query_one("#pmus", Tree)
- def find_search_results(event: str, node: TreeNode[str], \
+ tree: Tree[TreeValue] = self.query_one("#root", Tree)
+ def find_search_results(event: str, node: TreeNode[TreeValue], \
cursor_seen: bool = False, \
- match_after_cursor: TreeNode[str] | None = None) \
- -> Tuple[bool, TreeNode[str] | None]:
+ match_after_cursor: TreeNode[TreeValue] | None = None) \
+ -> Tuple[bool, TreeNode[TreeValue] | None]:
"""Find nodes that match the search remembering the one after the cursor."""
if not cursor_seen and node == tree.cursor_node:
cursor_seen = True
- if node.data and event in node.data:
+ if node.data and node.data.matches(event):
if cursor_seen and not match_after_cursor:
match_after_cursor = node
self.search_results.append(node)
@@ -222,17 +323,17 @@ class IListApp(App):
def action_collapse(self) -> None:
- """Collapse the potentially large number of events under a PMU."""
- tree: Tree[str] = self.query_one("#pmus", Tree)
+ """Collapse the part of the tree currently on."""
+ tree: Tree[str] = self.query_one("#root", Tree)
node = tree.cursor_node
- if node and node.parent and node.parent.parent:
+ if node and node.parent:
node.parent.collapse_all()
node.tree.scroll_to_node(node.parent)
def update_counts(self) -> None:
"""Called every interval to update counts."""
- if not self.evlist:
+ if not self.selected or not self.evlist:
return
def update_count(cpu: int, count: int):
@@ -261,8 +362,7 @@ class IListApp(App):
for cpu in evsel.cpus():
aggr = 0
for thread in evsel.threads():
- counts = evsel.read(cpu, thread)
- aggr += counts.val
+ aggr += self.selected.value(self.evlist, evsel, cpu, thread)
update_count(cpu, aggr)
total += aggr
update_count(-1, total)
@@ -275,8 +375,10 @@ class IListApp(App):
self.set_interval(self.interval, self.update_counts)
- def set_pmu_and_event(self, pmu: str, event: str) -> None:
+ def set_selected(self, value: TreeValue) -> None:
"""Updates the event/description and starts the counters."""
+ self.selected = value
+
# Remove previous event information.
if self.evlist:
self.evlist.disable()
@@ -288,34 +390,13 @@ class IListApp(App):
for line in lines:
line.remove()
- def pmu_event_description(pmu: str, event: str) -> str:
- """Find and format event description for {pmu}/{event}/."""
- def get_info(info: Dict[str, str], key: str):
- return (info[key] + "\n") if key in info else ""
-
- for p in perf.pmus():
- if p.name() != pmu:
- continue
- for info in p.events():
- if "name" not in info or info["name"] != event:
- continue
-
- desc = get_info(info, "topic")
- desc += get_info(info, "event_type_desc")
- desc += get_info(info, "desc")
- desc += get_info(info, "long_desc")
- desc += get_info(info, "encoding_desc")
- return desc
- return "description"
-
- # Parse event, update event text and description.
- full_name = event if event.startswith(pmu) or ':' in event else f"{pmu}/{event}/"
- self.query_one("#event_name", Label).update(full_name)
- self.query_one("#event_description", Static).update(pmu_event_description(pmu, event))
+ # Update event/metric text and description.
+ self.query_one("#event_name", Label).update(value.name())
+ self.query_one("#event_description", Static).update(value.description())
# Open the event.
try:
- self.evlist = perf.parse_events(full_name)
+ self.evlist = value.parse()
if self.evlist:
self.evlist.open()
self.evlist.enable()
@@ -323,7 +404,7 @@ class IListApp(App):
self.evlist = None
if not self.evlist:
- self.push_screen(ErrorScreen(f"Failed to open {full_name}"))
+ self.push_screen(ErrorScreen(f"Failed to open {value.name()}"))
return
# Add spark lines for all the CPUs. Note, must be done after
@@ -344,24 +425,44 @@ class IListApp(App):
def compose(self) -> ComposeResult:
"""Draws the app."""
- def pmu_event_tree() -> Tree:
- """Create tree of PMUs with events under."""
- tree: Tree[str] = Tree("PMUs", id="pmus")
- tree.root.expand()
+ def metric_event_tree() -> Tree:
+ """Create tree of PMUs and metricgroups with events or metrics under."""
+ tree: Tree[TreeValue] = Tree("Root", id="root")
+ pmus = tree.root.add("PMUs")
for pmu in perf.pmus():
pmu_name = pmu.name().lower()
- pmu_node = tree.root.add(pmu_name, data=pmu_name)
+ pmu_node = pmus.add(pmu_name)
for event in sorted(pmu.events(), key=lambda x: x["name"]):
if "name" in event:
e = event["name"].lower()
if "alias" in event:
- pmu_node.add_leaf(f'{e} ({event["alias"]})', data=e)
+ pmu_node.add_leaf(f'{e} ({event["alias"]})', data=PmuEvent(pmu_name, e))
else:
- pmu_node.add_leaf(e, data=e)
+ pmu_node.add_leaf(e, data=PmuEvent(pmu_name, e))
+ metrics = tree.root.add("Metrics")
+ groups = set()
+ for metric in perf.metrics():
+ groups.update(metric["MetricGroup"])
+
+ def add_metrics_to_tree(node: TreeNode[TreeValue], parent: str):
+ for metric in sorted(perf.metrics(), key=lambda x: x["MetricName"]):
+ if parent in metric["MetricGroup"]:
+ name = metric["MetricName"]
+ node.add_leaf(name, data=Metric(name))
+ child_group_name = f'{name}_group'
+ if child_group_name in groups:
+ add_metrics_to_tree(node.add(child_group_name), child_group_name)
+
+ for group in sorted(groups):
+ if group.endswith('_group'):
+ continue
+ add_metrics_to_tree(metrics.add(group), group)
+
+ tree.root.expand()
return tree
yield Header(id="header")
- yield Horizontal(Vertical(pmu_event_tree(), id="events"),
+ yield Horizontal(Vertical(metric_event_tree(), id="events"),
Vertical(Label("event name", id="event_name"),
Static("description", markup=False, id="event_description"),
))
@@ -371,12 +472,10 @@ class IListApp(App):
@on(Tree.NodeSelected)
- def on_tree_node_selected(self, event: Tree.NodeSelected[str]) -> None:
+ def on_tree_node_selected(self, event: Tree.NodeSelected[TreeValue]) -> None:
"""Called when a tree node is selected, selecting the event."""
- if event.node.parent and event.node.parent.parent:
- assert event.node.parent.data is not None
- assert event.node.data is not None
- self.set_pmu_and_event(event.node.parent.data, event.node.data)
+ if event.node.data:
+ self.set_selected(event.node.data)
if __name__ == "__main__":
--
2.50.0.727.gbf7dc18ff4-goog
Powered by blists - more mailing lists