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