xref: /linux/tools/perf/pmu-events/jevents.py (revision 8f426582e0e0c9bbd58e170e1b209334eb5df79e)
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