| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2021 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import collections |
| import json |
| import os |
| import tempfile |
| from typing import Any, Dict, List, Set |
| |
| from binary_cache_builder import BinaryCacheBuilder |
| from . test_utils import TestBase, TestHelper |
| |
| |
| class TestReportHtml(TestBase): |
| def test_long_callchain(self): |
| self.run_cmd(['report_html.py', '-i', |
| TestHelper.testdata_path('perf_with_long_callchain.data')]) |
| |
| def test_aggregated_by_thread_name(self): |
| # Calculate event_count for each thread name before aggregation. |
| event_count_for_thread_name = collections.defaultdict(lambda: 0) |
| # use "--min_func_percent 0" to avoid cutting any thread. |
| record_data = self.get_record_data(['--min_func_percent', '0', '-i', |
| TestHelper.testdata_path('aggregatable_perf1.data'), |
| TestHelper.testdata_path('aggregatable_perf2.data')]) |
| event = record_data['sampleInfo'][0] |
| for process in event['processes']: |
| for thread in process['threads']: |
| thread_name = record_data['threadNames'][str(thread['tid'])] |
| event_count_for_thread_name[thread_name] += thread['eventCount'] |
| |
| # Check event count for each thread after aggregation. |
| record_data = self.get_record_data(['--aggregate-by-thread-name', |
| '--min_func_percent', '0', '-i', |
| TestHelper.testdata_path('aggregatable_perf1.data'), |
| TestHelper.testdata_path('aggregatable_perf2.data')]) |
| event = record_data['sampleInfo'][0] |
| hit_count = 0 |
| for process in event['processes']: |
| for thread in process['threads']: |
| thread_name = record_data['threadNames'][str(thread['tid'])] |
| self.assertEqual(thread['eventCount'], |
| event_count_for_thread_name[thread_name]) |
| hit_count += 1 |
| self.assertEqual(hit_count, len(event_count_for_thread_name)) |
| |
| def test_no_empty_process(self): |
| """ Test not showing a process having no threads. """ |
| perf_data = TestHelper.testdata_path('two_process_perf.data') |
| record_data = self.get_record_data(['-i', perf_data]) |
| processes = record_data['sampleInfo'][0]['processes'] |
| self.assertEqual(len(processes), 2) |
| |
| # One process is removed because all its threads are removed for not |
| # reaching the min_func_percent limit. |
| record_data = self.get_record_data(['-i', perf_data, '--min_func_percent', '20']) |
| processes = record_data['sampleInfo'][0]['processes'] |
| self.assertEqual(len(processes), 1) |
| |
| def test_proguard_mapping_file(self): |
| """ Test --proguard-mapping-file option. """ |
| testdata_file = TestHelper.testdata_path('perf_need_proguard_mapping.data') |
| proguard_mapping_file = TestHelper.testdata_path('proguard_mapping.txt') |
| original_methodname = 'androidx.fragment.app.FragmentActivity.startActivityForResult' |
| # Can't show original method name without proguard mapping file. |
| record_data = self.get_record_data(['-i', testdata_file]) |
| self.assertNotIn(original_methodname, json.dumps(record_data)) |
| # Show original method name with proguard mapping file. |
| record_data = self.get_record_data( |
| ['-i', testdata_file, '--proguard-mapping-file', proguard_mapping_file]) |
| self.assertIn(original_methodname, json.dumps(record_data)) |
| |
| def get_record_data(self, options: List[str]) -> Dict[str, Any]: |
| args = ['report_html.py'] + options |
| if TestHelper.ndk_path: |
| args += ['--ndk_path', TestHelper.ndk_path] |
| self.run_cmd(args) |
| with open('report.html', 'r') as fh: |
| data = fh.read() |
| start_str = 'type="application/json"' |
| end_str = '</script>' |
| start_pos = data.find(start_str) |
| self.assertNotEqual(start_pos, -1) |
| start_pos = data.find('>', start_pos) |
| self.assertNotEqual(start_pos, -1) |
| start_pos += 1 |
| end_pos = data.find(end_str, start_pos) |
| self.assertNotEqual(end_pos, -1) |
| json_data = data[start_pos:end_pos] |
| return json.loads(json_data) |
| |
| def test_add_source_code(self): |
| """ Test --add_source_code option. """ |
| testdata_file = TestHelper.testdata_path('runtest_two_functions_arm64_perf.data') |
| |
| # Build binary_cache. |
| binary_cache_builder = BinaryCacheBuilder(TestHelper.ndk_path, False) |
| binary_cache_builder.build_binary_cache(testdata_file, [TestHelper.testdata_dir]) |
| |
| # Generate report.html. |
| source_dir = TestHelper.testdata_dir |
| record_data = self.get_record_data( |
| ['-i', testdata_file, '--add_source_code', '--source_dirs', str(source_dir)]) |
| |
| # Check source code info in samples. |
| source_code_list = [] |
| thread = record_data['sampleInfo'][0]['processes'][0]['threads'][0] |
| for lib in thread['libs']: |
| for function in lib['functions']: |
| for source_code_info in function.get('s') or []: |
| source_file = record_data['sourceFiles'][source_code_info['f']] |
| file_path = source_file['path'] |
| line_number = source_code_info['l'] |
| line_content = source_file['code'][str(line_number)] |
| event_count = source_code_info['e'] |
| subtree_event_count = source_code_info['s'] |
| s = (f'{file_path}:{line_number}:{line_content}:' + |
| f'{event_count}:{subtree_event_count}') |
| source_code_list.append(s) |
| check_items = ['two_functions.cpp:9: *p = i;\n:590184:590184', |
| 'two_functions.cpp:16: *p = i;\n:591577:591577', |
| 'two_functions.cpp:22: Function1();\n:0:590184', |
| 'two_functions.cpp:23: Function2();\n:0:591577'] |
| for item in check_items: |
| found = False |
| for source_code in source_code_list: |
| if item in source_code: |
| found = True |
| break |
| self.assertTrue(found, item) |
| |
| def test_add_disassembly(self): |
| """ Test --add_disassembly option. """ |
| testdata_file = TestHelper.testdata_path('runtest_two_functions_arm64_perf.data') |
| |
| # Build binary_cache. |
| binary_cache_builder = BinaryCacheBuilder(TestHelper.ndk_path, False) |
| binary_cache_builder.build_binary_cache(testdata_file, [TestHelper.testdata_dir]) |
| |
| # Generate report.html. |
| record_data = self.get_record_data(['-i', testdata_file, '--add_disassembly']) |
| |
| # Check disassembly in samples. |
| disassembly_list = [] |
| thread = record_data['sampleInfo'][0]['processes'][0]['threads'][0] |
| for lib in thread['libs']: |
| lib_name = record_data['libList'][lib['libId']] |
| for function in lib['functions']: |
| for addr_info in function.get('a') or []: |
| addr = addr_info['a'] |
| event_count = addr_info['e'] |
| subtree_event_count = addr_info['s'] |
| function_data = record_data['functionMap'][str(function['f'])] |
| function_name = function_data['f'] |
| for dis_line, dis_addr in function_data.get('d') or []: |
| if addr == dis_addr: |
| s = (f'{lib_name}:{function_name}:{addr}:' + |
| f'{event_count}:{subtree_event_count}') |
| disassembly_list.append(s) |
| |
| check_items = ['simpleperf_runtest_two_functions_arm64:Function1():0x1094:590184:590184', |
| 'simpleperf_runtest_two_functions_arm64:Function2():0x1104:591577:591577', |
| 'simpleperf_runtest_two_functions_arm64:main:0x113c:0:590184', |
| 'simpleperf_runtest_two_functions_arm64:main:0x1140:0:591577'] |
| for item in check_items: |
| found = False |
| for disassembly in disassembly_list: |
| if item in disassembly: |
| found = True |
| break |
| self.assertTrue(found, item) |
| |
| def test_trace_offcpu(self): |
| """ Test --trace-offcpu option. """ |
| testdata_file = TestHelper.testdata_path('perf_with_trace_offcpu_v2.data') |
| record_data = self.get_record_data(['-i', testdata_file, '--trace-offcpu', 'on-cpu']) |
| self.assertEqual(len(record_data['sampleInfo']), 1) |
| self.assertEqual(record_data['sampleInfo'][0]['eventName'], 'cpu-clock:u') |
| self.assertEqual(record_data['sampleInfo'][0]['eventCount'], 52000000) |
| |
| record_data = self.get_record_data(['-i', testdata_file, '--trace-offcpu', 'off-cpu']) |
| self.assertEqual(len(record_data['sampleInfo']), 1) |
| self.assertEqual(record_data['sampleInfo'][0]['eventName'], 'sched:sched_switch') |
| self.assertEqual(record_data['sampleInfo'][0]['eventCount'], 344124304) |
| |
| record_data = self.get_record_data(['-i', testdata_file, '--trace-offcpu', 'on-off-cpu']) |
| self.assertEqual(len(record_data['sampleInfo']), 2) |
| self.assertEqual(record_data['sampleInfo'][0]['eventName'], 'cpu-clock:u') |
| self.assertEqual(record_data['sampleInfo'][0]['eventCount'], 52000000) |
| self.assertEqual(record_data['sampleInfo'][1]['eventName'], 'sched:sched_switch') |
| self.assertEqual(record_data['sampleInfo'][1]['eventCount'], 344124304) |
| |
| record_data = self.get_record_data( |
| ['-i', testdata_file, '--trace-offcpu', 'mixed-on-off-cpu']) |
| self.assertEqual(len(record_data['sampleInfo']), 1) |
| self.assertEqual(record_data['sampleInfo'][0]['eventName'], 'cpu-clock:u') |
| self.assertEqual(record_data['sampleInfo'][0]['eventCount'], 396124304) |
| |
| def test_sample_filters(self): |
| def get_threads_for_filter(filter: str) -> Set[int]: |
| record_data = self.get_record_data( |
| ['-i', TestHelper.testdata_path('perf_display_bitmaps.data')] + filter.split()) |
| threads = set() |
| try: |
| for thread in record_data['sampleInfo'][0]['processes'][0]['threads']: |
| threads.add(thread['tid']) |
| except IndexError: |
| pass |
| return threads |
| |
| self.assertNotIn(31850, get_threads_for_filter('--exclude-pid 31850')) |
| self.assertIn(31850, get_threads_for_filter('--include-pid 31850')) |
| self.assertIn(31850, get_threads_for_filter('--pid 31850')) |
| self.assertNotIn(31881, get_threads_for_filter('--exclude-tid 31881')) |
| self.assertIn(31881, get_threads_for_filter('--include-tid 31881')) |
| self.assertIn(31881, get_threads_for_filter('--tid 31881')) |
| self.assertNotIn(31881, get_threads_for_filter( |
| '--exclude-process-name com.example.android.displayingbitmaps')) |
| self.assertIn(31881, get_threads_for_filter( |
| '--include-process-name com.example.android.displayingbitmaps')) |
| self.assertNotIn(31850, get_threads_for_filter( |
| '--exclude-thread-name com.example.android.displayingbitmaps')) |
| self.assertIn(31850, get_threads_for_filter( |
| '--include-thread-name com.example.android.displayingbitmaps')) |
| |
| with tempfile.NamedTemporaryFile('w', delete=False) as filter_file: |
| filter_file.write('GLOBAL_BEGIN 684943449406175\nGLOBAL_END 684943449406176') |
| filter_file.flush() |
| threads = get_threads_for_filter('--filter-file ' + filter_file.name) |
| self.assertIn(31881, threads) |
| self.assertNotIn(31850, threads) |
| os.unlink(filter_file.name) |