xref: /aosp_15_r20/system/extras/simpleperf/scripts/simpleperf_report_lib.py (revision 288bf5226967eb3dac5cce6c939ccc2a7f2b4fe5)
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