1#!/usr/bin/env python3 2# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) 3"""Convert directories of JSON events to C code.""" 4import argparse 5import csv 6import json 7import os 8import sys 9from typing import Callable 10from typing import Sequence 11 12# Global command line arguments. 13_args = None 14# List of event tables generated from "/sys" directories. 15_sys_event_tables = [] 16# Map from an event name to an architecture standard 17# JsonEvent. Architecture standard events are in json files in the top 18# f'{_args.starting_dir}/{_args.arch}' directory. 19_arch_std_events = {} 20# Track whether an events table is currently being defined and needs closing. 21_close_table = False 22 23 24def removesuffix(s: str, suffix: str) -> str: 25 """Remove the suffix from a string 26 27 The removesuffix function is added to str in Python 3.9. We aim for 3.6 28 compatibility and so provide our own function here. 29 """ 30 return s[0:-len(suffix)] if s.endswith(suffix) else s 31 32 33def file_name_to_table_name(parents: Sequence[str], dirname: str) -> str: 34 """Generate a C table name from directory names.""" 35 tblname = 'pme' 36 for p in parents: 37 tblname += '_' + p 38 tblname += '_' + dirname 39 return tblname.replace('-', '_') 40 41 42class JsonEvent: 43 """Representation of an event loaded from a json file dictionary.""" 44 45 def __init__(self, jd: dict): 46 """Constructor passed the dictionary of parsed json values.""" 47 48 def llx(x: int) -> str: 49 """Convert an int to a string similar to a printf modifier of %#llx.""" 50 return '0' if x == 0 else hex(x) 51 52 def fixdesc(s: str) -> str: 53 """Fix formatting issue for the desc string.""" 54 if s is None: 55 return None 56 return removesuffix(removesuffix(removesuffix(s, '. '), 57 '. '), '.').replace('\n', '\\n').replace( 58 '\"', '\\"').replace('\r', '\\r') 59 60 def convert_aggr_mode(aggr_mode: str) -> str: 61 """Returns the aggr_mode_class enum value associated with the JSON string.""" 62 if not aggr_mode: 63 return None 64 aggr_mode_to_enum = { 65 'PerChip': '1', 66 'PerCore': '2', 67 } 68 return aggr_mode_to_enum[aggr_mode] 69 70 def lookup_msr(num: str) -> str: 71 """Converts the msr number, or first in a list to the appropriate event field.""" 72 if not num: 73 return None 74 msrmap = { 75 0x3F6: 'ldlat=', 76 0x1A6: 'offcore_rsp=', 77 0x1A7: 'offcore_rsp=', 78 0x3F7: 'frontend=', 79 } 80 return msrmap[int(num.split(',', 1)[0], 0)] 81 82 def real_event(name: str, event: str) -> str: 83 """Convert well known event names to an event string otherwise use the event argument.""" 84 fixed = { 85 'inst_retired.any': 'event=0xc0,period=2000003', 86 'inst_retired.any_p': 'event=0xc0,period=2000003', 87 'cpu_clk_unhalted.ref': 'event=0x0,umask=0x03,period=2000003', 88 'cpu_clk_unhalted.thread': 'event=0x3c,period=2000003', 89 'cpu_clk_unhalted.core': 'event=0x3c,period=2000003', 90 'cpu_clk_unhalted.thread_any': 'event=0x3c,any=1,period=2000003', 91 } 92 if not name: 93 return None 94 if name.lower() in fixed: 95 return fixed[name.lower()] 96 return event 97 98 def unit_to_pmu(unit: str) -> str: 99 """Convert a JSON Unit to Linux PMU name.""" 100 if not unit: 101 return None 102 # Comment brought over from jevents.c: 103 # it's not realistic to keep adding these, we need something more scalable ... 104 table = { 105 'CBO': 'uncore_cbox', 106 'QPI LL': 'uncore_qpi', 107 'SBO': 'uncore_sbox', 108 'iMPH-U': 'uncore_arb', 109 'CPU-M-CF': 'cpum_cf', 110 'CPU-M-SF': 'cpum_sf', 111 'UPI LL': 'uncore_upi', 112 'hisi_sicl,cpa': 'hisi_sicl,cpa', 113 'hisi_sccl,ddrc': 'hisi_sccl,ddrc', 114 'hisi_sccl,hha': 'hisi_sccl,hha', 115 'hisi_sccl,l3c': 'hisi_sccl,l3c', 116 'imx8_ddr': 'imx8_ddr', 117 'L3PMC': 'amd_l3', 118 'DFPMC': 'amd_df', 119 'cpu_core': 'cpu_core', 120 'cpu_atom': 'cpu_atom', 121 } 122 return table[unit] if unit in table else f'uncore_{unit.lower()}' 123 124 eventcode = 0 125 if 'EventCode' in jd: 126 eventcode = int(jd['EventCode'].split(',', 1)[0], 0) 127 if 'ExtSel' in jd: 128 eventcode |= int(jd['ExtSel']) << 8 129 configcode = int(jd['ConfigCode'], 0) if 'ConfigCode' in jd else None 130 self.name = jd['EventName'].lower() if 'EventName' in jd else None 131 self.compat = jd.get('Compat') 132 self.desc = fixdesc(jd.get('BriefDescription')) 133 self.long_desc = fixdesc(jd.get('PublicDescription')) 134 precise = jd.get('PEBS') 135 msr = lookup_msr(jd.get('MSRIndex')) 136 msrval = jd.get('MSRValue') 137 extra_desc = '' 138 if 'Data_LA' in jd: 139 extra_desc += ' Supports address when precise' 140 if 'Errata' in jd: 141 extra_desc += '.' 142 if 'Errata' in jd: 143 extra_desc += ' Spec update: ' + jd['Errata'] 144 self.pmu = unit_to_pmu(jd.get('Unit')) 145 filter = jd.get('Filter') 146 self.unit = jd.get('ScaleUnit') 147 self.perpkg = jd.get('PerPkg') 148 self.aggr_mode = convert_aggr_mode(jd.get('AggregationMode')) 149 self.deprecated = jd.get('Deprecated') 150 self.metric_name = jd.get('MetricName') 151 self.metric_group = jd.get('MetricGroup') 152 self.metric_constraint = jd.get('MetricConstraint') 153 self.metric_expr = jd.get('MetricExpr') 154 if self.metric_expr: 155 self.metric_expr = self.metric_expr.replace('\\', '\\\\') 156 arch_std = jd.get('ArchStdEvent') 157 if precise and self.desc and not '(Precise Event)' in self.desc: 158 extra_desc += ' (Must be precise)' if precise == '2' else (' (Precise ' 159 'event)') 160 event = f'config={llx(configcode)}' if configcode is not None else f'event={llx(eventcode)}' 161 event_fields = [ 162 ('AnyThread', 'any='), 163 ('PortMask', 'ch_mask='), 164 ('CounterMask', 'cmask='), 165 ('EdgeDetect', 'edge='), 166 ('FCMask', 'fc_mask='), 167 ('Invert', 'inv='), 168 ('SampleAfterValue', 'period='), 169 ('UMask', 'umask='), 170 ] 171 for key, value in event_fields: 172 if key in jd and jd[key] != '0': 173 event += ',' + value + jd[key] 174 if filter: 175 event += f',{filter}' 176 if msr: 177 event += f',{msr}{msrval}' 178 if self.desc and extra_desc: 179 self.desc += extra_desc 180 if self.long_desc and extra_desc: 181 self.long_desc += extra_desc 182 if self.pmu: 183 if self.desc and not self.desc.endswith('. '): 184 self.desc += '. ' 185 self.desc = (self.desc if self.desc else '') + ('Unit: ' + self.pmu + ' ') 186 if arch_std and arch_std.lower() in _arch_std_events: 187 event = _arch_std_events[arch_std.lower()].event 188 # Copy from the architecture standard event to self for undefined fields. 189 for attr, value in _arch_std_events[arch_std.lower()].__dict__.items(): 190 if hasattr(self, attr) and not getattr(self, attr): 191 setattr(self, attr, value) 192 193 self.event = real_event(self.name, event) 194 195 def __repr__(self) -> str: 196 """String representation primarily for debugging.""" 197 s = '{\n' 198 for attr, value in self.__dict__.items(): 199 if value: 200 s += f'\t{attr} = {value},\n' 201 return s + '}' 202 203 def to_c_string(self, topic_local: str) -> str: 204 """Representation of the event as a C struct initializer.""" 205 206 def attr_string(attr: str, value: str) -> str: 207 return '\t.%s = \"%s\",\n' % (attr, value) 208 209 def str_if_present(self, attr: str) -> str: 210 if not getattr(self, attr): 211 return '' 212 return attr_string(attr, getattr(self, attr)) 213 214 s = '{\n' 215 for attr in ['name', 'event']: 216 s += str_if_present(self, attr) 217 if self.desc is not None: 218 s += attr_string('desc', self.desc) 219 else: 220 s += attr_string('desc', '(null)') 221 s += str_if_present(self, 'compat') 222 s += f'\t.topic = "{topic_local}",\n' 223 for attr in [ 224 'long_desc', 'pmu', 'unit', 'perpkg', 'aggr_mode', 'metric_expr', 225 'metric_name', 'metric_group', 'deprecated', 'metric_constraint' 226 ]: 227 s += str_if_present(self, attr) 228 s += '},\n' 229 return s 230 231 232def read_json_events(path: str) -> Sequence[JsonEvent]: 233 """Read json events from the specified file.""" 234 return json.load(open(path), object_hook=lambda d: JsonEvent(d)) 235 236 237def preprocess_arch_std_files(archpath: str) -> None: 238 """Read in all architecture standard events.""" 239 global _arch_std_events 240 for item in os.scandir(archpath): 241 if item.is_file() and item.name.endswith('.json'): 242 for event in read_json_events(item.path): 243 if event.name: 244 _arch_std_events[event.name.lower()] = event 245 246 247def print_events_table_prefix(tblname: str) -> None: 248 """Called when a new events table is started.""" 249 global _close_table 250 if _close_table: 251 raise IOError('Printing table prefix but last table has no suffix') 252 _args.output_file.write(f'static const struct pmu_event {tblname}[] = {{\n') 253 _close_table = True 254 255 256def print_events_table_entries(item: os.DirEntry, topic: str) -> None: 257 """Create contents of an events table.""" 258 if not _close_table: 259 raise IOError('Table entries missing prefix') 260 for event in read_json_events(item.path): 261 _args.output_file.write(event.to_c_string(topic)) 262 263 264def print_events_table_suffix() -> None: 265 """Optionally close events table.""" 266 global _close_table 267 if _close_table: 268 _args.output_file.write("""{ 269\t.name = 0, 270\t.event = 0, 271\t.desc = 0, 272}, 273}; 274""") 275 _close_table = False 276 277 278def process_one_file(parents: Sequence[str], item: os.DirEntry) -> None: 279 """Process a JSON file during the main walk.""" 280 global _sys_event_tables 281 282 def get_topic(topic: str) -> str: 283 return removesuffix(topic, '.json').replace('-', ' ') 284 285 def is_leaf_dir(path: str) -> bool: 286 for item in os.scandir(path): 287 if item.is_dir(): 288 return False 289 return True 290 291 # model directory, reset topic 292 if item.is_dir() and is_leaf_dir(item.path): 293 print_events_table_suffix() 294 295 tblname = file_name_to_table_name(parents, item.name) 296 if item.name == 'sys': 297 _sys_event_tables.append(tblname) 298 print_events_table_prefix(tblname) 299 return 300 301 # base dir or too deep 302 level = len(parents) 303 if level == 0 or level > 4: 304 return 305 306 # Ignore other directories. If the file name does not have a .json 307 # extension, ignore it. It could be a readme.txt for instance. 308 if not item.is_file() or not item.name.endswith('.json'): 309 return 310 311 print_events_table_entries(item, get_topic(item.name)) 312 313 314def print_mapping_table() -> None: 315 """Read the mapfile and generate the struct from cpuid string to event table.""" 316 with open(f'{_args.starting_dir}/{_args.arch}/mapfile.csv') as csvfile: 317 table = csv.reader(csvfile) 318 _args.output_file.write( 319 'const struct pmu_events_map pmu_events_map[] = {\n') 320 first = True 321 for row in table: 322 # Skip the first row or any row beginning with #. 323 if not first and len(row) > 0 and not row[0].startswith('#'): 324 tblname = file_name_to_table_name([], row[2].replace('/', '_')) 325 _args.output_file.write("""{ 326\t.cpuid = \"%s\", 327\t.version = \"%s\", 328\t.type = \"%s\", 329\t.table = %s 330}, 331""" % (row[0].replace('\\', '\\\\'), row[1], row[3], tblname)) 332 first = False 333 334 _args.output_file.write("""{ 335\t.cpuid = "testcpu", 336\t.version = "v1", 337\t.type = "core", 338\t.table = pme_test_soc_cpu, 339}, 340{ 341\t.cpuid = 0, 342\t.version = 0, 343\t.type = 0, 344\t.table = 0, 345}, 346}; 347""") 348 349 350def print_system_mapping_table() -> None: 351 """C struct mapping table array for tables from /sys directories.""" 352 _args.output_file.write( 353 '\nconst struct pmu_sys_events pmu_sys_event_tables[] = {\n') 354 for tblname in _sys_event_tables: 355 _args.output_file.write(f"""\t{{ 356\t\t.table = {tblname}, 357\t\t.name = \"{tblname}\", 358\t}}, 359""") 360 _args.output_file.write("""\t{ 361\t\t.table = 0 362\t}, 363}; 364""") 365 366 367def main() -> None: 368 global _args 369 370 def dir_path(path: str) -> str: 371 """Validate path is a directory for argparse.""" 372 if os.path.isdir(path): 373 return path 374 raise argparse.ArgumentTypeError(f'\'{path}\' is not a valid directory') 375 376 def ftw(path: str, parents: Sequence[str], 377 action: Callable[[Sequence[str], os.DirEntry], None]) -> None: 378 """Replicate the directory/file walking behavior of C's file tree walk.""" 379 for item in os.scandir(path): 380 action(parents, item) 381 if item.is_dir(): 382 ftw(item.path, parents + [item.name], action) 383 384 ap = argparse.ArgumentParser() 385 ap.add_argument('arch', help='Architecture name like x86') 386 ap.add_argument( 387 'starting_dir', 388 type=dir_path, 389 help='Root of tree containing architecture directories containing json files' 390 ) 391 ap.add_argument( 392 'output_file', type=argparse.FileType('w'), nargs='?', default=sys.stdout) 393 _args = ap.parse_args() 394 395 _args.output_file.write("#include \"pmu-events/pmu-events.h\"\n") 396 for path in [_args.arch, 'test']: 397 arch_path = f'{_args.starting_dir}/{path}' 398 if not os.path.isdir(arch_path): 399 raise IOError(f'Missing architecture directory in \'{arch_path}\'') 400 preprocess_arch_std_files(arch_path) 401 ftw(arch_path, [], process_one_file) 402 print_events_table_suffix() 403 404 print_mapping_table() 405 print_system_mapping_table() 406 407 408if __name__ == '__main__': 409 main() 410