diff options
Diffstat (limited to 'simpleperf/scripts/simpleperf_report_lib.py')
-rw-r--r-- | simpleperf/scripts/simpleperf_report_lib.py | 300 |
1 files changed, 287 insertions, 13 deletions
diff --git a/simpleperf/scripts/simpleperf_report_lib.py b/simpleperf/scripts/simpleperf_report_lib.py index 5f990ac8..0f4be8c8 100644 --- a/simpleperf/scripts/simpleperf_report_lib.py +++ b/simpleperf/scripts/simpleperf_report_lib.py @@ -21,13 +21,14 @@ """ import collections +from collections import namedtuple import ctypes as ct from pathlib import Path import struct from typing import Any, Dict, List, Optional, Union -from simpleperf_utils import (bytes_to_str, get_host_binary_path, is_windows, str_to_bytes, - ReportLibOptions) +from simpleperf_utils import (bytes_to_str, get_host_binary_path, is_windows, log_exit, + str_to_bytes, ReportLibOptions) def _is_null(p: Optional[ct._Pointer]) -> bool: @@ -237,8 +238,26 @@ class ReportLibStructure(ct.Structure): _fields_ = [] +def SetReportOptionsForReportLib(report_lib, options: ReportLibOptions): + if options.proguard_mapping_files: + for file_path in options.proguard_mapping_files: + report_lib.AddProguardMappingFile(file_path) + if options.show_art_frames: + report_lib.ShowArtFrames(True) + if options.remove_method: + for name in options.remove_method: + report_lib.RemoveMethod(name) + if options.trace_offcpu: + report_lib.SetTraceOffCpuMode(options.trace_offcpu) + if options.sample_filters: + report_lib.SetSampleFilter(options.sample_filters) + if options.aggregate_threads: + report_lib.AggregateThreads(options.aggregate_threads) + + # pylint: disable=invalid-name class ReportLib(object): + """ Read contents from perf.data. """ def __init__(self, native_lib_path: Optional[str] = None): if native_lib_path is None: @@ -255,6 +274,8 @@ class ReportLib(object): self._SetKallsymsFileFunc = self._lib.SetKallsymsFile self._ShowIpForUnknownSymbolFunc = self._lib.ShowIpForUnknownSymbol self._ShowArtFramesFunc = self._lib.ShowArtFrames + self._RemoveMethodFunc = self._lib.RemoveMethod + self._RemoveMethodFunc.restype = ct.c_bool self._MergeJavaMethodsFunc = self._lib.MergeJavaMethods self._AddProguardMappingFileFunc = self._lib.AddProguardMappingFile self._AddProguardMappingFileFunc.restype = ct.c_bool @@ -302,17 +323,7 @@ class ReportLib(object): def SetReportOptions(self, options: ReportLibOptions): """ Set report options in one call. """ - if options.proguard_mapping_files: - for file_path in options.proguard_mapping_files: - self.AddProguardMappingFile(file_path) - if options.show_art_frames: - self.ShowArtFrames(True) - if options.trace_offcpu: - self.SetTraceOffCpuMode(options.trace_offcpu) - if options.sample_filters: - self.SetSampleFilter(options.sample_filters) - if options.aggregate_threads: - self.AggregateThreads(options.aggregate_threads) + SetReportOptionsForReportLib(self, options) def SetLogSeverity(self, log_level: str = 'info'): """ Set log severity of native lib, can be verbose,debug,info,error,fatal.""" @@ -336,6 +347,11 @@ class ReportLib(object): """ Show frames of internal methods of the Java interpreter. """ self._ShowArtFramesFunc(self.getInstance(), show) + def RemoveMethod(self, method_name_regex: str): + """ Remove methods with name containing method_name_regex. """ + res = self._RemoveMethodFunc(self.getInstance(), _char_pt(method_name_regex)) + _check(res, f'failed to call RemoveMethod({method_name_regex})') + def MergeJavaMethods(self, merge: bool = True): """ This option merges jitted java methods with the same name but in different jit symfiles. If possible, it also merges jitted methods with interpreted methods, @@ -529,3 +545,261 @@ class ReportLib(object): if self._instance is None: raise Exception('Instance is Closed') return self._instance + + +ProtoSample = namedtuple('ProtoSample', ['ip', 'pid', 'tid', + 'thread_comm', 'time', 'in_kernel', 'cpu', 'period']) +ProtoEvent = namedtuple('ProtoEvent', ['name', 'tracing_data_format']) +ProtoSymbol = namedtuple( + 'ProtoSymbol', + ['dso_name', 'vaddr_in_file', 'symbol_name', 'symbol_addr', 'symbol_len', 'mapping']) +ProtoMapping = namedtuple('ProtoMapping', ['start', 'end', 'pgoff']) +ProtoCallChain = namedtuple('ProtoCallChain', ['nr', 'entries']) +ProtoCallChainEntry = namedtuple('ProtoCallChainEntry', ['ip', 'symbol']) + + +class ProtoFileReportLib: + """ Read contents from profile in cmd_report_sample.proto format. + It is generated by `simpleperf report-sample`. + """ + + @staticmethod + def is_supported_format(record_file: str): + with open(record_file, 'rb') as fh: + if fh.read(10) == b'SIMPLEPERF': + return True + + @staticmethod + def get_report_sample_pb2(): + try: + import report_sample_pb2 + return report_sample_pb2 + except ImportError as e: + log_exit(f'{e}\nprotobuf package is missing or too old. Please install it like ' + + '`pip install protobuf==4.21`.') + + def __init__(self): + self.record_file = None + self.report_sample_pb2 = ProtoFileReportLib.get_report_sample_pb2() + self.records: List[self.report_sample_pb2.Record] = [] + self.record_index = -1 + self.files: List[self.report_sample_pb2.File] = [] + self.thread_map: Dict[int, self.report_sample_pb2.Thread] = {} + self.meta_info: Optional[self.report_sample_pb2.MetaInfo] = None + self.fake_mapping_starts = [] + self.sample_queue: List[self.report_sample_pb2.Sample] = collections.deque() + self.trace_offcpu_mode = None + # mapping from thread id to the last off-cpu sample in the thread + self.offcpu_samples = {} + + def Close(self): + pass + + def SetReportOptions(self, options: ReportLibOptions): + """ Set report options in one call. """ + SetReportOptionsForReportLib(self, options) + + def SetLogSeverity(self, log_level: str = 'info'): + pass + + def SetSymfs(self, symfs_dir: str): + pass + + def SetRecordFile(self, record_file: str): + self.record_file = record_file + with open(record_file, 'rb') as fh: + data = fh.read() + _check(data[:10] == b'SIMPLEPERF', f'magic number mismatch: {data[:10]}') + version = struct.unpack('<H', data[10:12])[0] + _check(version == 1, f'version mismatch: {version}') + i = 12 + while i < len(data): + _check(i + 4 <= len(data), 'data format error') + size = struct.unpack('<I', data[i:i + 4])[0] + if size == 0: + break + i += 4 + _check(i + size <= len(data), 'data format error') + record = self.report_sample_pb2.Record() + record.ParseFromString(data[i: i + size]) + i += size + if record.HasField('sample') or record.HasField('context_switch'): + self.records.append(record) + elif record.HasField('file'): + self.files.append(record.file) + elif record.HasField('thread'): + self.thread_map[record.thread.thread_id] = record.thread + elif record.HasField('meta_info'): + self.meta_info = record.meta_info + if self.meta_info.trace_offcpu: + self.trace_offcpu_mode = 'mixed-on-off-cpu' + fake_mapping_start = 0 + for file in self.files: + self.fake_mapping_starts.append(fake_mapping_start) + fake_mapping_start += len(file.symbol) + 1 + + def AddProguardMappingFile(self, mapping_file: Union[str, Path]): + """ Add proguard mapping.txt to de-obfuscate method names. """ + raise NotImplementedError( + 'Adding proguard mapping files are not implemented for report_sample profiles') + + def ShowIpForUnknownSymbol(self): + pass + + def ShowArtFrames(self, show: bool = True): + raise NotImplementedError( + 'Showing art frames are not implemented for report_sample profiles') + + def RemoveMethod(self, method_name_regex: str): + """ Remove methods with name containing method_name_regex. """ + raise NotImplementedError("Removing method isn't implemented for report_sample profiles") + + def SetSampleFilter(self, filters: List[str]): + raise NotImplementedError('sample filters are not implemented for report_sample profiles') + + def GetSupportedTraceOffCpuModes(self) -> List[str]: + """ Get trace-offcpu modes supported by the recording file. It should be called after + SetRecordFile(). The modes are only available for profiles recorded with --trace-offcpu + option. All possible modes are: + on-cpu: report on-cpu samples with period representing time spent on cpu + off-cpu: report off-cpu samples with period representing time spent off cpu + on-off-cpu: report both on-cpu samples and off-cpu samples, which can be split + by event name. + mixed-on-off-cpu: report on-cpu and off-cpu samples under the same event name. + """ + _check(self.meta_info, + 'GetSupportedTraceOffCpuModes() should be called after SetRecordFile()') + if self.meta_info.trace_offcpu: + return ['on-cpu', 'off-cpu', 'on-off-cpu', 'mixed-on-off-cpu'] + return [] + + def SetTraceOffCpuMode(self, mode: str): + """ Set trace-offcpu mode. It should be called after SetRecordFile(). + """ + _check(mode in ['on-cpu', 'off-cpu', 'on-off-cpu', 'mixed-on-off-cpu'], 'invalide mode') + # Don't check if mode is in self.GetSupportedTraceOffCpuModes(). Because the profile may + # be generated by an old simpleperf. + self.trace_offcpu_mode = mode + + def AggregateThreads(self, thread_name_regex_list: List[str]): + """ Given a list of thread name regex, threads with names matching the same regex are merged + into one thread. As a result, samples from different threads (like a thread pool) can be + shown in one flamegraph. + """ + raise NotImplementedError( + 'Aggregating threads are not implemented for report_sample profiles') + + def GetNextSample(self) -> Optional[ProtoSample]: + if self.sample_queue: + self.sample_queue.popleft() + while not self.sample_queue: + self.record_index += 1 + if self.record_index >= len(self.records): + break + record = self.records[self.record_index] + if record.HasField('sample'): + self._process_sample_record(record.sample) + elif record.HasField('context_switch'): + self._process_context_switch(record.context_switch) + return self.GetCurrentSample() + + def _process_sample_record(self, sample) -> None: + if not self.trace_offcpu_mode: + self._add_to_sample_queue(sample) + return + event_name = self._get_event_name(sample.event_type_id) + is_offcpu = 'sched_switch' in event_name + + if self.trace_offcpu_mode == 'on-cpu': + if not is_offcpu: + self._add_to_sample_queue(sample) + return + + if prev_offcpu_sample := self.offcpu_samples.get(sample.thread_id): + # If there is a previous off-cpu sample, update its period. + prev_offcpu_sample.event_count = max(sample.time - prev_offcpu_sample.time, 1) + self._add_to_sample_queue(prev_offcpu_sample) + + if is_offcpu: + self.offcpu_samples[sample.thread_id] = sample + else: + self.offcpu_samples[sample.thread_id] = None + if self.trace_offcpu_mode in ('on-off-cpu', 'mixed-on-off-cpu'): + self._add_to_sample_queue(sample) + + def _process_context_switch(self, context_switch) -> None: + if not context_switch.switch_on: + return + if prev_offcpu_sample := self.offcpu_samples.get(context_switch.thread_id): + prev_offcpu_sample.event_count = max(context_switch.time - prev_offcpu_sample.time, 1) + self.offcpu_samples[context_switch.thread_id] = None + self._add_to_sample_queue(prev_offcpu_sample) + + def _add_to_sample_queue(self, sample) -> None: + self.sample_queue.append(sample) + + def GetCurrentSample(self) -> Optional[ProtoSample]: + if not self.sample_queue: + return None + sample = self.sample_queue[0] + thread = self.thread_map[sample.thread_id] + return ProtoSample( + ip=0, pid=thread.process_id, tid=thread.thread_id, thread_comm=thread.thread_name, + time=sample.time, in_kernel=False, cpu=0, period=sample.event_count) + + def GetEventOfCurrentSample(self) -> ProtoEvent: + sample = self.sample_queue[0] + event_type_id = 0 if self.trace_offcpu_mode == 'mixed-on-off-cpu' else sample.event_type_id + event_name = self._get_event_name(event_type_id) + return ProtoEvent(name=event_name, tracing_data_format=None) + + def _get_event_name(self, event_type_id: int) -> str: + return self.meta_info.event_type[event_type_id] + + def GetSymbolOfCurrentSample(self) -> ProtoSymbol: + sample = self.sample_queue[0] + node = sample.callchain[0] + return self._build_symbol(node) + + def GetCallChainOfCurrentSample(self) -> ProtoCallChain: + entries = [] + sample = self.sample_queue[0] + for node in sample.callchain[1:]: + symbol = self._build_symbol(node) + entries.append(ProtoCallChainEntry(ip=0, symbol=symbol)) + return ProtoCallChain(nr=len(entries), entries=entries) + + def _build_symbol(self, node) -> ProtoSymbol: + file = self.files[node.file_id] + if node.symbol_id == -1: + symbol_name = 'unknown' + fake_symbol_addr = self.fake_mapping_starts[node.file_id] + len(file.symbol) + fake_symbol_pgoff = 0 + else: + symbol_name = file.symbol[node.symbol_id] + fake_symbol_addr = self.fake_mapping_starts[node.file_id] = node.symbol_id + 1 + fake_symbol_pgoff = node.symbol_id + 1 + mapping = ProtoMapping(fake_symbol_addr, 1, fake_symbol_pgoff) + return ProtoSymbol(dso_name=file.path, vaddr_in_file=node.vaddr_in_file, + symbol_name=symbol_name, symbol_addr=0, symbol_len=1, mapping=[mapping]) + + def GetBuildIdForPath(self, path: str) -> str: + return '' + + def GetRecordCmd(self) -> str: + return '' + + def GetArch(self) -> str: + return '' + + def MetaInfo(self) -> Dict[str, str]: + return {} + + +def GetReportLib(record_file: str) -> Union[ReportLib, ProtoFileReportLib]: + if ProtoFileReportLib.is_supported_format(record_file): + lib = ProtoFileReportLib() + else: + lib = ReportLib() + lib.SetRecordFile(record_file) + return lib |