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