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