• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 collections
18import json
19import os
20import tempfile
21from typing import Any, Dict, List, Optional, Set
22
23from binary_cache_builder import BinaryCacheBuilder
24from . test_utils import TestBase, TestHelper
25
26
27class TestReportHtml(TestBase):
28    def test_long_callchain(self):
29        self.run_cmd(['report_html.py', '-i',
30                      TestHelper.testdata_path('perf_with_long_callchain.data')])
31
32    def test_aggregated_by_thread_name(self):
33        # Calculate event_count for each thread name before aggregation.
34        event_count_for_thread_name = collections.defaultdict(lambda: 0)
35        # use "--min_func_percent 0" to avoid cutting any thread.
36        record_data = self.get_record_data(['--min_func_percent', '0', '-i',
37                                            TestHelper.testdata_path('aggregatable_perf1.data'),
38                                            TestHelper.testdata_path('aggregatable_perf2.data')])
39        event = record_data['sampleInfo'][0]
40        for process in event['processes']:
41            for thread in process['threads']:
42                thread_name = record_data['threadNames'][str(thread['tid'])]
43                event_count_for_thread_name[thread_name] += thread['eventCount']
44
45        # Check event count for each thread after aggregation.
46        record_data = self.get_record_data(['--aggregate-by-thread-name',
47                                            '--min_func_percent', '0', '-i',
48                                            TestHelper.testdata_path('aggregatable_perf1.data'),
49                                            TestHelper.testdata_path('aggregatable_perf2.data')])
50        event = record_data['sampleInfo'][0]
51        hit_count = 0
52        for process in event['processes']:
53            for thread in process['threads']:
54                thread_name = record_data['threadNames'][str(thread['tid'])]
55                self.assertEqual(thread['eventCount'],
56                                 event_count_for_thread_name[thread_name])
57                hit_count += 1
58        self.assertEqual(hit_count, len(event_count_for_thread_name))
59
60    def test_no_empty_process(self):
61        """ Test not showing a process having no threads. """
62        perf_data = TestHelper.testdata_path('two_process_perf.data')
63        record_data = self.get_record_data(['-i', perf_data])
64        processes = record_data['sampleInfo'][0]['processes']
65        self.assertEqual(len(processes), 2)
66
67        # One process is removed because all its threads are removed for not
68        # reaching the min_func_percent limit.
69        record_data = self.get_record_data(['-i', perf_data, '--min_func_percent', '20'])
70        processes = record_data['sampleInfo'][0]['processes']
71        self.assertEqual(len(processes), 1)
72
73    def test_proguard_mapping_file(self):
74        """ Test --proguard-mapping-file option. """
75        testdata_file = TestHelper.testdata_path('perf_need_proguard_mapping.data')
76        proguard_mapping_file = TestHelper.testdata_path('proguard_mapping.txt')
77        original_methodname = 'androidx.fragment.app.FragmentActivity.startActivityForResult'
78        # Can't show original method name without proguard mapping file.
79        record_data = self.get_record_data(['-i', testdata_file])
80        self.assertNotIn(original_methodname, json.dumps(record_data))
81        # Show original method name with proguard mapping file.
82        record_data = self.get_record_data(
83            ['-i', testdata_file, '--proguard-mapping-file', proguard_mapping_file])
84        self.assertIn(original_methodname, json.dumps(record_data))
85
86    def get_record_data(self, options: List[str]) -> Dict[str, Any]:
87        json_data = self.get_record_data_string(options)
88        return json.loads(json_data)
89
90    def get_record_data_string(self, options: List[str]) -> str:
91        args = ['report_html.py'] + options
92        if TestHelper.ndk_path:
93            args += ['--ndk_path', TestHelper.ndk_path]
94        self.run_cmd(args)
95        with open('report.html', 'r') as fh:
96            data = fh.read()
97        start_str = 'type="application/json"'
98        end_str = '</script>'
99        start_pos = data.find(start_str)
100        self.assertNotEqual(start_pos, -1)
101        start_pos = data.find('>', start_pos)
102        self.assertNotEqual(start_pos, -1)
103        start_pos += 1
104        end_pos = data.find(end_str, start_pos)
105        self.assertNotEqual(end_pos, -1)
106        return data[start_pos:end_pos]
107
108    def test_add_source_code(self):
109        """ Test --add_source_code option. """
110        testdata_file = TestHelper.testdata_path('runtest_two_functions_arm64_perf.data')
111
112        # Build binary_cache.
113        binary_cache_builder = BinaryCacheBuilder(TestHelper.ndk_path, False)
114        binary_cache_builder.build_binary_cache(testdata_file, [TestHelper.testdata_dir])
115
116        # Generate report.html.
117        source_dir = TestHelper.testdata_dir
118        record_data = self.get_record_data(
119            ['-i', testdata_file, '--add_source_code', '--source_dirs', str(source_dir)])
120
121        # Check source code info in samples.
122        source_code_list = []
123        thread = record_data['sampleInfo'][0]['processes'][0]['threads'][0]
124        for lib in thread['libs']:
125            for function in lib['functions']:
126                for source_code_info in function.get('s') or []:
127                    source_file = record_data['sourceFiles'][source_code_info['f']]
128                    file_path = source_file['path']
129                    line_number = source_code_info['l']
130                    line_content = source_file['code'][str(line_number)]
131                    event_count = source_code_info['e']
132                    subtree_event_count = source_code_info['s']
133                    s = (f'{file_path}:{line_number}:{line_content}:' +
134                         f'{event_count}:{subtree_event_count}')
135                    source_code_list.append(s)
136        check_items = ['two_functions.cpp:9:    *p = i;\n:590184:590184',
137                       'two_functions.cpp:16:    *p = i;\n:591577:591577',
138                       'two_functions.cpp:22:    Function1();\n:0:590184',
139                       'two_functions.cpp:23:    Function2();\n:0:591577']
140        for item in check_items:
141            found = False
142            for source_code in source_code_list:
143                if item in source_code:
144                    found = True
145                    break
146            self.assertTrue(found, item)
147
148    def test_add_disassembly(self):
149        """ Test --add_disassembly option. """
150        testdata_file = TestHelper.testdata_path('runtest_two_functions_arm64_perf.data')
151
152        # Build binary_cache.
153        binary_cache_builder = BinaryCacheBuilder(TestHelper.ndk_path, False)
154        binary_cache_builder.build_binary_cache(testdata_file, [TestHelper.testdata_dir])
155
156        # Generate report.html.
157        record_data = self.get_record_data(['-i', testdata_file, '--add_disassembly'])
158
159        # Check disassembly in samples.
160        disassembly_list = []
161        thread = record_data['sampleInfo'][0]['processes'][0]['threads'][0]
162        for lib in thread['libs']:
163            lib_name = record_data['libList'][lib['libId']]
164            for function in lib['functions']:
165                for addr_info in function.get('a') or []:
166                    addr = addr_info['a']
167                    event_count = addr_info['e']
168                    subtree_event_count = addr_info['s']
169                    function_data = record_data['functionMap'][str(function['f'])]
170                    function_name = function_data['f']
171                    for dis_line, dis_addr in function_data.get('d') or []:
172                        if addr == dis_addr:
173                            s = (f'{lib_name}:{function_name}:{addr}:' +
174                                 f'{event_count}:{subtree_event_count}')
175                            disassembly_list.append(s)
176
177        check_items = ['simpleperf_runtest_two_functions_arm64:Function1():0x1094:590184:590184',
178                       'simpleperf_runtest_two_functions_arm64:Function2():0x1104:591577:591577',
179                       'simpleperf_runtest_two_functions_arm64:main:0x113c:0:590184',
180                       'simpleperf_runtest_two_functions_arm64:main:0x1140:0:591577']
181        for item in check_items:
182            found = False
183            for disassembly in disassembly_list:
184                if item in disassembly:
185                    found = True
186                    break
187            self.assertTrue(found, item)
188
189    def test_trace_offcpu(self):
190        """ Test --trace-offcpu option. """
191        testdata_file = TestHelper.testdata_path('perf_with_trace_offcpu_v2.data')
192        record_data = self.get_record_data(['-i', testdata_file, '--trace-offcpu', 'on-cpu'])
193        self.assertEqual(len(record_data['sampleInfo']), 1)
194        self.assertEqual(record_data['sampleInfo'][0]['eventName'], 'cpu-clock:u')
195        self.assertEqual(record_data['sampleInfo'][0]['eventCount'], 52000000)
196
197        record_data = self.get_record_data(['-i', testdata_file, '--trace-offcpu', 'off-cpu'])
198        self.assertEqual(len(record_data['sampleInfo']), 1)
199        self.assertEqual(record_data['sampleInfo'][0]['eventName'], 'sched:sched_switch')
200        self.assertEqual(record_data['sampleInfo'][0]['eventCount'], 344124304)
201
202        record_data = self.get_record_data(['-i', testdata_file, '--trace-offcpu', 'on-off-cpu'])
203        self.assertEqual(len(record_data['sampleInfo']), 2)
204        self.assertEqual(record_data['sampleInfo'][0]['eventName'], 'cpu-clock:u')
205        self.assertEqual(record_data['sampleInfo'][0]['eventCount'], 52000000)
206        self.assertEqual(record_data['sampleInfo'][1]['eventName'], 'sched:sched_switch')
207        self.assertEqual(record_data['sampleInfo'][1]['eventCount'], 344124304)
208
209        record_data = self.get_record_data(
210            ['-i', testdata_file, '--trace-offcpu', 'mixed-on-off-cpu'])
211        self.assertEqual(len(record_data['sampleInfo']), 1)
212        self.assertEqual(record_data['sampleInfo'][0]['eventName'], 'cpu-clock:u')
213        self.assertEqual(record_data['sampleInfo'][0]['eventCount'], 396124304)
214
215    def test_sample_filters(self):
216        def get_threads_for_filter(filter: str) -> Set[int]:
217            record_data = self.get_record_data(
218                ['-i', TestHelper.testdata_path('perf_display_bitmaps.data')] + filter.split())
219            threads = set()
220            try:
221                for thread in record_data['sampleInfo'][0]['processes'][0]['threads']:
222                    threads.add(thread['tid'])
223            except IndexError:
224                pass
225            return threads
226
227        self.assertNotIn(31850, get_threads_for_filter('--exclude-pid 31850'))
228        self.assertIn(31850, get_threads_for_filter('--include-pid 31850'))
229        self.assertIn(31850, get_threads_for_filter('--pid 31850'))
230        self.assertNotIn(31881, get_threads_for_filter('--exclude-tid 31881'))
231        self.assertIn(31881, get_threads_for_filter('--include-tid 31881'))
232        self.assertIn(31881, get_threads_for_filter('--tid 31881'))
233        self.assertNotIn(31881, get_threads_for_filter(
234            '--exclude-process-name com.example.android.displayingbitmaps'))
235        self.assertIn(31881, get_threads_for_filter(
236            '--include-process-name com.example.android.displayingbitmaps'))
237        self.assertNotIn(31850, get_threads_for_filter(
238            '--exclude-thread-name com.example.android.displayingbitmaps'))
239        self.assertIn(31850, get_threads_for_filter(
240            '--include-thread-name com.example.android.displayingbitmaps'))
241
242        with tempfile.NamedTemporaryFile('w', delete=False) as filter_file:
243            filter_file.write('GLOBAL_BEGIN 684943449406175\nGLOBAL_END 684943449406176')
244            filter_file.flush()
245            threads = get_threads_for_filter('--filter-file ' + filter_file.name)
246            self.assertIn(31881, threads)
247            self.assertNotIn(31850, threads)
248        os.unlink(filter_file.name)
249
250    def test_show_art_frames(self):
251        art_frame_str = 'art::interpreter::DoCall'
252        options = ['-i', TestHelper.testdata_path('perf_with_interpreter_frames.data')]
253        report = self.get_record_data_string(options)
254        self.assertNotIn(art_frame_str, report)
255        report = self.get_record_data_string(options + ['--show-art-frames'])
256        self.assertIn(art_frame_str, report)
257
258    def test_aggregate_threads(self):
259        def get_thread_names(aggregate_threads_option: Optional[List[str]]) -> Dict[str, int]:
260            options = ['-i', TestHelper.testdata_path('perf_display_bitmaps.data')]
261            if aggregate_threads_option:
262                options += ['--aggregate-threads'] + aggregate_threads_option
263            record_data = self.get_record_data(options)
264            thread_names = {}
265            try:
266                for thread in record_data['sampleInfo'][0]['processes'][0]['threads']:
267                    tid = str(thread['tid'])
268                    thread_names[record_data['threadNames'][tid]] = thread['sampleCount']
269            except IndexError:
270                pass
271            return thread_names
272        thread_names = get_thread_names(None)
273        self.assertEqual(thread_names['AsyncTask #3'], 6)
274        self.assertEqual(thread_names['AsyncTask #4'], 13)
275        thread_names = get_thread_names(['AsyncTask.*'])
276        self.assertEqual(thread_names['AsyncTask.*'], 19)
277        self.assertNotIn('AsyncTask #3', thread_names)
278        self.assertNotIn('AsyncTask #4', thread_names)
279