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 24import ctypes as ct 25from pathlib import Path 26import struct 27from typing import Any, Dict, List, Optional, Union 28 29from simpleperf_utils import (bytes_to_str, get_host_binary_path, is_windows, str_to_bytes, 30 ReportLibOptions) 31 32 33def _is_null(p: Optional[ct._Pointer]) -> bool: 34 if p: 35 return False 36 return ct.cast(p, ct.c_void_p).value is None 37 38 39def _char_pt(s: str) -> bytes: 40 return str_to_bytes(s) 41 42 43def _char_pt_to_str(char_pt: ct.c_char_p) -> str: 44 return bytes_to_str(char_pt) 45 46 47def _check(cond: bool, failmsg: str): 48 if not cond: 49 raise RuntimeError(failmsg) 50 51 52class SampleStruct(ct.Structure): 53 """ Instance of a sample in perf.data. 54 ip: the program counter of the thread generating the sample. 55 pid: process id (or thread group id) of the thread generating the sample. 56 tid: thread id. 57 thread_comm: thread name. 58 time: time at which the sample was generated. The value is in nanoseconds. 59 The clock is decided by the --clockid option in `simpleperf record`. 60 in_kernel: whether the instruction is in kernel space or user space. 61 cpu: the cpu generating the sample. 62 period: count of events have happened since last sample. For example, if we use 63 -e cpu-cycles, it means how many cpu-cycles have happened. 64 If we use -e cpu-clock, it means how many nanoseconds have passed. 65 """ 66 _fields_ = [('ip', ct.c_uint64), 67 ('pid', ct.c_uint32), 68 ('tid', ct.c_uint32), 69 ('_thread_comm', ct.c_char_p), 70 ('time', ct.c_uint64), 71 ('_in_kernel', ct.c_uint32), 72 ('cpu', ct.c_uint32), 73 ('period', ct.c_uint64)] 74 75 @property 76 def thread_comm(self) -> str: 77 return _char_pt_to_str(self._thread_comm) 78 79 @property 80 def in_kernel(self) -> bool: 81 return bool(self._in_kernel) 82 83 84class TracingFieldFormatStruct(ct.Structure): 85 """Format of a tracing field. 86 name: name of the field. 87 offset: offset of the field in tracing data. 88 elem_size: size of the element type. 89 elem_count: the number of elements in this field, more than one if the field is an array. 90 is_signed: whether the element type is signed or unsigned. 91 is_dynamic: whether the element is a dynamic string. 92 """ 93 _fields_ = [('_name', ct.c_char_p), 94 ('offset', ct.c_uint32), 95 ('elem_size', ct.c_uint32), 96 ('elem_count', ct.c_uint32), 97 ('is_signed', ct.c_uint32), 98 ('is_dynamic', ct.c_uint32)] 99 100 _unpack_key_dict = {1: 'b', 2: 'h', 4: 'i', 8: 'q'} 101 102 @property 103 def name(self) -> str: 104 return _char_pt_to_str(self._name) 105 106 def parse_value(self, data: ct.c_char_p) -> Union[str, bytes, List[bytes]]: 107 """ Parse value of a field in a tracepoint event. 108 The return value depends on the type of the field, and can be an int value, a string, 109 an array of int values, etc. If the type can't be parsed, return a byte array or an 110 array of byte arrays. 111 """ 112 if self.is_dynamic: 113 offset, max_len = struct.unpack('<HH', data[self.offset:self.offset + 4]) 114 length = 0 115 while length < max_len and bytes_to_str(data[offset + length]) != '\x00': 116 length += 1 117 return bytes_to_str(data[offset: offset + length]) 118 119 if self.elem_count > 1 and self.elem_size == 1: 120 # Probably the field is a string. 121 # Don't use self.is_signed, which has different values on x86 and arm. 122 length = 0 123 while length < self.elem_count and bytes_to_str(data[self.offset + length]) != '\x00': 124 length += 1 125 return bytes_to_str(data[self.offset: self.offset + length]) 126 unpack_key = self._unpack_key_dict.get(self.elem_size) 127 if unpack_key: 128 if not self.is_signed: 129 unpack_key = unpack_key.upper() 130 value = struct.unpack('%d%s' % (self.elem_count, unpack_key), 131 data[self.offset:self.offset + self.elem_count * self.elem_size]) 132 else: 133 # Since we don't know the element type, just return the bytes. 134 value = [] 135 offset = self.offset 136 for _ in range(self.elem_count): 137 value.append(data[offset: offset + self.elem_size]) 138 offset += self.elem_size 139 if self.elem_count == 1: 140 value = value[0] 141 return value 142 143 144class TracingDataFormatStruct(ct.Structure): 145 """Format of tracing data of a tracepoint event, like 146 https://www.kernel.org/doc/html/latest/trace/events.html#event-formats. 147 size: total size of all fields in the tracing data. 148 field_count: the number of fields. 149 fields: an array of fields. 150 """ 151 _fields_ = [('size', ct.c_uint32), 152 ('field_count', ct.c_uint32), 153 ('fields', ct.POINTER(TracingFieldFormatStruct))] 154 155 156class EventStruct(ct.Structure): 157 """Event type of a sample. 158 name: name of the event type. 159 tracing_data_format: only available when it is a tracepoint event. 160 """ 161 _fields_ = [('_name', ct.c_char_p), 162 ('tracing_data_format', TracingDataFormatStruct)] 163 164 @property 165 def name(self) -> str: 166 return _char_pt_to_str(self._name) 167 168 169class MappingStruct(ct.Structure): 170 """ A mapping area in the monitored threads, like the content in /proc/<pid>/maps. 171 start: start addr in memory. 172 end: end addr in memory. 173 pgoff: offset in the mapped shared library. 174 """ 175 _fields_ = [('start', ct.c_uint64), 176 ('end', ct.c_uint64), 177 ('pgoff', ct.c_uint64)] 178 179 180class SymbolStruct(ct.Structure): 181 """ Symbol info of the instruction hit by a sample or a callchain entry of a sample. 182 dso_name: path of the shared library containing the instruction. 183 vaddr_in_file: virtual address of the instruction in the shared library. 184 symbol_name: name of the function containing the instruction. 185 symbol_addr: start addr of the function containing the instruction. 186 symbol_len: length of the function in the shared library. 187 mapping: the mapping area hit by the instruction. 188 """ 189 _fields_ = [('_dso_name', ct.c_char_p), 190 ('vaddr_in_file', ct.c_uint64), 191 ('_symbol_name', ct.c_char_p), 192 ('symbol_addr', ct.c_uint64), 193 ('symbol_len', ct.c_uint64), 194 ('mapping', ct.POINTER(MappingStruct))] 195 196 @property 197 def dso_name(self) -> str: 198 return _char_pt_to_str(self._dso_name) 199 200 @property 201 def symbol_name(self) -> str: 202 return _char_pt_to_str(self._symbol_name) 203 204 205class CallChainEntryStructure(ct.Structure): 206 """ A callchain entry of a sample. 207 ip: the address of the instruction of the callchain entry. 208 symbol: symbol info of the callchain entry. 209 """ 210 _fields_ = [('ip', ct.c_uint64), 211 ('symbol', SymbolStruct)] 212 213 214class CallChainStructure(ct.Structure): 215 """ Callchain info of a sample. 216 nr: number of entries in the callchain. 217 entries: a pointer to an array of CallChainEntryStructure. 218 219 For example, if a sample is generated when a thread is running function C 220 with callchain function A -> function B -> function C. 221 Then nr = 2, and entries = [function B, function A]. 222 """ 223 _fields_ = [('nr', ct.c_uint32), 224 ('entries', ct.POINTER(CallChainEntryStructure))] 225 226 227class FeatureSectionStructure(ct.Structure): 228 """ A feature section in perf.data to store information like record cmd, device arch, etc. 229 data: a pointer to a buffer storing the section data. 230 data_size: data size in bytes. 231 """ 232 _fields_ = [('data', ct.POINTER(ct.c_char)), 233 ('data_size', ct.c_uint32)] 234 235 236class ReportLibStructure(ct.Structure): 237 _fields_ = [] 238 239 240# pylint: disable=invalid-name 241class ReportLib(object): 242 243 def __init__(self, native_lib_path: Optional[str] = None): 244 if native_lib_path is None: 245 native_lib_path = self._get_native_lib() 246 247 self._load_dependent_lib() 248 self._lib = ct.CDLL(native_lib_path) 249 self._CreateReportLibFunc = self._lib.CreateReportLib 250 self._CreateReportLibFunc.restype = ct.POINTER(ReportLibStructure) 251 self._DestroyReportLibFunc = self._lib.DestroyReportLib 252 self._SetLogSeverityFunc = self._lib.SetLogSeverity 253 self._SetSymfsFunc = self._lib.SetSymfs 254 self._SetRecordFileFunc = self._lib.SetRecordFile 255 self._SetKallsymsFileFunc = self._lib.SetKallsymsFile 256 self._ShowIpForUnknownSymbolFunc = self._lib.ShowIpForUnknownSymbol 257 self._ShowArtFramesFunc = self._lib.ShowArtFrames 258 self._MergeJavaMethodsFunc = self._lib.MergeJavaMethods 259 self._AddProguardMappingFileFunc = self._lib.AddProguardMappingFile 260 self._AddProguardMappingFileFunc.restype = ct.c_bool 261 self._GetSupportedTraceOffCpuModesFunc = self._lib.GetSupportedTraceOffCpuModes 262 self._GetSupportedTraceOffCpuModesFunc.restype = ct.c_char_p 263 self._SetTraceOffCpuModeFunc = self._lib.SetTraceOffCpuMode 264 self._SetTraceOffCpuModeFunc.restype = ct.c_bool 265 self._SetSampleFilterFunc = self._lib.SetSampleFilter 266 self._SetSampleFilterFunc.restype = ct.c_bool 267 self._AggregateThreadsFunc = self._lib.AggregateThreads 268 self._AggregateThreadsFunc.restype = ct.c_bool 269 self._GetNextSampleFunc = self._lib.GetNextSample 270 self._GetNextSampleFunc.restype = ct.POINTER(SampleStruct) 271 self._GetEventOfCurrentSampleFunc = self._lib.GetEventOfCurrentSample 272 self._GetEventOfCurrentSampleFunc.restype = ct.POINTER(EventStruct) 273 self._GetSymbolOfCurrentSampleFunc = self._lib.GetSymbolOfCurrentSample 274 self._GetSymbolOfCurrentSampleFunc.restype = ct.POINTER(SymbolStruct) 275 self._GetCallChainOfCurrentSampleFunc = self._lib.GetCallChainOfCurrentSample 276 self._GetCallChainOfCurrentSampleFunc.restype = ct.POINTER(CallChainStructure) 277 self._GetTracingDataOfCurrentSampleFunc = self._lib.GetTracingDataOfCurrentSample 278 self._GetTracingDataOfCurrentSampleFunc.restype = ct.POINTER(ct.c_char) 279 self._GetBuildIdForPathFunc = self._lib.GetBuildIdForPath 280 self._GetBuildIdForPathFunc.restype = ct.c_char_p 281 self._GetFeatureSection = self._lib.GetFeatureSection 282 self._GetFeatureSection.restype = ct.POINTER(FeatureSectionStructure) 283 self._instance = self._CreateReportLibFunc() 284 assert not _is_null(self._instance) 285 286 self.meta_info: Optional[Dict[str, str]] = None 287 self.current_sample: Optional[SampleStruct] = None 288 self.record_cmd: Optional[str] = None 289 290 def _get_native_lib(self) -> str: 291 return get_host_binary_path('libsimpleperf_report.so') 292 293 def _load_dependent_lib(self): 294 # As the windows dll is built with mingw we need to load 'libwinpthread-1.dll'. 295 if is_windows(): 296 self._libwinpthread = ct.CDLL(get_host_binary_path('libwinpthread-1.dll')) 297 298 def Close(self): 299 if self._instance: 300 self._DestroyReportLibFunc(self._instance) 301 self._instance = None 302 303 def SetReportOptions(self, options: ReportLibOptions): 304 """ Set report options in one call. """ 305 if options.proguard_mapping_files: 306 for file_path in options.proguard_mapping_files: 307 self.AddProguardMappingFile(file_path) 308 if options.show_art_frames: 309 self.ShowArtFrames(True) 310 if options.trace_offcpu: 311 self.SetTraceOffCpuMode(options.trace_offcpu) 312 if options.sample_filters: 313 self.SetSampleFilter(options.sample_filters) 314 if options.aggregate_threads: 315 self.AggregateThreads(options.aggregate_threads) 316 317 def SetLogSeverity(self, log_level: str = 'info'): 318 """ Set log severity of native lib, can be verbose,debug,info,error,fatal.""" 319 cond: bool = self._SetLogSeverityFunc(self.getInstance(), _char_pt(log_level)) 320 _check(cond, 'Failed to set log level') 321 322 def SetSymfs(self, symfs_dir: str): 323 """ Set directory used to find symbols.""" 324 cond: bool = self._SetSymfsFunc(self.getInstance(), _char_pt(symfs_dir)) 325 _check(cond, 'Failed to set symbols directory') 326 327 def SetRecordFile(self, record_file: str): 328 """ Set the path of record file, like perf.data.""" 329 cond: bool = self._SetRecordFileFunc(self.getInstance(), _char_pt(record_file)) 330 _check(cond, 'Failed to set record file') 331 332 def ShowIpForUnknownSymbol(self): 333 self._ShowIpForUnknownSymbolFunc(self.getInstance()) 334 335 def ShowArtFrames(self, show: bool = True): 336 """ Show frames of internal methods of the Java interpreter. """ 337 self._ShowArtFramesFunc(self.getInstance(), show) 338 339 def MergeJavaMethods(self, merge: bool = True): 340 """ This option merges jitted java methods with the same name but in different jit 341 symfiles. If possible, it also merges jitted methods with interpreted methods, 342 by mapping jitted methods to their corresponding dex files. 343 Side effects: 344 It only works at method level, not instruction level. 345 It makes symbol.vaddr_in_file and symbol.mapping not accurate for jitted methods. 346 Java methods are merged by default. 347 """ 348 self._MergeJavaMethodsFunc(self.getInstance(), merge) 349 350 def AddProguardMappingFile(self, mapping_file: Union[str, Path]): 351 """ Add proguard mapping.txt to de-obfuscate method names. """ 352 if not self._AddProguardMappingFileFunc(self.getInstance(), _char_pt(str(mapping_file))): 353 raise ValueError(f'failed to add proguard mapping file: {mapping_file}') 354 355 def SetKallsymsFile(self, kallsym_file: str): 356 """ Set the file path to a copy of the /proc/kallsyms file (for off device decoding) """ 357 cond: bool = self._SetKallsymsFileFunc(self.getInstance(), _char_pt(kallsym_file)) 358 _check(cond, 'Failed to set kallsyms file') 359 360 def GetSupportedTraceOffCpuModes(self) -> List[str]: 361 """ Get trace-offcpu modes supported by the recording file. It should be called after 362 SetRecordFile(). The modes are only available for profiles recorded with --trace-offcpu 363 option. All possible modes are: 364 on-cpu: report on-cpu samples with period representing time spent on cpu 365 off-cpu: report off-cpu samples with period representing time spent off cpu 366 on-off-cpu: report both on-cpu samples and off-cpu samples, which can be split 367 by event name. 368 mixed-on-off-cpu: report on-cpu and off-cpu samples under the same event name. 369 """ 370 modes_str = self._GetSupportedTraceOffCpuModesFunc(self.getInstance()) 371 _check(not _is_null(modes_str), 'Failed to call GetSupportedTraceOffCpuModes()') 372 modes_str = _char_pt_to_str(modes_str) 373 return modes_str.split(',') if modes_str else [] 374 375 def SetTraceOffCpuMode(self, mode: str): 376 """ Set trace-offcpu mode. It should be called after SetRecordFile(). The mode should be 377 one of the modes returned by GetSupportedTraceOffCpuModes(). 378 """ 379 res: bool = self._SetTraceOffCpuModeFunc(self.getInstance(), _char_pt(mode)) 380 _check(res, f'Failed to call SetTraceOffCpuMode({mode})') 381 382 def SetSampleFilter(self, filters: List[str]): 383 """ Set options used to filter samples. Available options are: 384 --exclude-pid pid1,pid2,... Exclude samples for selected processes. 385 --exclude-tid tid1,tid2,... Exclude samples for selected threads. 386 --exclude-process-name process_name_regex Exclude samples for processes with name 387 containing the regular expression. 388 --exclude-thread-name thread_name_regex Exclude samples for threads with name 389 containing the regular expression. 390 --include-pid pid1,pid2,... Include samples for selected processes. 391 --include-tid tid1,tid2,... Include samples for selected threads. 392 --include-process-name process_name_regex Include samples for processes with name 393 containing the regular expression. 394 --include-thread-name thread_name_regex Include samples for threads with name 395 containing the regular expression. 396 --filter-file <file> Use filter file to filter samples based on timestamps. The 397 file format is in doc/sampler_filter.md. 398 399 The filter argument should be a concatenation of options. 400 """ 401 filter_array = (ct.c_char_p * len(filters))() 402 filter_array[:] = [_char_pt(f) for f in filters] 403 res: bool = self._SetSampleFilterFunc(self.getInstance(), filter_array, len(filters)) 404 _check(res, f'Failed to call SetSampleFilter({filters})') 405 406 def AggregateThreads(self, thread_name_regex_list: List[str]): 407 """ Given a list of thread name regex, threads with names matching the same regex are merged 408 into one thread. As a result, samples from different threads (like a thread pool) can be 409 shown in one flamegraph. 410 """ 411 regex_array = (ct.c_char_p * len(thread_name_regex_list))() 412 regex_array[:] = [_char_pt(f) for f in thread_name_regex_list] 413 res: bool = self._AggregateThreadsFunc( 414 self.getInstance(), 415 regex_array, len(thread_name_regex_list)) 416 _check(res, f'Failed to call AggregateThreads({thread_name_regex_list})') 417 418 def GetNextSample(self) -> Optional[SampleStruct]: 419 """ Return the next sample. If no more samples, return None. """ 420 psample = self._GetNextSampleFunc(self.getInstance()) 421 if _is_null(psample): 422 self.current_sample = None 423 else: 424 self.current_sample = psample[0] 425 return self.current_sample 426 427 def GetCurrentSample(self) -> Optional[SampleStruct]: 428 return self.current_sample 429 430 def GetEventOfCurrentSample(self) -> EventStruct: 431 event = self._GetEventOfCurrentSampleFunc(self.getInstance()) 432 assert not _is_null(event) 433 return event[0] 434 435 def GetSymbolOfCurrentSample(self) -> SymbolStruct: 436 symbol = self._GetSymbolOfCurrentSampleFunc(self.getInstance()) 437 assert not _is_null(symbol) 438 return symbol[0] 439 440 def GetCallChainOfCurrentSample(self) -> CallChainStructure: 441 callchain = self._GetCallChainOfCurrentSampleFunc(self.getInstance()) 442 assert not _is_null(callchain) 443 return callchain[0] 444 445 def GetTracingDataOfCurrentSample(self) -> Optional[Dict[str, Any]]: 446 data = self._GetTracingDataOfCurrentSampleFunc(self.getInstance()) 447 if _is_null(data): 448 return None 449 event = self.GetEventOfCurrentSample() 450 result = collections.OrderedDict() 451 for i in range(event.tracing_data_format.field_count): 452 field = event.tracing_data_format.fields[i] 453 result[field.name] = field.parse_value(data) 454 return result 455 456 def GetBuildIdForPath(self, path: str) -> str: 457 build_id = self._GetBuildIdForPathFunc(self.getInstance(), _char_pt(path)) 458 assert not _is_null(build_id) 459 return _char_pt_to_str(build_id) 460 461 def GetRecordCmd(self) -> str: 462 if self.record_cmd is not None: 463 return self.record_cmd 464 self.record_cmd = '' 465 feature_data = self._GetFeatureSection(self.getInstance(), _char_pt('cmdline')) 466 if not _is_null(feature_data): 467 void_p = ct.cast(feature_data[0].data, ct.c_void_p) 468 arg_count = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value 469 void_p.value += 4 470 args = [] 471 for _ in range(arg_count): 472 str_len = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value 473 void_p.value += 4 474 char_p = ct.cast(void_p, ct.POINTER(ct.c_char)) 475 current_str = '' 476 for j in range(str_len): 477 c = bytes_to_str(char_p[j]) 478 if c != '\0': 479 current_str += c 480 if ' ' in current_str: 481 current_str = '"' + current_str + '"' 482 args.append(current_str) 483 void_p.value += str_len 484 self.record_cmd = ' '.join(args) 485 return self.record_cmd 486 487 def _GetFeatureString(self, feature_name: str) -> str: 488 feature_data = self._GetFeatureSection(self.getInstance(), _char_pt(feature_name)) 489 result = '' 490 if not _is_null(feature_data): 491 void_p = ct.cast(feature_data[0].data, ct.c_void_p) 492 str_len = ct.cast(void_p, ct.POINTER(ct.c_uint32)).contents.value 493 void_p.value += 4 494 char_p = ct.cast(void_p, ct.POINTER(ct.c_char)) 495 for i in range(str_len): 496 c = bytes_to_str(char_p[i]) 497 if c == '\0': 498 break 499 result += c 500 return result 501 502 def GetArch(self) -> str: 503 return self._GetFeatureString('arch') 504 505 def MetaInfo(self) -> Dict[str, str]: 506 """ Return a string to string map stored in meta_info section in perf.data. 507 It is used to pass some short meta information. 508 """ 509 if self.meta_info is None: 510 self.meta_info = {} 511 feature_data = self._GetFeatureSection(self.getInstance(), _char_pt('meta_info')) 512 if not _is_null(feature_data): 513 str_list = [] 514 data = feature_data[0].data 515 data_size = feature_data[0].data_size 516 current_str = '' 517 for i in range(data_size): 518 c = bytes_to_str(data[i]) 519 if c != '\0': 520 current_str += c 521 else: 522 str_list.append(current_str) 523 current_str = '' 524 for i in range(0, len(str_list), 2): 525 self.meta_info[str_list[i]] = str_list[i + 1] 526 return self.meta_info 527 528 def getInstance(self) -> ct._Pointer: 529 if self._instance is None: 530 raise Exception('Instance is Closed') 531 return self._instance 532