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 glob 18import os 19from pathlib import Path 20import re 21import shutil 22import subprocess 23import time 24from typing import List, Tuple 25 26from simpleperf_report_lib import ReportLib 27from simpleperf_utils import remove 28from . test_utils import TestBase, TestHelper, AdbHelper, INFERNO_SCRIPT 29 30 31class TestExampleBase(TestBase): 32 @classmethod 33 def prepare(cls, example_name, package_name, activity_name, abi=None, adb_root=False, 34 apk_name: str = 'app-debug.apk'): 35 cls.adb = AdbHelper(enable_switch_to_root=adb_root) 36 cls.example_path = TestHelper.testdata_path(example_name) 37 if not os.path.isdir(cls.example_path): 38 log_fatal("can't find " + cls.example_path) 39 apk_files = list(Path(cls.example_path).glob(f'**/{apk_name}')) 40 if not apk_files: 41 log_fatal(f"can't find {apk_name} under " + cls.example_path) 42 cls.apk_path = apk_files[0] 43 cls.package_name = package_name 44 cls.activity_name = activity_name 45 args = ["install", "-r"] 46 if abi: 47 args += ["--abi", abi] 48 args.append(cls.apk_path) 49 cls.adb.check_run(args) 50 cls.adb_root = adb_root 51 cls.has_perf_data_for_report = False 52 # On Android >= P (version 9), we can profile JITed and interpreted Java code. 53 # So only compile Java code on Android <= O (version 8). 54 cls.use_compiled_java_code = TestHelper.android_version <= 8 55 cls.testcase_dir = TestHelper.get_test_dir(cls.__name__) 56 57 @classmethod 58 def tearDownClass(cls): 59 if hasattr(cls, 'testcase_dir'): 60 remove(cls.testcase_dir) 61 if hasattr(cls, 'package_name'): 62 cls.adb.check_run(["uninstall", cls.package_name]) 63 64 def setUp(self): 65 super(TestExampleBase, self).setUp() 66 if TestHelper.android_version == 8 and ( 67 'ExampleJava' in self.id() or 'ExampleKotlin' in self.id()): 68 self.skipTest('Profiling java code needs wrap.sh on Android O (8.x)') 69 if 'TraceOffCpu' in self.id() and not TestHelper.is_trace_offcpu_supported(): 70 self.skipTest('trace-offcpu is not supported on device') 71 # Use testcase_dir to share a common perf.data for reporting. So we don't need to 72 # generate it for each test. 73 if not os.path.isdir(self.testcase_dir): 74 os.makedirs(self.testcase_dir) 75 os.chdir(self.testcase_dir) 76 self.run_app_profiler(compile_java_code=self.use_compiled_java_code) 77 os.chdir(self.test_dir) 78 79 for name in os.listdir(self.testcase_dir): 80 path = os.path.join(self.testcase_dir, name) 81 if os.path.isfile(path): 82 shutil.copy(path, self.test_dir) 83 elif os.path.isdir(path): 84 shutil.copytree(path, os.path.join(self.test_dir, name)) 85 86 def run(self, result=None): 87 self.__class__.test_result = result 88 super(TestExampleBase, self).run(result) 89 90 def run_app_profiler(self, record_arg="-g --duration 10", build_binary_cache=True, 91 start_activity=True, compile_java_code=False): 92 args = ['app_profiler.py', '--app', self.package_name, '-r', record_arg, '-o', 'perf.data'] 93 if not build_binary_cache: 94 args.append("-nb") 95 if compile_java_code: 96 args.append('--compile_java_code') 97 if start_activity: 98 args += ["-a", self.activity_name] 99 args += ["-lib", self.example_path] 100 if not self.adb_root: 101 args.append("--disable_adb_root") 102 self.run_cmd(args) 103 self.check_exist(filename="perf.data") 104 if build_binary_cache: 105 self.check_exist(dirname="binary_cache") 106 107 def check_file_under_dir(self, dirname, filename): 108 self.check_exist(dirname=dirname) 109 for _, _, files in os.walk(dirname): 110 for f in files: 111 if f == filename: 112 return 113 self.fail("Failed to call check_file_under_dir(dir=%s, file=%s)" % (dirname, filename)) 114 115 def check_annotation_summary( 116 self, summary_file: str, check_entries: List[Tuple[str, float, float]]): 117 """ check_entries is a list of (name, accumulated_period, period). 118 This function checks for each entry, if the line containing [name] 119 has at least required accumulated_period and period. 120 """ 121 self.check_exist(filename=summary_file) 122 with open(summary_file, 'r') as fh: 123 summary = fh.read() 124 fulfilled = [False for x in check_entries] 125 summary_check_re = re.compile(r'^\|\s*([\d.]+)%\s*\|\s*([\d.]+)%\s*\|') 126 for line in summary.split('\n'): 127 for i, (name, need_acc_period, need_period) in enumerate(check_entries): 128 if not fulfilled[i] and name in line: 129 m = summary_check_re.search(line) 130 if m: 131 acc_period = float(m.group(1)) 132 period = float(m.group(2)) 133 if acc_period >= need_acc_period and period >= need_period: 134 fulfilled[i] = True 135 136 self.check_fulfilled_entries(fulfilled, check_entries) 137 138 def check_inferno_report_html(self, check_entries, filename="report.html"): 139 self.check_exist(filename=filename) 140 with open(filename, 'r') as fh: 141 data = fh.read() 142 fulfilled = [False for _ in check_entries] 143 for line in data.split('\n'): 144 # each entry is a (function_name, min_percentage) pair. 145 for i, entry in enumerate(check_entries): 146 if fulfilled[i] or line.find(entry[0]) == -1: 147 continue 148 m = re.search(r'(\d+\.\d+)%', line) 149 if m and float(m.group(1)) >= entry[1]: 150 fulfilled[i] = True 151 break 152 self.check_fulfilled_entries(fulfilled, check_entries) 153 154 def common_test_app_profiler(self): 155 self.run_cmd(["app_profiler.py", "-h"]) 156 remove("binary_cache") 157 self.run_app_profiler(build_binary_cache=False) 158 self.assertFalse(os.path.isdir("binary_cache")) 159 args = ["binary_cache_builder.py"] 160 if not self.adb_root: 161 args.append("--disable_adb_root") 162 self.run_cmd(args) 163 self.check_exist(dirname="binary_cache") 164 remove("binary_cache") 165 self.run_app_profiler(build_binary_cache=True) 166 self.run_app_profiler() 167 self.run_app_profiler(start_activity=False) 168 169 def common_test_report(self): 170 self.run_cmd(["report.py", "-h"]) 171 self.run_cmd(["report.py"]) 172 self.run_cmd(["report.py", "-i", "perf.data"]) 173 self.run_cmd(["report.py", "-g"]) 174 self.run_cmd(["report.py", "--self-kill-for-testing", "-g", "--gui"]) 175 176 def common_test_annotate(self): 177 self.run_cmd(["annotate.py", "-h"]) 178 remove("annotated_files") 179 self.run_cmd(["annotate.py", "-s", self.example_path, '--summary-width', '1000']) 180 self.check_exist(dirname="annotated_files") 181 182 def common_test_report_sample(self, check_strings): 183 self.run_cmd(["report_sample.py", "-h"]) 184 self.run_cmd(["report_sample.py"]) 185 output = self.run_cmd(["report_sample.py", "-i", "perf.data"], return_output=True) 186 self.check_strings_in_content(output, check_strings) 187 188 def common_test_pprof_proto_generator(self, check_strings_with_lines, 189 check_strings_without_lines): 190 self.run_cmd(["pprof_proto_generator.py", "-h"]) 191 self.run_cmd(["pprof_proto_generator.py"]) 192 remove("pprof.profile") 193 self.run_cmd(["pprof_proto_generator.py", "-i", "perf.data", "-o", "pprof.profile"]) 194 self.check_exist(filename="pprof.profile") 195 self.run_cmd(["pprof_proto_generator.py", "--show"]) 196 output = self.run_cmd(["pprof_proto_generator.py", "--show", "pprof.profile"], 197 return_output=True) 198 self.check_strings_in_content(output, check_strings_with_lines + ["has_line_numbers: True"]) 199 remove("binary_cache") 200 self.run_cmd(["pprof_proto_generator.py"]) 201 output = self.run_cmd(["pprof_proto_generator.py", "--show", "pprof.profile"], 202 return_output=True) 203 self.check_strings_in_content(output, check_strings_without_lines + 204 ["has_line_numbers: False"]) 205 206 def common_test_inferno(self): 207 self.run_cmd([INFERNO_SCRIPT, "-h"]) 208 remove("perf.data") 209 append_args = [] if self.adb_root else ["--disable_adb_root"] 210 self.run_cmd([INFERNO_SCRIPT, "-p", self.package_name, "-t", "3"] + append_args) 211 self.check_exist(filename="perf.data") 212 self.run_cmd([INFERNO_SCRIPT, "-p", self.package_name, "-f", "1000", "-du", "-t", "1"] + 213 append_args) 214 self.run_cmd([INFERNO_SCRIPT, "-p", self.package_name, "-e", "100000 cpu-cycles", 215 "-t", "1"] + append_args) 216 self.run_cmd([INFERNO_SCRIPT, "-sc"]) 217 218 def common_test_report_html(self): 219 self.run_cmd(['report_html.py', '-h']) 220 self.run_cmd(['report_html.py']) 221 self.run_cmd(['report_html.py', '--add_source_code', '--source_dirs', 'testdata']) 222 self.run_cmd(['report_html.py', '--add_disassembly']) 223 # Test with multiple perf.data. 224 shutil.move('perf.data', 'perf2.data') 225 self.run_app_profiler(record_arg='-g -f 1000 --duration 3 -e task-clock:u') 226 self.run_cmd(['report_html.py', '-i', 'perf.data', 'perf2.data']) 227 228 229class TestRecordingRealApps(TestBase): 230 def setUp(self): 231 super(TestRecordingRealApps, self).setUp() 232 self.adb = TestHelper.adb 233 self.installed_packages = [] 234 235 def tearDown(self): 236 for package in self.installed_packages: 237 self.adb.run(['shell', 'pm', 'uninstall', package]) 238 super(TestRecordingRealApps, self).tearDown() 239 240 def install_apk(self, apk_path, package_name): 241 self.adb.run(['uninstall', package_name]) 242 self.adb.run(['install', '-t', apk_path]) 243 self.installed_packages.append(package_name) 244 245 def start_app(self, start_cmd): 246 subprocess.Popen(self.adb.adb_path + ' ' + start_cmd, shell=True, 247 stdout=TestHelper.log_fh, stderr=TestHelper.log_fh) 248 249 def record_data(self, package_name, record_arg): 250 self.run_cmd(['app_profiler.py', '--app', package_name, '-r', record_arg]) 251 252 def check_symbol_in_record_file(self, symbol_name): 253 self.run_cmd(['report.py', '--children', '-o', 'report.txt']) 254 self.check_strings_in_file('report.txt', [symbol_name]) 255 256 def test_recording_displaybitmaps(self): 257 self.install_apk(TestHelper.testdata_path('DisplayBitmaps.apk'), 258 'com.example.android.displayingbitmaps') 259 self.install_apk(TestHelper.testdata_path('DisplayBitmapsTest.apk'), 260 'com.example.android.displayingbitmaps.test') 261 self.start_app('shell am instrument -w -r -e debug false -e class ' + 262 'com.example.android.displayingbitmaps.tests.GridViewTest ' + 263 'com.example.android.displayingbitmaps.test/' + 264 'androidx.test.runner.AndroidJUnitRunner') 265 self.record_data('com.example.android.displayingbitmaps', '-e cpu-clock -g --duration 10') 266 if TestHelper.android_version >= 9: 267 self.check_symbol_in_record_file('androidx.test.espresso') 268 269 def test_recording_endless_tunnel(self): 270 self.install_apk(TestHelper.testdata_path( 271 'EndlessTunnel.apk'), 'com.google.sample.tunnel') 272 # Test using --launch to start the app. 273 self.run_cmd(['app_profiler.py', '--app', 'com.google.sample.tunnel', 274 '--launch', '-r', '-e cpu-clock -g --duration 10']) 275 self.check_symbol_in_record_file('PlayScene::DoFrame') 276 277 # Check app versioncode. 278 report = ReportLib() 279 meta_info = report.MetaInfo() 280 self.assertEqual(meta_info.get('app_versioncode'), '1') 281 report.Close() 282