xref: /openbmc/linux/tools/perf/pmu-events/metric.py (revision 3def3387)
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 re
7from typing import Dict, List, Optional, Set, Tuple, Union
8
9
10class Expression:
11  """Abstract base class of elements in a metric expression."""
12
13  def ToPerfJson(self) -> str:
14    """Returns a perf json file encoded representation."""
15    raise NotImplementedError()
16
17  def ToPython(self) -> str:
18    """Returns a python expr parseable representation."""
19    raise NotImplementedError()
20
21  def Simplify(self):
22    """Returns a simplified version of self."""
23    raise NotImplementedError()
24
25  def Equals(self, other) -> bool:
26    """Returns true when two expressions are the same."""
27    raise NotImplementedError()
28
29  def Substitute(self, name: str, expression: 'Expression') -> 'Expression':
30    raise NotImplementedError()
31
32  def __str__(self) -> str:
33    return self.ToPerfJson()
34
35  def __or__(self, other: Union[int, float, 'Expression']) -> 'Operator':
36    return Operator('|', self, other)
37
38  def __ror__(self, other: Union[int, float, 'Expression']) -> 'Operator':
39    return Operator('|', other, self)
40
41  def __xor__(self, other: Union[int, float, 'Expression']) -> 'Operator':
42    return Operator('^', self, other)
43
44  def __and__(self, other: Union[int, float, 'Expression']) -> 'Operator':
45    return Operator('&', self, other)
46
47  def __rand__(self, other: Union[int, float, 'Expression']) -> 'Operator':
48    return Operator('&', other, self)
49
50  def __lt__(self, other: Union[int, float, 'Expression']) -> 'Operator':
51    return Operator('<', self, other)
52
53  def __gt__(self, other: Union[int, float, 'Expression']) -> 'Operator':
54    return Operator('>', self, other)
55
56  def __add__(self, other: Union[int, float, 'Expression']) -> 'Operator':
57    return Operator('+', self, other)
58
59  def __radd__(self, other: Union[int, float, 'Expression']) -> 'Operator':
60    return Operator('+', other, self)
61
62  def __sub__(self, other: Union[int, float, 'Expression']) -> 'Operator':
63    return Operator('-', self, other)
64
65  def __rsub__(self, other: Union[int, float, 'Expression']) -> 'Operator':
66    return Operator('-', other, self)
67
68  def __mul__(self, other: Union[int, float, 'Expression']) -> 'Operator':
69    return Operator('*', self, other)
70
71  def __rmul__(self, other: Union[int, float, 'Expression']) -> 'Operator':
72    return Operator('*', other, self)
73
74  def __truediv__(self, other: Union[int, float, 'Expression']) -> 'Operator':
75    return Operator('/', self, other)
76
77  def __rtruediv__(self, other: Union[int, float, 'Expression']) -> 'Operator':
78    return Operator('/', other, self)
79
80  def __mod__(self, other: Union[int, float, 'Expression']) -> 'Operator':
81    return Operator('%', self, other)
82
83
84def _Constify(val: Union[bool, int, float, Expression]) -> Expression:
85  """Used to ensure that the nodes in the expression tree are all Expression."""
86  if isinstance(val, bool):
87    return Constant(1 if val else 0)
88  if isinstance(val, (int, float)):
89    return Constant(val)
90  return val
91
92
93# Simple lookup for operator precedence, used to avoid unnecessary
94# brackets. Precedence matches that of the simple expression parser
95# but differs from python where comparisons are lower precedence than
96# the bitwise &, ^, | but not the logical versions that the expression
97# parser doesn't have.
98_PRECEDENCE = {
99    '|': 0,
100    '^': 1,
101    '&': 2,
102    '<': 3,
103    '>': 3,
104    '+': 4,
105    '-': 4,
106    '*': 5,
107    '/': 5,
108    '%': 5,
109}
110
111
112class Operator(Expression):
113  """Represents a binary operator in the parse tree."""
114
115  def __init__(self, operator: str, lhs: Union[int, float, Expression],
116               rhs: Union[int, float, Expression]):
117    self.operator = operator
118    self.lhs = _Constify(lhs)
119    self.rhs = _Constify(rhs)
120
121  def Bracket(self,
122              other: Expression,
123              other_str: str,
124              rhs: bool = False) -> str:
125    """If necessary brackets the given other value.
126
127    If ``other`` is an operator then a bracket is necessary when
128    this/self operator has higher precedence. Consider: '(a + b) * c',
129    ``other_str`` will be 'a + b'. A bracket is necessary as without
130    the bracket 'a + b * c' will evaluate 'b * c' first. However, '(a
131    * b) + c' doesn't need a bracket as 'a * b' will always be
132    evaluated first. For 'a / (b * c)' (ie the same precedence level
133    operations) then we add the bracket to best match the original
134    input, but not for '(a / b) * c' where the bracket is unnecessary.
135
136    Args:
137      other (Expression): is a lhs or rhs operator
138      other_str (str): ``other`` in the appropriate string form
139      rhs (bool):  is ``other`` on the RHS
140
141    Returns:
142      str: possibly bracketed other_str
143    """
144    if isinstance(other, Operator):
145      if _PRECEDENCE.get(self.operator, -1) > _PRECEDENCE.get(
146          other.operator, -1):
147        return f'({other_str})'
148      if rhs and _PRECEDENCE.get(self.operator, -1) == _PRECEDENCE.get(
149          other.operator, -1):
150        return f'({other_str})'
151    return other_str
152
153  def ToPerfJson(self):
154    return (f'{self.Bracket(self.lhs, self.lhs.ToPerfJson())} {self.operator} '
155            f'{self.Bracket(self.rhs, self.rhs.ToPerfJson(), True)}')
156
157  def ToPython(self):
158    return (f'{self.Bracket(self.lhs, self.lhs.ToPython())} {self.operator} '
159            f'{self.Bracket(self.rhs, self.rhs.ToPython(), True)}')
160
161  def Simplify(self) -> Expression:
162    lhs = self.lhs.Simplify()
163    rhs = self.rhs.Simplify()
164    if isinstance(lhs, Constant) and isinstance(rhs, Constant):
165      return Constant(ast.literal_eval(lhs + self.operator + rhs))
166
167    if isinstance(self.lhs, Constant):
168      if self.operator in ('+', '|') and lhs.value == '0':
169        return rhs
170
171      # Simplify multiplication by 0 except for the slot event which
172      # is deliberately introduced using this pattern.
173      if self.operator == '*' and lhs.value == '0' and (
174          not isinstance(rhs, Event) or 'slots' not in rhs.name.lower()):
175        return Constant(0)
176
177      if self.operator == '*' and lhs.value == '1':
178        return rhs
179
180    if isinstance(rhs, Constant):
181      if self.operator in ('+', '|') and rhs.value == '0':
182        return lhs
183
184      if self.operator == '*' and rhs.value == '0':
185        return Constant(0)
186
187      if self.operator == '*' and self.rhs.value == '1':
188        return lhs
189
190    return Operator(self.operator, lhs, rhs)
191
192  def Equals(self, other: Expression) -> bool:
193    if isinstance(other, Operator):
194      return self.operator == other.operator and self.lhs.Equals(
195          other.lhs) and self.rhs.Equals(other.rhs)
196    return False
197
198  def Substitute(self, name: str, expression: Expression) -> Expression:
199    if self.Equals(expression):
200      return Event(name)
201    lhs = self.lhs.Substitute(name, expression)
202    rhs = None
203    if self.rhs:
204      rhs = self.rhs.Substitute(name, expression)
205    return Operator(self.operator, lhs, rhs)
206
207
208class Select(Expression):
209  """Represents a select ternary in the parse tree."""
210
211  def __init__(self, true_val: Union[int, float, Expression],
212               cond: Union[int, float, Expression],
213               false_val: Union[int, float, Expression]):
214    self.true_val = _Constify(true_val)
215    self.cond = _Constify(cond)
216    self.false_val = _Constify(false_val)
217
218  def ToPerfJson(self):
219    true_str = self.true_val.ToPerfJson()
220    cond_str = self.cond.ToPerfJson()
221    false_str = self.false_val.ToPerfJson()
222    return f'({true_str} if {cond_str} else {false_str})'
223
224  def ToPython(self):
225    return (f'Select({self.true_val.ToPython()}, {self.cond.ToPython()}, '
226            f'{self.false_val.ToPython()})')
227
228  def Simplify(self) -> Expression:
229    cond = self.cond.Simplify()
230    true_val = self.true_val.Simplify()
231    false_val = self.false_val.Simplify()
232    if isinstance(cond, Constant):
233      return false_val if cond.value == '0' else true_val
234
235    if true_val.Equals(false_val):
236      return true_val
237
238    return Select(true_val, cond, false_val)
239
240  def Equals(self, other: Expression) -> bool:
241    if isinstance(other, Select):
242      return self.cond.Equals(other.cond) and self.false_val.Equals(
243          other.false_val) and self.true_val.Equals(other.true_val)
244    return False
245
246  def Substitute(self, name: str, expression: Expression) -> Expression:
247    if self.Equals(expression):
248      return Event(name)
249    true_val = self.true_val.Substitute(name, expression)
250    cond = self.cond.Substitute(name, expression)
251    false_val = self.false_val.Substitute(name, expression)
252    return Select(true_val, cond, false_val)
253
254
255class Function(Expression):
256  """A function in an expression like min, max, d_ratio."""
257
258  def __init__(self,
259               fn: str,
260               lhs: Union[int, float, Expression],
261               rhs: Optional[Union[int, float, Expression]] = None):
262    self.fn = fn
263    self.lhs = _Constify(lhs)
264    self.rhs = _Constify(rhs)
265
266  def ToPerfJson(self):
267    if self.rhs:
268      return f'{self.fn}({self.lhs.ToPerfJson()}, {self.rhs.ToPerfJson()})'
269    return f'{self.fn}({self.lhs.ToPerfJson()})'
270
271  def ToPython(self):
272    if self.rhs:
273      return f'{self.fn}({self.lhs.ToPython()}, {self.rhs.ToPython()})'
274    return f'{self.fn}({self.lhs.ToPython()})'
275
276  def Simplify(self) -> Expression:
277    lhs = self.lhs.Simplify()
278    rhs = self.rhs.Simplify() if self.rhs else None
279    if isinstance(lhs, Constant) and isinstance(rhs, Constant):
280      if self.fn == 'd_ratio':
281        if rhs.value == '0':
282          return Constant(0)
283        Constant(ast.literal_eval(f'{lhs} / {rhs}'))
284      return Constant(ast.literal_eval(f'{self.fn}({lhs}, {rhs})'))
285
286    return Function(self.fn, lhs, rhs)
287
288  def Equals(self, other: Expression) -> bool:
289    if isinstance(other, Function):
290      result = self.fn == other.fn and self.lhs.Equals(other.lhs)
291      if self.rhs:
292        result = result and self.rhs.Equals(other.rhs)
293      return result
294    return False
295
296  def Substitute(self, name: str, expression: Expression) -> Expression:
297    if self.Equals(expression):
298      return Event(name)
299    lhs = self.lhs.Substitute(name, expression)
300    rhs = None
301    if self.rhs:
302      rhs = self.rhs.Substitute(name, expression)
303    return Function(self.fn, lhs, rhs)
304
305
306def _FixEscapes(s: str) -> str:
307  s = re.sub(r'([^\\]),', r'\1\\,', s)
308  return re.sub(r'([^\\])=', r'\1\\=', s)
309
310
311class Event(Expression):
312  """An event in an expression."""
313
314  def __init__(self, name: str, legacy_name: str = ''):
315    self.name = _FixEscapes(name)
316    self.legacy_name = _FixEscapes(legacy_name)
317
318  def ToPerfJson(self):
319    result = re.sub('/', '@', self.name)
320    return result
321
322  def ToPython(self):
323    return f'Event(r"{self.name}")'
324
325  def Simplify(self) -> Expression:
326    return self
327
328  def Equals(self, other: Expression) -> bool:
329    return isinstance(other, Event) and self.name == other.name
330
331  def Substitute(self, name: str, expression: Expression) -> Expression:
332    return self
333
334
335class Constant(Expression):
336  """A constant within the expression tree."""
337
338  def __init__(self, value: Union[float, str]):
339    ctx = decimal.Context()
340    ctx.prec = 20
341    dec = ctx.create_decimal(repr(value) if isinstance(value, float) else value)
342    self.value = dec.normalize().to_eng_string()
343    self.value = self.value.replace('+', '')
344    self.value = self.value.replace('E', 'e')
345
346  def ToPerfJson(self):
347    return self.value
348
349  def ToPython(self):
350    return f'Constant({self.value})'
351
352  def Simplify(self) -> Expression:
353    return self
354
355  def Equals(self, other: Expression) -> bool:
356    return isinstance(other, Constant) and self.value == other.value
357
358  def Substitute(self, name: str, expression: Expression) -> Expression:
359    return self
360
361
362class Literal(Expression):
363  """A runtime literal within the expression tree."""
364
365  def __init__(self, value: str):
366    self.value = value
367
368  def ToPerfJson(self):
369    return self.value
370
371  def ToPython(self):
372    return f'Literal({self.value})'
373
374  def Simplify(self) -> Expression:
375    return self
376
377  def Equals(self, other: Expression) -> bool:
378    return isinstance(other, Literal) and self.value == other.value
379
380  def Substitute(self, name: str, expression: Expression) -> Expression:
381    return self
382
383
384def min(lhs: Union[int, float, Expression], rhs: Union[int, float,
385                                                       Expression]) -> Function:
386  # pylint: disable=redefined-builtin
387  # pylint: disable=invalid-name
388  return Function('min', lhs, rhs)
389
390
391def max(lhs: Union[int, float, Expression], rhs: Union[int, float,
392                                                       Expression]) -> Function:
393  # pylint: disable=redefined-builtin
394  # pylint: disable=invalid-name
395  return Function('max', lhs, rhs)
396
397
398def d_ratio(lhs: Union[int, float, Expression],
399            rhs: Union[int, float, Expression]) -> Function:
400  # pylint: disable=redefined-builtin
401  # pylint: disable=invalid-name
402  return Function('d_ratio', lhs, rhs)
403
404
405def source_count(event: Event) -> Function:
406  # pylint: disable=redefined-builtin
407  # pylint: disable=invalid-name
408  return Function('source_count', event)
409
410
411class Metric:
412  """An individual metric that will specifiable on the perf command line."""
413  groups: Set[str]
414  expr: Expression
415  scale_unit: str
416  constraint: bool
417
418  def __init__(self,
419               name: str,
420               description: str,
421               expr: Expression,
422               scale_unit: str,
423               constraint: bool = False):
424    self.name = name
425    self.description = description
426    self.expr = expr.Simplify()
427    # Workraound valid_only_metric hiding certain metrics based on unit.
428    scale_unit = scale_unit.replace('/sec', ' per sec')
429    if scale_unit[0].isdigit():
430      self.scale_unit = scale_unit
431    else:
432      self.scale_unit = f'1{scale_unit}'
433    self.constraint = constraint
434    self.groups = set()
435
436  def __lt__(self, other):
437    """Sort order."""
438    return self.name < other.name
439
440  def AddToMetricGroup(self, group):
441    """Callback used when being added to a MetricGroup."""
442    self.groups.add(group.name)
443
444  def Flatten(self) -> Set['Metric']:
445    """Return a leaf metric."""
446    return set([self])
447
448  def ToPerfJson(self) -> Dict[str, str]:
449    """Return as dictionary for Json generation."""
450    result = {
451        'MetricName': self.name,
452        'MetricGroup': ';'.join(sorted(self.groups)),
453        'BriefDescription': self.description,
454        'MetricExpr': self.expr.ToPerfJson(),
455        'ScaleUnit': self.scale_unit
456    }
457    if self.constraint:
458      result['MetricConstraint'] = 'NO_NMI_WATCHDOG'
459
460    return result
461
462
463class _MetricJsonEncoder(json.JSONEncoder):
464  """Special handling for Metric objects."""
465
466  def default(self, o):
467    if isinstance(o, Metric):
468      return o.ToPerfJson()
469    return json.JSONEncoder.default(self, o)
470
471
472class MetricGroup:
473  """A group of metrics.
474
475  Metric groups may be specificd on the perf command line, but within
476  the json they aren't encoded. Metrics may be in multiple groups
477  which can facilitate arrangements similar to trees.
478  """
479
480  def __init__(self, name: str, metric_list: List[Union[Metric,
481                                                        'MetricGroup']]):
482    self.name = name
483    self.metric_list = metric_list
484    for metric in metric_list:
485      metric.AddToMetricGroup(self)
486
487  def AddToMetricGroup(self, group):
488    """Callback used when a MetricGroup is added into another."""
489    for metric in self.metric_list:
490      metric.AddToMetricGroup(group)
491
492  def Flatten(self) -> Set[Metric]:
493    """Returns a set of all leaf metrics."""
494    result = set()
495    for x in self.metric_list:
496      result = result.union(x.Flatten())
497
498    return result
499
500  def ToPerfJson(self) -> str:
501    return json.dumps(sorted(self.Flatten()), indent=2, cls=_MetricJsonEncoder)
502
503  def __str__(self) -> str:
504    return self.ToPerfJson()
505
506
507class _RewriteIfExpToSelect(ast.NodeTransformer):
508  """Transformer to convert if-else nodes to Select expressions."""
509
510  def visit_IfExp(self, node):
511    # pylint: disable=invalid-name
512    self.generic_visit(node)
513    call = ast.Call(
514        func=ast.Name(id='Select', ctx=ast.Load()),
515        args=[node.body, node.test, node.orelse],
516        keywords=[])
517    ast.copy_location(call, node.test)
518    return call
519
520
521def ParsePerfJson(orig: str) -> Expression:
522  """A simple json metric expression decoder.
523
524  Converts a json encoded metric expression by way of python's ast and
525  eval routine. First tokens are mapped to Event calls, then
526  accidentally converted keywords or literals are mapped to their
527  appropriate calls. Python's ast is used to match if-else that can't
528  be handled via operator overloading. Finally the ast is evaluated.
529
530  Args:
531    orig (str): String to parse.
532
533  Returns:
534    Expression: The parsed string.
535  """
536  # pylint: disable=eval-used
537  py = orig.strip()
538  py = re.sub(r'([a-zA-Z][^-+/\* \\\(\),]*(?:\\.[^-+/\* \\\(\),]*)*)',
539              r'Event(r"\1")', py)
540  py = re.sub(r'#Event\(r"([^"]*)"\)', r'Literal("#\1")', py)
541  py = re.sub(r'([0-9]+)Event\(r"(e[0-9]+)"\)', r'\1\2', py)
542  keywords = ['if', 'else', 'min', 'max', 'd_ratio', 'source_count']
543  for kw in keywords:
544    py = re.sub(rf'Event\(r"{kw}"\)', kw, py)
545
546  try:
547    parsed = ast.parse(py, mode='eval')
548  except SyntaxError as e:
549    raise SyntaxError(f'Parsing expression:\n{orig}') from e
550  _RewriteIfExpToSelect().visit(parsed)
551  parsed = ast.fix_missing_locations(parsed)
552  return _Constify(eval(compile(parsed, orig, 'eval')))
553
554
555def RewriteMetricsInTermsOfOthers(metrics: List[Tuple[str, Expression]]
556                                  )-> Dict[str, Expression]:
557  """Shorten metrics by rewriting in terms of others.
558
559  Args:
560    metrics (list): pairs of metric names and their expressions.
561  Returns:
562    Dict: mapping from a metric name to a shortened expression.
563  """
564  updates: Dict[str, Expression] = dict()
565  for outer_name, outer_expression in metrics:
566    updated = outer_expression
567    while True:
568      for inner_name, inner_expression in metrics:
569        if inner_name.lower() == outer_name.lower():
570          continue
571        if inner_name in updates:
572          inner_expression = updates[inner_name]
573        updated = updated.Substitute(inner_name, inner_expression)
574      if updated.Equals(outer_expression):
575        break
576      if outer_name in updates and updated.Equals(updates[outer_name]):
577        break
578      updates[outer_name] = updated
579  return updates
580