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
This commit is contained in:
Christoph Pillmayer 2026-04-16 16:58:09 +02:00
parent bf8cfffef5
commit d8bdacfce8

View file

@ -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 = """<!--
Copyright (c) {year} Arm, Ltd.
SPDX-License-Identifier: MIT
Generated from libGPUCounters @ {rev}.
https://github.com/ARM-software/libGPUCounters
which is:
Copyright (c) 2023-2025 Arm Limited
SPDX-License-Identifier: MIT
-->
"""
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()