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