emdbg.debug.px4.perf

  1# Copyright (c) 2023, Auterion AG
  2# SPDX-License-Identifier: BSD-3-Clause
  3
  4from __future__ import annotations
  5from .base import Base
  6from .utils import format_units
  7from typing import Callable, Any
  8import rich.box, rich.markup
  9from rich.table import Table
 10from functools import cached_property
 11import math
 12
 13
 14class PerfCounter(Base):
 15    """
 16    Pretty Printing Perf Counters
 17    """
 18
 19    def __init__(self, gdb, perf_ptr: "gdb.Value"):
 20        super().__init__(gdb)
 21        self._perf = perf_ptr.cast(gdb.lookup_type("struct perf_ctr_count").pointer())
 22        if self.type == "PC_ELAPSED":
 23            self._perf = self._perf.cast(gdb.lookup_type("struct perf_ctr_elapsed").pointer())
 24        elif self.type == "PC_INTERVAL":
 25            self._perf = self._perf.cast(gdb.lookup_type("struct perf_ctr_interval").pointer())
 26        # print(self._perf, self.short_type, self.events, self.name)
 27
 28    @cached_property
 29    def name(self) -> str:
 30        """Name of the counter"""
 31        try:
 32            return self._perf["name"].string()
 33        except:
 34            return "?"
 35
 36    @cached_property
 37    def events(self) -> int:
 38        """How many events were counted"""
 39        return int(self._perf["event_count"])
 40
 41    @cached_property
 42    def _types(self):
 43        return self._gdb.types.make_enum_dict(self._gdb.lookup_type("enum perf_counter_type"))
 44
 45    @cached_property
 46    def type(self) -> str:
 47        """Counter type name"""
 48        for name, value in self._types.items():
 49            if value == self._perf["type"]:
 50                return name
 51        return "UNKNOWN"
 52
 53    @cached_property
 54    def short_type(self) -> str:
 55        """The short name of the type"""
 56        return self.type.replace("PC_", "").capitalize()
 57
 58    @cached_property
 59    def elapsed(self) -> int | None:
 60        """
 61        How much time has elapsed in microseconds.
 62        Only applies to Elapsed counters.
 63        """
 64        if self.type == "PC_ELAPSED":
 65            return int(self._perf["time_total"])
 66        return None
 67
 68    @cached_property
 69    def first(self) -> int | None:
 70        """
 71        The first time in microseconds.
 72        Only applies to Interval counters.
 73        """
 74        if self.type == "PC_INTERVAL":
 75            return int(self._perf["time_first"])
 76        return None
 77
 78    @cached_property
 79    def last(self) -> int | None:
 80        """
 81        The last time in microseconds.
 82        Only applies to Interval counters.
 83        """
 84        if self.type == "PC_INTERVAL":
 85            return int(self._perf["time_last"])
 86        return None
 87
 88    @cached_property
 89    def interval(self) -> int | None:
 90        """
 91        The interval time in microseconds.
 92        Only applies to Interval counters.
 93        """
 94        if self.type == "PC_INTERVAL":
 95            return self.last - self.first
 96        return None
 97
 98    @cached_property
 99    def average(self) -> int | None:
