xref: /aosp_15_r20/system/extras/simpleperf/scripts/test/report_lib_test.py (revision 288bf5226967eb3dac5cce6c939ccc2a7f2b4fe5)
1#!/usr/bin/env python3
2#
3# Copyright (C) 2021 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
17import os
18from pathlib import Path
19import shutil
20import subprocess
21import tempfile
22from typing import Dict, List, Optional, Set
23
24from simpleperf_report_lib import ReportLib, ProtoFileReportLib
25from simpleperf_utils import get_host_binary_path, ReadElf
26from . test_utils import TestBase, TestHelper
27
28
29class TestReportLib(TestBase):
30    def setUp(self):
31        super(TestReportLib, self).setUp()
32        self.report_lib = ReportLib()
33
34    def tearDown(self):
35        self.report_lib.Close()
36        super(TestReportLib, self).tearDown()
37
38    def test_build_id(self):
39        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_symbols.data'))
40        build_id = self.report_lib.GetBuildIdForPath('/data/t2')
41        self.assertEqual(build_id, '0x70f1fe24500fc8b0d9eb477199ca1ca21acca4de')
42
43    def test_symbol(self):
44        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_symbols.data'))
45        found_func2 = False
46        while self.report_lib.GetNextSample():
47            symbol = self.report_lib.GetSymbolOfCurrentSample()
48            if symbol.symbol_name == 'func2(int, int)':
49                found_func2 = True
50                self.assertEqual(symbol.symbol_addr, 0x4004ed)
51                self.assertEqual(symbol.symbol_len, 0x14)
52        self.assertTrue(found_func2)
53
54    def test_sample(self):
55        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_symbols.data'))
56        found_sample = False
57        while self.report_lib.GetNextSample():
58            sample = self.report_lib.GetCurrentSample()
59            if sample.ip == 0x4004ff and sample.time == 7637889424953:
60                found_sample = True
61                self.assertEqual(sample.pid, 15926)
62                self.assertEqual(sample.tid, 15926)
63                self.assertEqual(sample.thread_comm, 't2')
64                self.assertEqual(sample.cpu, 5)
65                self.assertEqual(sample.period, 694614)
66                event = self.report_lib.GetEventOfCurrentSample()
67                self.assertEqual(event.name, 'cpu-cycles')
68                callchain = self.report_lib.GetCallChainOfCurrentSample()
69                self.assertEqual(callchain.nr, 0)
70        self.assertTrue(found_sample)
71
72    def test_meta_info(self):
73        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_trace_offcpu_v2.data'))
74        meta_info = self.report_lib.MetaInfo()
75        self.assertTrue("simpleperf_version" in meta_info)
76        self.assertEqual(meta_info["system_wide_collection"], "false")
77        self.assertEqual(meta_info["trace_offcpu"], "true")
78        self.assertEqual(meta_info["event_type_info"], "cpu-clock,1,0\nsched:sched_switch,2,91")
79        self.assertTrue("product_props" in meta_info)
80
81    def test_event_name_from_meta_info(self):
82        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_tracepoint_event.data'))
83        event_names = set()
84        while self.report_lib.GetNextSample():
85            event_names.add(self.report_lib.GetEventOfCurrentSample().name)
86        self.assertTrue('sched:sched_switch' in event_names)
87        self.assertTrue('cpu-cycles' in event_names)
88
89    def test_record_cmd(self):
90        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_trace_offcpu_v2.data'))
91        self.assertEqual(self.report_lib.GetRecordCmd(),
92                         '/data/user/0/com.google.samples.apps.sunflower/simpleperf record ' +
93                         '--app com.google.samples.apps.sunflower --add-meta-info ' +
94                         'app_type=debuggable --in-app --tracepoint-events ' +
95                         '/data/local/tmp/tracepoint_events --out-fd 3 --stop-signal-fd 4 -g ' +
96                         '--size-limit 500k --trace-offcpu -e cpu-clock:u')
97
98    def test_offcpu(self):
99        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_trace_offcpu_v2.data'))
100        total_period = 0
101        sleep_function_period = 0
102        sleep_function_name = "__epoll_pwait"
103        while self.report_lib.GetNextSample():
104            sample = self.report_lib.GetCurrentSample()
105            total_period += sample.period
106            if self.report_lib.GetSymbolOfCurrentSample().symbol_name == sleep_function_name:
107                sleep_function_period += sample.period
108                continue
109            callchain = self.report_lib.GetCallChainOfCurrentSample()
110            for i in range(callchain.nr):
111                if callchain.entries[i].symbol.symbol_name == sleep_function_name:
112                    sleep_function_period += sample.period
113                    break
114            self.assertEqual(self.report_lib.GetEventOfCurrentSample().name, 'cpu-clock:u')
115        sleep_percentage = float(sleep_function_period) / total_period
116        self.assertGreater(sleep_percentage, 0.30)
117
118    def test_show_art_frames(self):
119        def has_art_frame(report_lib):
120            report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_interpreter_frames.data'))
121            result = False
122            while report_lib.GetNextSample():
123                callchain = report_lib.GetCallChainOfCurrentSample()
124                for i in range(callchain.nr):
125                    if callchain.entries[i].symbol.symbol_name == 'artMterpAsmInstructionStart':
126                        result = True
127                        break
128            report_lib.Close()
129            return result
130
131        report_lib = ReportLib()
132        self.assertFalse(has_art_frame(report_lib))
133        report_lib = ReportLib()
134        report_lib.ShowArtFrames(False)
135        self.assertFalse(has_art_frame(report_lib))
136        report_lib = ReportLib()
137        report_lib.ShowArtFrames(True)
138        self.assertTrue(has_art_frame(report_lib))
139
140    def test_remove_method(self):
141        def get_methods(report_lib) -> Set[str]:
142            methods = set()
143            report_lib.SetRecordFile(TestHelper.testdata_path('perf_display_bitmaps.data'))
144            while True:
145                sample = report_lib.GetNextSample()
146                if not sample:
147                    break
148                methods.add(report_lib.GetSymbolOfCurrentSample().symbol_name)
149                callchain = report_lib.GetCallChainOfCurrentSample()
150                for i in range(callchain.nr):
151                    methods.add(callchain.entries[i].symbol.symbol_name)
152            report_lib.Close()
153            return methods
154
155        report_lib = ReportLib()
156        report_lib.RemoveMethod('android.view')
157        methods = get_methods(report_lib)
158        self.assertFalse(any('android.view' in method for method in methods))
159        self.assertTrue(any('android.widget' in method for method in methods))
160
161    def test_merge_java_methods(self):
162        def parse_dso_names(report_lib):
163            dso_names = set()
164            report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_interpreter_frames.data'))
165            while report_lib.GetNextSample():
166                dso_names.add(report_lib.GetSymbolOfCurrentSample().dso_name)
167                callchain = report_lib.GetCallChainOfCurrentSample()
168                for i in range(callchain.nr):
169                    dso_names.add(callchain.entries[i].symbol.dso_name)
170            report_lib.Close()
171            has_jit_symfiles = any('TemporaryFile-' in name for name in dso_names)
172            has_jit_cache = '[JIT cache]' in dso_names
173            return has_jit_symfiles, has_jit_cache
174
175        report_lib = ReportLib()
176        self.assertEqual(parse_dso_names(report_lib), (False, True))
177
178        report_lib = ReportLib()
179        report_lib.MergeJavaMethods(True)
180        self.assertEqual(parse_dso_names(report_lib), (False, True))
181
182        report_lib = ReportLib()
183        report_lib.MergeJavaMethods(False)
184        self.assertEqual(parse_dso_names(report_lib), (True, False))
185
186    def test_jited_java_methods(self):
187        report_lib = ReportLib()
188        report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_jit_symbol.data'))
189        has_jit_cache = False
190        while report_lib.GetNextSample():
191            if report_lib.GetSymbolOfCurrentSample().dso_name == '[JIT app cache]':
192                has_jit_cache = True
193            callchain = report_lib.GetCallChainOfCurrentSample()
194            for i in range(callchain.nr):
195                if callchain.entries[i].symbol.dso_name == '[JIT app cache]':
196                    has_jit_cache = True
197        report_lib.Close()
198        self.assertTrue(has_jit_cache)
199
200    def test_tracing_data(self):
201        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_tracepoint_event.data'))
202        has_tracing_data = False
203        while self.report_lib.GetNextSample():
204            event = self.report_lib.GetEventOfCurrentSample()
205            tracing_data = self.report_lib.GetTracingDataOfCurrentSample()
206            if event.name == 'sched:sched_switch':
207                self.assertIsNotNone(tracing_data)
208                self.assertIn('prev_pid', tracing_data)
209                self.assertIn('next_comm', tracing_data)
210                if tracing_data['prev_pid'] == 9896 and tracing_data['next_comm'] == 'swapper/4':
211                    has_tracing_data = True
212            else:
213                self.assertIsNone(tracing_data)
214        self.assertTrue(has_tracing_data)
215
216    def test_dynamic_field_in_tracing_data(self):
217        self.report_lib.SetRecordFile(TestHelper.testdata_path(
218            'perf_with_tracepoint_event_dynamic_field.data'))
219        has_dynamic_field = False
220        while self.report_lib.GetNextSample():
221            event = self.report_lib.GetEventOfCurrentSample()
222            tracing_data = self.report_lib.GetTracingDataOfCurrentSample()
223            if event.name == 'kprobes:myopen':
224                self.assertIsNotNone(tracing_data)
225                self.assertIn('name', tracing_data)
226                if tracing_data['name'] == '/sys/kernel/debug/tracing/events/kprobes/myopen/format':
227                    has_dynamic_field = True
228            else:
229                self.assertIsNone(tracing_data)
230        self.assertTrue(has_dynamic_field)
231
232    def test_add_proguard_mapping_file(self):
233        with self.assertRaises(ValueError):
234            self.report_lib.AddProguardMappingFile('non_exist_file')
235        proguard_mapping_file = TestHelper.testdata_path('proguard_mapping.txt')
236        self.report_lib.AddProguardMappingFile(proguard_mapping_file)
237
238    def test_set_trace_offcpu_mode(self):
239        # GetSupportedTraceOffCpuModes() before SetRecordFile() triggers RuntimeError.
240        with self.assertRaises(RuntimeError):
241            self.report_lib.GetSupportedTraceOffCpuModes()
242        # SetTraceOffCpuModes() before SetRecordFile() triggers RuntimeError.
243        with self.assertRaises(RuntimeError):
244            self.report_lib.SetTraceOffCpuMode('on-cpu')
245
246        mode_dict = {
247            'on-cpu': {
248                'cpu-clock:u': (208, 52000000),
249                'sched:sched_switch': (0, 0),
250            },
251            'off-cpu': {
252                'cpu-clock:u': (0, 0),
253                'sched:sched_switch': (91, 344124304),
254            },
255            'on-off-cpu': {
256                'cpu-clock:u': (208, 52000000),
257                'sched:sched_switch': (91, 344124304),
258            },
259            'mixed-on-off-cpu': {
260                'cpu-clock:u': (299, 396124304),
261                'sched:sched_switch': (0, 0),
262            },
263        }
264
265        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_with_trace_offcpu_v2.data'))
266        self.assertEqual(set(self.report_lib.GetSupportedTraceOffCpuModes()), set(mode_dict.keys()))
267        for mode, expected_values in mode_dict.items():
268            self.report_lib.Close()
269            self.report_lib = ReportLib()
270            self.report_lib.SetRecordFile(
271                TestHelper.testdata_path('perf_with_trace_offcpu_v2.data'))
272            self.report_lib.SetTraceOffCpuMode(mode)
273
274            cpu_clock_period = 0
275            cpu_clock_samples = 0
276            sched_switch_period = 0
277            sched_switch_samples = 0
278            while self.report_lib.GetNextSample():
279                sample = self.report_lib.GetCurrentSample()
280                event = self.report_lib.GetEventOfCurrentSample()
281                if event.name == 'cpu-clock:u':
282                    cpu_clock_period += sample.period
283                    cpu_clock_samples += 1
284                else:
285                    self.assertEqual(event.name, 'sched:sched_switch')
286                    sched_switch_period += sample.period
287                    sched_switch_samples += 1
288            self.assertEqual(cpu_clock_samples, expected_values['cpu-clock:u'][0])
289            self.assertEqual(cpu_clock_period, expected_values['cpu-clock:u'][1])
290            self.assertEqual(sched_switch_samples, expected_values['sched:sched_switch'][0])
291            self.assertEqual(sched_switch_period, expected_values['sched:sched_switch'][1])
292
293        # Check trace-offcpu modes on a profile not recorded with --trace-offcpu.
294        self.report_lib.Close()
295        self.report_lib = ReportLib()
296        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf.data'))
297        self.assertEqual(self.report_lib.GetSupportedTraceOffCpuModes(), [])
298        with self.assertRaises(RuntimeError):
299            self.report_lib.SetTraceOffCpuMode('on-cpu')
300
301    def test_set_sample_filter(self):
302        """ Test using ReportLib.SetSampleFilter(). """
303        def get_threads_for_filter(filters: List[str]) -> Set[int]:
304            self.report_lib.Close()
305            self.report_lib = ReportLib()
306            self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_display_bitmaps.data'))
307            self.report_lib.SetSampleFilter(filters)
308            threads = set()
309            while self.report_lib.GetNextSample():
310                sample = self.report_lib.GetCurrentSample()
311                threads.add(sample.tid)
312            return threads
313
314        self.assertNotIn(31850, get_threads_for_filter(['--exclude-pid', '31850']))
315        self.assertIn(31850, get_threads_for_filter(['--include-pid', '31850']))
316        self.assertNotIn(31881, get_threads_for_filter(['--exclude-tid', '31881']))
317        self.assertIn(31881, get_threads_for_filter(['--include-tid', '31881']))
318        self.assertNotIn(31881, get_threads_for_filter(
319            ['--exclude-process-name', 'com.example.android.displayingbitmaps']))
320        self.assertIn(31881, get_threads_for_filter(
321            ['--include-process-name', 'com.example.android.displayingbitmaps']))
322        self.assertNotIn(31850, get_threads_for_filter(
323            ['--exclude-thread-name', 'com.example.android.displayingbitmaps']))
324        self.assertIn(31850, get_threads_for_filter(
325            ['--include-thread-name', 'com.example.android.displayingbitmaps']))
326
327        # Check that thread name can have space.
328        self.assertNotIn(31856, get_threads_for_filter(
329            ['--exclude-thread-name', 'Jit thread pool']))
330        self.assertIn(31856, get_threads_for_filter(['--include-thread-name', 'Jit thread pool']))
331
332        with tempfile.NamedTemporaryFile('w', delete=False) as filter_file:
333            filter_file.write('GLOBAL_BEGIN 684943449406175\nGLOBAL_END 684943449406176')
334            filter_file.flush()
335            threads = get_threads_for_filter(['--filter-file', filter_file.name])
336            self.assertIn(31881, threads)
337            self.assertNotIn(31850, threads)
338        os.unlink(filter_file.name)
339
340    def test_set_sample_filter_for_cpu(self):
341        """ Test --cpu in ReportLib.SetSampleFilter(). """
342        def get_cpus_for_filter(filters: List[str]) -> Set[int]:
343            self.report_lib.Close()
344            self.report_lib = ReportLib()
345            self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_display_bitmaps.data'))
346            self.report_lib.SetSampleFilter(filters)
347            cpus = set()
348            while self.report_lib.GetNextSample():
349                sample = self.report_lib.GetCurrentSample()
350                cpus.add(sample.cpu)
351            return cpus
352
353        cpus = get_cpus_for_filter(['--cpu', '0,1-2'])
354        self.assertIn(0, cpus)
355        self.assertIn(1, cpus)
356        self.assertIn(2, cpus)
357        self.assertNotIn(3, cpus)
358
359    def test_aggregate_threads(self):
360        """ Test using ReportLib.AggregateThreads(). """
361        def get_thread_names(aggregate_regex_list: Optional[List[str]]) -> Dict[str, int]:
362            self.report_lib.Close()
363            self.report_lib = ReportLib()
364            self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_display_bitmaps.data'))
365            if aggregate_regex_list:
366                self.report_lib.AggregateThreads(aggregate_regex_list)
367            thread_names = {}
368            while self.report_lib.GetNextSample():
369                sample = self.report_lib.GetCurrentSample()
370                thread_names[sample.thread_comm] = thread_names.get(sample.thread_comm, 0) + 1
371            return thread_names
372        thread_names = get_thread_names(None)
373        self.assertEqual(thread_names['AsyncTask #3'], 6)
374        self.assertEqual(thread_names['AsyncTask #4'], 13)
375        thread_names = get_thread_names(['AsyncTask.*'])
376        self.assertEqual(thread_names['AsyncTask.*'], 19)
377        self.assertNotIn('AsyncTask #3', thread_names)
378        self.assertNotIn('AsyncTask #4', thread_names)
379
380    def test_use_vmlinux(self):
381        """ Test if we can use vmlinux in symfs_dir. """
382        record_file = TestHelper.testdata_path('perf_test_vmlinux.data')
383        # Create a symfs_dir.
384        symfs_dir = Path('symfs_dir')
385        symfs_dir.mkdir()
386        shutil.copy(TestHelper.testdata_path('vmlinux'), symfs_dir)
387        kernel_build_id = ReadElf(TestHelper.ndk_path).get_build_id(symfs_dir / 'vmlinux')
388        (symfs_dir / 'build_id_list').write_text('%s=vmlinux' % kernel_build_id)
389
390        # Check if vmlinux in symfs_dir is used, when we set record file before setting symfs_dir.
391        self.report_lib.SetRecordFile(record_file)
392        self.report_lib.SetSymfs(str(symfs_dir))
393        sample = self.report_lib.GetNextSample()
394        self.assertIsNotNone(sample)
395        symbol = self.report_lib.GetSymbolOfCurrentSample()
396        self.assertEqual(symbol.dso_name, "[kernel.kallsyms]")
397        # vaddr_in_file and symbol_addr are adjusted after using vmlinux.
398        self.assertEqual(symbol.vaddr_in_file, 0xffffffc008fb3e28)
399        self.assertEqual(symbol.symbol_name, "_raw_spin_unlock_irq")
400        self.assertEqual(symbol.symbol_addr, 0xffffffc008fb3e0c)
401        self.assertEqual(symbol.symbol_len, 0x4c)
402
403    def test_get_process_name(self):
404        self.report_lib.SetRecordFile(TestHelper.testdata_path('perf_display_bitmaps.data'))
405        expected_process_name = 'com.example.android.displayingbitmaps'
406        while self.report_lib.GetNextSample():
407            process_name = self.report_lib.GetProcessNameOfCurrentSample()
408            self.assertEqual(process_name, expected_process_name)
409
410
411class TestProtoFileReportLib(TestBase):
412    def test_smoke(self):
413        report_lib = ProtoFileReportLib()
414        report_lib.SetRecordFile(TestHelper.testdata_path('display_bitmaps.proto_data'))
415        sample_count = 0
416        while True:
417            sample = report_lib.GetNextSample()
418            if sample is None:
419                report_lib.Close()
420                break
421            sample_count += 1
422            event = report_lib.GetEventOfCurrentSample()
423            self.assertEqual(event.name, 'cpu-clock')
424            report_lib.GetSymbolOfCurrentSample()
425            report_lib.GetCallChainOfCurrentSample()
426        self.assertEqual(sample_count, 525)
427
428    def convert_perf_data_to_proto_file(self, perf_data_path: str) -> str:
429        simpleperf_path = get_host_binary_path('simpleperf')
430        proto_file_path = 'perf.trace'
431        subprocess.check_call([simpleperf_path, 'report-sample', '--show-callchain', '--protobuf',
432                               '--remove-gaps', '0', '-i', perf_data_path, '-o', proto_file_path])
433        return proto_file_path
434
435    def test_set_trace_offcpu_mode(self):
436        report_lib = ProtoFileReportLib()
437        # GetSupportedTraceOffCpuModes() before SetRecordFile() triggers RuntimeError.
438        with self.assertRaises(RuntimeError):
439            report_lib.GetSupportedTraceOffCpuModes()
440
441        mode_dict = {
442            'on-cpu': {
443                'cpu-clock:u': (208, 52000000),
444                'sched:sched_switch': (0, 0),
445            },
446            'off-cpu': {
447                'cpu-clock:u': (0, 0),
448                'sched:sched_switch': (91, 344124304),
449            },
450            'on-off-cpu': {
451                'cpu-clock:u': (208, 52000000),
452                'sched:sched_switch': (91, 344124304),
453            },
454            'mixed-on-off-cpu': {
455                'cpu-clock:u': (299, 396124304),
456                'sched:sched_switch': (0, 0),
457            },
458        }
459
460        proto_file_path = self.convert_perf_data_to_proto_file(
461            TestHelper.testdata_path('perf_with_trace_offcpu_v2.data'))
462        report_lib.SetRecordFile(proto_file_path)
463        self.assertEqual(set(report_lib.GetSupportedTraceOffCpuModes()), set(mode_dict.keys()))
464        for mode, expected_values in mode_dict.items():
465            report_lib.Close()
466            report_lib = ProtoFileReportLib()
467            report_lib.SetRecordFile(proto_file_path)
468            report_lib.SetTraceOffCpuMode(mode)
469
470            cpu_clock_period = 0
471            cpu_clock_samples = 0
472            sched_switch_period = 0
473            sched_switch_samples = 0
474            while report_lib.GetNextSample():
475                sample = report_lib.GetCurrentSample()
476                event = report_lib.GetEventOfCurrentSample()
477                if event.name == 'cpu-clock:u':
478                    cpu_clock_period += sample.period
479                    cpu_clock_samples += 1
480                else:
481                    self.assertEqual(event.name, 'sched:sched_switch')
482                    sched_switch_period += sample.period
483                    sched_switch_samples += 1
484            self.assertEqual(cpu_clock_samples, expected_values['cpu-clock:u'][0])
485            self.assertEqual(cpu_clock_period, expected_values['cpu-clock:u'][1])
486            self.assertEqual(sched_switch_samples, expected_values['sched:sched_switch'][0])
487            self.assertEqual(sched_switch_period, expected_values['sched:sched_switch'][1])
488
489        # Check trace-offcpu modes on a profile not recorded with --trace-offcpu.
490        report_lib.Close()
491        report_lib = ProtoFileReportLib()
492        proto_file_path = self.convert_perf_data_to_proto_file(
493            TestHelper.testdata_path('perf.data'))
494        report_lib.SetRecordFile(proto_file_path)
495        self.assertEqual(report_lib.GetSupportedTraceOffCpuModes(), [])
496