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