100        """
101        The average time in microseconds.
102        Only applies to Elapsed and Interval counters.
103        """
104        if self.type == "PC_ELAPSED":
105            return self.elapsed / self.events if self.events else 0
106        elif self.type == "PC_INTERVAL":
107            return (self.last - self.first) / self.events if self.events else 0
108        return None
109
110    @cached_property
111    def least(self) -> int | None:
112        """
113        The least time in microseconds.
114        Only applies to Elapsed and Interval counters.
115        """
116        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
117            return int(self._perf["time_least"])
118        return None
119
120    @cached_property
121    def most(self) -> int | None:
122        """
123        The most time in microseconds.
124        Only applies to Elapsed and Interval counters.
125        """
126        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
127            return int(self._perf["time_most"])
128        return None
129
130    @cached_property
131    def rms(self) -> int | None:
132        """
133        The root mean square in microseconds.
134        Only applies to Elapsed and Interval counters.
135        """
136        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
137            return 1e6 * math.sqrt(float(self._perf["M2"]) / (self.events - 1)) if self.events > 1 else 0
138        return None
139
140
141_PREVIOUS_COUNTERS = {}
142def all_perf_counters_as_table(gdb, filter_: Callable[[PerfCounter], bool] = None,
143                               sort_key: Callable[[PerfCounter], Any] = None) -> Table | None:
144    """
145    Pretty print all perf counters as a table. Counters that did not change
146    since the last call are dimmed.
147
148    :param filter_: A function to filter the perf counters.
149    :param sort_key: A function to sort the perf counters by key.
150    :returns: A rich table with all perf counters or `None` if no counters found.
151    """
152    if (queue := gdb.lookup_static_symbol("perf_counters")) is None:
153        return None
154    queue = queue.value()
155    item, tail = queue["head"], queue["tail"]
156    counters = []
157    loop_count = 0
158    while item and item != tail:
159        pc = PerfCounter(gdb, item)
160        counters.append(pc)
161        item = item["flink"]
162        loop_count += 1
163        if loop_count > 1000: break
164    # Filter may result in no matches
165    if not counters:
166        return None
167
168    global _PREVIOUS_COUNTERS
169    changed = {c for c in counters
170               if (events := _PREVIOUS_COUNTERS.get(int(c._perf))) is not None
171               and events != c.events}
172    _PREVIOUS_COUNTERS |= {int(c._perf): c.events for c in counters}
173
174    # Filter out the counters
175    if filter_ is not None:
176        counters = [c for c in counters if filter_(c)]
177        if not counters:
178            return None
179
180    table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
181    table.add_column("perf_ctr_count*", justify="right", no_wrap=True)
182    table.add_column("Name")
183    table.add_column("Events", justify="right")
184    table.add_column("Elapsed", justify="right")
185    table.add_column("Average", justify="right")
186    table.add_column("Least", justify="right")
187    table.add_column("Most", justify="right")
188    table.add_column("RMS", justify="right")
189    table.add_column("Interval", justify="right")
190    table.add_column("First", justify="right")
191    table.add_column("Last", justify="right")
192
193    # Sort the rows by name by default and format the table
194    for counter in sorted(counters, key=sort_key or (lambda p: p.name)):
195        table.add_row(hex(counter._perf), rich.markup.escape(counter.name), str(counter.events),
196                      format_units(counter.elapsed, "t:µs", fmt=".1f", if_zero="-"),
197                      format_units(counter.average, "t:µs", fmt=".1f", if_zero="-"),
198                      format_units(counter.least, "t:µs", fmt=".1f", if_zero="-"),
199                      format_units(counter.most, "t:µs", fmt=".1f", if_zero="-"),
200                      format_units(counter.rms, "t:µs", fmt=".3f", if_zero="-"),
201                      format_units(counter.interval, "t:µs", fmt=".1f", if_zero="-"),
202                      format_units(counter.first, "t:µs", fmt=".1f", if_zero="-"),
203                      format_units(counter.last, "t:µs", fmt=".1f", if_zero="-"),
204                      style="dim" if changed and counter not in changed else None)
205    return table
class PerfCounter(emdbg.debug.px4.base.Base):
 15class PerfCounter(Base):
 16    """
 17    Pretty Printing Perf Counters
 18    """
 19
 20    def __init__(self, gdb, perf_ptr: "gdb.Value"):
 21        super().__init__(gdb)
 22        self._perf = perf_ptr.cast(gdb.lookup_type("struct perf_ctr_count").pointer())
 23        if self.type == "PC_ELAPSED":
 24            self._perf = self._perf.cast(gdb.lookup_type("struct perf_ctr_elapsed").pointer())
 25        elif self.type == "PC_INTERVAL":
 26            self._perf = self._perf.cast(gdb.lookup_type("struct perf_ctr_interval").pointer())
 27        # print(self._perf, self.short_type, self.events, self.name)
 28
 29    @cached_property
 30    def name(self) -> str:
 31        """Name of the counter"""
 32        try:
 33            return self._perf["name"].string()
 34        except:
 35            return "?"
 36
 37    @cached_property
 38    def events(self) -> int:
 39        """How many events were counted"""
 40        return int(self._perf["event_count"])
 41
 42    @cached_property
 43    def _types(self):
 44        return self._gdb.types.make_enum_dict(self._gdb.lookup_type("enum perf_counter_type"))
 45
 46    @cached_property
 47    def type(self) -> str:
 48        """Counter type name"""
 49        for name, value in self._types.items():
 50            if value == self._perf["type"]:
 51                return name
 52        return "UNKNOWN"
 53
 54    @cached_property
 55    def short_type(self) -> str:
 56        """The short name of the type"""
 57        return self.type.replace("PC_", "").capitalize()
 58
 59    @cached_property
 60    def elapsed(self) -> int | None:
 61        """
 62        How much time has elapsed in microseconds.
 63        Only applies to Elapsed counters.
 64        """
 65        if self.type == "PC_ELAPSED":
 66            return int(self._perf["time_total"])
 67        return None
 68
 69    @cached_property
 70    def first(self) -> int | None:
 71        """
 72        The first time in microseconds.
 73        Only applies to Interval counters.
 74        """
 75        if self.type == "PC_INTERVAL":
 76            return int(self._perf["time_first"])
 77        return None
 78
 79    @cached_property
 80    def last(self) -> int | None:
 81        """
 82        The last time in microseconds.
 83        Only applies to Interval counters.
 84        """
 85        if self.type == "PC_INTERVAL":
 86            return int(self._perf["time_last"])
 87        return None
 88
 89    @cached_property
 90    def interval(self) -> int | None:
 91        """
 92        The interval time in microseconds.
 93        Only applies to Interval counters.
 94        """
 95        if self.type == "PC_INTERVAL":
 96            return self.last - self.first
 97        return None
 98
 99    @cached_property
