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