blob: bcdbf93d367a4f0fd06f317de47fc13e87d59e39 [file] [log] [blame]
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import dataclasses as dc
import enum
from typing import BinaryIO, Dict, Generator, List, Type, Union
from typing import Generic, Tuple, TypeVar, get_type_hints
from perfetto.trace_uri_resolver import util
TraceUri = str
TraceGenerator = Generator[bytes, None, None]
TraceContent = Union[BinaryIO, TraceGenerator]
_T = TypeVar('_T')
@dc.dataclass
class ConstraintClass(Generic[_T]):
class Op(enum.Enum):
EQ = '='
NE = '!='
LE = '<='
GE = '>='
GT = '>'
LT = '<'
def __str__(self):
return self.value
value: _T
op: Op = Op.EQ
Constraint = Union[_T, ConstraintClass[_T]]
ConstraintWithList = Union[Constraint[_T], Constraint[List[_T]]]
class TraceUriResolver:
""""Resolves a trace URI (e.g. 'ants:trace_id=1234') into a list of traces.
This class can be subclassed to provide a pluggable mechanism for looking
up traces using URI strings.
For example:
class CustomTraceResolver(TraceUriResolver):
PREFIX = 'custom'
def __init__(self, build_branch: List[str] = None, id: str = None):
self.build_branch = build_branch
self.id = id
self.db = init_db()
def resolve(self):
traces = self.db.lookup(
id=self.id, build_branch=self.build_branch)['path']
return [
TraceUriResolver.Result(
trace=t['path'],
args={'iteration': t['iteration'], 'device': t['device']}
)
for t in traces
]
Trace resolvers can be passed to trace processor directly:
with TraceProcessor(CustomTraceResolver(id='abcdefg')) as tp:
tp.query('select * from slice')
Alternatively, a trace addesses can be passed:
config = TraceProcessorConfig(
resolver_registry=ResolverRegistry(resolvers=[CustomTraceResolver])
)
with TraceProcessor('custom:id=abcdefg', config=config) as tp:
tp.query('select * from slice')
"""
# Subclasses should set PREFIX to match the trace address prefix they
# want to handle.
PREFIX: str = None
@dc.dataclass
class Result:
# TraceUri is present here because it allows recursive lookups (i.e.
# a resolver which returns a path to a trace).
trace: Union[TraceUri, TraceContent]
# metadata allows additional key-value pairs to be provided which are
# associated for trace. For example, test names and iteration numbers
# could be provivded for traces originating from lab tests.
metadata: Dict[str, str]
def __init__(self,
trace: Union[TraceUri, TraceContent],
metadata: Dict[str, str] = dict()):
self.trace = trace
self.metadata = metadata
def resolve(self) -> List['TraceUriResolver.Result']:
"""Resolves a list of traces.
Subclasses should implement this method and resolve the parameters
specified in the constructor to a list of traces.
"""
raise Exception('resolve is unimplemented for this resolver')
@classmethod
def from_trace_uri(cls: Type['TraceUriResolver'],
uri: TraceUri) -> 'TraceUriResolver':
"""Creates a resolver from a URI.
URIs have the form:
android_ci:day=2021-01-01;devices=blueline,crosshatch;key>=value
This is converted to a dictionary of the form:
{'day': '2021-01-01', 'id': ['blueline', 'crosshatch'],
'key': ConstraintClass('value', Op.GE)}
and passed as kwargs to the constructor of the trace resolver (see class
documentation for info).
Generally, sublcasses should not override this method as the standard
trace address format should work for most usecases. Instead, simply
define your constructor with the parameters you expect to see in the
trace address.
"""
return cls(**_args_dict_from_uri(uri, get_type_hints(cls.__init__)))
def _read_op(arg_str: str, op_start_ind: int) -> ConstraintClass.Op:
"""Parse operator from string.
Given string and an expected start index for operator it returns Op object or
raises error if operator was not found.
For example:
_read_op('a>4', 1) returns Op.GE
_read_op('a>4', 0) raises ValueError
_read_op('a>4', 3) raises ValueError
"""
first = arg_str[op_start_ind] if op_start_ind < len(arg_str) else None
second = arg_str[op_start_ind +
1] if op_start_ind + 1 < len(arg_str) else None
Op = ConstraintClass.Op
if first == '>':
return Op.GE if second == '=' else Op.GT
elif first == '<':
return Op.LE if second == '=' else Op.LT
elif first == '!' and second == '=':
return Op.NE
elif first == '=':
return Op.EQ
raise ValueError('Could not find valid operator in uri arg_str: ' + arg_str)
def _parse_arg(arg_str: str) -> Tuple[str, ConstraintClass.Op, str]:
"""Parse argument string and return a tuple (key, operator, value).
Given a string like 'branch_num>=4000', it returns a tuple ('branch_num',
Op.GE,'4000'). Raises ValueError exceptions in case ill formed arg_str is
passed like '>30', 'key>', 'key', 'key--31'
"""
op_start_ind = 0
for ind, c in enumerate(arg_str):
if not c.isalnum() and c != '_':
op_start_ind = ind
break
if op_start_ind == 0:
raise ValueError('Could not find valid key in arg_str: ' + arg_str)
key = arg_str[:op_start_ind]
op = _read_op(arg_str, op_start_ind)
value = arg_str[op_start_ind + len(str(op)):]
if not value:
raise ValueError('Empty value in trace uri arg_str: ' + arg_str)
return (key, op, value)
def _args_dict_from_uri(uri: str,
type_hints) -> Dict[str, ConstraintWithList[str]]:
"""Creates an the args dictionary from a trace URI.
URIs have the form:
android_ci:day=2021-01-01;devices=blueline,crosshatch;key>=value
This is converted to a dictionary of the form:
{'day': '2021-01-01', 'id': ['blueline', 'crosshatch'],
'key': ConstraintClass('value', Op.GE)}
"""
_, args_str = util.parse_trace_uri(uri)
if not args_str:
return {}
args_lst = args_str.split(';')
args_dict = dict()
for arg in args_lst:
(key, op, value) = _parse_arg(arg)
lst = value.split(',')
if len(lst) > 1:
args_dict[key] = lst
else:
args_dict[key] = value
if key not in type_hints:
if op != ConstraintClass.Op.EQ:
raise ValueError(f'{key} only supports "=" operator')
continue
have_constraint = False
type_hint = type_hints[key]
type_args = type_hint.__args__ if hasattr(type_hint, '__args__') else ()
for type_arg in type_args:
type_origin = type_arg.__origin__ if hasattr(type_arg,
'__origin__') else None
if type_origin is ConstraintClass:
have_constraint = True
break
if not have_constraint and op != ConstraintClass.Op.EQ:
raise ValueError('Operator other than "=" passed to argument which '
'does not have constraint type: ' + arg)
if have_constraint:
args_dict[key] = ConstraintClass(args_dict[key], op)
return args_dict