503 lines
15 KiB
Python
503 lines
15 KiB
Python
|
# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||
|
"""Parse or generate representations of perf metrics."""
|
||
|
import ast
|
||
|
import decimal
|
||
|
import json
|
||
|
import re
|
||
|
from typing import Dict, List, Optional, Set, Union
|
||
|
|
||
|
|
||
|
class Expression:
|
||
|
"""Abstract base class of elements in a metric expression."""
|
||
|
|
||
|
def ToPerfJson(self) -> str:
|
||
|
"""Returns a perf json file encoded representation."""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
def ToPython(self) -> str:
|
||
|
"""Returns a python expr parseable representation."""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
def Simplify(self):
|
||
|
"""Returns a simplified version of self."""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
def Equals(self, other) -> bool:
|
||
|
"""Returns true when two expressions are the same."""
|
||
|
raise NotImplementedError()
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
return self.ToPerfJson()
|
||
|
|
||
|
def __or__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('|', self, other)
|
||
|
|
||
|
def __ror__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('|', other, self)
|
||
|
|
||
|
def __xor__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('^', self, other)
|
||
|
|
||
|
def __and__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('&', self, other)
|
||
|
|
||
|
def __lt__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('<', self, other)
|
||
|
|
||
|
def __gt__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('>', self, other)
|
||
|
|
||
|
def __add__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('+', self, other)
|
||
|
|
||
|
def __radd__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('+', other, self)
|
||
|
|
||
|
def __sub__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('-', self, other)
|
||
|
|
||
|
def __rsub__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('-', other, self)
|
||
|
|
||
|
def __mul__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('*', self, other)
|
||
|
|
||
|
def __rmul__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('*', other, self)
|
||
|
|
||
|
def __truediv__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('/', self, other)
|
||
|
|
||
|
def __rtruediv__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('/', other, self)
|
||
|
|
||
|
def __mod__(self, other: Union[int, float, 'Expression']) -> 'Operator':
|
||
|
return Operator('%', self, other)
|
||
|
|
||
|
|
||
|
def _Constify(val: Union[bool, int, float, Expression]) -> Expression:
|
||
|
"""Used to ensure that the nodes in the expression tree are all Expression."""
|
||
|
if isinstance(val, bool):
|
||
|
return Constant(1 if val else 0)
|
||
|
if isinstance(val, (int, float)):
|
||
|
return Constant(val)
|
||
|
return val
|
||
|
|
||
|
|
||
|
# Simple lookup for operator precedence, used to avoid unnecessary
|
||
|
# brackets. Precedence matches that of python and the simple expression parser.
|
||
|
_PRECEDENCE = {
|
||
|
'|': 0,
|
||
|
'^': 1,
|
||
|
'&': 2,
|
||
|
'<': 3,
|
||
|
'>': 3,
|
||
|
'+': 4,
|
||
|
'-': 4,
|
||
|
'*': 5,
|
||
|
'/': 5,
|
||
|
'%': 5,
|
||
|
}
|
||
|
|
||
|
|
||
|
class Operator(Expression):
|
||
|
"""Represents a binary operator in the parse tree."""
|
||
|
|
||
|
def __init__(self, operator: str, lhs: Union[int, float, Expression],
|
||
|
rhs: Union[int, float, Expression]):
|
||
|
self.operator = operator
|
||
|
self.lhs = _Constify(lhs)
|
||
|
self.rhs = _Constify(rhs)
|
||
|
|
||
|
def Bracket(self,
|
||
|
other: Expression,
|
||
|
other_str: str,
|
||
|
rhs: bool = False) -> str:
|
||
|
"""If necessary brackets the given other value.
|
||
|
|
||
|
If ``other`` is an operator then a bracket is necessary when
|
||
|
this/self operator has higher precedence. Consider: '(a + b) * c',
|
||
|
``other_str`` will be 'a + b'. A bracket is necessary as without
|
||
|
the bracket 'a + b * c' will evaluate 'b * c' first. However, '(a
|
||
|
* b) + c' doesn't need a bracket as 'a * b' will always be
|
||
|
evaluated first. For 'a / (b * c)' (ie the same precedence level
|
||
|
operations) then we add the bracket to best match the original
|
||
|
input, but not for '(a / b) * c' where the bracket is unnecessary.
|
||
|
|
||
|
Args:
|
||
|
other (Expression): is a lhs or rhs operator
|
||
|
other_str (str): ``other`` in the appropriate string form
|
||
|
rhs (bool): is ``other`` on the RHS
|
||
|
|
||
|
Returns:
|
||
|
str: possibly bracketed other_str
|
||
|
"""
|
||
|
if isinstance(other, Operator):
|
||
|
if _PRECEDENCE.get(self.operator, -1) > _PRECEDENCE.get(
|
||
|
other.operator, -1):
|
||
|
return f'({other_str})'
|
||
|
if rhs and _PRECEDENCE.get(self.operator, -1) == _PRECEDENCE.get(
|
||
|
other.operator, -1):
|
||
|
return f'({other_str})'
|
||
|
return other_str
|
||
|
|
||
|
def ToPerfJson(self):
|
||
|
return (f'{self.Bracket(self.lhs, self.lhs.ToPerfJson())} {self.operator} '
|
||
|
f'{self.Bracket(self.rhs, self.rhs.ToPerfJson(), True)}')
|
||
|
|
||
|
def ToPython(self):
|
||
|
return (f'{self.Bracket(self.lhs, self.lhs.ToPython())} {self.operator} '
|
||
|
f'{self.Bracket(self.rhs, self.rhs.ToPython(), True)}')
|
||
|
|
||
|
def Simplify(self) -> Expression:
|
||
|
lhs = self.lhs.Simplify()
|
||
|
rhs = self.rhs.Simplify()
|
||
|
if isinstance(lhs, Constant) and isinstance(rhs, Constant):
|
||
|
return Constant(ast.literal_eval(lhs + self.operator + rhs))
|
||
|
|
||
|
if isinstance(self.lhs, Constant):
|
||
|
if self.operator in ('+', '|') and lhs.value == '0':
|
||
|
return rhs
|
||
|
|
||
|
# Simplify multiplication by 0 except for the slot event which
|
||
|
# is deliberately introduced using this pattern.
|
||
|
if self.operator == '*' and lhs.value == '0' and (
|
||
|
not isinstance(rhs, Event) or 'slots' not in rhs.name.lower()):
|
||
|
return Constant(0)
|
||
|
|
||
|
if self.operator == '*' and lhs.value == '1':
|
||
|
return rhs
|
||
|
|
||
|
if isinstance(rhs, Constant):
|
||
|
if self.operator in ('+', '|') and rhs.value == '0':
|
||
|
return lhs
|
||
|
|
||
|
if self.operator == '*' and rhs.value == '0':
|
||
|
return Constant(0)
|
||
|
|
||
|
if self.operator == '*' and self.rhs.value == '1':
|
||
|
return lhs
|
||
|
|
||
|
return Operator(self.operator, lhs, rhs)
|
||
|
|
||
|
def Equals(self, other: Expression) -> bool:
|
||
|
if isinstance(other, Operator):
|
||
|
return self.operator == other.operator and self.lhs.Equals(
|
||
|
other.lhs) and self.rhs.Equals(other.rhs)
|
||
|
return False
|
||
|
|
||
|
|
||
|
class Select(Expression):
|
||
|
"""Represents a select ternary in the parse tree."""
|
||
|
|
||
|
def __init__(self, true_val: Union[int, float, Expression],
|
||
|
cond: Union[int, float, Expression],
|
||
|
false_val: Union[int, float, Expression]):
|
||
|
self.true_val = _Constify(true_val)
|
||
|
self.cond = _Constify(cond)
|
||
|
self.false_val = _Constify(false_val)
|
||
|
|
||
|
def ToPerfJson(self):
|
||
|
true_str = self.true_val.ToPerfJson()
|
||
|
cond_str = self.cond.ToPerfJson()
|
||
|
false_str = self.false_val.ToPerfJson()
|
||
|
return f'({true_str} if {cond_str} else {false_str})'
|
||
|
|
||
|
def ToPython(self):
|
||
|
return (f'Select({self.true_val.ToPython()}, {self.cond.ToPython()}, '
|
||
|
f'{self.false_val.ToPython()})')
|
||
|
|
||
|
def Simplify(self) -> Expression:
|
||
|
cond = self.cond.Simplify()
|
||
|
true_val = self.true_val.Simplify()
|
||
|
false_val = self.false_val.Simplify()
|
||
|
if isinstance(cond, Constant):
|
||
|
return false_val if cond.value == '0' else true_val
|
||
|
|
||
|
if true_val.Equals(false_val):
|
||
|
return true_val
|
||
|
|
||
|
return Select(true_val, cond, false_val)
|
||
|
|
||
|
def Equals(self, other: Expression) -> bool:
|
||
|
if isinstance(other, Select):
|
||
|
return self.cond.Equals(other.cond) and self.false_val.Equals(
|
||
|
other.false_val) and self.true_val.Equals(other.true_val)
|
||
|
return False
|
||
|
|
||
|
|
||
|
class Function(Expression):
|
||
|
"""A function in an expression like min, max, d_ratio."""
|
||
|
|
||
|
def __init__(self,
|
||
|
fn: str,
|
||
|
lhs: Union[int, float, Expression],
|
||
|
rhs: Optional[Union[int, float, Expression]] = None):
|
||
|
self.fn = fn
|
||
|
self.lhs = _Constify(lhs)
|
||
|
self.rhs = _Constify(rhs)
|
||
|
|
||
|
def ToPerfJson(self):
|
||
|
if self.rhs:
|
||
|
return f'{self.fn}({self.lhs.ToPerfJson()}, {self.rhs.ToPerfJson()})'
|
||
|
return f'{self.fn}({self.lhs.ToPerfJson()})'
|
||
|
|
||
|
def ToPython(self):
|
||
|
if self.rhs:
|
||
|
return f'{self.fn}({self.lhs.ToPython()}, {self.rhs.ToPython()})'
|
||
|
return f'{self.fn}({self.lhs.ToPython()})'
|
||
|
|
||
|
def Simplify(self) -> Expression:
|
||
|
lhs = self.lhs.Simplify()
|
||
|
rhs = self.rhs.Simplify() if self.rhs else None
|
||
|
if isinstance(lhs, Constant) and isinstance(rhs, Constant):
|
||
|
if self.fn == 'd_ratio':
|
||
|
if rhs.value == '0':
|
||
|
return Constant(0)
|
||
|
Constant(ast.literal_eval(f'{lhs} / {rhs}'))
|
||
|
return Constant(ast.literal_eval(f'{self.fn}({lhs}, {rhs})'))
|
||
|
|
||
|
return Function(self.fn, lhs, rhs)
|
||
|
|
||
|
def Equals(self, other: Expression) -> bool:
|
||
|
if isinstance(other, Function):
|
||
|
return self.fn == other.fn and self.lhs.Equals(
|
||
|
other.lhs) and self.rhs.Equals(other.rhs)
|
||
|
return False
|
||
|
|
||
|
|
||
|
def _FixEscapes(s: str) -> str:
|
||
|
s = re.sub(r'([^\\]),', r'\1\\,', s)
|
||
|
return re.sub(r'([^\\])=', r'\1\\=', s)
|
||
|
|
||
|
|
||
|
class Event(Expression):
|
||
|
"""An event in an expression."""
|
||
|
|
||
|
def __init__(self, name: str, legacy_name: str = ''):
|
||
|
self.name = _FixEscapes(name)
|
||
|
self.legacy_name = _FixEscapes(legacy_name)
|
||
|
|
||
|
def ToPerfJson(self):
|
||
|
result = re.sub('/', '@', self.name)
|
||
|
return result
|
||
|
|
||
|
def ToPython(self):
|
||
|
return f'Event(r"{self.name}")'
|
||
|
|
||
|
def Simplify(self) -> Expression:
|
||
|
return self
|
||
|
|
||
|
def Equals(self, other: Expression) -> bool:
|
||
|
return isinstance(other, Event) and self.name == other.name
|
||
|
|
||
|
|
||
|
class Constant(Expression):
|
||
|
"""A constant within the expression tree."""
|
||
|
|
||
|
def __init__(self, value: Union[float, str]):
|
||
|
ctx = decimal.Context()
|
||
|
ctx.prec = 20
|
||
|
dec = ctx.create_decimal(repr(value) if isinstance(value, float) else value)
|
||
|
self.value = dec.normalize().to_eng_string()
|
||
|
self.value = self.value.replace('+', '')
|
||
|
self.value = self.value.replace('E', 'e')
|
||
|
|
||
|
def ToPerfJson(self):
|
||
|
return self.value
|
||
|
|
||
|
def ToPython(self):
|
||
|
return f'Constant({self.value})'
|
||
|
|
||
|
def Simplify(self) -> Expression:
|
||
|
return self
|
||
|
|
||
|
def Equals(self, other: Expression) -> bool:
|
||
|
return isinstance(other, Constant) and self.value == other.value
|
||
|
|
||
|
|
||
|
class Literal(Expression):
|
||
|
"""A runtime literal within the expression tree."""
|
||
|
|
||
|
def __init__(self, value: str):
|
||
|
self.value = value
|
||
|
|
||
|
def ToPerfJson(self):
|
||
|
return self.value
|
||
|
|
||
|
def ToPython(self):
|
||
|
return f'Literal({self.value})'
|
||
|
|
||
|
def Simplify(self) -> Expression:
|
||
|
return self
|
||
|
|
||
|
def Equals(self, other: Expression) -> bool:
|
||
|
return isinstance(other, Literal) and self.value == other.value
|
||
|
|
||
|
|
||
|
def min(lhs: Union[int, float, Expression], rhs: Union[int, float,
|
||
|
Expression]) -> Function:
|
||
|
# pylint: disable=redefined-builtin
|
||
|
# pylint: disable=invalid-name
|
||
|
return Function('min', lhs, rhs)
|
||
|
|
||
|
|
||
|
def max(lhs: Union[int, float, Expression], rhs: Union[int, float,
|
||
|
Expression]) -> Function:
|
||
|
# pylint: disable=redefined-builtin
|
||
|
# pylint: disable=invalid-name
|
||
|
return Function('max', lhs, rhs)
|
||
|
|
||
|
|
||
|
def d_ratio(lhs: Union[int, float, Expression],
|
||
|
rhs: Union[int, float, Expression]) -> Function:
|
||
|
# pylint: disable=redefined-builtin
|
||
|
# pylint: disable=invalid-name
|
||
|
return Function('d_ratio', lhs, rhs)
|
||
|
|
||
|
|
||
|
def source_count(event: Event) -> Function:
|
||
|
# pylint: disable=redefined-builtin
|
||
|
# pylint: disable=invalid-name
|
||
|
return Function('source_count', event)
|
||
|
|
||
|
|
||
|
class Metric:
|
||
|
"""An individual metric that will specifiable on the perf command line."""
|
||
|
groups: Set[str]
|
||
|
expr: Expression
|
||
|
scale_unit: str
|
||
|
constraint: bool
|
||
|
|
||
|
def __init__(self,
|
||
|
name: str,
|
||
|
description: str,
|
||
|
expr: Expression,
|
||
|
scale_unit: str,
|
||
|
constraint: bool = False):
|
||
|
self.name = name
|
||
|
self.description = description
|
||
|
self.expr = expr.Simplify()
|
||
|
# Workraound valid_only_metric hiding certain metrics based on unit.
|
||
|
scale_unit = scale_unit.replace('/sec', ' per sec')
|
||
|
if scale_unit[0].isdigit():
|
||
|
self.scale_unit = scale_unit
|
||
|
else:
|
||
|
self.scale_unit = f'1{scale_unit}'
|
||
|
self.constraint = constraint
|
||
|
self.groups = set()
|
||
|
|
||
|
def __lt__(self, other):
|
||
|
"""Sort order."""
|
||
|
return self.name < other.name
|
||
|
|
||
|
def AddToMetricGroup(self, group):
|
||
|
"""Callback used when being added to a MetricGroup."""
|
||
|
self.groups.add(group.name)
|
||
|
|
||
|
def Flatten(self) -> Set['Metric']:
|
||
|
"""Return a leaf metric."""
|
||
|
return set([self])
|
||
|
|
||
|
def ToPerfJson(self) -> Dict[str, str]:
|
||
|
"""Return as dictionary for Json generation."""
|
||
|
result = {
|
||
|
'MetricName': self.name,
|
||
|
'MetricGroup': ';'.join(sorted(self.groups)),
|
||
|
'BriefDescription': self.description,
|
||
|
'MetricExpr': self.expr.ToPerfJson(),
|
||
|
'ScaleUnit': self.scale_unit
|
||
|
}
|
||
|
if self.constraint:
|
||
|
result['MetricConstraint'] = 'NO_NMI_WATCHDOG'
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
class _MetricJsonEncoder(json.JSONEncoder):
|
||
|
"""Special handling for Metric objects."""
|
||
|
|
||
|
def default(self, o):
|
||
|
if isinstance(o, Metric):
|
||
|
return o.ToPerfJson()
|
||
|
return json.JSONEncoder.default(self, o)
|
||
|
|
||
|
|
||
|
class MetricGroup:
|
||
|
"""A group of metrics.
|
||
|
|
||
|
Metric groups may be specificd on the perf command line, but within
|
||
|
the json they aren't encoded. Metrics may be in multiple groups
|
||
|
which can facilitate arrangements similar to trees.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, name: str, metric_list: List[Union[Metric,
|
||
|
'MetricGroup']]):
|
||
|
self.name = name
|
||
|
self.metric_list = metric_list
|
||
|
for metric in metric_list:
|
||
|
metric.AddToMetricGroup(self)
|
||
|
|
||
|
def AddToMetricGroup(self, group):
|
||
|
"""Callback used when a MetricGroup is added into another."""
|
||
|
for metric in self.metric_list:
|
||
|
metric.AddToMetricGroup(group)
|
||
|
|
||
|
def Flatten(self) -> Set[Metric]:
|
||
|
"""Returns a set of all leaf metrics."""
|
||
|
result = set()
|
||
|
for x in self.metric_list:
|
||
|
result = result.union(x.Flatten())
|
||
|
|
||
|
return result
|
||
|
|
||
|
def ToPerfJson(self) -> str:
|
||
|
return json.dumps(sorted(self.Flatten()), indent=2, cls=_MetricJsonEncoder)
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
return self.ToPerfJson()
|
||
|
|
||
|
|
||
|
class _RewriteIfExpToSelect(ast.NodeTransformer):
|
||
|
|
||
|
def visit_IfExp(self, node):
|
||
|
# pylint: disable=invalid-name
|
||
|
self.generic_visit(node)
|
||
|
call = ast.Call(
|
||
|
func=ast.Name(id='Select', ctx=ast.Load()),
|
||
|
args=[node.body, node.test, node.orelse],
|
||
|
keywords=[])
|
||
|
ast.copy_location(call, node.test)
|
||
|
return call
|
||
|
|
||
|
|
||
|
def ParsePerfJson(orig: str) -> Expression:
|
||
|
"""A simple json metric expression decoder.
|
||
|
|
||
|
Converts a json encoded metric expression by way of python's ast and
|
||
|
eval routine. First tokens are mapped to Event calls, then
|
||
|
accidentally converted keywords or literals are mapped to their
|
||
|
appropriate calls. Python's ast is used to match if-else that can't
|
||
|
be handled via operator overloading. Finally the ast is evaluated.
|
||
|
|
||
|
Args:
|
||
|
orig (str): String to parse.
|
||
|
|
||
|
Returns:
|
||
|
Expression: The parsed string.
|
||
|
"""
|
||
|
# pylint: disable=eval-used
|
||
|
py = orig.strip()
|
||
|
py = re.sub(r'([a-zA-Z][^-+/\* \\\(\),]*(?:\\.[^-+/\* \\\(\),]*)*)',
|
||
|
r'Event(r"\1")', py)
|
||
|
py = re.sub(r'#Event\(r"([^"]*)"\)', r'Literal("#\1")', py)
|
||
|
py = re.sub(r'([0-9]+)Event\(r"(e[0-9]+)"\)', r'\1\2', py)
|
||
|
keywords = ['if', 'else', 'min', 'max', 'd_ratio', 'source_count']
|
||
|
for kw in keywords:
|
||
|
py = re.sub(rf'Event\(r"{kw}"\)', kw, py)
|
||
|
|
||
|
parsed = ast.parse(py, mode='eval')
|
||
|
_RewriteIfExpToSelect().visit(parsed)
|
||
|
parsed = ast.fix_missing_locations(parsed)
|
||
|
return _Constify(eval(compile(parsed, orig, 'eval')))
|