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 411def has_event(event: Event) -> Function: 412 # pylint: disable=redefined-builtin 413 # pylint: disable=invalid-name 414 return Function('has_event', event) 415 416 417class Metric: 418 """An individual metric that will specifiable on the perf command line.""" 419 groups: Set[str] 420 expr: Expression 421 scale_unit: str 422 constraint: bool 423 424 def __init__(self, 425 name: str, 426 description: str, 427 expr: Expression, 428 scale_unit: str, 429 constraint: bool = False): 430 self.name = name 431 self.description = description 432 self.expr = expr.Simplify() 433 # Workraound valid_only_metric hiding certain metrics based on unit. 434 scale_unit = scale_unit.replace('/sec', ' per sec') 435 if scale_unit[0].isdigit(): 436 self.scale_unit = scale_unit 437 else: 438 self.scale_unit = f'1{scale_unit}' 439 self.constraint = constraint 440 self.groups = set() 441 442 def __lt__(self, other): 443 """Sort order.""" 444 return self.name < other.name 445 446 def AddToMetricGroup(self, group): 447 """Callback used when being added to a MetricGroup.""" 448 self.groups.add(group.name) 449 450 def Flatten(self) -> Set['Metric']: 451 """Return a leaf metric.""" 452 return set([self]) 453 454 def ToPerfJson(self) -> Dict[str, str]: 455 """Return as dictionary for Json generation.""" 456 result = { 457 'MetricName': self.name, 458 'MetricGroup': ';'.join(sorted(self.groups)), 459 'BriefDescription': self.description, 460 'MetricExpr': self.expr.ToPerfJson(), 461 'ScaleUnit': self.scale_unit 462 } 463 if self.constraint: 464 result['MetricConstraint'] = 'NO_NMI_WATCHDOG' 465 466 return result 467 468 469class _MetricJsonEncoder(json.JSONEncoder): 470 """Special handling for Metric objects.""" 471 472 def default(self, o): 473 if isinstance(o, Metric): 474 return o.ToPerfJson() 475 return json.JSONEncoder.default(self, o) 476 477 478class MetricGroup: 479 """A group of metrics. 480 481 Metric groups may be specificd on the perf command line, but within 482 the json they aren't encoded. Metrics may be in multiple groups 483 which can facilitate arrangements similar to trees. 484 """ 485 486 def __init__(self, name: str, metric_list: List[Union[Metric, 487 'MetricGroup']]): 488 self.name = name 489 self.metric_list = metric_list 490 for metric in metric_list: 491 metric.AddToMetricGroup(self) 492 493 def AddToMetricGroup(self, group): 494 """Callback used when a MetricGroup is added into another.""" 495 for metric in self.metric_list: 496 metric.AddToMetricGroup(group) 497 498 def Flatten(self) -> Set[Metric]: 499 """Returns a set of all leaf metrics.""" 500 result = set() 501 for x in self.metric_list: 502 result = result.union(x.Flatten()) 503 504 return result 505 506 def ToPerfJson(self) -> str: 507 return json.dumps(sorted(self.Flatten()), indent=2, cls=_MetricJsonEncoder) 508 509 def __str__(self) -> str: 510 return self.ToPerfJson() 511 512 513class _RewriteIfExpToSelect(ast.NodeTransformer): 514 """Transformer to convert if-else nodes to Select expressions.""" 515 516 def visit_IfExp(self, node): 517 # pylint: disable=invalid-name 518 self.generic_visit(node) 519 call = ast.Call( 520 func=ast.Name(id='Select', ctx=ast.Load()), 521 args=[node.body, node.test, node.orelse], 522 keywords=[]) 523 ast.copy_location(call, node.test) 524 return call 525 526 527def ParsePerfJson(orig: str) -> Expression: 528 """A simple json metric expression decoder. 529 530 Converts a json encoded metric expression by way of python's ast and 531 eval routine. First tokens are mapped to Event calls, then 532 accidentally converted keywords or literals are mapped to their 533 appropriate calls. Python's ast is used to match if-else that can't 534 be handled via operator overloading. Finally the ast is evaluated. 535 536 Args: 537 orig (str): String to parse. 538 539 Returns: 540 Expression: The parsed string. 541 """ 542 # pylint: disable=eval-used 543 py = orig.strip() 544 py = re.sub(r'([a-zA-Z][^-+/\* \\\(\),]*(?:\\.[^-+/\* \\\(\),]*)*)', 545 r'Event(r"\1")', py) 546 py = re.sub(r'#Event\(r"([^"]*)"\)', r'Literal("#\1")', py) 547 py = re.sub(r'([0-9]+)Event\(r"(e[0-9]+)"\)', r'\1\2', py) 548 keywords = ['if', 'else', 'min', 'max', 'd_ratio', 'source_count', 'has_event'] 549 for kw in keywords: 550 py = re.sub(rf'Event\(r"{kw}"\)', kw, py) 551 552 try: 553 parsed = ast.parse(py, mode='eval') 554 except SyntaxError as e: 555 raise SyntaxError(f'Parsing expression:\n{orig}') from e 556 _RewriteIfExpToSelect().visit(parsed) 557 parsed = ast.fix_missing_locations(parsed) 558 return _Constify(eval(compile(parsed, orig, 'eval'))) 559 560 561def RewriteMetricsInTermsOfOthers(metrics: List[Tuple[str, str, Expression]] 562 )-> Dict[Tuple[str, str], Expression]: 563 """Shorten metrics by rewriting in terms of others. 564 565 Args: 566 metrics (list): pmus, metric names and their expressions. 567 Returns: 568 Dict: mapping from a pmu, metric name pair to a shortened expression. 569 """ 570 updates: Dict[Tuple[str, str], Expression] = dict() 571 for outer_pmu, outer_name, outer_expression in metrics: 572 if outer_pmu is None: 573 outer_pmu = 'cpu' 574 updated = outer_expression 575 while True: 576 for inner_pmu, inner_name, inner_expression in metrics: 577 if inner_pmu is None: 578 inner_pmu = 'cpu' 579 if inner_pmu.lower() != outer_pmu.lower(): 580 continue 581 if inner_name.lower() == outer_name.lower(): 582 continue 583 if (inner_pmu, inner_name) in updates: 584 inner_expression = updates[(inner_pmu, inner_name)] 585 updated = updated.Substitute(inner_name, inner_expression) 586 if updated.Equals(outer_expression): 587 break 588 if (outer_pmu, outer_name) in updates and updated.Equals(updates[(outer_pmu, outer_name)]): 589 break 590 updates[(outer_pmu, outer_name)] = updated 591 return updates 592