• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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