Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

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.

Reviewed-by: Howard Chu <howardchu95@gmail.com>
Signed-off-by: Ian Rogers <irogers@google.com>
Tested-by: Arnaldo Carvalho de Melo <acme@redhat.com>
Cc: Adrian Hunter <adrian.hunter@intel.com>
Cc: Alexander Shishkin <alexander.shishkin@linux.intel.com>
Cc: Andi Kleen <ak@linux.intel.com>
Cc: Chun-Tse Shao <ctshao@google.com>
Cc: Collin Funk <collin.funk1@gmail.com>
Cc: Dr. David Alan Gilbert <linux@treblig.org>
Cc: Gautam Menghani <gautam@linux.ibm.com>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: James Clark <james.clark@linaro.org>
Cc: Jiri Olsa <jolsa@kernel.org>
Cc: Kan Liang <kan.liang@linux.intel.com>
Cc: Mark Rutland <mark.rutland@arm.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Namhyung Kim <namhyung@kernel.org>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Thomas Falcon <thomas.falcon@intel.com>
Cc: Thomas Richter <tmricht@linux.ibm.com>
Cc: Tiezhu Yang <yangtiezhu@loongson.cn>
Cc: Weilin Wang <weilin.wang@intel.com>
Cc: Xu Yang <xu.yang_2@nxp.com>
Link: https://lore.kernel.org/r/20250819013941.209033-11-irogers@google.com
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>

authored by

Ian Rogers and committed by
Arnaldo Carvalho de Melo
a3f4104d 47b3e957

