Path: blob/master/tools/lib/python/abi/system_symbols.py
38186 views
#!/usr/bin/env python31# pylint: disable=R0902,R0912,R0914,R0915,R17022# Copyright(c) 2025: Mauro Carvalho Chehab <[email protected]>.3# SPDX-License-Identifier: GPL-2.045"""6Parse ABI documentation and produce results from it.7"""89import os10import re11import sys1213from concurrent import futures14from datetime import datetime15from random import shuffle1617from abi.helpers import AbiDebug1819class SystemSymbols:20"""Stores arguments for the class and initialize class vars"""2122def graph_add_file(self, path, link=None):23"""24add a file path to the sysfs graph stored at self.root25"""2627if path in self.files:28return2930name = ""31ref = self.root32for edge in path.split("/"):33name += edge + "/"34if edge not in ref:35ref[edge] = {"__name": [name.rstrip("/")]}3637ref = ref[edge]3839if link and link not in ref["__name"]:40ref["__name"].append(link.rstrip("/"))4142self.files.add(path)4344def print_graph(self, root_prefix="", root=None, level=0):45"""Prints a reference tree graph using UTF-8 characters"""4647if not root:48root = self.root49level = 05051# Prevent endless traverse52if level > 5:53return5455if level > 0:56prefix = "├──"57last_prefix = "└──"58else:59prefix = ""60last_prefix = ""6162items = list(root.items())6364names = root.get("__name", [])65for k, edge in items:66if k == "__name":67continue6869if not k:70k = "/"7172if len(names) > 1:73k += " links: " + ",".join(names[1:])7475if edge == items[-1][1]:76print(root_prefix + last_prefix + k)77p = root_prefix78if level > 0:79p += " "80self.print_graph(p, edge, level + 1)81else:82print(root_prefix + prefix + k)83p = root_prefix + "│ "84self.print_graph(p, edge, level + 1)8586def _walk(self, root):87"""88Walk through sysfs to get all devnodes that aren't ignored.8990By default, uses /sys as sysfs mounting point. If another91directory is used, it replaces them to /sys at the patches.92"""9394with os.scandir(root) as obj:95for entry in obj:96path = os.path.join(root, entry.name)97if self.sysfs:98p = path.replace(self.sysfs, "/sys", count=1)99else:100p = path101102if self.re_ignore.search(p):103return104105# Handle link first to avoid directory recursion106if entry.is_symlink():107real = os.path.realpath(path)108if not self.sysfs:109self.aliases[path] = real110else:111real = real.replace(self.sysfs, "/sys", count=1)112113# Add absfile location to graph if it doesn't exist114if not self.re_ignore.search(real):115# Add link to the graph116self.graph_add_file(real, p)117118elif entry.is_file():119self.graph_add_file(p)120121elif entry.is_dir():122self._walk(path)123124def __init__(self, abi, sysfs="/sys", hints=False):125"""126Initialize internal variables and get a list of all files inside127sysfs that can currently be parsed.128129Please notice that there are several entries on sysfs that aren't130documented as ABI. Ignore those.131132The real paths will be stored under self.files. Aliases will be133stored in separate, as self.aliases.134"""135136self.abi = abi137self.log = abi.log138139if sysfs != "/sys":140self.sysfs = sysfs.rstrip("/")141else:142self.sysfs = None143144self.hints = hints145146self.root = {}147self.aliases = {}148self.files = set()149150dont_walk = [151# Those require root access and aren't documented at ABI152f"^{sysfs}/kernel/debug",153f"^{sysfs}/kernel/tracing",154f"^{sysfs}/fs/pstore",155f"^{sysfs}/fs/bpf",156f"^{sysfs}/fs/fuse",157158# This is not documented at ABI159f"^{sysfs}/module",160161f"^{sysfs}/fs/cgroup", # this is big and has zero docs under ABI162f"^{sysfs}/firmware", # documented elsewhere: ACPI, DT bindings163"sections|notes", # aren't actually part of ABI164165# kernel-parameters.txt - not easy to parse166"parameters",167]168169self.re_ignore = re.compile("|".join(dont_walk))170171print(f"Reading {sysfs} directory contents...", file=sys.stderr)172self._walk(sysfs)173174def check_file(self, refs, found):175"""Check missing ABI symbols for a given sysfs file"""176177res_list = []178179try:180for names in refs:181fname = names[0]182183res = {184"found": False,185"fname": fname,186"msg": "",187}188res_list.append(res)189190re_what = self.abi.get_regexes(fname)191if not re_what:192self.abi.log.warning(f"missing rules for {fname}")193continue194195for name in names:196for r in re_what:197if self.abi.debug & AbiDebug.UNDEFINED:198self.log.debug("check if %s matches '%s'", name, r.pattern)199if r.match(name):200res["found"] = True201if found:202res["msg"] += f" {fname}: regex:\n\t"203continue204205if self.hints and not res["found"]:206res["msg"] += f" {fname} not found. Tested regexes:\n"207for r in re_what:208res["msg"] += " " + r.pattern + "\n"209210except KeyboardInterrupt:211pass212213return res_list214215def _ref_interactor(self, root):216"""Recursive function to interact over the sysfs tree"""217218for k, v in root.items():219if isinstance(v, dict):220yield from self._ref_interactor(v)221222if root == self.root or k == "__name":223continue224225if self.abi.re_string:226fname = v["__name"][0]227if self.abi.re_string.search(fname):228yield v229else:230yield v231232233def get_fileref(self, all_refs, chunk_size):234"""Interactor to group refs into chunks"""235236n = 0237refs = []238239for ref in all_refs:240refs.append(ref)241242n += 1243if n >= chunk_size:244yield refs245n = 0246refs = []247248yield refs249250def check_undefined_symbols(self, max_workers=None, chunk_size=50,251found=None, dry_run=None):252"""Seach ABI for sysfs symbols missing documentation"""253254self.abi.parse_abi()255256if self.abi.debug & AbiDebug.GRAPH:257self.print_graph()258259all_refs = []260for ref in self._ref_interactor(self.root):261all_refs.append(ref["__name"])262263if dry_run:264print("Would check", file=sys.stderr)265for ref in all_refs:266print(", ".join(ref))267268return269270print("Starting to search symbols (it may take several minutes):",271file=sys.stderr)272start = datetime.now()273old_elapsed = None274275# Python doesn't support multithreading due to limitations on its276# global lock (GIL). While Python 3.13 finally made GIL optional,277# there are still issues related to it. Also, we want to have278# backward compatibility with older versions of Python.279#280# So, use instead multiprocess. However, Python is very slow passing281# data from/to multiple processes. Also, it may consume lots of memory282# if the data to be shared is not small. So, we need to group workload283# in chunks that are big enough to generate performance gains while284# not being so big that would cause out-of-memory.285286num_refs = len(all_refs)287print(f"Number of references to parse: {num_refs}", file=sys.stderr)288289if not max_workers:290max_workers = os.cpu_count()291elif max_workers > os.cpu_count():292max_workers = os.cpu_count()293294max_workers = max(max_workers, 1)295296max_chunk_size = int((num_refs + max_workers - 1) / max_workers)297chunk_size = min(chunk_size, max_chunk_size)298chunk_size = max(1, chunk_size)299300if max_workers > 1:301executor = futures.ProcessPoolExecutor302303# Place references in a random order. This may help improving304# performance, by mixing complex/simple expressions when creating305# chunks306shuffle(all_refs)307else:308# Python has a high overhead with processes. When there's just309# one worker, it is faster to not create a new process.310# Yet, User still deserves to have a progress print. So, use311# python's "thread", which is actually a single process, using312# an internal schedule to switch between tasks. No performance313# gains for non-IO tasks, but still it can be quickly interrupted314# from time to time to display progress.315executor = futures.ThreadPoolExecutor316317not_found = []318f_list = []319with executor(max_workers=max_workers) as exe:320for refs in self.get_fileref(all_refs, chunk_size):321if refs:322try:323f_list.append(exe.submit(self.check_file, refs, found))324325except KeyboardInterrupt:326return327328total = len(f_list)329330if not total:331if self.abi.re_string:332print(f"No ABI symbol matches {self.abi.search_string}")333else:334self.abi.log.warning("No ABI symbols found")335return336337print(f"{len(f_list):6d} jobs queued on {max_workers} workers",338file=sys.stderr)339340while f_list:341try:342t = futures.wait(f_list, timeout=1,343return_when=futures.FIRST_COMPLETED)344345done = t[0]346347for fut in done:348res_list = fut.result()349350for res in res_list:351if not res["found"]:352not_found.append(res["fname"])353if res["msg"]:354print(res["msg"])355356f_list.remove(fut)357except KeyboardInterrupt:358return359360except RuntimeError as e:361self.abi.log.warning(f"Future: {e}")362break363364if sys.stderr.isatty():365elapsed = str(datetime.now() - start).split(".", maxsplit=1)[0]366if len(f_list) < total:367elapsed += f" ({total - len(f_list)}/{total} jobs completed). "368if elapsed != old_elapsed:369print(elapsed + "\r", end="", flush=True,370file=sys.stderr)371old_elapsed = elapsed372373elapsed = str(datetime.now() - start).split(".", maxsplit=1)[0]374print(elapsed, file=sys.stderr)375376for f in sorted(not_found):377print(f"{f} not found.")378379380