From d8bdacfce85eff5a482c3dad066913ea1f92dece Mon Sep 17 00:00:00 2001 From: Christoph Pillmayer Date: Thu, 16 Apr 2026 16:58:09 +0200 Subject: [PATCH] pan/perf: Add libGPUCounters to xml translator libGPUCounters (1) contains all required information to generate the counter definitions used in mesa for Bifrost+ architectures. This script gathers the required information from the xml definitions in libGPUCounters and outputs pan/perf xmls. It also already includes support for derived counters, meaning counters which are computed from other counters actually created by HW. For those, we recursively resolve the variables in the equation until only HW counters and configuration values are left. It makes sense to do it here already since the datastructures make it a simple addition and the codegen doesn't need to handle it at compile time later that way. Derived counters that require MALI_CONFIG_TIME_SPAN are skipped for now. libGPUCounters also does not generate the equations for those and it makes hooking up the derived counters in pan simpler when we don't have to estimate the duration of a sample in some way. 1) https://github.com/ARM-software/libGPUCounters --- src/panfrost/perf/pan_gen_perf_defs.py | 286 +++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 src/panfrost/perf/pan_gen_perf_defs.py diff --git a/src/panfrost/perf/pan_gen_perf_defs.py b/src/panfrost/perf/pan_gen_perf_defs.py new file mode 100644 index 00000000000..fadb46e1c66 --- /dev/null +++ b/src/panfrost/perf/pan_gen_perf_defs.py @@ -0,0 +1,286 @@ +# Copyright (c) 2026 Arm Ltd. +# SPDX-License-Identifier: MIT + +from argparse import ArgumentParser +from pathlib import Path +from dataclasses import dataclass +import datetime +import subprocess +import xml.etree.ElementTree as et +import re + +COUNTERINFO_PATH = "./specification/database/counterinfo" +HARDWARE_LAYOUT_PATH = "./specification/database/hardwarelayout" + +HW_LAYOUT_LUT: dict[str, "HardwareLayout"] = {} + +OUTPUT_COPYRIGHT = """ + +""" + + +def get_revision(path): + cmd = ["git", "rev-parse", "HEAD"] + res = subprocess.run(cmd, capture_output=True, cwd=path.as_posix()) + if res.returncode != 0: + return None + else: + return res.stdout.decode().strip() + + +def map_nn(v, f): + return None if v is None else f(v) + + +def get_elem_text(xml, name): + e = xml.find(name) + if e is not None: + return e.text + else: + return None + + +@dataclass(frozen=True) +class CounterHwLocation: + block: str + counter_index: int + + +@dataclass +class HardwareLayout: + gpu_name: str + # map source name to (block index, counter index) + locations: dict[str, CounterHwLocation] + + @staticmethod + def from_xml(xml: et.Element) -> "HardwareLayout": + gpu_name = xml.get("gpu") + assert gpu_name is not None + locations = {} + for cbe in xml.findall("CounterBlock"): + cb_name = cbe.get("type") + assert cb_name is not None + for counter in cbe.findall("Counter"): + source_name = counter.get("name") + counter_index = counter.get("index") + assert counter_index is not None + locations[source_name] = CounterHwLocation( + cb_name, int(counter_index)) + + return HardwareLayout(gpu_name=gpu_name, locations=locations) + + +def parse_hw_layout(path: Path): + xml = et.parse(path) + return HardwareLayout.from_xml(xml.getroot()) + + +def parse_supported_gpus(xml): + supported_list = xml.find("SupportedGPUs") + return [e.text for e in supported_list.findall("GPU")] + + +def group_from_filename(fname): + # This maps to the values of the "type" field in the CounterBlock xml blocks. + fname_to_dbkey = { + "GPUFrontEnd": "GPU Front-end", + "L2Cache": "Memory System", + "Tiler": "Tiler", + "ShaderCore": "Shader Core", + "Constants": "Constants", + "Content": "Content", + } + for name, key in fname_to_dbkey.items(): + if name in fname: + return key + assert False and "could not find group from filename" + + +@dataclass +class CounterInfo: + machine_name: str + supported_gpus: list[str] + group: str + equation: str = "" + source_name: str = "" + # Can be used as a fallback to find hw offsets if source_name isn't available. + source_alias_name: str = "" + human_name: str = "" + short_desc: str = "" + units: str = "" + + @staticmethod + def from_xml(xml, group): + machine_name = get_elem_text(xml, "MachineName") + assert machine_name is not None + supported = parse_supported_gpus(xml) + + desc_raw = get_elem_text(xml, "ShortDescription") or "" + desc_san = " ".join(map(str.strip, desc_raw.splitlines())).strip() + + return CounterInfo( + machine_name, + supported, + group, + equation=map_nn(get_elem_text(xml, "Equation"), str.strip) or "", + source_name=get_elem_text(xml, "SourceName") or "", + source_alias_name=get_elem_text(xml, "SourceAlias") or "", + human_name=get_elem_text(xml, "HumanName") or "", + short_desc=desc_san, + units=(get_elem_text(xml, "Units") or "").strip(), + ) + + def is_derived(self): + return not self.source_name + + def get_hw_offsets(self, gpu: str) -> CounterHwLocation: + assert self.source_name != "" + assert gpu in self.supported_gpus + locs = HW_LAYOUT_LUT[gpu].locations + if self.source_name in locs: + return locs[self.source_name] + else: + # If the normal source name doesn't work try the alias + # Needed for example for RT_RAY_BOX_ISSUED on G1 which is using the + # alias RT_BOX_ISSUE_CYCLES there. + assert self.source_alias_name != "" + return locs[self.source_alias_name] + + def is_supported(self): + return "MALI_CONFIG_TIME_SPAN" not in self.equation + + +@dataclass +class ProductInfo: + product_id: str + database_key: str + + +def parse_counters(path: Path): + group = group_from_filename(path.name) + xml = et.parse(path) + return [CounterInfo.from_xml(e, group) for e in xml.findall("CounterInfo")] + + +def resolve_equation(eq: str, counters_gpu: list[CounterInfo]): + sorted_c = sorted(counters_gpu, key=lambda c: len(c.machine_name)) + max_len = max([len(c.machine_name) for c in sorted_c]) + + # This loop replaces variables which aren't hardware counters or config values + # until only all have been replaced. + # Iterate backwards from the largest to the smallest variable to make this work: + # eq = MaliMainQueueTask * MaliMainQueueTaskSize * MaliMainQueueTaskSize + + progress = True + while progress: + progress = False + for l in range(max_len, 0, -1): + for c in filter(lambda c: len(c.machine_name) == l, sorted_c): + if c.machine_name in eq: + if c.is_derived(): + repl = f"({c.equation})" + else: + assert c.source_name is not None + repl = f"({c.source_name})" + + eq = eq.replace(c.machine_name, repl) + progress = True + break + + # There was a change, need to restart because we might have added + # a variable with len(name) > l. + if progress: + break + return eq + + +def counter_list_to_xml(counters: list[CounterInfo], gpu: str): + gpu_xml = gpu.replace("Mali-", "").replace("Mali", "").strip() + root = et.Element("metrics", attrib={"id": gpu_xml}) + + IGNORE_CATS = {"Constants", "Content"} + + cat_names = set([c.group for c in counters]) + categories = dict() + for c in sorted(cat_names): + if c in IGNORE_CATS: + continue + categories[c] = et.SubElement(root, "category", attrib={"name": c}) + + for counter in sorted(counters, key=lambda c: c.machine_name): + if not counter.is_supported(): + continue + + if counter.group in IGNORE_CATS: + continue + p = categories[counter.group] + + attrib = { + "name": counter.machine_name, + "title": counter.human_name, + "description": counter.short_desc, + "units": counter.units, + } + + if counter.is_derived(): + attrib["equation"] = resolve_equation(counter.equation, counters) + else: + attrib["counter"] = counter.source_name + attrib["offset"] = str(counter.get_hw_offsets(gpu).counter_index) + + et.SubElement(p, "event", attrib) + + return root + + +def main(): + p = ArgumentParser() + p.add_argument("lib_gpu_counters", type=Path, + help="Path to libGPUCounter source") + p.add_argument( + "--output-path", type=Path, default=Path(__file__).parent / "generated" + ) + args = p.parse_args() + + for f in (args.lib_gpu_counters / HARDWARE_LAYOUT_PATH).glob("*.xml"): + l = parse_hw_layout(f) + HW_LAYOUT_LUT[l.gpu_name] = l + + counters: list[CounterInfo] = [] + for f in (args.lib_gpu_counters / COUNTERINFO_PATH).glob("*.xml"): + counters += parse_counters(f) + + args.output_path.mkdir(exist_ok=True) + + # Generate one file for each GPU. + all_gpus = set().union(*(c.supported_gpus for c in counters)) + for gpu in all_gpus: + gpu_counters = [c for c in counters if gpu in c.supported_gpus] + xml = counter_list_to_xml(gpu_counters, gpu) + et.indent(xml) + + fname = gpu.replace("Mali-", "").replace("Mali", "").strip() + ".xml" + year = datetime.datetime.now().year + rev = get_revision(args.lib_gpu_counters) + assert(rev is not None) + + with open(args.output_path / fname, "wb") as f: + f.write( + OUTPUT_COPYRIGHT.format( + year=year, rev=rev).encode(encoding="utf-8") + ) + f.write(et.tostring(xml, encoding="utf-8")) + f.write("\n".encode(encoding="utf-8")) + + +if __name__ == "__main__": + main()