xref: /linux/tools/perf/pmu-events/metric.py (revision 67f8bc848ee31831336bd478e57d2f993551902e)
1# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
2"""Parse or generate representations of perf metrics."""
3import ast
4import decimal
5import json
6import os
7import re
8from enum import Enum
9from typing import Dict, List, Optional, Set, Tuple, Union
10
11all_pmus = set()
12all_events = set()
13experimental_events = set()
14all_events_all_models = set()
15
16def LoadEvents(directory: str) -> None:
17  """Populate a global set of all known events for the purpose of validating Event names"""
18  global all_pmus
19  global all_events
20  global experimental_events
21  global all_events_all_models
22  all_events = {
23      "context\\-switches",
24      "cpu\\-cycles",
25      "cycles",
26      "duration_time",
27      "instructions",
28  }
29  for file in os.listdir(os.fsencode(directory)):
30    filename = os.fsdecode(file)
31    if filename.endswith(".json"):
32      try:
33        for x in json.load(open(f"{directory}/{filename}")):
34          if "Unit" in x:
35            all_pmus.add(x["Unit"])
36          if "EventName" in x:
37            all_events.add(x["EventName"])
38            if "Experimental" in x and x["Experimental"] == "1":
39              experimental_events.add(x["EventName"])
40          elif "ArchStdEvent" in x:
41            all_events.add(x["ArchStdEvent"])
42      except json.decoder.JSONDecodeError:
43        # The generated directory may be the same as the input, which
44        # causes partial json files. Ignore errors.
45        pass
46  all_events_all_models = all_events.copy()
47  for root, dirs, files in os.walk(directory + ".."):
48    for filename in files:
49      if filename.endswith(".json"):
50        try:
51          for x in json.load(open(f"{root}/{filename}")):
52            if "EventName" in x:
53              all_events_all_models.add(x["EventName"])
54            elif "ArchStdEvent" in x:
55              all_events_all_models.add(x["ArchStdEvent"])
56        except json.decoder.JSONDecodeError:
57          # The generated directory may be the same as the input, which
58          # causes partial json files. Ignore errors.
59          pass
60
61
62def CheckPmu(name: str) -> bool:
63  return name in all_pmus
64
65
66def CheckEvent(name: str) -> bool:
67  """Check the event name exists in the set of all loaded events"""
68  global all_events
69  if len(all_events) == 0:
70    # No events loaded so assume any event is good.
71    return True
72
73  if ':' in name:
74    # Remove trailing modifier.
75    name = name[:name.find(':')]
76  elif '/' in name:
77    # Name could begin with a PMU or an event, for now assume it is good.
78    return True
79
80  return name in all_events
81
82def CheckEveryEvent(*names: str) -> None:
83  """Check all the events exist in at least one json file"""
84  global all_events_all_models
85  if len(all_events_all_models) == 0:
86    assert len(names) == 1, f"Cannot determine valid events in {names}"
87    # No events loaded so assume any event is good.
88    return
89
90  for name in names:
91    # Remove trailing modifier.
92    if ':' in name:
93      name = name[:name.find(':')]
94    elif '/' in name:
95      name = name[:name.find('/')]
96      if any([name.startswith(x) for x in ['amd', 'arm', 'cpu', 'msr', 'power', 'cha', 'uncore']]):
97        continue
98    if name not in all_events_all_models:
99      raise Exception(f"Is {name} a named json event?")
100
101
102def IsExperimentalEvent(name: str) -> bool:
103  global experimental_events
104  if ':' in name:
105    # Remove trailing modifier.
106    name = name[:name.find(':')]
107  elif '/' in name:
108    # Name could begin with a PMU or an event, for now assume it is not experimental.
109    return False
110
111  return name in experimental_events
112
113
114class MetricConstraint(Enum):
115  GROUPED_EVENTS = 0
116  NO_GROUP_EVENTS = 1
117  NO_GROUP_EVENTS_NMI = 2
118  NO_GROUP_EVENTS_SMT = 3
119
120class Expression:
121  """Abstract base class of elements in a metric expression."""
122
123  def ToPerfJson(self) -> str:
124    """Returns a perf json file encoded representation."""
125    raise NotImplementedError()
126
127  def ToPython(self) -> str:
128    """Returns a python expr parseable representation."""
129    raise NotImplementedError()
130
131  def Simplify(self):
132    """Returns a simplified version of self."""
133    raise NotImplementedError()
134
135  def HasExperimentalEvents(self) -> bool:
136    """Are experimental events used in the expression?"""
137    raise NotImplementedError()
138
139  def Equals(self, other) -> bool:
140    """Returns true when two expressions are the same."""
141    raise NotImplementedError()
142
143  def Substitute(self, name: str, expression: 'Expression') -> 'Expression':
144    raise NotImplementedError()
145
146  def __str__(self) -> str:
147    return self.ToPerfJson()
148
149  def __or__(self, other: Union[int, float, 'Expression']) -> 'Operator':
150    return Operator('|', self, other)
151
152  def __ror__(self, other: Union[int, float, 'Expression']) -> 'Operator':
153    return Operator('|', other, self)
154
155  def __xor__(self, other: Union[int, float, 'Expression']) -> 'Operator':
156    return Operator('^', self, other)
157
158  def __and__(self, other: Union[int, float, 'Expression']) -> 'Operator':
159    return Operator('&', self, other)
160
161  def __rand__(self, other: Union[int, float, 'Expression']) -> 'Operator':
162    return Operator('&', other, self)
163
164  def __lt__(self, other: Union[int, float, 'Expression']) -> 'Operator':
165    return Operator('<', self, other)
166
167  def __gt__(self, other: Union[int, float, 'Expression']) -> 'Operator':
168    return Operator('>', self, other)
169
170  def __add__(self, other: Union[int, float, 'Expression']) -> 'Operator':
171    return Operator('+', self, other)
172
173  def __radd__(self, other: Union[int, float, 'Expression']) -> 'Operator':
174    return Operator('+', other, self)
175
176  def __sub__(self, other: Union[int, float, 'Expression']) -> 'Operator':
177    return Operator('-', self, other)
178
179  def __rsub__(self, other: Union[int, float, 'Expression']) -> 'Operator':
180    return Operator('-', other, self)
181
182  def __mul__(self, other: Union[int, float, 'Expression']) -> 'Operator':
183    return Operator('*', self, other)
184
185  def __rmul__(self, other: Union[int, float, 'Expression']) -> 'Operator':
186    return Operator('*', other, self)
187
188  def __truediv__(self, other: Union[int, float, 'Expression']) -> 'Operator':
189    return Operator('/', self, other)
190
191  def __rtruediv__(self, other: Union[int, float, 'Expression']) -> 'Operator':
192    return Operator('/', other, self)
193
194  def __mod__(self, other: Union[int, float, 'Expression']) -> 'Operator':
195    return Operator('%', self, other)
196
197
198def _Constify(val: Union[bool, int, float, Expression]) -> Expression:
199  """Used to ensure that the nodes in the expression tree are all Expression."""
200  if isinstance(val, bool):
201    return Constant(1 if val else 0)
202  if isinstance(val, (int, float)):
203    return Constant(val)
204  return val
205
206
207# Simple lookup for operator precedence, used to avoid unnecessary
208# brackets. Precedence matches that of the simple expression parser
209# but differs from python where comparisons are lower precedence than
210# the bitwise &, ^, | but not the logical versions that the expression
211# parser doesn't have.
212_PRECEDENCE = {
213    '|': 0,
214    '^': 1,
215    '&': 2,
216    '<': 3,
217    '>': 3,
218    '+': 4,
219    '-': 4,
220    '*': 5,
221    '/': 5,
222    '%': 5,
223}
224
225
226class Operator(Expression):
227  """Represents a binary operator in the parse tree."""
228
229  def __init__(self, operator: str, lhs: Union[int, float, Expression],
230               rhs: Union[int, float, Expression]):
231    self.operator = operator
232    self.lhs = _Constify(lhs)
233    self.rhs = _Constify(rhs)
234
235  def Bracket(self,
236              other: Expression,
237              other_str: str,
238              rhs: bool = False) -> str:
239    """If necessary brackets the given other value.
240
241    If ``other`` is an operator then a bracket is necessary when
242    this/self operator has higher precedence. Consider: '(a + b) * c',
243    ``other_str`` will be 'a + b'. A bracket is necessary as without
244    the bracket 'a + b * c' will evaluate 'b * c' first. However, '(a
245    * b) + c' doesn't need a bracket as 'a * b' will always be
246    evaluated first. For 'a / (b * c)' (ie the same precedence level
247    operations) then we add the bracket to best match the original
248    input, but not for '(a / b) * c' where the bracket is unnecessary.
249
250    Args:
251      other (Expression): is a lhs or rhs operator
252      other_str (str): ``other`` in the appropriate string form
253      rhs (bool):  is ``other`` on the RHS
254
255    Returns:
256      str: possibly bracketed other_str
257    """
258    if isinstance(other, Operator):
259      if _PRECEDENCE.get(self.operator, -1) > _PRECEDENCE.get(
260          other.operator, -1):
261        return f'({other_str})'
262      if rhs and _PRECEDENCE.get(self.operator, -1) == _PRECEDENCE.get(
263          other.operator, -1):
264        return f'({other_str})'
265    return other_str
266
267  def ToPerfJson(self):
268    return (f'{self.Bracket(self.lhs, self.lhs.ToPerfJson())} {self.operator} '
269            f'{self.Bracket(self.rhs, self.rhs.ToPerfJson(), True)}')
270
271  def ToPython(self):
272    return (f'{self.Bracket(self.lhs, self.lhs.ToPython())} {self.operator} '
273            f'{self.Bracket(self.rhs, self.rhs.ToPython(), True)}')
274
275  def Simplify(self) -> Expression:
276    lhs = self.lhs.Simplify()
277    rhs = self.rhs.Simplify()
278    if isinstance(lhs, Constant) and isinstance(rhs, Constant):
279      return Constant(ast.literal_eval(lhs.value + self.operator + rhs.value))
280
281    if isinstance(self.lhs, Constant):
282      if self.operator in ('+', '|') and lhs.value == '0':
283        return rhs
284
285      # Simplify multiplication by 0 except for the slot event which
286      # is deliberately introduced using this pattern.
287      if self.operator == '*' and lhs.value == '0' and (
288          not isinstance(rhs, Event) or 'slots' not in rhs.name.lower()):
289        return Constant(0)
290
291      if self.operator == '*' and lhs.value == '1':
292        return rhs
293
294    if isinstance(rhs, Constant):
295      if self.operator in ('+', '|') and rhs.value == '0':
296        return lhs
297
298      if self.operator == '*' and rhs.value == '0':
299        return Constant(0)
300
301      if self.operator == '*' and rhs.value == '1':
302        return lhs
303
304    return Operator(self.operator, lhs, rhs)
305
306  def HasExperimentalEvents(self) -> bool:
307    return self.lhs.HasExperimentalEvents() or self.rhs.HasExperimentalEvents()
308
309  def Equals(self, other: Expression) -> bool:
310    if isinstance(other, Operator):
311      return self.operator == other.operator and self.lhs.Equals(
312          other.lhs) and self.rhs.Equals(other.rhs)
313    return False
314
315  def Substitute(self, name: str, expression: Expression) -> Expression:
316    if self.Equals(expression):
317      return Event(name)
318    lhs = self.lhs.Substitute(name, expression)
319    rhs = self.rhs.Substitute(name, expression)
320    return Operator(self.operator, lhs, rhs)
321
322
323class Select(Expression):
324  """Represents a select ternary in the parse tree."""
325
326  def __init__(self, true_val: Union[int, float, Expression],
327               cond: Union[int, float, Expression],
328               false_val: Union[int, float, Expression]):
329    self.true_val = _Constify(true_val)
330    self.cond = _Constify(cond)
331    self.false_val = _Constify(false_val)
332
333  def ToPerfJson(self):
334    true_str = self.true_val.ToPerfJson()
335    cond_str = self.cond.ToPerfJson()
336    false_str = self.false_val.ToPerfJson()
337    return f'({true_str} if {cond_str} else {false_str})'
338
339  def ToPython(self):
340    return (f'Select({self.true_val.ToPython()}, {self.cond.ToPython()}, '
341            f'{self.false_val.ToPython()})')
342
343  def Simplify(self) -> Expression:
344    cond = self.cond.Simplify()
345    true_val = self.true_val.Simplify()
346    false_val = self.false_val.Simplify()
347    if isinstance(cond, Constant):
348      return false_val if cond.value == '0' else true_val
349
350    if true_val.Equals(false_val):
351      return true_val
352
353    return Select(true_val, cond, false_val)
354
355  def HasExperimentalEvents(self) -> bool:
356    return (self.cond.HasExperimentalEvents() or self.true_val.HasExperimentalEvents() or
357            self.false_val.HasExperimentalEvents())
358
359  def Equals(self, other: Expression) -> bool:
360    if isinstance(other, Select):
361      return self.cond.Equals(other.cond) and self.false_val.Equals(
362          other.false_val) and self.true_val.Equals(other.true_val)
363    return False
364
365  def Substitute(self, name: str, expression: Expression) -> Expression:
366    if self.Equals(expression):
367      return Event(name)
368    true_val = self.true_val.Substitute(name, expression)
369    cond = self.cond.Substitute(name, expression)
370    false_val = self.false_val.Substitute(name, expression)
371    return Select(true_val, cond, false_val)
372
373
374class Function(Expression):
375  """A function in an expression like min, max, d_ratio."""
376
377  def __init__(self,
378               fn: str,
379               lhs: Union[int, float, Expression],
380               rhs: Optional[Union[int, float, Expression]] = None):
381    self.fn = fn
382    self.lhs = _Constify(lhs)
383    self.rhs = None
384    if rhs is not None:
385      self.rhs = _Constify(rhs)
386
387  def ToPerfJson(self):
388    if self.rhs:
389      return f'{self.fn}({self.lhs.ToPerfJson()}, {self.rhs.ToPerfJson()})'
390    return f'{self.fn}({self.lhs.ToPerfJson()})'
391
392  def ToPython(self):
393    if self.rhs:
394      return f'{self.fn}({self.lhs.ToPython()}, {self.rhs.ToPython()})'
395    return f'{self.fn}({self.lhs.ToPython()})'
396
397  def Simplify(self) -> Expression:
398    lhs = self.lhs.Simplify()
399    rhs = self.rhs.Simplify() if self.rhs else None
400    if isinstance(lhs, Constant) and isinstance(rhs, Constant):
401      if self.fn == 'd_ratio':
402        if rhs.value == '0':
403          return Constant(0)
404        Constant(ast.literal_eval(f'{lhs} / {rhs}'))
405      return Constant(ast.literal_eval(f'{self.fn}({lhs}, {rhs})'))
406
407    return Function(self.fn, lhs, rhs)
408
409  def HasExperimentalEvents(self) -> bool:
410    return (self.lhs.HasExperimentalEvents() or
411            (self.rhs is not None and self.rhs.HasExperimentalEvents()))
412
413  def Equals(self, other: Expression) -> bool:
414    if isinstance(other, Function):
415      result = self.fn == other.fn and self.lhs.Equals(other.lhs)
416      if self.rhs:
417        result = result and self.rhs.Equals(other.rhs)
418      return result
419    return False
420
421  def Substitute(self, name: str, expression: Expression) -> Expression:
422    if self.Equals(expression):
423      return Event(name)
424    lhs = self.lhs.Substitute(name, expression)
425    rhs = None
426    if self.rhs:
427      rhs = self.rhs.Substitute(name, expression)
428    return Function(self.fn, lhs, rhs)
429
430
431def _FixEscapes(s: str) -> str:
432  s = re.sub(r'([^\\]),', r'\1\\,', s)
433  return re.sub(r'([^\\])=', r'\1\\=', s)
434
435
436class Event(Expression):
437  """An event in an expression."""
438
439  def __init__(self, *args: str):
440    error = ""
441    CheckEveryEvent(*args)
442    for name in args:
443      if CheckEvent(name):
444        self.name = _FixEscapes(name)
445        return
446      if error:
447        error += " or " + name
448      else:
449        error = name
450    global all_events
451    raise Exception(f"No event {error} in:\n{all_events}")
452
453  def HasExperimentalEvents(self) -> bool:
454    return IsExperimentalEvent(self.name)
455
456  def ToPerfJson(self):
457    result = re.sub('/', '@', self.name)
458    return result
459
460  def ToPython(self):
461    return f'Event(r"{self.name}")'
462
463  def Simplify(self) -> Expression:
464    return self
465
466  def Equals(self, other: Expression) -> bool:
467    return isinstance(other, Event) and self.name == other.name
468
469  def Substitute(self, name: str, expression: Expression) -> Expression:
470    return self
471
472
473class MetricRef(Expression):
474  """A metric reference in an expression."""
475
476  def __init__(self, name: str):
477    self.name = _FixEscapes(name)
478
479  def ToPerfJson(self):
480    return self.name
481
482  def ToPython(self):
483    return f'MetricRef(r"{self.name}")'
484
485  def Simplify(self) -> Expression:
486    return self
487
488  def HasExperimentalEvents(self) -> bool:
489    return False
490
491  def Equals(self, other: Expression) -> bool:
492    return isinstance(other, MetricRef) and self.name == other.name
493
494  def Substitute(self, name: str, expression: Expression) -> Expression:
495    return self
496
497
498class Constant(Expression):
499  """A constant within the expression tree."""
500
501  def __init__(self, value: Union[float, str]):
502    ctx = decimal.Context()
503    ctx.prec = 20
504    dec = ctx.create_decimal(repr(value) if isinstance(value, float) else value)
505    self.value = dec.normalize().to_eng_string()
506    self.value = self.value.replace('+', '')
507    self.value = self.value.replace('E', 'e')
508
509  def ToPerfJson(self):
510    return self.value
511
512  def ToPython(self):
513    return f'Constant({self.value})'
514
515  def Simplify(self) -> Expression:
516    return self
517
518  def HasExperimentalEvents(self) -> bool:
519    return False
520
521  def Equals(self, other: Expression) -> bool:
522    return isinstance(other, Constant) and self.value == other.value
523
524  def Substitute(self, name: str, expression: Expression) -> Expression:
525    return self
526
527
528class Literal(Expression):
529  """A runtime literal within the expression tree."""
530
531  def __init__(self, value: str):
532    self.value = value
533
534  def ToPerfJson(self):
535    return self.value
536
537  def ToPython(self):
538    return f'Literal({self.value})'
539
540  def Simplify(self) -> Expression:
541    return self
542
543  def HasExperimentalEvents(self) -> bool:
544    return False
545
546  def Equals(self, other: Expression) -> bool:
547    return isinstance(other, Literal) and self.value == other.value
548
549  def Substitute(self, name: str, expression: Expression) -> Expression:
550    return self
551
552
553def min(lhs: Union[int, float, Expression], rhs: Union[int, float,
554                                                       Expression]) -> Function:
555  # pylint: disable=redefined-builtin
556  # pylint: disable=invalid-name
557  return Function('min', lhs, rhs)
558
559
560def max(lhs: Union[int, float, Expression], rhs: Union[int, float,
561                                                       Expression]) -> Function:
562  # pylint: disable=redefined-builtin
563  # pylint: disable=invalid-name
564  return Function('max', lhs, rhs)
565
566
567def d_ratio(lhs: Union[int, float, Expression],
568            rhs: Union[int, float, Expression]) -> Function:
569  # pylint: disable=redefined-builtin
570  # pylint: disable=invalid-name
571  return Function('d_ratio', lhs, rhs)
572
573
574def source_count(event: Event) -> Function:
575  # pylint: disable=redefined-builtin
576  # pylint: disable=invalid-name
577  return Function('source_count', event)
578
579
580def aggr_nr(event: Event) -> Function:
581  # pylint: disable=invalid-name
582  return Function('aggr_nr', event)
583
584
585def has_event(event: Event) -> Function:
586  # pylint: disable=redefined-builtin
587  # pylint: disable=invalid-name
588  return Function('has_event', event)
589
590def strcmp_cpuid_str(cpuid: Event) -> Function:
591  # pylint: disable=redefined-builtin
592  # pylint: disable=invalid-name
593  return Function('strcmp_cpuid_str', cpuid)
594
595class Metric:
596  """An individual metric that will specifiable on the perf command line."""
597  groups: Set[str]
598  expr: Expression
599  scale_unit: str
600  constraint: MetricConstraint
601  threshold: Optional[Expression]
602
603  def __init__(self,
604               name: str,
605               description: str,
606               expr: Expression,
607               scale_unit: str,
608               constraint: MetricConstraint = MetricConstraint.GROUPED_EVENTS,
609               threshold: Optional[Expression] = None):
610    self.name = name
611    self.description = description
612    self.expr = expr.Simplify()
613    if self.expr.HasExperimentalEvents():
614      self.description += " (metric should be considered experimental as it contains experimental events)."
615    # Workraound valid_only_metric hiding certain metrics based on unit.
616    scale_unit = scale_unit.replace('/sec', ' per sec')
617    if scale_unit[0].isdigit():
618      self.scale_unit = scale_unit
619    else:
620      self.scale_unit = f'1{scale_unit}'
621    self.constraint = constraint
622    self.threshold = threshold
623    self.groups = set()
624
625  def __lt__(self, other):
626    """Sort order."""
627    if self.name != other.name:
628      return self.name < other.name
629    if not self.expr.Equals(other.expr):
630      return self.expr.ToPerfJson() < other.expr.ToPerfJson()
631    return self.description < other.description
632
633  def AddToMetricGroup(self, group):
634    """Callback used when being added to a MetricGroup."""
635    if group.name:
636      self.groups.add(group.name)
637
638  def Flatten(self) -> Set['Metric']:
639    """Return a leaf metric."""
640    return set([self])
641
642  def ToPerfJson(self) -> Dict[str, str]:
643    """Return as dictionary for Json generation."""
644    result = {
645        'MetricName': self.name,
646        'MetricGroup': ';'.join(sorted(self.groups)),
647        'BriefDescription': self.description,
648        'MetricExpr': self.expr.ToPerfJson(),
649        'ScaleUnit': self.scale_unit
650    }
651    if self.constraint != MetricConstraint.GROUPED_EVENTS:
652      result['MetricConstraint'] = self.constraint.name
653    if self.threshold:
654      result['MetricThreshold'] = self.threshold.ToPerfJson()
655
656    return result
657
658  def ToMetricGroupDescriptions(self, root: bool = True) -> Dict[str, str]:
659    return {}
660
661class MetricGroup:
662  """A group of metrics.
663
664  Metric groups may be specificd on the perf command line, but within
665  the json they aren't encoded. Metrics may be in multiple groups
666  which can facilitate arrangements similar to trees.
667  """
668
669  def __init__(self, name: str,
670               metric_list: List[Union[Optional[Metric], Optional['MetricGroup']]],
671               description: Optional[str] = None):
672    self.name = name
673    self.metric_list = []
674    self.description = description
675    for metric in metric_list:
676      if metric:
677        self.metric_list.append(metric)
678        metric.AddToMetricGroup(self)
679
680  def AddToMetricGroup(self, group):
681    """Callback used when a MetricGroup is added into another."""
682    for metric in self.metric_list:
683      metric.AddToMetricGroup(group)
684
685  def Flatten(self) -> Set[Metric]:
686    """Returns a set of all leaf metrics."""
687    result: Set[Metric] = set()
688    for x in self.metric_list:
689      result = result.union(x.Flatten())
690
691    return result
692
693  def ToPerfJson(self) -> List[Dict[str, str]]:
694    result = []
695    for x in sorted(self.Flatten()):
696      result.append(x.ToPerfJson())
697    return result
698
699  def ToMetricGroupDescriptions(self, root: bool = True) -> Dict[str, str]:
700    result = {self.name: self.description} if self.description else {}
701    for x in self.metric_list:
702      result.update(x.ToMetricGroupDescriptions(False))
703    return result
704
705  def __str__(self) -> str:
706    return str(self.ToPerfJson())
707
708
709def JsonEncodeMetric(x: MetricGroup):
710  class MetricJsonEncoder(json.JSONEncoder):
711    """Special handling for Metric objects."""
712
713    def default(self, o):
714      if isinstance(o, Metric) or isinstance(o, MetricGroup):
715        return o.ToPerfJson()
716      return json.JSONEncoder.default(self, o)
717
718  return json.dumps(x, indent=2, cls=MetricJsonEncoder)
719
720
721def JsonEncodeMetricGroupDescriptions(x: MetricGroup):
722  return json.dumps(x.ToMetricGroupDescriptions(), indent=2)
723
724
725class _RewriteIfExpToSelect(ast.NodeTransformer):
726  """Transformer to convert if-else nodes to Select expressions."""
727
728  def visit_IfExp(self, node):
729    # pylint: disable=invalid-name
730    self.generic_visit(node)
731    call = ast.Call(
732        func=ast.Name(id='Select', ctx=ast.Load()),
733        args=[node.body, node.test, node.orelse],
734        keywords=[])
735    ast.copy_location(call, node.test)
736    return call
737
738
739def ParsePerfJson(orig: str) -> Expression:
740  """A simple json metric expression decoder.
741
742  Converts a json encoded metric expression by way of python's ast and
743  eval routine. First tokens are mapped to Event calls, then
744  accidentally converted keywords or literals are mapped to their
745  appropriate calls. Python's ast is used to match if-else that can't
746  be handled via operator overloading. Finally the ast is evaluated.
747
748  Args:
749    orig (str): String to parse.
750
751  Returns:
752    Expression: The parsed string.
753  """
754  # pylint: disable=eval-used
755  py = orig.strip()
756  # First try to convert everything that looks like a string (event name) into Event(r"EVENT_NAME").
757  # This isn't very selective so is followed up by converting some unwanted conversions back again
758  py = re.sub(r'([a-zA-Z][^-+/\* \\\(\),]*(?:\\.[^-+/\* \\\(\),]*)*)',
759              r'Event(r"\1")', py)
760  # If it started with a # it should have been a literal, rather than an event name
761  py = re.sub(r'#Event\(r"([^"]*)"\)', r'Literal("#\1")', py)
762  # Fix events wrongly broken at a ','
763  while True:
764    prev_py = py
765    py = re.sub(r'Event\(r"([^"]*)"\),Event\(r"([^"]*)"\)', r'Event(r"\1,\2")', py)
766    if py == prev_py:
767      break
768  # Convert accidentally converted hex constants ("0Event(r"xDEADBEEF)"") back to a constant,
769  # but keep it wrapped in Event(), otherwise Python drops the 0x prefix and it gets interpreted as
770  # a double by the Bison parser
771  py = re.sub(r'0Event\(r"[xX]([0-9a-fA-F]*)"\)', r'Event("0x\1")', py)
772  # Convert accidentally converted scientific notation constants back
773  py = re.sub(r'([0-9]+)Event\(r"(e[0-9]*)"\)', r'\1\2', py)
774  # Convert all the known keywords back from events to just the keyword
775  keywords = ['if', 'else', 'min', 'max', 'd_ratio', 'source_count', 'aggr_nr', 'has_event', 'strcmp_cpuid_str']
776  for kw in keywords:
777    py = re.sub(rf'Event\(r"{kw}"\)', kw, py)
778  try:
779    parsed = ast.parse(py, mode='eval')
780  except SyntaxError as e:
781    raise SyntaxError(f'Parsing expression:\n{orig}') from e
782  _RewriteIfExpToSelect().visit(parsed)
783  parsed = ast.fix_missing_locations(parsed)
784  return _Constify(eval(compile(parsed, orig, 'eval')))
785
786def RewriteMetricsInTermsOfOthers(metrics: List[Tuple[str, str, Expression]]
787                                  )-> Dict[Tuple[str, str], Expression]:
788  """Shorten metrics by rewriting in terms of others.
789
790  Args:
791    metrics (list): pmus, metric names and their expressions.
792  Returns:
793    Dict: mapping from a pmu, metric name pair to a shortened expression.
794  """
795  updates: Dict[Tuple[str, str], Expression] = dict()
796  for outer_pmu, outer_name, outer_expression in metrics:
797    if outer_pmu is None:
798      outer_pmu = 'cpu'
799    updated = outer_expression
800    while True:
801      for inner_pmu, inner_name, inner_expression in metrics:
802        if inner_pmu is None:
803          inner_pmu = 'cpu'
804        if inner_pmu.lower() != outer_pmu.lower():
805          continue
806        if inner_name.lower() == outer_name.lower():
807          continue
808        if (inner_pmu, inner_name) in updates:
809          inner_expression = updates[(inner_pmu, inner_name)]
810        updated = updated.Substitute(inner_name, inner_expression)
811      if updated.Equals(outer_expression):
812        break
813      if (outer_pmu, outer_name) in updates and updated.Equals(updates[(outer_pmu, outer_name)]):
814        break
815      updates[(outer_pmu, outer_name)] = updated
816  return updates
817