100    def average(self) -> int | None:
101        """
102        The average time in microseconds.
103        Only applies to Elapsed and Interval counters.
104        """
105        if self.type == "PC_ELAPSED":
106            return self.elapsed / self.events if self.events else 0
107        elif self.type == "PC_INTERVAL":
108            return (self.last - self.first) / self.events if self.events else 0
109        return None
110
111    @cached_property
112    def least(self) -> int | None:
113        """
114        The least time in microseconds.
115        Only applies to Elapsed and Interval counters.
116        """
117        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
118            return int(self._perf["time_least"])
119        return None
120
121    @cached_property
122    def most(self) -> int | None:
123        """
124        The most time in microseconds.
125        Only applies to Elapsed and Interval counters.
126        """
127        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
128            return int(self._perf["time_most"])
129        return None
130
131    @cached_property
132    def rms(self) -> int | None:
133        """
134        The root mean square in microseconds.
135        Only applies to Elapsed and Interval counters.
136        """
137        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
138            return 1e6 * math.sqrt(float(self._perf["M2"]) / (self.events - 1)) if self.events > 1 else 0
139        return None

Pretty Printing Perf Counters

PerfCounter(gdb, perf_ptr: "'gdb.Value'")
20    def __init__(self, gdb, perf_ptr: "gdb.Value"):
21        super().__init__(gdb)
22        self._perf = perf_ptr.cast(gdb.lookup_type("struct perf_ctr_count").pointer())
23        if self.type == "PC_ELAPSED":
24            self._perf = self._perf.cast(gdb.lookup_type("struct perf_ctr_elapsed").pointer())
25        elif self.type == "PC_INTERVAL":
26            self._perf = self._perf.cast(gdb.lookup_type("struct perf_ctr_interval").pointer())
27        # print(self._perf, self.short_type, self.events, self.name)
name: str
29    @cached_property
30    def name(self) -> str:
31        """Name of the counter"""
32        try:
33            return self._perf["name"].string()
34        except:
35            return "?"

Name of the counter

events: int
37    @cached_property
38    def events(self) -> int:
39        """How many events were counted"""
40        return int(self._perf["event_count"])

How many events were counted

type: str
46    @cached_property
47    def type(self) -> str:
48        """Counter type name"""
49        for name, value in self._types.items():
50            if value == self._perf["type"]:
51                return name
52        return "UNKNOWN"

Counter type name

short_type: str
54    @cached_property
55    def short_type(self) -> str:
56        """The short name of the type"""
57        return self.type.replace("PC_", "").capitalize()

The short name of the type

elapsed: int | None
59    @cached_property
60    def elapsed(self) -> int | None:
61        """
62        How much time has elapsed in microseconds.
63        Only applies to Elapsed counters.
64        """
65        if self.type == "PC_ELAPSED":
66            return int(self._perf["time_total"])
67        return None

How much time has elapsed in microseconds. Only applies to Elapsed counters.

first: int | None
69    @cached_property
70    def first(self) -> int | None:
71        """
72        The first time in microseconds.
73        Only applies to Interval counters.
74        """
75        if self.type == "PC_INTERVAL":
76            return int(self._perf["time_first"])
77        return None

The first time in microseconds. Only applies to Interval counters.

last: int | None
79    @cached_property
80    def last(self) -> int | None:
81        """
82        The last time in microseconds.
83        Only applies to Interval counters.
84        """
85        if self.type == "PC_INTERVAL":
86            return int(self._perf["time_last"])
87        return None

The last time in microseconds. Only applies to Interval counters.

interval: int | None
89    @cached_property
90    def interval(self) -> int | None:
91        """
92        The interval time in microseconds.
93        Only applies to Interval counters.
94        """
95        if self.type == "PC_INTERVAL":
96            return self.last - self.first
97        return None

The interval time in microseconds. Only applies to Interval counters.

average: int | None
 99    @cached_property
