|  | #!/usr/bin/env python3 | 
|  | # | 
|  | # Pretty-printer for simple trace backend binary trace files | 
|  | # | 
|  | # Copyright IBM, Corp. 2010 | 
|  | # | 
|  | # This work is licensed under the terms of the GNU GPL, version 2.  See | 
|  | # the COPYING file in the top-level directory. | 
|  | # | 
|  | # For help see docs/devel/tracing.rst | 
|  |  | 
|  | import sys | 
|  | import struct | 
|  | import inspect | 
|  | import warnings | 
|  | from tracetool import read_events, Event | 
|  | from tracetool.backend.simple import is_string | 
|  |  | 
|  | __all__ = ['Analyzer', 'Analyzer2', 'process', 'run'] | 
|  |  | 
|  | # This is the binary format that the QEMU "simple" trace backend | 
|  | # emits. There is no specification documentation because the format is | 
|  | # not guaranteed to be stable. Trace files must be parsed with the | 
|  | # same trace-events-all file and the same simpletrace.py file that | 
|  | # QEMU was built with. | 
|  | header_event_id = 0xffffffffffffffff | 
|  | header_magic    = 0xf2b177cb0aa429b4 | 
|  | dropped_event_id = 0xfffffffffffffffe | 
|  |  | 
|  | record_type_mapping = 0 | 
|  | record_type_event = 1 | 
|  |  | 
|  | log_header_fmt = '=QQQ' | 
|  | rec_header_fmt = '=QQII' | 
|  | rec_header_fmt_len = struct.calcsize(rec_header_fmt) | 
|  |  | 
|  | class SimpleException(Exception): | 
|  | pass | 
|  |  | 
|  | def read_header(fobj, hfmt): | 
|  | '''Read a trace record header''' | 
|  | hlen = struct.calcsize(hfmt) | 
|  | hdr = fobj.read(hlen) | 
|  | if len(hdr) != hlen: | 
|  | raise SimpleException('Error reading header. Wrong filetype provided?') | 
|  | return struct.unpack(hfmt, hdr) | 
|  |  | 
|  | def get_mapping(fobj): | 
|  | (event_id, ) = struct.unpack('=Q', fobj.read(8)) | 
|  | (len, ) = struct.unpack('=L', fobj.read(4)) | 
|  | name = fobj.read(len).decode() | 
|  |  | 
|  | return (event_id, name) | 
|  |  | 
|  | def read_record(fobj): | 
|  | """Deserialize a trace record from a file into a tuple (event_num, timestamp, pid, args).""" | 
|  | event_id, timestamp_ns, record_length, record_pid = read_header(fobj, rec_header_fmt) | 
|  | args_payload = fobj.read(record_length - rec_header_fmt_len) | 
|  | return (event_id, timestamp_ns, record_pid, args_payload) | 
|  |  | 
|  | def read_trace_header(fobj): | 
|  | """Read and verify trace file header""" | 
|  | _header_event_id, _header_magic, log_version = read_header(fobj, log_header_fmt) | 
|  | if _header_event_id != header_event_id: | 
|  | raise ValueError(f'Not a valid trace file, header id {_header_event_id} != {header_event_id}') | 
|  | if _header_magic != header_magic: | 
|  | raise ValueError(f'Not a valid trace file, header magic {_header_magic} != {header_magic}') | 
|  |  | 
|  | if log_version not in [0, 2, 3, 4]: | 
|  | raise ValueError(f'Unknown version {log_version} of tracelog format!') | 
|  | if log_version != 4: | 
|  | raise ValueError(f'Log format {log_version} not supported with this QEMU release!') | 
|  |  | 
|  | def read_trace_records(events, fobj, read_header): | 
|  | """Deserialize trace records from a file, yielding record tuples (event, event_num, timestamp, pid, arg1, ..., arg6). | 
|  |  | 
|  | Args: | 
|  | event_mapping (str -> Event): events dict, indexed by name | 
|  | fobj (file): input file | 
|  | read_header (bool): whether headers were read from fobj | 
|  |  | 
|  | """ | 
|  | frameinfo = inspect.getframeinfo(inspect.currentframe()) | 
|  | dropped_event = Event.build("Dropped_Event(uint64_t num_events_dropped)", | 
|  | frameinfo.lineno + 1, frameinfo.filename) | 
|  |  | 
|  | event_mapping = {e.name: e for e in events} | 
|  | event_mapping["dropped"] = dropped_event | 
|  | event_id_to_name = {dropped_event_id: "dropped"} | 
|  |  | 
|  | # If there is no header assume event ID mapping matches events list | 
|  | if not read_header: | 
|  | for event_id, event in enumerate(events): | 
|  | event_id_to_name[event_id] = event.name | 
|  |  | 
|  | while True: | 
|  | t = fobj.read(8) | 
|  | if len(t) == 0: | 
|  | break | 
|  |  | 
|  | (rectype, ) = struct.unpack('=Q', t) | 
|  | if rectype == record_type_mapping: | 
|  | event_id, event_name = get_mapping(fobj) | 
|  | event_id_to_name[event_id] = event_name | 
|  | else: | 
|  | event_id, timestamp_ns, pid, args_payload = read_record(fobj) | 
|  | event_name = event_id_to_name[event_id] | 
|  |  | 
|  | try: | 
|  | event = event_mapping[event_name] | 
|  | except KeyError as e: | 
|  | raise SimpleException( | 
|  | f'{e} event is logged but is not declared in the trace events' | 
|  | 'file, try using trace-events-all instead.' | 
|  | ) | 
|  |  | 
|  | offset = 0 | 
|  | args = [] | 
|  | for type, _ in event.args: | 
|  | if is_string(type): | 
|  | (length,) = struct.unpack_from('=L', args_payload, offset=offset) | 
|  | offset += 4 | 
|  | s = args_payload[offset:offset+length] | 
|  | offset += length | 
|  | args.append(s) | 
|  | else: | 
|  | (value,) = struct.unpack_from('=Q', args_payload, offset=offset) | 
|  | offset += 8 | 
|  | args.append(value) | 
|  |  | 
|  | yield (event_mapping[event_name], event_name, timestamp_ns, pid) + tuple(args) | 
|  |  | 
|  | class Analyzer: | 
|  | """[Deprecated. Refer to Analyzer2 instead.] | 
|  |  | 
|  | A trace file analyzer which processes trace records. | 
|  |  | 
|  | An analyzer can be passed to run() or process().  The begin() method is | 
|  | invoked, then each trace record is processed, and finally the end() method | 
|  | is invoked. When Analyzer is used as a context-manager (using the `with` | 
|  | statement), begin() and end() are called automatically. | 
|  |  | 
|  | If a method matching a trace event name exists, it is invoked to process | 
|  | that trace record.  Otherwise the catchall() method is invoked. | 
|  |  | 
|  | Example: | 
|  | The following method handles the runstate_set(int new_state) trace event:: | 
|  |  | 
|  | def runstate_set(self, new_state): | 
|  | ... | 
|  |  | 
|  | The method can also take a timestamp argument before the trace event | 
|  | arguments:: | 
|  |  | 
|  | def runstate_set(self, timestamp, new_state): | 
|  | ... | 
|  |  | 
|  | Timestamps have the uint64_t type and are in nanoseconds. | 
|  |  | 
|  | The pid can be included in addition to the timestamp and is useful when | 
|  | dealing with traces from multiple processes:: | 
|  |  | 
|  | def runstate_set(self, timestamp, pid, new_state): | 
|  | ... | 
|  | """ | 
|  |  | 
|  | def begin(self): | 
|  | """Called at the start of the trace.""" | 
|  | pass | 
|  |  | 
|  | def catchall(self, event, rec): | 
|  | """Called if no specific method for processing a trace event has been found.""" | 
|  | pass | 
|  |  | 
|  | def _build_fn(self, event): | 
|  | fn = getattr(self, event.name, None) | 
|  | if fn is None: | 
|  | # Return early to avoid costly call to inspect.getfullargspec | 
|  | return self.catchall | 
|  |  | 
|  | event_argcount = len(event.args) | 
|  | fn_argcount = len(inspect.getfullargspec(fn)[0]) - 1 | 
|  | if fn_argcount == event_argcount + 1: | 
|  | # Include timestamp as first argument | 
|  | return lambda _, rec: fn(*(rec[1:2] + rec[3:3 + event_argcount])) | 
|  | elif fn_argcount == event_argcount + 2: | 
|  | # Include timestamp and pid | 
|  | return lambda _, rec: fn(*rec[1:3 + event_argcount]) | 
|  | else: | 
|  | # Just arguments, no timestamp or pid | 
|  | return lambda _, rec: fn(*rec[3:3 + event_argcount]) | 
|  |  | 
|  | def _process_event(self, rec_args, *, event, event_id, timestamp_ns, pid, **kwargs): | 
|  | warnings.warn( | 
|  | "Use of deprecated Analyzer class. Refer to Analyzer2 instead.", | 
|  | DeprecationWarning, | 
|  | ) | 
|  |  | 
|  | if not hasattr(self, '_fn_cache'): | 
|  | # NOTE: Cannot depend on downstream subclasses to have | 
|  | # super().__init__() because of legacy. | 
|  | self._fn_cache = {} | 
|  |  | 
|  | rec = (event_id, timestamp_ns, pid, *rec_args) | 
|  | if event_id not in self._fn_cache: | 
|  | self._fn_cache[event_id] = self._build_fn(event) | 
|  | self._fn_cache[event_id](event, rec) | 
|  |  | 
|  | def end(self): | 
|  | """Called at the end of the trace.""" | 
|  | pass | 
|  |  | 
|  | def __enter__(self): | 
|  | self.begin() | 
|  | return self | 
|  |  | 
|  | def __exit__(self, exc_type, exc_val, exc_tb): | 
|  | if exc_type is None: | 
|  | self.end() | 
|  | return False | 
|  |  | 
|  | class Analyzer2(Analyzer): | 
|  | """A trace file analyzer which processes trace records. | 
|  |  | 
|  | An analyzer can be passed to run() or process().  The begin() method is | 
|  | invoked, then each trace record is processed, and finally the end() method | 
|  | is invoked. When Analyzer is used as a context-manager (using the `with` | 
|  | statement), begin() and end() are called automatically. | 
|  |  | 
|  | If a method matching a trace event name exists, it is invoked to process | 
|  | that trace record.  Otherwise the catchall() method is invoked. | 
|  |  | 
|  | The methods are called with a set of keyword-arguments. These can be ignored | 
|  | using `**kwargs` or defined like any keyword-argument. | 
|  |  | 
|  | The following keyword-arguments are available, but make sure to have an | 
|  | **kwargs to allow for unmatched arguments in the future: | 
|  | event: Event object of current trace | 
|  | event_id: The id of the event in the current trace file | 
|  | timestamp_ns: The timestamp in nanoseconds of the trace | 
|  | pid: The process id recorded for the given trace | 
|  |  | 
|  | Example: | 
|  | The following method handles the runstate_set(int new_state) trace event:: | 
|  |  | 
|  | def runstate_set(self, new_state, **kwargs): | 
|  | ... | 
|  |  | 
|  | The method can also explicitly take a timestamp keyword-argument with the | 
|  | trace event arguments:: | 
|  |  | 
|  | def runstate_set(self, new_state, *, timestamp_ns, **kwargs): | 
|  | ... | 
|  |  | 
|  | Timestamps have the uint64_t type and are in nanoseconds. | 
|  |  | 
|  | The pid can be included in addition to the timestamp and is useful when | 
|  | dealing with traces from multiple processes: | 
|  |  | 
|  | def runstate_set(self, new_state, *, timestamp_ns, pid, **kwargs): | 
|  | ... | 
|  | """ | 
|  |  | 
|  | def catchall(self, *rec_args, event, timestamp_ns, pid, event_id, **kwargs): | 
|  | """Called if no specific method for processing a trace event has been found.""" | 
|  | pass | 
|  |  | 
|  | def _process_event(self, rec_args, *, event, **kwargs): | 
|  | fn = getattr(self, event.name, self.catchall) | 
|  | fn(*rec_args, event=event, **kwargs) | 
|  |  | 
|  | def process(events, log, analyzer, read_header=True): | 
|  | """Invoke an analyzer on each event in a log. | 
|  | Args: | 
|  | events (file-object or list or str): events list or file-like object or file path as str to read event data from | 
|  | log (file-object or str): file-like object or file path as str to read log data from | 
|  | analyzer (Analyzer): Instance of Analyzer to interpret the event data | 
|  | read_header (bool, optional): Whether to read header data from the log data. Defaults to True. | 
|  | """ | 
|  |  | 
|  | if isinstance(events, str): | 
|  | with open(events, 'r') as f: | 
|  | events_list = read_events(f, events) | 
|  | elif isinstance(events, list): | 
|  | # Treat as a list of events already produced by tracetool.read_events | 
|  | events_list = events | 
|  | else: | 
|  | # Treat as an already opened file-object | 
|  | events_list = read_events(events, events.name) | 
|  |  | 
|  | if isinstance(log, str): | 
|  | with open(log, 'rb') as log_fobj: | 
|  | _process(events_list, log_fobj, analyzer, read_header) | 
|  | else: | 
|  | # Treat `log` as an already opened file-object. We will not close it, | 
|  | # as we do not own it. | 
|  | _process(events_list, log, analyzer, read_header) | 
|  |  | 
|  | def _process(events, log_fobj, analyzer, read_header=True): | 
|  | """Internal function for processing | 
|  |  | 
|  | Args: | 
|  | events (list): list of events already produced by tracetool.read_events | 
|  | log_fobj (file): file-object to read log data from | 
|  | analyzer (Analyzer): the Analyzer to interpret the event data | 
|  | read_header (bool, optional): Whether to read header data from the log data. Defaults to True. | 
|  | """ | 
|  |  | 
|  | if read_header: | 
|  | read_trace_header(log_fobj) | 
|  |  | 
|  | with analyzer: | 
|  | for event, event_id, timestamp_ns, record_pid, *rec_args in read_trace_records(events, log_fobj, read_header): | 
|  | analyzer._process_event( | 
|  | rec_args, | 
|  | event=event, | 
|  | event_id=event_id, | 
|  | timestamp_ns=timestamp_ns, | 
|  | pid=record_pid, | 
|  | ) | 
|  |  | 
|  | def run(analyzer): | 
|  | """Execute an analyzer on a trace file given on the command-line. | 
|  |  | 
|  | This function is useful as a driver for simple analysis scripts.  More | 
|  | advanced scripts will want to call process() instead.""" | 
|  |  | 
|  | try: | 
|  | # NOTE: See built-in `argparse` module for a more robust cli interface | 
|  | *no_header, trace_event_path, trace_file_path = sys.argv[1:] | 
|  | assert no_header == [] or no_header == ['--no-header'], 'Invalid no-header argument' | 
|  | except (AssertionError, ValueError): | 
|  | raise SimpleException(f'usage: {sys.argv[0]} [--no-header] <trace-events> <trace-file>\n') | 
|  |  | 
|  | with open(trace_event_path, 'r') as events_fobj, open(trace_file_path, 'rb') as log_fobj: | 
|  | process(events_fobj, log_fobj, analyzer, read_header=not no_header) | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | class Formatter2(Analyzer2): | 
|  | def __init__(self): | 
|  | self.last_timestamp_ns = None | 
|  |  | 
|  | def catchall(self, *rec_args, event, timestamp_ns, pid, event_id): | 
|  | if self.last_timestamp_ns is None: | 
|  | self.last_timestamp_ns = timestamp_ns | 
|  | delta_ns = timestamp_ns - self.last_timestamp_ns | 
|  | self.last_timestamp_ns = timestamp_ns | 
|  |  | 
|  | fields = [ | 
|  | f'{name}={r}' if is_string(type) else f'{name}=0x{r:x}' | 
|  | for r, (type, name) in zip(rec_args, event.args) | 
|  | ] | 
|  | print(f'{event.name} {delta_ns / 1000:0.3f} {pid=} ' + ' '.join(fields)) | 
|  |  | 
|  | try: | 
|  | run(Formatter2()) | 
|  | except SimpleException as e: | 
|  | sys.stderr.write(str(e) + "\n") | 
|  | sys.exit(1) |