1#!/usr/bin/env python3 2# 3# Copyright (C) 2016 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18"""simpleperf_report_lib.py: a python wrapper of libsimpleperf_report.so. 19 Used to access samples in perf.data. 20 21""" 22 23import collections 24from collections import namedtuple 25import ctypes as ct 26from pathlib import Path 27import struct 28from typing import Any, Dict, List, Optional, Union 29 30from simpleperf_utils import (bytes_to_str, get_host_binary_path, is_windows, log_exit, 31 str_to_bytes, ReportLibOptions) 32 33 34def _is_null(p: Optional[ct._Pointer]) -> bool: 35 if p: 36 return False 37 return ct.cast(p, ct.c_void_p).value is None 38 39 40def _char_pt(s: str) -> bytes: 41 return str_to_bytes(s) 42 43 44def _char_pt_to_str(char_pt: ct.c_char_p) -> str: 45 return bytes_to_str(char_pt) 46 47 48def _check(cond: bool, failmsg: str): 49 if not cond: 50 raise RuntimeError(failmsg) 51 52 53class SampleStruct(ct.Structure): 54 """ Instance of a sample in perf.data. 55 ip: the program counter of the thread generating the sample. 56 pid: process id (or thread group id) of the thread generating the sample. 57 tid: thread id. 58 thread_comm: thread name. 59 time: time at which the sample was generated. The value is in nanoseconds. 60 The clock is decided by the --clockid option in `simpleperf record`. 61 in_kernel: whether the instruction is in kernel space or user space. 62 cpu: the cpu generating the sample. 63 period: count of events have happened since last sample. For example, if we use 64 -e cpu-cycles, it means how many cpu-cycles have happened. 65 If we use -e cpu-clock, it means how many nanoseconds have passed. 66 """ 67 _fields_ = [('ip', ct.c_uint64), 68 ('pid', ct.c_uint32), 69 ('tid', ct.c_uint32), 70 ('_thread_comm', ct.c_char_p), 71 ('time', ct.c_uint64), 72 ('_in_kernel', ct.c_uint32), 73 ('cpu', ct.c_uint32), 74 ('period', ct.c_uint64)] 75 76 @property 77 def thread_comm(self) -> str: 78 return _char_pt_to_str(self._thread_comm) 79 80 @property 81 def in_kernel(self) -> bool: 82 return bool(self._in_kernel) 83 84 85class TracingFieldFormatStruct(ct.Structure): 86 """Format of a tracing field. 87 name: name of the field. 88 offset: offset of the field in tracing data. 89 elem_size: size of the element type. 90 elem_count: the number of elements in this field, more than one if the field is an array. 91 is_signed: whether the element type is signed or unsigned. 92 is_dynamic: whether the element is a dynamic string. 93 """ 94 _fields_ = [('_name', ct.c_char_p), 95 ('offset', ct.c_uint32), 96 ('elem_size', ct.c_uint32), 97 ('elem_count', ct.c_uint32), 98 ('is_signed', ct.c_uint32), 99 ('is_dynamic', ct.c_uint32)] 100 101 _unpack_key_dict = {1: 'b', 2: 'h', 4: 'i', 8: 'q'} 102 103 @property 104 def name(self) -> str: 105 return _char_pt_to_str(self._name) 106 107 def parse_value(self, data: ct.c_char_p) -> Union[str, bytes, List[bytes]]: 108 """ Parse value of a field in a tracepoint event. 109 The return value depends on the type of the field, and can be an int value, a string, 110 an array of int values, etc. If the type can't be parsed, return a byte array or an 111 array of byte arrays. 112 """ 113 if self.is_dynamic: 114 offset, max_len = struct.unpack('<HH', data[self.offset:self.offset + 4]) 115 length = 0 116 while length < max_len and bytes_to_str(data[offset + length]) != '\x00': 117 length += 1 118 return bytes_to_str(data[offset: offset + length]) 119 120 if self.elem_count > 1 and self.elem_size == 1: 121 # Probably the field is a string. 122 # Don't use self.is_signed, which has different values on x86 and arm. 123 length = 0 124 while length < self.elem_count and bytes_to_str(data[self.offset + length]) != '\x00': 125 length += 1 126 return bytes_to_str(data[self.offset: self.offset + length]) 127 unpack_key = self._unpack_key_dict.get(self.elem_size) 128 if unpack_key: 129 if not self.is_signed: 130 unpack_key = unpack_key.upper() 131 value = struct.unpack('%d%s' % (self.elem_count, unpack_key), 132 data[self.offset:self.offset + self.elem_count * self.elem_size]) 133 else: 134 # Since we don't know the element type, just return the bytes. 135 value = [] 136 offset = self.offset 137 for _ in range(self.elem_count): 138 value.append(data[offset: offset + self.elem_size]) 139 offset += self.elem_size 140 if self.elem_count == 1: 141 value = value[0] 142 return value 143 144 145class TracingDataFormatStruct(ct.Structure): 146 """Format of tracing data of a tracepoint event, like 147 https://www.kernel.org/doc/html/latest/trace/events.html#event-formats. 148 size: total size of all fields in the tracing data. 149 field_count: the number of fields. 150 fields: an array of fields. 151 """ 152 _fields_ = [('size', ct.c_uint32), 153 ('field_count', ct.c_uint32), 154 ('fields', ct.POINTER(TracingFieldFormatStruct))] 155 156 157class EventStruct(ct.Structure): 158 """Event type of a sample. 159 name: name of the event type. 160 tracing_data_format: only available when it is a tracepoint event. 161 """ 162 _fields_ = [('_name', ct.c_char_p), 163 ('tracing_data_format', TracingDataFormatStruct)] 164 165 @property 166 def name(self) -> str: 167 return _char_pt_to_str(self._name) 168 169 170class MappingStruct(ct.Structure): 171 """ A mapping area in the monitored threads, like the content in /proc/<pid>/maps. 172 start: start addr in memory. 173 end: end addr in memory. 174 pgoff: offset in the mapped shared library. 175 """ 176 _fields_ = [('start', ct.c_uint64), 177 ('end', ct.c_uint64), 178 ('pgoff', ct.c_uint64)] 179 180 181class SymbolStruct(ct.Structure): 182 """ Symbol info of the instruction hit by a sample or a callchain entry of a sample. 183 dso_name: path of the shared library containing the instruction. 184 vaddr_in_file: virtual address of the instruction in the shared library. 185 symbol_name: name of the function containing the instruction. 186 symbol_addr: start addr of the function containing the instruction. 187 symbol_len: length of the function in the shared library. 188 mapping: the mapping area hit by the instruction. 189 """ 190 _fields_ = [('_dso_name', ct.c_char_p), 191 ('vaddr_in_file', ct.c_uint64), 192 ('_symbol_name', ct.c_char_p), 193 ('symbol_addr', ct.c_uint64), 194 ('symbol_len', ct.c_uint64), 195 ('mapping', ct.POINTER(MappingStruct))] 196 197 @property 198 def dso_name(self) -> str: 199 return _char_pt_to_str(self._dso_name) 200 201 @property 202 def symbol_name(self) -> str: 203 return _char_pt_to_str(self._symbol_name) 204 205 206class CallChainEntryStructure(ct.Structure): 207 """ A callchain entry of a sample. 208 ip: the address of the instruction of the callchain entry. 209 symbol: symbol info of the callchain entry. 210 """ 211 _fields_ = [('ip', ct.c_uint64), 212 ('symbol', SymbolStruct)] 213 214 215class CallChainStructure(ct.Structure): 216 """ Callchain info of a sample. 217 nr: number of entries in the callchain. 218 entries: a pointer to an array of CallChainEntryStructure. 219 220 For example, if a sample is generated when a thread is running function C 221 with callchain function A -> function B -> function C. 222 Then nr = 2, and entries = [function B, function A]. 223 """ 224 _fields_ = [('nr', ct.c_uint32), 225 ('entries', ct.POINTER(CallChainEntryStructure))] 226 227 228class EventCounterStructure(ct.Structure): 229 """ An event counter. 230 name: the name of the event. 231 id: the id of the counter. 232 count: the count value for corresponding counter id. 233 """ 234 _fields_ = [('_name', ct.c_char_p), 235 ('id', ct.c_uint64), 236 ('count', ct.c_uint64)] 237 238 @property 239 def name(self) -> str: 240 return _char_pt_to_str(self._name) 241 242class EventCountersViewStructure(ct.Structure): 243 """ An array of event counter. 244 nr: number of event counters in the array. 245 event_counter: a pointer to an array of EventCounterStructure. 246 """ 247 _fields_ = [('nr', ct.c_size_t), 248 ('event_counter', ct.POINTER(EventCounterStructure))] 249 250 251 252class FeatureSectionStructure(ct.Structure): 253 """ A feature section in perf.data to store information like record cmd, device arch, etc. 254 data: a pointer to a buffer storing the section data. 255 data_size: data size in bytes. 256 """ 257 _fields_ = [('data', ct.POINTER(ct.c_char)), 258 ('data_size', ct.c_uint32)] 259 260 261class ReportLibStructure(ct.Structure): 262 _fields_ = [] 263 264 265def SetReportOptionsForReportLib(report_lib, options: ReportLibOptions): 266 if options.proguard_mapping_files: 267 for file_path in options.proguard_mapping_files: 268 report_lib.AddProguardMappingFile(file_path) 269 if options.show_art_frames: 270 report_lib.ShowArtFrames(True) 271 if options.remove_method: 272 for name in options.remove_method: 273 report_lib.RemoveMethod(name) 274 if options.trace_offcpu: 275 report_lib.SetTraceOffCpuMode(options.trace_offcpu) 276 if options.sample_filters: 277 report_lib.SetSampleFilter(options.sample_filters) 278 if options.aggregate_threads: 279 report_lib.AggregateThreads(options.aggregate_threads) 280 281 282# pylint: disable=invalid-name 283class ReportLib(object): 284 """ Read contents from perf.data. """ 285 286 def __init__(self, native_lib_path: Optional[str] = None): 287 if native_lib_path is None: 288 native_lib_path = self._get_native_lib() 289 290 self._load_dependent_lib() 291 self._lib = ct.CDLL(native_lib_path) 292 self._CreateReportLibFunc = self._lib.CreateReportLib 293 self._CreateReportLibFunc.restype = ct.POINTER(ReportLibStructure) 294 self._DestroyReportLibFunc = self._lib.DestroyReportLib 295 self._SetLogSeverityFunc = self._lib.SetLogSeverity 296 self._SetSymfsFunc = self._lib.SetSymfs 297 self._SetRecordFileFunc = self._lib.SetRecordFile 298 self._SetKallsymsFileFunc = self._lib.SetKallsymsFile 299 self._ShowIpForUnknownSymbolFunc = self._lib.ShowIpForUnknownSymbol 300 self._ShowArtFramesFunc = self._lib.ShowArtFrames 301 self._RemoveMethodFunc = self._lib.RemoveMethod 302 self._RemoveMethodFunc.restype = ct.c_bool 303 self._MergeJavaMethodsFunc = self._lib.MergeJavaMethods 304 self._AddProguardMappingFileFunc = self._lib.AddProguardMappingFile 305 self._AddProguardMappingFileFunc.restype = ct.c_bool 306 self._GetSupportedTraceOffCpuModesFunc = self._lib.GetSupportedTraceOffCpuModes 307 self._GetSupportedTraceOffCpuModesFunc.restype = ct.c_char_p 308 self._SetTraceOffCpuModeFunc = self._lib.SetTraceOffCpuMode 309 self._SetTraceOffCpuModeFunc.restype = ct.c_bool 310 self._SetSampleFilterFunc = self._lib.SetSampleFilter 311 self._SetSampleFilterFunc.restype = ct.c_bool 312 self._AggregateThreadsFunc = self._lib.AggregateThreads 313 self._AggregateThreadsFunc.restype = ct.c_bool 314 self._GetNextSampleFunc = self._lib.GetNextSample 315 self._GetNextSampleFunc.restype = ct.POINTER(SampleStruct) 316 self._GetEventOfCurrentSampleFunc = self._lib.GetEventOfCurrentSample 317 self._GetEventOfCurrentSampleFunc.restype = ct.POINTER(EventStruct) 318 self._GetSymbolOfCurrentSampleFunc = self._lib.GetSymbolOfCurrentSample 319 self._GetSymbolOfCurrentSampleFunc.restype = ct.POINTER(SymbolStruct) 320 self._GetCallChainOfCurrentSampleFunc = self._lib.GetCallChainOfCurrentSample 321 self._GetCallChainOfCurrentSampleFunc.restype = ct.POINTER(CallChainStructure) 322 self._GetEventCountersOfCurrentSampleFunc = self._lib.GetEventCountersOfCurrentSample 323 self._GetEventCountersOfCurrentSampleFunc.restype = ct.POINTER(EventCountersViewStructure) 324 self._GetTracingDataOfCurrentSampleFunc = self._lib.GetTracingDataOfCurrentSample 325 self._GetTracingDataOfCurrentSampleFunc.restype = ct.POINTER(ct.c_char) 326 self._GetProcessNameOfCurrentSampleFunc = self._lib.GetProcessNameOfCurrentSample 327 self._GetProcessNameOfCurrentSampleFunc.restype = ct.c_char_p 328 self._GetBuildIdForPathFunc = self._lib.GetBuildIdForPath 329 self._GetBuildIdForPathFunc.restype = ct.c_char_p 330 self._GetFeatureSection = self._lib.GetFeatureSection 331 self._GetFeatureSection.restype = ct.POINTER(FeatureSectionStructure) 332 self._instance = self._CreateReportLibFunc() 333 assert not _is_null(self._instance) 334 335 self.meta_info: Optional[Dict[str, str]] = None 336 self.current_sample: Optional[SampleStruct] = None 337 self.record_cmd: Optional[str] = None 338 339 def _get_native_lib(self) -> str: 340 return get_host_binary_path('libsimpleperf_report.so') 341 342 def _load_dependent_lib(self): 343 # As the windows dll is built with mingw we need to load 'libwinpthread-1.dll'. 344 if is_windows(): 345 self._libwinpthread = ct.CDLL(get_host_binary_path('libwinpthread-1.dll')) 346 347 def Close(self): 348 if self._instance: 349 self._DestroyReportLibFunc(self._instance) 350 self._instance = None 351 352 def SetReportOptions(self, options: ReportLibOptions): 353 """ Set report options in one call. """ 354 SetReportOptionsForReportLib(self, options) 355 356 def SetLogSeverity(self, log_level: str = 'info'): 357 """ Set log severity of native lib, can be verbose,debug,info,error,fatal.""" 358 cond: bool = self._SetLogSeverityFunc(self.getInstance(), _char_pt(log_level)) 359 _check(cond, 'Failed to set log level') 360 361 def SetSymfs(self, symfs_dir: str): 362 """ Set directory used to find symbols.""" 363 cond: bool = self._SetSymfsFunc(self.getInstance(), _char_pt(symfs_dir)) 364 _check(cond, 'Failed to set symbols directory') 365 366 def SetRecordFile(self, record_file: str): 367 """ Set the path of record file, like perf.data.""" 368 cond: bool = self._SetRecordFileFunc(self.getInstance(), _char_pt(record_file)) 369 _check(cond, 'Failed to set record file') 370 371 def ShowIpForUnknownSymbol(self): 372 self._ShowIpForUnknownSymbolFunc(self.getInstance()) 373 374 def ShowArtFrames(self, show: bool = True): 375 """ Show frames of internal methods of the Java interpreter. """ 376 self._ShowArtFramesFunc(self.getInstance(), show) 377 378 def RemoveMethod(self, method_name_regex: str): 379 """ Remove methods with name containing method_name_regex. """ 380 res = self._RemoveMethodFunc(self.getInstance(), _char_pt(method_name_regex)) 381 _check(res, f'failed to call RemoveMethod({method_name_regex})') 382 383 def MergeJavaMethods(self, merge: bool = True): 384 """ This option merges jitted java methods with the same name but in different jit 385 symfiles. If possible, it also merges jitted methods with interpreted methods, 386 by mapping jitted methods to their corresponding dex files. 387 Side effects: 388 It only works at method level, not instruction level. 389 It makes symbol.vaddr_in_file and symbol.mapping not accurate for jitted methods. 390 Java methods are merged by default. 391 """ 392 self._MergeJavaMethodsFunc(self.getInstance(), merge) 393 394 def AddProguardMappingFile(self, mapping_file: Union[str, Path]): 395 """ Add proguard mapping.txt to de-obfuscate method names. """ 396 if not self._AddProguardMappingFileFunc(self.getInstance(), _char_pt(str(mapping_file))): 397 raise ValueError(f'failed to add proguard mapping file: {mapping_file}') 398 399 def SetKallsymsFile(self, kallsym_file: str): 400 """ Set the file path to a copy of the /proc/kallsyms file (for off device decoding) """ 401 cond: bool = self._SetKallsymsFileFunc(self.getInstance(), _char_pt(kallsym_file)) 402 _check(cond, 'Failed to set kallsyms file') 403 404 def GetSupportedTraceOffCpuModes(self) -> List[str]: 405 """ Get trace-offcpu modes supported by the recording file. It should be called after 406 SetRecordFile(). The modes are only available for profiles recorded with --trace-offcpu 407 option. All possible modes are: 408 on-cpu: report on-cpu samples with period representing time spent on cpu 409 off-cpu: report off-cpu samples with period representing time spent off cpu 410 on-off-cpu: report both on-cpu samples and off-cpu samples, which can be split 411 by event name. 412 mixed-on-off-cpu: report on-cpu and off-cpu samples under the same event name. 413 """ 414 modes_str = self._GetSupportedTraceOffCpuModesFunc(self.getInstance()) 415 _check(not _is_null(modes_str), 'Failed to call GetSupportedTraceOffCpuModes()') 416 modes_str = _char_pt_to_str(modes_str) 417 return modes_str.split(',') if modes_str else [] 418 419 def SetTraceOffCpuMode(self, mode: str): 420 """ Set trace-offcpu mode. It should be called after SetRecordFile(). The mode should be 421 one of the modes returned by GetSupportedTraceOffCpuModes(). 422 """ 423 res: bool = self._SetTraceOffCpuModeFunc(self.getInstance(), _char_pt(mode)) 424 _check(res, f'Failed to call SetTraceOffCpuMode({mode})') 425 426 def SetSampleFilter(self, filters: List[str]): 427 """ Set options used to filter samples. Available options are: 428 --exclude-pid pid1,pid2,... Exclude samples for selected processes. 429 --exclude-tid tid1,tid2,... Exclude samples for selected threads. 430 --exclude-process-name process_name_regex Exclude samples for processes with name 431 containing the regular expression. 432 --exclude-thread-name thread_name_regex Exclude samples for threads with name 433 containing the regular expression. 434 --include-pid pid1,pid2,... Include samples for selected processes. 435 --include-tid tid1,tid2,... Include samples for selected threads. 436 --include-process-name process_name_regex Include samples for processes with name 437 containing the regular expression. 438 --include-thread-name thread_name_regex Include samples for threads with name 439 containing the regular expression. 440 --filter-file <file> Use filter file to filter samples based on timestamps. The 441 file format is in doc/sampler_filter.md. 442 443 The filter argument should be a concatenation of options. 444 """ 445 filter_array = (ct.c_char_p * len(filters))() 446 filter_array[:] = [_char_pt(f) for f in filters] 447 res: bool = self._SetSampleFilterFunc(self.getInstance(), filter_array, len(filters)) 448 _check(res, f'Failed to call SetSampleFilter({filters})') 449 450 def AggregateThreads(self, thread_name_regex_list: List[str]): 451 """ Given a list of thread name regex, threads with names matching the same regex are merged 452 into one thread. As a result, samples from different threads (like a thread pool) can be 453 shown in one flamegraph. 454 """ 455 regex_array = (ct.c_char_p * len(thread_name_regex_list))() 456 regex_array[:] = [_char_pt(f) for f in thread_name_regex_list] 457 res: bool = self._AggregateThreadsFunc( 458 self.getInstance(), 459 regex_array, len(thread_name_regex_list)) 460 _check(res, f'Failed to call AggregateThreads({thread_name_regex_list})') 461 462 def GetNextSample(self) -> Optional[SampleStruct]: 463 """ Return the next sample. If no more samples, return None. """ 464 psample = self._GetNextSampleFunc(self.getInstance()) 465 if _is_null(psample): 466 self.current_sample = None 467 else: 468 self.current_sample = psample[0] 469 return self.current_sample 470 471 def GetCurrentSample(self) -> Optional[SampleStruct]: 472 return self.current_sample 473 474 def GetEventOfCurrentSample(self) -> EventStruct: 475 event = self._GetEventOfCurrentSampleFunc(self.getInstance()) 476 assert not _is_null(event) 477 return event[0] 478 479 def GetSymbolOfCurrentSample(self) -> SymbolStruct: 480 symbol = self._GetSymbolOfCurrentSampleFunc(self.getInstance()) 481 assert not _is_null(symbol) 482 return symbol[0] 483 484 def GetCallChainOfCurrentSample(self) -> CallChainStructure: 485 callchain = self._GetCallChainOfCurrentSampleFunc(self.getInstance()) 486 assert not _is_null(callchain) 487 return callchain[0] 488 489 def GetEventCountersOfCurrentSample(self) -> EventCountersViewStructure: 490 event_counters = self._GetEventCountersOfCurrentSampleFunc(self.getInstance()) 491 assert not _is_null(event_counters) 492 return event_counters[0] 493 494 def GetTracingDataOfCurrentSample(self) -> Optional[Dict[str, Any]]: 495 data = self._GetTracingDataOfCurrentSampleFunc(self.getInstance()) 496 if _is_null(data): 497 return None 498 event = self.GetEventOfCurrentSample() 499 result = collections.OrderedDict() 500 for i in range(event.tracing_data_format.field_count): 501 field = event.tracing_data_format.fields[i] 502 result[field.name] = field.parse_value(data) 503 return result 504 505 def GetProcessNameOfCurrentSample(self) -> str: 506 return _char_pt_to_str(self._GetProcessNameOfCurrentSampleFunc(self.getInstance())) 507 508 def GetBuildIdForPath(self, path: str) -> str: 509 build_id = self._GetBuildIdForPathFunc(self.getInstance(), _char_pt(path)) 510 assert not _is_null(build_id) 511 return _char_pt_to_str(build_id) 512 513 def GetRecordCmd(self) -> str: 514 if self.record_cmd is not None: 515 return self.record_cmd 516 self.record_cmd = '' 517 feature_data = self._GetFeatureSection(self.getInstance(), _char_pt('cmdline')) 518 if not _is_null(feature_data): 519 void_p = ct.cast(feature_data[0].data, ct.c_void_p) 520 arg_count = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value 521 void_p.value += 4 522 args = [] 523 for _ in range(arg_count): 524 str_len = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value 525 void_p.value += 4 526 char_p = ct.cast(void_p, ct.POINTER(ct.c_char)) 527 current_str = '' 528 for j in range(str_len): 529 c = bytes_to_str(char_p[j]) 530 if c != '\0': 531 current_str += c 532 if ' ' in current_str: 533 current_str = '"' + current_str + '"' 534 args.append(current_str) 535 void_p.value += str_len 536 self.record_cmd = ' '.join(args) 537 return self.record_cmd 538 539 def _GetFeatureString(self, feature_name: str) -> str: 540 feature_data = self._GetFeatureSection(self.getInstance(), _char_pt(feature_name)) 541 result = '' 542 if not _is_null(feature_data): 543 void_p = ct.cast(feature_data[0].data, ct.c_void_p) 544 str_len = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value 545 void_p.value += 4 546 char_p = ct.cast(void_p, ct.POINTER(ct.c_char)) 547 for i in range(str_len): 548 c = bytes_to_str(char_p[i]) 549 if c == '\0': 550 break 551 result += c 552 return result 553 554 def GetArch(self) -> str: 555 return self._GetFeatureString('arch') 556 557 def MetaInfo(self) -> Dict[str, str]: 558 """ Return a string to string map stored in meta_info section in perf.data. 559 It is used to pass some short meta information. 560 """ 561 if self.meta_info is None: 562 self.meta_info = {} 563 feature_data = self._GetFeatureSection(self.getInstance(), _char_pt('meta_info')) 564 if not _is_null(feature_data): 565 str_list = [] 566 data = feature_data[0].data 567 data_size = feature_data[0].data_size 568 current_str = '' 569 for i in range(data_size): 570 c = bytes_to_str(data[i]) 571 if c != '\0': 572 current_str += c 573 else: 574 str_list.append(current_str) 575 current_str = '' 576 for i in range(0, len(str_list), 2): 577 self.meta_info[str_list[i]] = str_list[i + 1] 578 return self.meta_info 579 580 def getInstance(self) -> ct._Pointer: 581 if self._instance is None: 582 raise Exception('Instance is Closed') 583 return self._instance 584 585 586ProtoSample = namedtuple('ProtoSample', ['ip', 'pid', 'tid', 587 'thread_comm', 'time', 'in_kernel', 'cpu', 'period']) 588ProtoEvent = namedtuple('ProtoEvent', ['name', 'tracing_data_format']) 589ProtoSymbol = namedtuple( 590 'ProtoSymbol', 591 ['dso_name', 'vaddr_in_file', 'symbol_name', 'symbol_addr', 'symbol_len', 'mapping']) 592ProtoMapping = namedtuple('ProtoMapping', ['start', 'end', 'pgoff']) 593ProtoCallChain = namedtuple('ProtoCallChain', ['nr', 'entries']) 594ProtoCallChainEntry = namedtuple('ProtoCallChainEntry', ['ip', 'symbol']) 595 596 597class ProtoFileReportLib: 598 """ Read contents from profile in cmd_report_sample.proto format. 599 It is generated by `simpleperf report-sample`. 600 """ 601 602 @staticmethod 603 def is_supported_format(record_file: str): 604 with open(record_file, 'rb') as fh: 605 if fh.read(10) == b'SIMPLEPERF': 606 return True 607 608 @staticmethod 609 def get_report_sample_pb2(): 610 try: 611 import report_sample_pb2 612 return report_sample_pb2 613 except ImportError as e: 614 log_exit(f'{e}\nprotobuf package is missing or too old. Please install it like ' + 615 '`pip install protobuf==4.21`.') 616 617 def __init__(self): 618 self.record_file = None 619 self.report_sample_pb2 = ProtoFileReportLib.get_report_sample_pb2() 620 self.records: List[self.report_sample_pb2.Record] = [] 621 self.record_index = -1 622 self.files: List[self.report_sample_pb2.File] = [] 623 self.thread_map: Dict[int, self.report_sample_pb2.Thread] = {} 624 self.meta_info: Optional[self.report_sample_pb2.MetaInfo] = None 625 self.fake_mapping_starts = [] 626 self.sample_queue: List[self.report_sample_pb2.Sample] = collections.deque() 627 self.trace_offcpu_mode = None 628 # mapping from thread id to the last off-cpu sample in the thread 629 self.offcpu_samples = {} 630 631 def Close(self): 632 pass 633 634 def SetReportOptions(self, options: ReportLibOptions): 635 """ Set report options in one call. """ 636 SetReportOptionsForReportLib(self, options) 637 638 def SetLogSeverity(self, log_level: str = 'info'): 639 pass 640 641 def SetSymfs(self, symfs_dir: str): 642 pass 643 644 def SetRecordFile(self, record_file: str): 645 self.record_file = record_file 646 with open(record_file, 'rb') as fh: 647 data = fh.read() 648 _check(data[:10] == b'SIMPLEPERF', f'magic number mismatch: {data[:10]}') 649 version = struct.unpack('<H', data[10:12])[0] 650 _check(version == 1, f'version mismatch: {version}') 651 i = 12 652 while i < len(data): 653 _check(i + 4 <= len(data), 'data format error') 654 size = struct.unpack('<I', data[i:i + 4])[0] 655 if size == 0: 656 break 657 i += 4 658 _check(i + size <= len(data), 'data format error') 659 record = self.report_sample_pb2.Record() 660 record.ParseFromString(data[i: i + size]) 661 i += size 662 if record.HasField('sample') or record.HasField('context_switch'): 663 self.records.append(record) 664 elif record.HasField('file'): 665 self.files.append(record.file) 666 elif record.HasField('thread'): 667 self.thread_map[record.thread.thread_id] = record.thread 668 elif record.HasField('meta_info'): 669 self.meta_info = record.meta_info 670 if self.meta_info.trace_offcpu: 671 self.trace_offcpu_mode = 'mixed-on-off-cpu' 672 fake_mapping_start = 0 673 for file in self.files: 674 self.fake_mapping_starts.append(fake_mapping_start) 675 fake_mapping_start += len(file.symbol) + 1 676 677 def AddProguardMappingFile(self, mapping_file: Union[str, Path]): 678 """ Add proguard mapping.txt to de-obfuscate method names. """ 679 raise NotImplementedError( 680 'Adding proguard mapping files are not implemented for report_sample profiles') 681 682 def ShowIpForUnknownSymbol(self): 683 pass 684 685 def ShowArtFrames(self, show: bool = True): 686 raise NotImplementedError( 687 'Showing art frames are not implemented for report_sample profiles') 688 689 def RemoveMethod(self, method_name_regex: str): 690 """ Remove methods with name containing method_name_regex. """ 691 raise NotImplementedError("Removing method isn't implemented for report_sample profiles") 692 693 def SetSampleFilter(self, filters: List[str]): 694 raise NotImplementedError('sample filters are not implemented for report_sample profiles') 695 696 def GetSupportedTraceOffCpuModes(self) -> List[str]: 697 """ Get trace-offcpu modes supported by the recording file. It should be called after 698 SetRecordFile(). The modes are only available for profiles recorded with --trace-offcpu 699 option. All possible modes are: 700 on-cpu: report on-cpu samples with period representing time spent on cpu 701 off-cpu: report off-cpu samples with period representing time spent off cpu 702 on-off-cpu: report both on-cpu samples and off-cpu samples, which can be split 703 by event name. 704 mixed-on-off-cpu: report on-cpu and off-cpu samples under the same event name. 705 """ 706 _check(self.meta_info, 707 'GetSupportedTraceOffCpuModes() should be called after SetRecordFile()') 708 if self.meta_info.trace_offcpu: 709 return ['on-cpu', 'off-cpu', 'on-off-cpu', 'mixed-on-off-cpu'] 710 return [] 711 712 def SetTraceOffCpuMode(self, mode: str): 713 """ Set trace-offcpu mode. It should be called after SetRecordFile(). 714 """ 715 _check(mode in ['on-cpu', 'off-cpu', 'on-off-cpu', 'mixed-on-off-cpu'], 'invalide mode') 716 # Don't check if mode is in self.GetSupportedTraceOffCpuModes(). Because the profile may 717 # be generated by an old simpleperf. 718 self.trace_offcpu_mode = mode 719 720 def AggregateThreads(self, thread_name_regex_list: List[str]): 721 """ Given a list of thread name regex, threads with names matching the same regex are merged 722 into one thread. As a result, samples from different threads (like a thread pool) can be 723 shown in one flamegraph. 724 """ 725 raise NotImplementedError( 726 'Aggregating threads are not implemented for report_sample profiles') 727 728 def GetNextSample(self) -> Optional[ProtoSample]: 729 if self.sample_queue: 730 self.sample_queue.popleft() 731 while not self.sample_queue: 732 self.record_index += 1 733 if self.record_index >= len(self.records): 734 break 735 record = self.records[self.record_index] 736 if record.HasField('sample'): 737 self._process_sample_record(record.sample) 738 elif record.HasField('context_switch'): 739 self._process_context_switch(record.context_switch) 740 return self.GetCurrentSample() 741 742 def _process_sample_record(self, sample) -> None: 743 if not self.trace_offcpu_mode: 744 self._add_to_sample_queue(sample) 745 return 746 event_name = self._get_event_name(sample.event_type_id) 747 is_offcpu = 'sched_switch' in event_name 748 749 if self.trace_offcpu_mode == 'on-cpu': 750 if not is_offcpu: 751 self._add_to_sample_queue(sample) 752 return 753 754 if prev_offcpu_sample := self.offcpu_samples.get(sample.thread_id): 755 # If there is a previous off-cpu sample, update its period. 756 prev_offcpu_sample.event_count = max(sample.time - prev_offcpu_sample.time, 1) 757 self._add_to_sample_queue(prev_offcpu_sample) 758 759 if is_offcpu: 760 self.offcpu_samples[sample.thread_id] = sample 761 else: 762 self.offcpu_samples[sample.thread_id] = None 763 if self.trace_offcpu_mode in ('on-off-cpu', 'mixed-on-off-cpu'): 764 self._add_to_sample_queue(sample) 765 766 def _process_context_switch(self, context_switch) -> None: 767 if not context_switch.switch_on: 768 return 769 if prev_offcpu_sample := self.offcpu_samples.get(context_switch.thread_id): 770 prev_offcpu_sample.event_count = max(context_switch.time - prev_offcpu_sample.time, 1) 771 self.offcpu_samples[context_switch.thread_id] = None 772 self._add_to_sample_queue(prev_offcpu_sample) 773 774 def _add_to_sample_queue(self, sample) -> None: 775 self.sample_queue.append(sample) 776 777 def GetCurrentSample(self) -> Optional[ProtoSample]: 778 if not self.sample_queue: 779 return None 780 sample = self.sample_queue[0] 781 thread = self.thread_map[sample.thread_id] 782 return ProtoSample( 783 ip=0, pid=thread.process_id, tid=thread.thread_id, thread_comm=thread.thread_name, 784 time=sample.time, in_kernel=False, cpu=0, period=sample.event_count) 785 786 def GetEventOfCurrentSample(self) -> ProtoEvent: 787 sample = self.sample_queue[0] 788 event_type_id = 0 if self.trace_offcpu_mode == 'mixed-on-off-cpu' else sample.event_type_id 789 event_name = self._get_event_name(event_type_id) 790 return ProtoEvent(name=event_name, tracing_data_format=None) 791 792 def _get_event_name(self, event_type_id: int) -> str: 793 return self.meta_info.event_type[event_type_id] 794 795 def GetSymbolOfCurrentSample(self) -> ProtoSymbol: 796 sample = self.sample_queue[0] 797 node = sample.callchain[0] 798 return self._build_symbol(node) 799 800 def GetCallChainOfCurrentSample(self) -> ProtoCallChain: 801 entries = [] 802 sample = self.sample_queue[0] 803 for node in sample.callchain[1:]: 804 symbol = self._build_symbol(node) 805 entries.append(ProtoCallChainEntry(ip=0, symbol=symbol)) 806 return ProtoCallChain(nr=len(entries), entries=entries) 807 808 def _build_symbol(self, node) -> ProtoSymbol: 809 file = self.files[node.file_id] 810 if node.symbol_id == -1: 811 symbol_name = 'unknown' 812 fake_symbol_addr = self.fake_mapping_starts[node.file_id] + len(file.symbol) 813 fake_symbol_pgoff = 0 814 else: 815 symbol_name = file.symbol[node.symbol_id] 816 fake_symbol_addr = self.fake_mapping_starts[node.file_id] = node.symbol_id + 1 817 fake_symbol_pgoff = node.symbol_id + 1 818 mapping = ProtoMapping(fake_symbol_addr, 1, fake_symbol_pgoff) 819 return ProtoSymbol(dso_name=file.path, vaddr_in_file=node.vaddr_in_file, 820 symbol_name=symbol_name, symbol_addr=0, symbol_len=1, mapping=[mapping]) 821 822 def GetBuildIdForPath(self, path: str) -> str: 823 return '' 824 825 def GetRecordCmd(self) -> str: 826 return '' 827 828 def GetArch(self) -> str: 829 return '' 830 831 def MetaInfo(self) -> Dict[str, str]: 832 return {} 833 834 835def GetReportLib(record_file: str) -> Union[ReportLib, ProtoFileReportLib]: 836 if ProtoFileReportLib.is_supported_format(record_file): 837 lib = ProtoFileReportLib() 838 else: 839 lib = ReportLib() 840 lib.SetRecordFile(record_file) 841 return lib 842