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