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