xref: /linux/tools/perf/pmu-events/metric.py (revision ca156ab12b2e2718b35dd6d30e0aea7be0edb11d)
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 + self.operator + rhs))
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 self.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 = None
320    if self.rhs:
321      rhs = self.rhs.Substitute(name, expression)
322    return Operator(self.operator, lhs, rhs)
323
324
325class Select(Expression):
326  """Represents a select ternary in the parse tree."""
327
328  def __init__(self, true_val: Union[int, float, Expression],
329               cond: Union[int, float, Expression],
330               false_val: Union[int, float, Expression]):
331    self.true_val = _Constify(true_val)
332    self.cond = _Constify(cond)
333    self.false_val = _Constify(false_val)
334
335  def ToPerfJson(self):
336    true_str = self.true_val.ToPerfJson()
337    cond_str = self.cond.ToPerfJson()
338    false_str = self.false_val.ToPerfJson()
339    return f'({true_str} if {cond_str} else {false_str})'
340
341  def ToPython(self):
342    return (f'Select({self.true_val.ToPython()}, {self.cond.ToPython()}, '
343            f'{self.false_val.ToPython()})')
344
345  def Simplify(self) -> Expression:
346    cond = self.cond.Simplify()
347    true_val = self.true_val.Simplify()
348    false_val = self.false_val.Simplify()
349    if isinstance(cond, Constant):
350      return false_val if cond.value == '0' else true_val
351
352    if true_val.Equals(false_val):
353      return true_val
354
355    return Select(true_val, cond, false_val)
356
357  def HasExperimentalEvents(self) -> bool:
358    return (self.cond.HasExperimentalEvents() or self.true_val.HasExperimentalEvents() or
359            self.false_val.HasExperimentalEvents())
360
361  def Equals(self, other: Expression) -> bool:
362    if isinstance(other, Select):
363      return self.cond.Equals(other.cond) and self.false_val.Equals(
364          other.false_val) and self.true_val.Equals(other.true_val)
365    return False
366
367  def Substitute(self, name: str, expression: Expression) -> Expression:
368    if self.Equals(expression):
369      return Event(name)
370    true_val = self.true_val.Substitute(name, expression)
371    cond = self.cond.Substitute(name, expression)
372    false_val = self.false_val.Substitute(name, expression)
373    return Select(true_val, cond, false_val)
374
375
376class Function(Expression):
377  """A function in an expression like min, max, d_ratio."""
378
379  def __init__(self,
380               fn: str,
381               lhs: Union[int, float, Expression],
382               rhs: Optional[Union[int, float, Expression]] = None):
383    self.fn = fn
384    self.lhs = _Constify(lhs)
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 (self.rhs and self.rhs.HasExperimentalEvents())
411
412  def Equals(self, other: Expression) -> bool:
413    if isinstance(other, Function):
414      result = self.fn == other.fn and self.lhs.Equals(other.lhs)
415      if self.rhs:
416        result = result and self.rhs.Equals(other.rhs)
417      return result
418    return False
419
420  def Substitute(self, name: str, expression: Expression) -> Expression:
421    if self.Equals(expression):
422      return Event(name)
423    lhs = self.lhs.Substitute(name, expression)
424    rhs = None
425    if self.rhs:
426      rhs = self.rhs.Substitute(name, expression)
427    return Function(self.fn, lhs, rhs)
428
429
430def _FixEscapes(s: str) -> str:
431  s = re.sub(r'([^\\]),', r'\1\\,', s)
432  return re.sub(r'([^\\])=', r'\1\\=', s)
433
434
435class Event(Expression):
436  """An event in an expression."""
437
438  def __init__(self, *args: str):
439    error = ""
440    CheckEveryEvent(*args)
441    for name in args:
442      if CheckEvent(name):
443        self.name = _FixEscapes(name)
444        return
445      if error:
446        error += " or " + name
447      else:
448        error = name
449    global all_events
450    raise Exception(f"No event {error} in:\n{all_events}")
451
452  def HasExperimentalEvents(self) -> bool:
453    return IsExperimentalEvent(self.name)
454
455  def ToPerfJson(self):
456    result = re.sub('/', '@', self.name)
457    return result
458
459  def ToPython(self):
460    return f'Event(r"{self.name}")'
461
462  def Simplify(self) -> Expression:
463    return self
464
465  def Equals(self, other: Expression) -> bool:
466    return isinstance(other, Event) and self.name == other.name
467
468  def Substitute(self, name: str, expression: Expression) -> Expression:
469    return self
470
471
472class MetricRef(Expression):
473  """A metric reference in an expression."""
474
475  def __init__(self, name: str):
476    self.name = _FixEscapes(name)
477
478  def ToPerfJson(self):
479    return self.name
480
481  def ToPython(self):
482    return f'MetricRef(r"{self.name}")'
483
484  def Simplify(self) -> Expression:
485    return self
486
487  def HasExperimentalEvents(self) -> bool:
488    return False
489
490  def Equals(self, other: Expression) -> bool:
491    return isinstance(other, MetricRef) and self.name == other.name
492
493  def Substitute(self, name: str, expression: Expression) -> Expression:
494    return self
495
496
497class Constant(Expression):
498  """A constant within the expression tree."""
499
500  def __init__(self, value: Union[float, str]):
501    ctx = decimal.Context()
502    ctx.prec = 20
503    dec = ctx.create_decimal(repr(value) if isinstance(value, float) else value)
504    self.value = dec.normalize().to_eng_string()
505    self.value = self.value.replace('+', '')
506    self.value = self.value.replace('E', 'e')
507
508  def ToPerfJson(self):
509    return self.value
510
511  def ToPython(self):
512    return f'Constant({self.value})'
513
514  def Simplify(self) -> Expression:
515    return self
516
517  def HasExperimentalEvents(self) -> bool:
518    return False
519
520  def Equals(self, other: Expression) -> bool:
521    return isinstance(other, Constant) and self.value == other.value
522
523  def Substitute(self, name: str, expression: Expression) -> Expression:
524    return self
525
526
527class Literal(Expression):
528  """A runtime literal within the expression tree."""
529
530  def __init__(self, value: str):
531    self.value = value
532
533  def ToPerfJson(self):
534    return self.value
535
536  def ToPython(self):
537    return f'Literal({self.value})'
538
539  def Simplify(self) -> Expression:
540    return self
541
542  def HasExperimentalEvents(self) -> bool:
543    return False
544
545  def Equals(self, other: Expression) -> bool:
546    return isinstance(other, Literal) and self.value == other.value
547
548  def Substitute(self, name: str, expression: Expression) -> Expression:
549    return self
550
551
552def min(lhs: Union[int, float, Expression], rhs: Union[int, float,
553                                                       Expression]) -> Function:
554  # pylint: disable=redefined-builtin
555  # pylint: disable=invalid-name
556  return Function('min', lhs, rhs)
557
558
559def max(lhs: Union[int, float, Expression], rhs: Union[int, float,
560                                                       Expression]) -> Function:
561  # pylint: disable=redefined-builtin
562  # pylint: disable=invalid-name
563  return Function('max', lhs, rhs)
564
565
566def d_ratio(lhs: Union[int, float, Expression],
567            rhs: Union[int, float, Expression]) -> Function:
568  # pylint: disable=redefined-builtin
569  # pylint: disable=invalid-name
570  return Function('d_ratio', lhs, rhs)
571
572
573def source_count(event: Event) -> Function:
574  # pylint: disable=redefined-builtin
575  # pylint: disable=invalid-name
576  return Function('source_count', event)
577
578
579def aggr_nr(event: Event) -> Function:
580  # pylint: disable=invalid-name
581  return Function('aggr_nr', event)
582
583
584def has_event(event: Event) -> Function:
585  # pylint: disable=redefined-builtin
586  # pylint: disable=invalid-name
587  return Function('has_event', event)
588
589def strcmp_cpuid_str(cpuid: Event) -> Function:
590  # pylint: disable=redefined-builtin
591  # pylint: disable=invalid-name
592  return Function('strcmp_cpuid_str', cpuid)
593
594class Metric:
595  """An individual metric that will specifiable on the perf command line."""
596  groups: Set[str]
597  expr: Expression
598  scale_unit: str
599  constraint: MetricConstraint
600  threshold: Optional[Expression]
601
602  def __init__(self,
603               name: str,
604               description: str,
605               expr: Expression,
606               scale_unit: str,
607               constraint: MetricConstraint = MetricConstraint.GROUPED_EVENTS,
608               threshold: Optional[Expression] = None):
609    self.name = name
610    self.description = description
611    self.expr = expr.Simplify()
612    if self.expr.HasExperimentalEvents():
613      self.description += " (metric should be considered experimental as it contains experimental events)."
614    # Workraound valid_only_metric hiding certain metrics based on unit.
615    scale_unit = scale_unit.replace('/sec', ' per sec')
616    if scale_unit[0].isdigit():
617      self.scale_unit = scale_unit
618    else:
619      self.scale_unit = f'1{scale_unit}'
620    self.constraint = constraint
621    self.threshold = threshold
622    self.groups = set()
623
624  def __lt__(self, other):
625    """Sort order."""
626    return self.name < other.name
627
628  def AddToMetricGroup(self, group):
629    """Callback used when being added to a MetricGroup."""
630    if group.name:
631      self.groups.add(group.name)
632
633  def Flatten(self) -> Set['Metric']:
634    """Return a leaf metric."""
635    return set([self])
636
637  def ToPerfJson(self) -> Dict[str, str]:
638    """Return as dictionary for Json generation."""
639    result = {
640        'MetricName': self.name,
641        'MetricGroup': ';'.join(sorted(self.groups)),
642        'BriefDescription': self.description,
643        'MetricExpr': self.expr.ToPerfJson(),
644        'ScaleUnit': self.scale_unit
645    }
646    if self.constraint != MetricConstraint.GROUPED_EVENTS:
647      result['MetricConstraint'] = self.constraint.name
648    if self.threshold:
649      result['MetricThreshold'] = self.threshold.ToPerfJson()
650
651    return result
652
653  def ToMetricGroupDescriptions(self, root: bool = True) -> Dict[str, str]:
654    return {}
655
656class MetricGroup:
657  """A group of metrics.
658
659  Metric groups may be specificd on the perf command line, but within
660  the json they aren't encoded. Metrics may be in multiple groups
661  which can facilitate arrangements similar to trees.
662  """
663
664  def __init__(self, name: str,
665               metric_list: List[Union[Optional[Metric], Optional['MetricGroup']]],
666               description: Optional[str] = None):
667    self.name = name
668    self.metric_list = []
669    self.description = description
670    for metric in metric_list:
671      if metric:
672        self.metric_list.append(metric)
673        metric.AddToMetricGroup(self)
674
675  def AddToMetricGroup(self, group):
676    """Callback used when a MetricGroup is added into another."""
677    for metric in self.metric_list:
678      metric.AddToMetricGroup(group)
679
680  def Flatten(self) -> Set[Metric]:
681    """Returns a set of all leaf metrics."""
682    result = set()
683    for x in self.metric_list:
684      result = result.union(x.Flatten())
685
686    return result
687
688  def ToPerfJson(self) -> List[Dict[str, str]]:
689    result = []
690    for x in sorted(self.Flatten()):
691      result.append(x.ToPerfJson())
692    return result
693
694  def ToMetricGroupDescriptions(self, root: bool = True) -> Dict[str, str]:
695    result = {self.name: self.description} if self.description else {}
696    for x in self.metric_list:
697      result.update(x.ToMetricGroupDescriptions(False))
698    return result
699
700  def __str__(self) -> str:
701    return str(self.ToPerfJson())
702
703
704def JsonEncodeMetric(x: MetricGroup):
705  class MetricJsonEncoder(json.JSONEncoder):
706    """Special handling for Metric objects."""
707
708    def default(self, o):
709      if isinstance(o, Metric) or isinstance(o, MetricGroup):
710        return o.ToPerfJson()
711      return json.JSONEncoder.default(self, o)
712
713  return json.dumps(x, indent=2, cls=MetricJsonEncoder)
714
715
716def JsonEncodeMetricGroupDescriptions(x: MetricGroup):
717  return json.dumps(x.ToMetricGroupDescriptions(), indent=2)
718
719
720class _RewriteIfExpToSelect(ast.NodeTransformer):
721  """Transformer to convert if-else nodes to Select expressions."""
722
723  def visit_IfExp(self, node):
724    # pylint: disable=invalid-name
725    self.generic_visit(node)
726    call = ast.Call(
727        func=ast.Name(id='Select', ctx=ast.Load()),
728        args=[node.body, node.test, node.orelse],
729        keywords=[])
730    ast.copy_location(call, node.test)
731    return call
732
733
734def ParsePerfJson(orig: str) -> Expression:
735  """A simple json metric expression decoder.
736
737  Converts a json encoded metric expression by way of python's ast and
738  eval routine. First tokens are mapped to Event calls, then
739  accidentally converted keywords or literals are mapped to their
740  appropriate calls. Python's ast is used to match if-else that can't
741  be handled via operator overloading. Finally the ast is evaluated.
742
743  Args:
744    orig (str): String to parse.
745
746  Returns:
747    Expression: The parsed string.
748  """
749  # pylint: disable=eval-used
750  py = orig.strip()
751  # First try to convert everything that looks like a string (event name) into Event(r"EVENT_NAME").
752  # This isn't very selective so is followed up by converting some unwanted conversions back again
753  py = re.sub(r'([a-zA-Z][^-+/\* \\\(\),]*(?:\\.[^-+/\* \\\(\),]*)*)',
754              r'Event(r"\1")', py)
755  # If it started with a # it should have been a literal, rather than an event name
756  py = re.sub(r'#Event\(r"([^"]*)"\)', r'Literal("#\1")', py)
757  # Fix events wrongly broken at a ','
758  while True:
759    prev_py = py
760    py = re.sub(r'Event\(r"([^"]*)"\),Event\(r"([^"]*)"\)', r'Event(r"\1,\2")', py)
761    if py == prev_py:
762      break
763  # Convert accidentally converted hex constants ("0Event(r"xDEADBEEF)"") back to a constant,
764  # but keep it wrapped in Event(), otherwise Python drops the 0x prefix and it gets interpreted as
765  # a double by the Bison parser
766  py = re.sub(r'0Event\(r"[xX]([0-9a-fA-F]*)"\)', r'Event("0x\1")', py)
767  # Convert accidentally converted scientific notation constants back
768  py = re.sub(r'([0-9]+)Event\(r"(e[0-9]*)"\)', r'\1\2', py)
769  # Convert all the known keywords back from events to just the keyword
770  keywords = ['if', 'else', 'min', 'max', 'd_ratio', 'source_count', 'aggr_nr', 'has_event', 'strcmp_cpuid_str']
771  for kw in keywords:
772    py = re.sub(rf'Event\(r"{kw}"\)', kw, py)
773  try:
774    parsed = ast.parse(py, mode='eval')
775  except SyntaxError as e:
776    raise SyntaxError(f'Parsing expression:\n{orig}') from e
777  _RewriteIfExpToSelect().visit(parsed)
778  parsed = ast.fix_missing_locations(parsed)
779  return _Constify(eval(compile(parsed, orig, 'eval')))
780
781def RewriteMetricsInTermsOfOthers(metrics: List[Tuple[str, str, Expression]]
782                                  )-> Dict[Tuple[str, str], Expression]:
783  """Shorten metrics by rewriting in terms of others.
784
785  Args:
786    metrics (list): pmus, metric names and their expressions.
787  Returns:
788    Dict: mapping from a pmu, metric name pair to a shortened expression.
789  """
790  updates: Dict[Tuple[str, str], Expression] = dict()
791  for outer_pmu, outer_name, outer_expression in metrics:
792    if outer_pmu is None:
793      outer_pmu = 'cpu'
794    updated = outer_expression
795    while True:
796      for inner_pmu, inner_name, inner_expression in metrics:
797        if inner_pmu is None:
798          inner_pmu = 'cpu'
799        if inner_pmu.lower() != outer_pmu.lower():
800          continue
801        if inner_name.lower() == outer_name.lower():
802          continue
803        if (inner_pmu, inner_name) in updates:
804          inner_expression = updates[(inner_pmu, inner_name)]
805        updated = updated.Substitute(inner_name, inner_expression)
806      if updated.Equals(outer_expression):
807        break
808      if (outer_pmu, outer_name) in updates and updated.Equals(updates[(outer_pmu, outer_name)]):
809        break
810      updates[(outer_pmu, outer_name)] = updated
811  return updates
812