Loading...
Note: File does not exist in v4.17.
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')))