summaryrefslogtreecommitdiff
path: root/simpleperf/scripts/simpleperf_report_lib.py
diff options
context:
space:
mode:
Diffstat (limited to 'simpleperf/scripts/simpleperf_report_lib.py')
-rw-r--r--simpleperf/scripts/simpleperf_report_lib.py300
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