+168 -58
+168 -58
tools/perf/python/ilist.py
··· 2 2 # SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) 3 3 """Interactive perf list.""" 4 4 5 + from abc import ABC, abstractmethod 5 6 import argparse 7 + from dataclasses import dataclass 8 + import math 6 9 from typing import Any, Dict, Optional, Tuple 7 10 import perf 8 11 from textual import on 9 12 from textual.app import App, ComposeResult 10 13 from textual.binding import Binding 11 14 from textual.containers import Horizontal, HorizontalGroup, Vertical, VerticalScroll 15 + from textual.css.query import NoMatches 12 16 from textual.command import SearchIcon 13 17 from textual.screen import ModalScreen 14 18 from textual.widgets import Button, Footer, Header, Input, Label, Sparkline, Static, Tree 15 19 from textual.widgets.tree import TreeNode 20 + 21 + 22 + def get_info(info: Dict[str, str], key: str): 23 + return (info[key] + "\n") if key in info else "" 24 + 25 + 26 + class TreeValue(ABC): 27 + """Abstraction for the data of value in the tree.""" 28 + 29 + @abstractmethod 30 + def name(self) -> str: 31 + pass 32 + 33 + @abstractmethod 34 + def description(self) -> str: 35 + pass 36 + 37 + @abstractmethod 38 + def matches(self, query: str) -> bool: 39 + pass 40 + 41 + @abstractmethod 42 + def parse(self) -> perf.evlist: 43 + pass 44 + 45 + @abstractmethod 46 + def value(self, evlist: perf.evlist, evsel: perf.evsel, cpu: int, thread: int) -> float: 47 + pass 48 + 49 + 50 + @dataclass 51 + class Metric(TreeValue): 52 + """A metric in the tree.""" 53 + metric_name: str 54 + 55 + def name(self) -> str: 56 + return self.metric_name 57 + 58 + def description(self) -> str: 59 + """Find and format metric description.""" 60 + for metric in perf.metrics(): 61 + if metric["MetricName"] != self.metric_name: 62 + continue 63 + desc = get_info(metric, "BriefDescription") 64 + desc += get_info(metric, "PublicDescription") 65 + desc += get_info(metric, "MetricExpr") 66 + desc += get_info(metric, "MetricThreshold") 67 + return desc 68 + return "description" 69 + 70 + def matches(self, query: str) -> bool: 71 + return query in self.metric_name 72 + 73 + def parse(self) -> perf.evlist: 74 + return perf.parse_metrics(self.metric_name) 75 + 76 + def value(self, evlist: perf.evlist, evsel: perf.evsel, cpu: int, thread: int) -> float: 77 + val = evlist.compute_metric(self.metric_name, cpu, thread) 78 + return 0 if math.isnan(val) else val 79 + 80 + 81 + @dataclass 82 + class PmuEvent(TreeValue): 83 + """A PMU and event within the tree.""" 84 + pmu: str 85 + event: str 86 + 87 + def name(self) -> str: 88 + if self.event.startswith(self.pmu) or ':' in self.event: 89 + return self.event 90 + else: 91 + return f"{self.pmu}/{self.event}/" 92 + 93 + def description(self) -> str: 94 + """Find and format event description for {pmu}/{event}/.""" 95 + for p in perf.pmus(): 96 + if p.name() != self.pmu: 97 + continue 98 + for info in p.events(): 99 + if "name" not in info or info["name"] != self.event: 100 + continue 101 + 102 + desc = get_info(info, "topic") 103 + desc += get_info(info, "event_type_desc") 104 + desc += get_info(info, "desc") 105 + desc += get_info(info, "long_desc") 106 + desc += get_info(info, "encoding_desc") 107 + return desc 108 + return "description" 109 + 110 + def matches(self, query: str) -> bool: 111 + return query in self.pmu or query in self.event 112 + 113 + def parse(self) -> perf.evlist: 114 + return perf.parse_events(self.name()) 115 + 116 + def value(self, evlist: perf.evlist, evsel: perf.evsel, cpu: int, thread: int) -> float: 117 + return evsel.read(cpu, thread).val 16 118 17 119 18 120 class ErrorScreen(ModalScreen[bool]): ··· 228 126 def __init__(self, interval: float) -> None: 229 127 self.interval = interval 230 128 self.evlist = None 231 - self.search_results: list[TreeNode[str]] = [] 232 - self.cur_search_result: TreeNode[str] | None = None 129 + self.selected: Optional[TreeValue] = None 130 + self.search_results: list[TreeNode[TreeValue]] = [] 131 + self.cur_search_result: TreeNode[TreeValue] | None = None 233 132 super().__init__() 234 133 235 134 def expand_and_select(self, node: TreeNode[Any]) -> None: ··· 248 145 l = len(self.search_results) 249 146 250 147 if l < 1: 251 - tree: Tree[str] = self.query_one("#pmus", Tree) 148 + tree: Tree[TreeValue] = self.query_one("#root", Tree) 252 149 if previous: 253 150 tree.action_cursor_up() 254 151 else: ··· 283 180 event = event.lower() 284 181 search_label.update(f'Searching for events matching "{event}"') 285 182 286 - tree: Tree[str] = self.query_one("#pmus", Tree) 183 + tree: Tree[str] = self.query_one("#root", Tree) 287 184 288 185 def find_search_results(event: str, node: TreeNode[str], 289 186 cursor_seen: bool = False, ··· 292 189 """Find nodes that match the search remembering the one after the cursor.""" 293 190 if not cursor_seen and node == tree.cursor_node: 294 191 cursor_seen = True 295 - if node.data and event in node.data: 192 + if node.data and node.data.matches(event): 296 193 if cursor_seen and not match_after_cursor: 297 194 match_after_cursor = node 298 195 self.search_results.append(node) ··· 306 203 self.search_results.clear() 307 204 (_, self.cur_search_result) = find_search_results(event, tree.root) 308 205 if len(self.search_results) < 1: 309 - self.push_screen(ErrorScreen(f"Failed to find pmu/event {event}")) 206 + self.push_screen(ErrorScreen(f"Failed to find pmu/event or metric {event}")) 310 207 search_label.display = False 311 208 elif self.cur_search_result: 312 209 self.expand_and_select(self.cur_search_result) ··· 324 221 self.set_searched_tree_node(previous=True) 325 222 326 223 def action_collapse(self) -> None: 327 - """Collapse the potentially large number of events under a PMU.""" 328 - tree: Tree[str] = self.query_one("#pmus", Tree) 224 + """Collapse the part of the tree currently on.""" 225 + tree: Tree[str] = self.query_one("#root", Tree) 329 226 node = tree.cursor_node 330 - if node and node.parent and node.parent.parent: 227 + if node and node.parent: 331 228 node.parent.collapse_all() 332 229 node.tree.scroll_to_node(node.parent) 333 230 334 231 def update_counts(self) -> None: 335 232 """Called every interval to update counts.""" 336 - if not self.evlist: 233 + if not self.selected or not self.evlist: 337 234 return 338 235 339 236 def update_count(cpu: int, count: int): ··· 362 259 for cpu in evsel.cpus(): 363 260 aggr = 0 364 261 for thread in evsel.threads(): 365 - counts = evsel.read(cpu, thread) 366 - aggr += counts.val 262 + aggr += self.selected.value(self.evlist, evsel, cpu, thread) 367 263 update_count(cpu, aggr) 368 264 total += aggr 369 265 update_count(-1, total) ··· 373 271 self.update_counts() 374 272 self.set_interval(self.interval, self.update_counts) 375 273 376 - def set_pmu_and_event(self, pmu: str, event: str) -> None: 274 + def set_selected(self, value: TreeValue) -> None: 377 275 """Updates the event/description and starts the counters.""" 276 + try: 277 + label_name = self.query_one("#event_name", Label) 278 + event_description = self.query_one("#event_description", Static) 279 + lines = self.query_one("#lines") 280 + except NoMatches: 281 + # A race with rendering, ignore the update as we can't 282 + # mount the assumed output widgets. 283 + return 284 + 285 + self.selected = value 286 + 378 287 # Remove previous event information. 379 288 if self.evlist: 380 289 self.evlist.disable() 381 290 self.evlist.close() 382 - lines = self.query(CounterSparkline) 383 - for line in lines: 291 + old_lines = self.query(CounterSparkline) 292 + for line in old_lines: 384 293 line.remove() 385 - lines = self.query(Counter) 386 - for line in lines: 387 - line.remove() 294 + old_counters = self.query(Counter) 295 + for counter in old_counters: 296 + counter.remove() 388 297 389 - def pmu_event_description(pmu: str, event: str) -> str: 390 - """Find and format event description for {pmu}/{event}/.""" 391 - def get_info(info: Dict[str, str], key: str): 392 - return (info[key] + "\n") if key in info else "" 393 - 394 - for p in perf.pmus(): 395 - if p.name() != pmu: 396 - continue 397 - for info in p.events(): 398 - if "name" not in info or info["name"] != event: 399 - continue 400 - 401 - desc = get_info(info, "topic") 402 - desc += get_info(info, "event_type_desc") 403 - desc += get_info(info, "desc") 404 - desc += get_info(info, "long_desc") 405 - desc += get_info(info, "encoding_desc") 406 - return desc 407 - return "description" 408 - 409 - # Parse event, update event text and description. 410 - full_name = event if event.startswith(pmu) or ':' in event else f"{pmu}/{event}/" 411 - self.query_one("#event_name", Label).update(full_name) 412 - self.query_one("#event_description", Static).update(pmu_event_description(pmu, event)) 298 + # Update event/metric text and description. 299 + label_name.update(value.name()) 300 + event_description.update(value.description()) 413 301 414 302 # Open the event. 415 303 try: 416 - self.evlist = perf.parse_events(full_name) 304 + self.evlist = value.parse() 417 305 if self.evlist: 418 306 self.evlist.open() 419 307 self.evlist.enable() ··· 411 319 self.evlist = None 412 320 413 321 if not self.evlist: 414 - self.push_screen(ErrorScreen(f"Failed to open {full_name}")) 322 + self.push_screen(ErrorScreen(f"Failed to open {value.name()}")) 415 323 return 416 324 417 325 # Add spark lines for all the CPUs. Note, must be done after 418 326 # open so that the evlist CPUs have been computed by propagate 419 327 # maps. 420 - lines = self.query_one("#lines") 421 328 line = CounterSparkline(cpu=-1) 422 329 lines.mount(line) 423 330 for cpu in self.evlist.all_cpus(): ··· 430 339 431 340 def compose(self) -> ComposeResult: 432 341 """Draws the app.""" 433 - def pmu_event_tree() -> Tree: 434 - """Create tree of PMUs with events under.""" 435 - tree: Tree[str] = Tree("PMUs", id="pmus") 436 - tree.root.expand() 342 + def metric_event_tree() -> Tree: 343 + """Create tree of PMUs and metricgroups with events or metrics under.""" 344 + tree: Tree[TreeValue] = Tree("Root", id="root") 345 + pmus = tree.root.add("PMUs") 437 346 for pmu in perf.pmus(): 438 347 pmu_name = pmu.name().lower() 439 - pmu_node = tree.root.add(pmu_name, data=pmu_name) 348 + pmu_node = pmus.add(pmu_name) 440 349 try: 441 350 for event in sorted(pmu.events(), key=lambda x: x["name"]): 442 351 if "name" in event: 443 352 e = event["name"].lower() 444 353 if "alias" in event: 445 - pmu_node.add_leaf(f'{e} ({event["alias"]})', data=e) 354 + pmu_node.add_leaf(f'{e} ({event["alias"]})', 355 + data=PmuEvent(pmu_name, e)) 446 356 else: 447 - pmu_node.add_leaf(e, data=e) 357 + pmu_node.add_leaf(e, data=PmuEvent(pmu_name, e)) 448 358 except: 449 359 # Reading events may fail with EPERM, ignore. 450 360 pass 361 + metrics = tree.root.add("Metrics") 362 + groups = set() 363 + for metric in perf.metrics(): 364 + groups.update(metric["MetricGroup"]) 365 + 366 + def add_metrics_to_tree(node: TreeNode[TreeValue], parent: str): 367 + for metric in sorted(perf.metrics(), key=lambda x: x["MetricName"]): 368 + if parent in metric["MetricGroup"]: 369 + name = metric["MetricName"] 370 + node.add_leaf(name, data=Metric(name)) 371 + child_group_name = f'{name}_group' 372 + if child_group_name in groups: 373 + add_metrics_to_tree(node.add(child_group_name), child_group_name) 374 + 375 + for group in sorted(groups): 376 + if group.endswith('_group'): 377 + continue 378 + add_metrics_to_tree(metrics.add(group), group) 379 + 380 + tree.root.expand() 451 381 return tree 452 382 453 383 yield Header(id="header") 454 - yield Horizontal(Vertical(pmu_event_tree(), id="events"), 384 + yield Horizontal(Vertical(metric_event_tree(), id="events"), 455 385 Vertical(Label("event name", id="event_name"), 456 386 Static("description", markup=False, id="event_description"), 457 387 )) ··· 481 369 yield Footer(id="footer") 482 370 483 371 @on(Tree.NodeSelected) 484 - def on_tree_node_selected(self, event: Tree.NodeSelected[str]) -> None: 372 + def on_tree_node_selected(self, event: Tree.NodeSelected[TreeValue]) -> None: 485 373 """Called when a tree node is selected, selecting the event.""" 486 - if event.node.parent and event.node.parent.parent: 487 - assert event.node.parent.data is not None 488 - assert event.node.data is not None 489 - self.set_pmu_and_event(event.node.parent.data, event.node.data) 374 + if event.node.data: 375 + self.set_selected(event.node.data) 490 376 491 377 492 378 if __name__ == "__main__":