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()