From nobody Tue Oct 7 05:43:02 2025 Received: from mail-pl1-f202.google.com (mail-pl1-f202.google.com [209.85.214.202]) (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 D3FA726CE18 for ; Mon, 14 Jul 2025 16:44:44 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.214.202 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1752511486; cv=none; b=U9Ir6w1iDyqMWQSyQWPyoDBeonuy9DVbg/OnHw9bKZuXeh4vlWZqaSO/V5rz/JUXfW7L+01VX1sw1S3ugKcPR79pJV87kiSbBSoJmHhHdueC0EOcB45BaAcgxDLwnD8GFf8XYI/qyNxAn8eS2qLOve1bKSbyABMvaopwKy2nlwU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1752511486; c=relaxed/simple; bh=vXMUnBilME4tu2XzTt9CmDt/b7eelw3B6JPuIQ83nHk=; h=Date:In-Reply-To:Mime-Version:References:Message-ID:Subject:From: To:Content-Type; b=hUhnAyLk+PG4RIFdtHAS4PpU1NyE2yziBRPyhP7mR8kk7fc1GPvT2JEq81IUH+kGAC5G2AUp7EfXjiPmc2ndi/ZW6UPtVBAOk0K6IIudW+DihXBuYIiYDD3ORzuvK+OKbacntup2zhrBqW8z6eOGIrgGxA6ff8lxxeQA3uktI90= 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=yd185fWF; arc=none smtp.client-ip=209.85.214.202 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="yd185fWF" Received: by mail-pl1-f202.google.com with SMTP id d9443c01a7336-2369dd58602so43346205ad.1 for ; Mon, 14 Jul 2025 09:44:44 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20230601; t=1752511484; x=1753116284; darn=vger.kernel.org; h=to:from:subject:message-id:references:mime-version:in-reply-to:date :from:to:cc:subject:date:message-id:reply-to; bh=1O3ItjTkR+dtzaO+31J4vFqTfFByAiC07fEMVMoQW7E=; b=yd185fWF1zUL8YtXw/6dDqKOrvA0zd7VxJkxust0aKDzg/FeXlae2iRKbRsdIysuJ/ 0ZsQR6QkBTlFsaBIb3WE9Mpn7dLSrE8KEErVztj0PHwvC8Ar3oVKbmbFi3LE0nz0U8X5 aElj9CEp5mJI9toVprYLbqh85oL95EEu6pmhHOAWTsyDiMid/85aGKUa29+Ke3We5QlD aQ4pmd7grEUOMr/znxrrd4/tFWyfLFa5CsuMhy8QAYCuFE6Jf446+GvG9MR3iUpVylpi 4e/gQsPi6wScPZXgE9atnAnxseXVotAs7Nz6WYFrmLG7MikPMGKyly12zMWHM7Qpf9h0 /Mzw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1752511484; x=1753116284; h=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; bh=1O3ItjTkR+dtzaO+31J4vFqTfFByAiC07fEMVMoQW7E=; b=q77xgqIgbZGI4EKfxC9vpgD3rU90Lnl/MQXsz/olMTzOCbR0TrZF9EGvBcmqqMVsyn 63jXyQdfutj0NsUJtGZBBUfOTuFKM7ML14VTg+V6snkuzfbKf2xwPZUrC4lQiREERCXl ok0i0Zr5X4x7YI32f0S1qW5uDYeFTmNnX5n47BTMjl1WugwZaOJcy/lxDR4BnHAVJ8nZ YuaQ5m3rpKdTJLCwZaTl9ZJzaX4cyNILc+GFteodHL7N3JkQ8qohtypvuj5BuOvGpZFg CISYgn7Rj/VfFCStHP5iMBu3biIYoFHB3vXiphDESRd58zkDcwPRzagI55NeyyFDSg9Y gYOA== X-Forwarded-Encrypted: i=1; AJvYcCW5OJh0wqtR6XJEmNRMl76hC0hvZcEtDFEbN2Vq1YjqHbF4JDCfGTxGDBLWdYzLAvUH9DKoG7lI0pZ/3VI=@vger.kernel.org X-Gm-Message-State: AOJu0YwZwpdRLqdmnYZMVAlkDcZUg4VZWEsJH9gMNtxzrRTbQ19y1zPL nNwje1vJMGjV7EisqpGXl38K2Xo9XRQDqwmJU+TJjJDIgWNLQ3rYX8YFEfr0NxWE8SFfzjWnelf jl6ochu98oA== X-Google-Smtp-Source: AGHT+IHg8RvwQTF9Ugdtl5BnBrAIuUShWW8wSS6vf+Jl4GNAK5tNjcawVZkzrzKmm1OX9mZVXPLnUvHxgLHp X-Received: from plgs8.prod.google.com ([2002:a17:902:ea08:b0:234:a456:85ba]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a17:903:32ca:b0:236:6f5f:cab4 with SMTP id d9443c01a7336-23dee1888f5mr220339635ad.5.1752511483941; Mon, 14 Jul 2025 09:44:43 -0700 (PDT) Date: Mon, 14 Jul 2025 09:44:04 -0700 In-Reply-To: <20250714164405.111477-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: <20250714164405.111477-1-irogers@google.com> X-Mailer: git-send-email 2.50.0.727.gbf7dc18ff4-goog Message-ID: <20250714164405.111477-17-irogers@google.com> Subject: [PATCH v7 16/16] perf ilist: Add support for metrics From: Ian Rogers To: Peter Zijlstra , Ingo Molnar , Arnaldo Carvalho de Melo , Namhyung Kim , Mark Rutland , Alexander Shishkin , Jiri Olsa , Ian Rogers , Adrian Hunter , Kan Liang , James Clark , Xu Yang , "Masami Hiramatsu (Google)" , Collin Funk , Howard Chu , Weilin Wang , Andi Kleen , "Dr. David Alan Gilbert" , Thomas Richter , Tiezhu Yang , Gautam Menghani , Thomas Falcon , Chun-Tse Shao , linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 --- tools/perf/python/ilist.py | 211 +++++++++++++++++++++++++++---------- 1 file changed, 155 insertions(+), 56 deletions(-) diff --git a/tools/perf/python/ilist.py b/tools/perf/python/ilist.py index b21f4c93247e..188c3706b4c7 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.""" =20 +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, Sparklin= e, Static, Tree from textual.widgets.tree import TreeNode =20 +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, thre= ad: int) -> float: + pass + + +@dataclass +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"] !=3D self.metric_name: + continue + desc =3D get_info(metric, "BriefDescription") + desc +=3D get_info(metric, "PublicDescription") + desc +=3D get_info(metric, "MetricExpr") + desc +=3D 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, thre= ad: int) -> float: + val =3D evlist.compute_metric(self.metric_name, cpu, thread) + return 0 if math.isnan(val) else val + + +@dataclass +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() !=3D self.pmu: + continue + for info in p.events(): + if "name" not in info or info["name"] !=3D self.event: + continue + + desc =3D get_info(info, "topic") + desc +=3D get_info(info, "event_type_desc") + desc +=3D get_info(info, "desc") + desc +=3D get_info(info, "long_desc") + desc +=3D 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, thre= ad: int) -> float: + return evsel.read(cpu, thread).val + + class ErrorScreen(ModalScreen[bool]): """Pop up dialog for errors.""" =20 @@ -123,8 +223,9 @@ class IListApp(App): def __init__(self, interval: float) -> None: self.interval =3D interval self.evlist =3D None - self.search_results: list[TreeNode[str]] =3D [] - self.cur_search_result: TreeNode[str] | None =3D None + self.selected: Optional[TreeValue] =3D None + self.search_results: list[TreeNode[TreeValue]] =3D [] + self.cur_search_result: TreeNode[TreeValue] | None =3D None super().__init__() =20 =20 @@ -145,7 +246,7 @@ class IListApp(App): l =3D len(self.search_results) =20 if l < 1: - tree: Tree[str] =3D self.query_one("#pmus", Tree) + tree: Tree[TreeValue] =3D self.query_one("#root", Tree) if previous: tree.action_cursor_up() else: @@ -180,15 +281,15 @@ class IListApp(App): event =3D event.lower() search_label.update(f'Searching for events matching "{event}"') =20 - tree: Tree[str] =3D self.query_one("#pmus", Tree) - def find_search_results(event: str, node: TreeNode[str], \ + tree: Tree[TreeValue] =3D self.query_one("#root", Tree) + def find_search_results(event: str, node: TreeNode[TreeValue],= \ cursor_seen: bool =3D False, \ - match_after_cursor: TreeNode[str] | No= ne =3D None) \ - -> Tuple[bool, TreeNode[str] | None]: + match_after_cursor: TreeNode[TreeValue= ] | None =3D None) \ + -> Tuple[bool, TreeNode[TreeValue] | None]: """Find nodes that match the search remembering the one af= ter the cursor.""" if not cursor_seen and node =3D=3D tree.cursor_node: cursor_seen =3D 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 =3D node self.search_results.append(node) @@ -202,7 +303,7 @@ class IListApp(App): self.search_results.clear() (_ , self.cur_search_result) =3D find_search_results(event, tr= ee.root) if len(self.search_results) < 1: - self.push_screen(ErrorScreen(f"Failed to find pmu/event {e= vent}")) + self.push_screen(ErrorScreen(f"Failed to find pmu/event or= metric {event}")) search_label.display =3D False elif self.cur_search_result: self.expand_and_select(self.cur_search_result) @@ -223,17 +324,17 @@ class IListApp(App): =20 =20 def action_collapse(self) -> None: - """Collapse the potentially large number of events under a PMU.""" - tree: Tree[str] =3D self.query_one("#pmus", Tree) + """Collapse the part of the tree currently on.""" + tree: Tree[str] =3D self.query_one("#root", Tree) node =3D 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) =20 =20 def update_counts(self) -> None: """Called every interval to update counts.""" - if not self.evlist: + if not self.selected or not self.evlist: return =20 def update_count(cpu: int, count: int): @@ -262,8 +363,7 @@ class IListApp(App): for cpu in evsel.cpus(): aggr =3D 0 for thread in evsel.threads(): - counts =3D evsel.read(cpu, thread) - aggr +=3D counts.val + aggr +=3D self.selected.value(self.evlist, evsel, cpu,= thread) update_count(cpu, aggr) total +=3D aggr update_count(-1, total) @@ -276,8 +376,10 @@ class IListApp(App): self.set_interval(self.interval, self.update_counts) =20 =20 - 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 =3D value + # Remove previous event information. if self.evlist: self.evlist.disable() @@ -289,34 +391,13 @@ class IListApp(App): for line in lines: line.remove() =20 - 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() !=3D pmu: - continue - for info in p.events(): - if "name" not in info or info["name"] !=3D event: - continue - - desc =3D get_info(info, "topic") - desc +=3D get_info(info, "event_type_desc") - desc +=3D get_info(info, "desc") - desc +=3D get_info(info, "long_desc") - desc +=3D get_info(info, "encoding_desc") - return desc - return "description" - - # Parse event, update event text and description. - full_name =3D 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_desc= ription(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.descript= ion()) =20 # Open the event. try: - self.evlist =3D perf.parse_events(full_name) + self.evlist =3D value.parse() if self.evlist: self.evlist.open() self.evlist.enable() @@ -324,7 +405,7 @@ class IListApp(App): self.evlist =3D None =20 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 =20 # Add spark lines for all the CPUs. Note, must be done after @@ -345,28 +426,48 @@ class IListApp(App): =20 def compose(self) -> ComposeResult: """Draws the app.""" - def pmu_event_tree() -> Tree: - """Create tree of PMUs with events under.""" - tree: Tree[str] =3D Tree("PMUs", id=3D"pmus") - tree.root.expand() + def metric_event_tree() -> Tree: + """Create tree of PMUs and metricgroups with events or metrics= under.""" + tree: Tree[TreeValue] =3D Tree("Root", id=3D"root") + pmus =3D tree.root.add("PMUs") for pmu in perf.pmus(): pmu_name =3D pmu.name().lower() - pmu_node =3D tree.root.add(pmu_name, data=3Dpmu_name) + pmu_node =3D pmus.add(pmu_name) try: for event in sorted(pmu.events(), key=3Dlambda x: x["n= ame"]): if "name" in event: e =3D event["name"].lower() if "alias" in event: - pmu_node.add_leaf(f'{e} ({event["alias"]})= ', data=3De) + pmu_node.add_leaf(f'{e} ({event["alias"]})= ', data=3DPmuEvent(pmu_name, e)) else: - pmu_node.add_leaf(e, data=3De) + pmu_node.add_leaf(e, data=3DPmuEvent(pmu_n= ame, e)) except: # Reading events may fail with EPERM, ignore. pass + metrics =3D tree.root.add("Metrics") + groups =3D 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=3Dlambda x: x["Me= tricName"]): + if parent in metric["MetricGroup"]: + name =3D metric["MetricName"] + node.add_leaf(name, data=3DMetric(name)) + child_group_name =3D 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 =20 yield Header(id=3D"header") - yield Horizontal(Vertical(pmu_event_tree(), id=3D"events"), + yield Horizontal(Vertical(metric_event_tree(), id=3D"events"), Vertical(Label("event name", id=3D"event_name"), Static("description", markup=3DFalse, id= =3D"event_description"), )) @@ -376,12 +477,10 @@ class IListApp(App): =20 =20 @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) =20 =20 if __name__ =3D=3D "__main__": --=20 2.50.0.727.gbf7dc18ff4-goog