100    def average(self) -> int | None:
101        """
102        The average time in microseconds.
103        Only applies to Elapsed and Interval counters.
104        """
105        if self.type == "PC_ELAPSED":
106            return self.elapsed / self.events if self.events else 0
107        elif self.type == "PC_INTERVAL":
108            return (self.last - self.first) / self.events if self.events else 0
109        return None

The average time in microseconds. Only applies to Elapsed and Interval counters.

least: int | None
111    @cached_property
112    def least(self) -> int | None:
113        """
114        The least time in microseconds.
115        Only applies to Elapsed and Interval counters.
116        """
117        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
118            return int(self._perf["time_least"])
119        return None

The least time in microseconds. Only applies to Elapsed and Interval counters.

most: int | None
121    @cached_property
122    def most(self) -> int | None:
123        """
124        The most time in microseconds.
125        Only applies to Elapsed and Interval counters.
126        """
127        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
128            return int(self._perf["time_most"])
129        return None

The most time in microseconds. Only applies to Elapsed and Interval counters.

rms: int | None
131    @cached_property
132    def rms(self) -> int | None:
133        """
134        The root mean square in microseconds.
135        Only applies to Elapsed and Interval counters.
136        """
137        if self.type in ["PC_ELAPSED", "PC_INTERVAL"]:
138            return 1e6 * math.sqrt(float(self._perf["M2"]) / (self.events - 1)) if self.events > 1 else 0
139        return None

The root mean square in microseconds. Only applies to Elapsed and Interval counters.

def all_perf_counters_as_table( gdb, filter_: Callable[[PerfCounter], bool] = None, sort_key: Callable[[PerfCounter], Any] = None) -> rich.table.Table | None:
143def all_perf_counters_as_table(gdb, filter_: Callable[[PerfCounter], bool] = None,
144                               sort_key: Callable[[PerfCounter], Any] = None) -> Table | None:
145    """
146    Pretty print all perf counters as a table. Counters that did not change
147    since the last call are dimmed.
148
149    :param filter_: A function to filter the perf counters.
150    :param sort_key: A function to sort the perf counters by key.
151    :returns: A rich table with all perf counters or `None` if no counters found.
152    """
153    if (queue := gdb.lookup_static_symbol("perf_counters")) is None:
154        return None
155    queue = queue.value()
156    item, tail = queue["head"], queue["tail"]
157    counters = []
158    loop_count = 0
159    while item and item != tail:
160        pc = PerfCounter(gdb, item)
161        counters.append(pc)
162        item = item["flink"]
163        loop_count += 1
164        if loop_count > 1000: break
165    # Filter may result in no matches
166    if not counters:
167        return None
168
169    global _PREVIOUS_COUNTERS
170    changed = {c for c in counters
171               if (events := _PREVIOUS_COUNTERS.get(int(c._perf))) is not None
172               and events != c.events}
173    _PREVIOUS_COUNTERS |= {int(c._perf): c.events for c in counters}
174
175    # Filter out the counters
176    if filter_ is not None:
177        counters = [c for c in counters if filter_(c)]
178        if not counters:
179            return None
180
181    table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
182    table.add_column("perf_ctr_count*", justify="right", no_wrap=True)
183    table.add_column("Name")
184    table.add_column("Events", justify="right")
185    table.add_column("Elapsed", justify="right")
186    table.add_column("Average", justify="right")
187    table.add_column("Least", justify="right")
188    table.add_column("Most", justify="right")
189    table.add_column("RMS", justify="right")
190    table.add_column("Interval", justify="right")
191    table.add_column("First", justify="right")
192    table.add_column("Last", justify="right")
193
194    # Sort the rows by name by default and format the table
195    for counter in sorted(counters, key=sort_key or (lambda p: p.name)):
196        table.add_row(hex(counter._perf), rich.markup.escape(counter.name), str(counter.events),
197                      format_units(counter.elapsed, "t:µs", fmt=".1f", if_zero="-"),
198                      format_units(counter.average, "t:µs", fmt=".1f", if_zero="-"),
199                      format_units(counter.least, "t:µs", fmt=".1f", if_zero="-"),
200                      format_units(counter.most, "t:µs", fmt=".1f", if_zero="-"),
201                      format_units(counter.rms, "t:µs", fmt=".3f", if_zero="-"),
202                      format_units(counter.interval, "t:µs", fmt=".1f", if_zero="-"),
203                      format_units(counter.first, "t:µs", fmt=".1f", if_zero="-"),
204                      format_units(counter.last, "t:µs", fmt=".1f", if_zero="-"),
205                      style="dim" if changed and counter not in changed else None)
206    return table

Pretty print all perf counters as a table. Counters that did not change since the last call are dimmed.

Parameters
  • filter_: A function to filter the perf counters.
  • sort_key: A function to sort the perf counters by key. :returns: A rich table with all perf counters or None if no counters found.