From nobody Mon Oct 6 08:25:33 2025 Received: from mail-pf1-f201.google.com (mail-pf1-f201.google.com [209.85.210.201]) (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 C25DEA95C for ; Wed, 23 Jul 2025 23:23:55 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1753313037; cv=none; b=MaWMNnGUom3a9cwkB6GLwExZaL9WfeR/wWraq9Pi+4jJjqns9Ms77Ln8xy7YzkqlWTeapH0dKnkAUcsnN5E+JPF0ysWORRlOesdsHNW4XSM2lJXyKrPoatvisn0045hg0K2JKyUlE9bejUjkq2dpzTRB0YrxnRVOyqQfKUanH5Y= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1753313037; c=relaxed/simple; bh=0p2VZhsqQDr6Aw2LuSDZ/RNj1w9ofDx+04KMdddf5DA=; h=Date:In-Reply-To:Mime-Version:References:Message-ID:Subject:From: To:Cc:Content-Type; b=a8ycYCp3/Myc/KQQ3o+hfOxqsbO8BG8CC8ujwkvJw5hYHXunblb5/pkVWkhVJDI8nGLKDyLk6oXoDHBlVzEpOXXaTCuXZf6PDzD1q7z7fYaz1CotIt64S1ewOaxalNRbXOszUw6QK95HONcLhEz6vIR0xAL+tlafOI+fmZcpiHk= 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=jbAd1ZAq; arc=none smtp.client-ip=209.85.210.201 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="jbAd1ZAq" Received: by mail-pf1-f201.google.com with SMTP id d2e1a72fcca58-74927be2ec0so581040b3a.0 for ; Wed, 23 Jul 2025 16:23:55 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20230601; t=1753313035; x=1753917835; darn=vger.kernel.org; h=cc:to:from:subject:message-id:references:mime-version:in-reply-to :date:from:to:cc:subject:date:message-id:reply-to; bh=6NNH/8/lvsRr+ontcNP33Y53kfjTOg19B0JPdMFPpCw=; b=jbAd1ZAqnV7xcoF64uriKfOWJDZH0WWpi0809lrlGyDtGbbhmbkbGuSMCQzcef/AfY MOXMsqtw3eJBwxnVll66qNJyAZW5u2AYHDHPiv2LX/iHx9nr40AEWQLTz7Y0Fl5zBPyG 8Iq3niGsAKUjq7ZZQYLJLmyHPuXwtUgSQnPiHYpyzy8uYepoBeSaokpKfWW7xrz8KVHJ OKB0h876kHQgQ5rgu4ovchI9FVPDCgYvLKRR6jlvFFjKi3Ut2Q/8xkah9mvU0nBC42Cs 7ryUa/UCvUHTglk3Ror3o7jC7keMeNMy3vdVKVQ7DXYyRa8X6juYfGB9jCDP5Szwy9Fb pPZw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1753313035; x=1753917835; h=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; bh=6NNH/8/lvsRr+ontcNP33Y53kfjTOg19B0JPdMFPpCw=; b=OqSutfmPYkEB++lwa/G1txhasp5F4AD1POjvmzSSGzKk+k2AP2c0FbCW2zR5mzf/H8 WfPvqS3cQlXhQwepK7rKYGGzJKefmcNWdtb9aUlf9wIwpq+vu9/sDsRuwDgAnEcnRWVP Rv0hfcgVTUmMstt/qB58tHPuqMgkT6yrPW8BW78xG0+gslrPaDXUNbJzUKdjdGHvhXB3 2vCiXFLAkeBGfCZYFWWXP0aN0r8HAMR8ityWUdLKIeQceZJHrkcYhHw2Qw157QXl+rpO bfRFWCnmr0crq7tRlVkl4qYhVusCEgdG/iSHzlPWulOjCpCaVHviDNNmeoAV7Yx7rbjm EcNQ== X-Forwarded-Encrypted: i=1; AJvYcCWLo2rLDVRMoOOOsZmJIX7PIq2zup7QblXcd6tEqHOkaoVsT7tQcWV8YgkhJbQzIYSokEBitaZSsGLVdHo=@vger.kernel.org X-Gm-Message-State: AOJu0Yy5MpbESBDpwYmYDbuzUGJkttQR9hxt/JwEVrI4euO903g8gF8V JuyKZ5UnQBXtM7GvXbGqd1Gl0aVj90hh711EPxdYZPY3YcFyAvq1/kuhkHDz6hFtV5Z9y/mUjI8 Tgv81ycr7Ng== X-Google-Smtp-Source: AGHT+IE5/7Km/7y6LL7ZC1nZ1dlArFXrF9Rs15E8zp+UuKlziJbybSsz1F41qW+3osOWRyTHFJkvampGYN5J X-Received: from pfbde7.prod.google.com ([2002:a05:6a00:4687:b0:748:ed01:df48]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a05:6a00:2182:b0:736:35d4:f03f with SMTP id d2e1a72fcca58-76034c4cbadmr6204564b3a.6.1753313034970; Wed, 23 Jul 2025 16:23:54 -0700 (PDT) Date: Wed, 23 Jul 2025 16:22:17 -0700 In-Reply-To: <20250723232217.516179-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: <20250723232217.516179-1-irogers@google.com> X-Mailer: git-send-email 2.50.0.727.gbf7dc18ff4-goog Message-ID: <20250723232217.516179-17-irogers@google.com> Subject: [PATCH v8 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 Cc: Arnaldo Carvalho de Melo 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 Tested-by: Arnaldo Carvalho de Melo --- tools/perf/python/ilist.py | 232 +++++++++++++++++++++++++++---------- 1 file changed, 170 insertions(+), 62 deletions(-) diff --git a/tools/perf/python/ilist.py b/tools/perf/python/ilist.py index b21f4c93247e..8ecd4a9f5034 100755 --- a/tools/perf/python/ilist.py +++ b/tools/perf/python/ilist.py @@ -2,18 +2,119 @@ # 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 from textual.binding import Binding from textual.containers import Horizontal, HorizontalGroup, Vertical, Vert= icalScroll +from textual.css.query import NoMatches from textual.command import SearchIcon 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 +224,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 +247,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 +282,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 +304,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 +325,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 +364,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,47 +377,37 @@ 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.""" + try: + label_name =3D self.query_one("#event_name", Label) + event_description =3D self.query_one("#event_description", Sta= tic) + lines =3D self.query_one("#lines") + except NoMatches: + # A race with rendering, ignore the update as we can't + # mount the assumed output widgets. + return + + self.selected =3D value + # Remove previous event information. if self.evlist: self.evlist.disable() self.evlist.close() - lines =3D self.query(CounterSparkline) - for line in lines: - line.remove() - lines =3D self.query(Counter) - for line in lines: + old_lines =3D self.query(CounterSparkline) + for line in old_lines: line.remove() + old_counters =3D self.query(Counter) + for counter in old_counters: + counter.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. + label_name.update(value.name()) + event_description.update(value.description()) =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,13 +415,12 @@ 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 # open so that the evlist CPUs have been computed by propagate # maps. - lines =3D self.query_one("#lines") line =3D CounterSparkline(cpu=3D-1) lines.mount(line) for cpu in self.evlist.all_cpus(): @@ -345,28 +435,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 +486,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