- [Annotate source code](#annotate-source-code)
- [Answers to common issues](#answers-to-common-issues)
- [The correct way to pull perf.data on host](#the-correct-way-to-pull-perfdata-on-host)
+ - [Why we suggest profiling on android >= N devices](#why-we-suggest-profiling-on-android-n-devices)
## Simpleperf introduction
instead of release build type. It is understandable because we can't profile others' apps.
However, on a rooted Android device, the application doesn't need to be debuggable.
-**2. Run on an Android device >= L.**
-Profiling on emulators are not yet supported. And to profile Java code, we need
-the jvm running in oat mode, which is only available >= L.
+**2. Run on an Android >= N device.**
+We suggest profiling on an Android >= N device. The reason is [here](#why-we-suggest-profiling-on-android-n-devices).
+
**3. On Android O, add `wrap.sh` in the apk.**
-To profile Java code, we need the jvm running in oat mode. But on Android O,
+To profile Java code, we need ART running in oat mode. But on Android O,
debuggable applications are forced to run in jit mode. To work around this,
we need to add a `wrap.sh` in the apk. So if you are running on Android O device,
Check [here](https://android.googlesource.com/platform/system/extras/+/master/simpleperf/demo/SimpleperfExamplePureJava/app/profiling.gradle)
# Start the app if needed
$ adb shell am start -n com.example.simpleperf.simpleperfexamplepurejava/.MainActivity
- # Run `ps` in the app's context. On Android >= O devicces, run `ps -e` instead.
- $ adb shell run-as com.example.simpleperf.simpleperfexamplepurejava ps | grep simpleperf
- u0_a151 6885 3346 1590504 53980 SyS_epoll_ 6fc2024b6c S com.example.simpleperf.simpleperfexamplepurejava
+ $ adb shell pidof com.example.simpleperf.simpleperfexamplepurejava
+ 6885
So the id of the app process is `6885`. We will use this number in the command lines below,
-please replace this number with what you get by running `ps` command.
+please replace this number with what you get by running `pidof` command.
+On Android <= M, pidof may not exist or work well, and you can try
+`ps | grep com.example.simpleperf.simpleperfexamplepurejava` instead.
**4. Download simpleperf to the app's data directory**
$ adb shell "run-as com.example.simpleperf.simpleperfexamplepurejava cat perf.data | tee /data/local/tmp/perf.data >/dev/null"
$ adb pull /data/local/tmp/perf.data
- # Report samples using corresponding simpleperf executable on host.
- # On windows, use "bin\windows\x86_64\simpleperf" instead.
- $ bin/linux/x86_64/simpleperf report
+ # Report samples using report.py, report.py is a python wrapper of simpleperf report command.
+ $ python report.py
...
Overhead Command Pid Tid Shared Object Symbol
83.54% Thread-2 6885 6900 /data/app/com.example.simpleperf.simpleperfexamplepurejava-2/oat/arm64/base.odex void com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()
Besides command lines, We can use `app-profiler.py` to profile Android applications.
It downloads simpleperf on device, records perf.data, and collects profiling
-results and native binaries on host. It is configured by `app-profiler.config`.
-
-**1. Fill `app-profiler.config`**
-
- Change `app_package_name` line to app_package_name="com.example.simpleperf.simpleperfexamplepurejava"
- Change `apk_file_path` line to apk_file_path = "../SimpleperfExamplePureJava/app/build/outputs/apk/app-profiling.apk"
- Change `android_studio_project_dir` line to android_studio_project_dir = "../SimpleperfExamplePureJava/"
- Change `record_options` line to record_options = "--duration 10"
-
-`apk_file_path` is needed to fully compile the application on Android L/M. It is
-not necessary on Android >= N.
-
-`android_studio_project_dir` is used to search native libraries in the
-application. It is not necessary for profiling.
+results and native binaries on host.
-`record_options` can be set to any option accepted by simpleperf record command.
+**1. Record perf.data by running `app-profiler.py`**
-**2. Run `app-profiler.py`**
-
- $ python app_profiler.py
+ $ python app_profiler.py --app com.example.simpleperf.simpleperfexamplepurejava \
+ --apk ../SimpleperfExamplePureJava/app/build/outputs/apk/app-profiling.apk \
+ -r "-e cpu-cycles:u --duration 10"
If running successfully, it will collect profiling data in perf.data in current
directory, and related native binaries in binary_cache/.
-**3. Report perf.data**
+**2. Report perf.data**
We can use `report.py` to report perf.data.
$ adb shell run-as com.example.simpleperf.simpleperfexamplepurejava ./simpleperf record -g -p 6685 --duration 10
-When using python scripts, change `app-profiler.config` as below:
+When using app_profiler.py, add "-g" in record option as below:
- Change `record_options` line to record_options = "--duration 10 -g"
+ $ python app_profiler.py --app com.example.simpleperf.simpleperfexamplepurejava \
+ --apk ../SimpleperfExamplePureJava/app/build/outputs/apk/app-profiling.apk \
+ -r "-e cpu-cycles:u --duration 10 -g"
Recording dwarf based call graph needs support of debug information
in native binaries. So if using native libraries in the application,
$ adb shell run-as com.example.simpleperf.simpleperfexamplepurejava ./simpleperf record --call-graph fp -p 6685 --duration 10
-When using python scripts, change `app-profiler.config` as below:
+When using app_profiler.py, add "--call-graph fp" in record option as below:
- Change `record_options` line to record_options = "--duration 10 --call-graph fp"
+ $ python app_profiler.py --app com.example.simpleperf.simpleperfexamplepurejava \
+ --apk ../SimpleperfExamplePureJava/app/build/outputs/apk/app-profiling.apk \
+ -r "-e cpu-cycles:u --duration 10 --call-graph fp"
Recording stack frame based call graphs needs support of stack frame
register. Notice that on arm architecture, the stack frame register
|--16.22%-- int com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.callFunction(int)
| |--99.97%-- [hit in function]
-To report call graph using python scripts, add `-g` option.
+To report call graph using report.py, add `-g` option.
+ # In text mode
$ python report.py -g
+ # In GUI mode
+ $ python report.py -g --gui
# Double-click an item started with '+' to show its callgraph.
### Visualize profiling data
## Answers to common issues
### The correct way to pull perf.data on host
+
As perf.data is generated in app's context, it can't be pulled directly to host.
One way is to `adb shell run-as xxx cat perf.data >perf.data`. However, it
doesn't work well on Windows, because the content can be modified when it goes
$adb shell "run-as xxx cat perf.data | tee /data/local/tmp/perf.data >/dev/null"
$adb pull /data/local/tmp/perf.data
+
+### Why we suggest profiling on Android >= N devices?
+```
+1. Running on a device reflects a real running situation, so we suggest
+profiling on real devices instead of emulators.
+2. To profile Java code, we need ART running in oat mode, which is only
+available >= L for rooted devices, and >= N for non-rooted devices.
+3. Old Android versions are likely to be shipped with old kernels (< 3.18),
+which may not support profiling features like dwarf based call graph.
+4. Old Android versions are likely to be shipped with Arm32 chips. In Arm32
+mode, stack frame based call graph doesn't work well.
+```
+
## Inferno
![logo](./inferno/inferno_small.png)
3. Show profiling data:
```
a. show call graph in txt mode
- # On windows, use "bin\windows\x86\simpleperf" instead.
- $ bin/linux/x86_64/simpleperf report -g | more
- If on other hosts, use corresponding simpleperf binary.
+ $ python report.py -g | more
b. show call graph in gui mode
- $ python report.py -g
+ $ python report.py -g --gui
c. show samples in source code
$ python annotate.py -s ../demo/SimpleperfExamplePureJava
- $ gvim annotated_files/java/com/example/simpleperf/simpleperfexamplepurejava/MainActivity.java
+ $ find annotated_files -name "MainActivity.java"
check the annoated source file MainActivity.java.
```
3. Show profiling data:
```
a. show call graph in txt mode
- # On windows, use "bin\windows\x86\simpleperf" instead.
- $ bin/linux/x86_64/simpleperf report -g | more
- If on other hosts, use corresponding simpleperf binary.
+ $ python report.py -g | more
b. show call graph in gui mode
- $ python report.py -g
+ $ python report.py -g --gui
c. show samples in source code
$ python annotate.py -s ../demo/SimpleperfExampleWithNative
- $ find . -name "native-lib.cpp" | xargs gvim
+ $ find annotated_files -name "native-lib.cpp"
check the annoated source file native-lib.cpp.
```
3. Show profiling data:
```
a. show call graph in txt mode
- # On windows, use "bin\windows\x86\simpleperf" instead.
- $ bin/linux/x86_64/simpleperf report -g | more
- If on other hosts, use corresponding simpleperf binary.
+ $ python report.py -g | more
b. show call graph in gui mode
- $ python report.py -g
+ $ python report.py -g --gui
c. show samples in source code
$ python annotate.py -s ../demo/SimpleperfExampleOfKotlin
- $ find . -name "MainActivity.kt" | xargs gvim
- check the annoated source file MainActivity.kt.
+ $ find . -name "MainActivity.kt"
```
LOCAL_PATH := $(call my-dir)
SIMPLEPERF_SCRIPT_LIST := $(wildcard $(LOCAL_PATH)/*.py $(LOCAL_PATH)/*.config) \
- $(LOCAL_PATH)/../README.md
+ $(LOCAL_PATH)/../README.md \
+ $(LOCAL_PATH)/testdata
SIMPLEPERF_SCRIPT_LIST := $(filter-out $(LOCAL_PATH)/update.py,$(SIMPLEPERF_SCRIPT_LIST))
source_files = self.source_file_dict.get(filename)
if source_files is None:
return None
- match_count = 0
- result = None
+ best_path_count = 0
+ best_path = None
+ best_suffix_len = 0
for path in source_files:
- if path.find(file) != -1:
- match_count += 1
- result = path
- if match_count > 1:
+ suffix_len = len(os.path.commonprefix((path[::-1], file[::-1])))
+ if suffix_len > best_suffix_len:
+ best_suffix_len = suffix_len
+ best_path = path
+ best_path_count = 1
+ elif suffix_len == best_suffix_len:
+ best_path_count += 1
+ if best_path_count > 1:
log_warning('multiple source for %s, select %s' % (file, result))
- return result
+ return best_path
def _annotate_files(self):
def __init__(self, config):
self.check_config(config)
self.config = config
- self.adb = AdbHelper()
+ self.adb = AdbHelper(enable_switch_to_root=not config['disable_adb_root'])
self.is_root_device = False
self.android_version = 0
self.device_arch = None
self.app_arch = None
self.app_pid = None
+ self.has_symfs_on_device = False
def check_config(self, config):
def _find_app_process(self):
- ps_args = ['-e'] if self.android_version >= 8 else []
- result, output = self.adb.run_and_return_output(['shell', 'ps'] + ps_args)
+ # On Android >= N, pidof is available. Otherwise, we can use ps.
+ if self.android_version >= 7:
+ result, output = self.adb.run_and_return_output(['shell', 'pidof',
+ self.config['app_package_name']])
+ return int(output) if result else None
+ result, output = self.adb.run_and_return_output(['shell', 'ps'], log_output=False)
if not result:
return None
- output = output.split('\n')
- for line in output:
+ for line in output.split('\n'):
strs = line.split()
if len(strs) > 2 and strs[-1].find(self.config['app_package_name']) != -1:
return int(strs[1])
if self.app_pid is None:
log_exit("can't find process for app [%s]" % self.config['app_package_name'])
if self.device_arch in ['aarch64', 'x86_64']:
- output = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
+ output = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid], log_output=False)
if output.find('linker64') != -1:
self.app_arch = self.device_arch
else:
filename_dict[file] = path
else:
log_info('%s is worse than %s' % (path, old_path))
- maps = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid])
+ maps = self.run_in_app_dir(['cat', '/proc/%d/maps' % self.app_pid], log_output=False)
searched_lib = dict()
for item in maps.split():
if item.endswith('.so') and searched_lib.get(item) is None:
self.adb.check_run(['push', path, '/data/local/tmp'])
self.run_in_app_dir(['mkdir', '-p', dirname])
self.run_in_app_dir(['cp', '/data/local/tmp/' + filename, dirname])
+ self.has_symfs_on_device = True
def _is_lib_better(self, new_path, old_path):
subproc = None
returncode = None
try:
- args = self.get_run_in_app_dir_args([
- './simpleperf', 'record', self.config['record_options'], '-p',
- str(self.app_pid), '--symfs', '.'])
+ args = ['./simpleperf', 'record', self.config['record_options'],
+ '-p', str(self.app_pid)]
+ if self.has_symfs_on_device:
+ args += ['--symfs', '.']
+ args = self.get_run_in_app_dir_args(args)
adb_args = [self.adb.adb_path] + args
log_debug('run adb cmd: %s' % adb_args)
subproc = subprocess.Popen(adb_args)
binary_cache_builder.build_binary_cache()
- def run_in_app_dir(self, args, stdout_file=None, check_result=True):
+ def run_in_app_dir(self, args, stdout_file=None, check_result=True, log_output=True):
args = self.get_run_in_app_dir_args(args)
if check_result:
- return self.adb.check_run_and_return_output(args, stdout_file)
+ return self.adb.check_run_and_return_output(args, stdout_file, log_output=log_output)
else:
- return self.adb.run_and_return_output(args, stdout_file)
+ return self.adb.run_and_return_output(args, stdout_file, log_output=log_output)
def get_run_in_app_dir_args(self, args):
parser.add_argument('--config', default='app_profiler.config', help=
"""Set configuration file. Default is app_profiler.config. The configurations
can be overridden by options in cmdline.""")
- parser.add_argument('-p', '--package_name', help=
+ parser.add_argument('-p', '--app', help=
"""The package name of the profiled Android app.""")
parser.add_argument('-lib', '--native_lib_dir', help=
"""Path to find debug version of native shared libraries used in the app.""")
parser.add_argument('-nb', '--skip_collect_binaries', action='store_true', help=
"""By default we collect binaries used in profiling data from device to
binary_cache directory. It can be used to annotate source code. This option skips it.""")
+ parser.add_argument('--disable_adb_root', action='store_true', help=
+"""Force adb to run in non root mode.""")
args = parser.parse_args()
config = load_config(args.config)
- if args.package_name:
- config['app_package_name'] = args.package_name
+ if args.app:
+ config['app_package_name'] = args.app
if args.native_lib_dir:
config['native_lib_dir'] = args.native_lib_dir
if args.skip_recompile:
config['perf_data_path'] = args.perf_data_path
if args.skip_collect_binaries:
config['collect_binaries'] = False
+ config['disable_adb_root'] = args.disable_adb_root
profiler = AppProfiler(config)
profiler.profile()
for symfs_dir in self.symfs_dirs:
if not os.path.isdir(symfs_dir):
log_exit("symfs_dir '%s' is not a directory" % symfs_dir)
- self.adb = AdbHelper()
+ self.adb = AdbHelper(enable_switch_to_root=not config['disable_adb_root'])
self.readelf_path = find_tool_path('readelf')
if not self.readelf_path and self.symfs_dirs:
log_warning("Debug shared libraries on host are not used because can't find readelf.")
parser.add_argument('-lib', '--native_lib_dir', nargs='+', help=
"""Path to find debug version of native shared libraries used in the app.""",
action='append')
+ parser.add_argument('--disable_adb_root', action='store_true', help=
+"""Force adb to run in non root mode.""")
args = parser.parse_args()
config = {}
config['perf_data_path'] = args.perf_data_path
config['symfs_dirs'] = flatten_arg_list(args.native_lib_dir)
+ config['disable_adb_root'] = args.disable_adb_root
builder = BinaryCacheBuilder(config)
builder.build_binary_cache()
import argparse
import os
import os.path
-import profile_pb2
import re
import shutil
import sys
from simpleperf_report_lib import *
from utils import *
+try:
+ import google.protobuf
+except:
+ log_exit('google.protobuf module is missing. Please install it first.')
+
+import profile_pb2
def load_pprof_profile(filename):
profile = profile_pb2.Profile()
generate report file, and display it.
"""
+import os
import os.path
import re
import subprocess
self.display_call_tree(tree, id, child, indent + 1)
-def display_report_file(report_file):
- fh = open(report_file, 'r')
- lines = fh.readlines()
- fh.close()
+def display_report_file(report_file, self_kill_after_sec):
+ fh = open(report_file, 'r')
+ lines = fh.readlines()
+ fh.close()
- lines = [x.rstrip() for x in lines]
- event_reports = parse_event_reports(lines)
+ lines = [x.rstrip() for x in lines]
+ event_reports = parse_event_reports(lines)
- if event_reports:
- root = Tk()
- for i in range(len(event_reports)):
- report = event_reports[i]
- parent = root if i == 0 else Toplevel(root)
- ReportWindow(parent, report.context, report.title_line, report.report_items)
- root.mainloop()
+ if event_reports:
+ root = Tk()
+ for i in range(len(event_reports)):
+ report = event_reports[i]
+ parent = root if i == 0 else Toplevel(root)
+ ReportWindow(parent, report.context, report.title_line, report.report_items)
+ if self_kill_after_sec:
+ root.after(self_kill_after_sec * 1000, lambda: root.destroy())
+ root.mainloop()
-def call_simpleperf_report(args, report_file):
- output_fh = open(report_file, 'w')
- simpleperf_path = get_host_binary_path('simpleperf')
- args = [simpleperf_path, 'report', '--full-callgraph'] + args
- subprocess.check_call(args, stdout=output_fh)
- output_fh.close()
+def call_simpleperf_report(args, show_gui, self_kill_after_sec):
+ simpleperf_path = get_host_binary_path('simpleperf')
+ if not show_gui:
+ subprocess.check_call([simpleperf_path, 'report'] + args)
+ else:
+ report_file = 'perf.report'
+ subprocess.check_call([simpleperf_path, 'report', '--full-callgraph'] + args +
+ ['-o', report_file])
+ display_report_file(report_file, self_kill_after_sec=self_kill_after_sec)
+
+
+def get_simpleperf_report_help_msg():
+ simpleperf_path = get_host_binary_path('simpleperf')
+ args = [simpleperf_path, 'report', '-h']
+ proc = subprocess.Popen(args, stdout=subprocess.PIPE)
+ (stdoutdata, _) = proc.communicate()
+ return stdoutdata[stdoutdata.find('\n') + 1:]
def main():
- if len(sys.argv) == 2 and os.path.isfile(sys.argv[1]):
- display_report_file(sys.argv[1])
- else:
- call_simpleperf_report(sys.argv[1:], 'perf.report')
- display_report_file('perf.report')
+ self_kill_after_sec = 0
+ args = sys.argv[1:]
+ if args and args[0] == "--self-kill-for-testing":
+ self_kill_after_sec = 1
+ args = args[1:]
+ if len(args) == 1 and os.path.isfile(args[0]):
+ display_report_file(args[0], self_kill_after_sec=self_kill_after_sec)
+
+ i = 0
+ args_for_report_cmd = []
+ show_gui = False
+ while i < len(args):
+ if args[i] == '-h' or args[i] == '--help':
+ print('report.py A python wrapper for simpleperf report command.')
+ print('report.py [')
+ print('Options supported by simpleperf report command:')
+ print(get_simpleperf_report_help_msg())
+ print('\nOptions supported by report.py:')
+ print('--gui Show report result in a gui window.')
+ print('\nIt also supports showing a report generated by simpleperf report cmd:')
+ print('\n python report.py report_file')
+ sys.exit(0)
+ elif args[i] == '--gui':
+ show_gui = True
+ i += 1
+ else:
+ args_for_report_cmd.append(args[i])
+ i += 1
+
+ call_simpleperf_report(args_for_report_cmd, show_gui, self_kill_after_sec)
if __name__ == '__main__':
- main()
+ main()
--- /dev/null
+#!/usr/bin/env python
+#
+# Copyright (C) 2017 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.
+#
+"""test.py: Tests for simpleperf python scripts.
+
+These are smoke tests Using examples to run python scripts.
+For each example, we go through the steps of running each python script.
+Examples are collected from simpleperf/demo, which includes:
+ SimpleperfExamplePureJava
+ SimpleperfExampleWithNative
+ SimpleperfExampleOfKotlin
+
+Tested python scripts include:
+ app_profiler.py
+ report.py
+ annotate.py
+ report_sample.py
+ pprof_proto_generator.py
+
+Test using both `adb root` and `adb unroot`.
+
+"""
+
+import os
+import re
+import shutil
+import sys
+import tempfile
+import unittest
+from utils import *
+
+has_google_protobuf = True
+try:
+ import google.protobuf
+except:
+ has_google_protobuf = False
+
+
+class TestExamplesBase(unittest.TestCase):
+ @classmethod
+ def prepare(cls, example_name, package_name, activity_name, abi=None, adb_root=False):
+ cls.adb = AdbHelper(enable_switch_to_root=adb_root)
+ cls.example_path = os.path.join("testdata", example_name)
+ if not os.path.isdir(cls.example_path):
+ log_fatal("can't find " + cls.example_path)
+ cls.apk_path = os.path.join(cls.example_path, "app-profiling.apk")
+ if not os.path.isfile(cls.apk_path):
+ log_fatal("can't find " + cls.apk_path)
+ cls.package_name = package_name
+ cls.activity_name = activity_name
+ cls.python_path = sys.executable
+ args = ["install", "-r"]
+ if abi:
+ args += ["--abi", abi]
+ args.append(cls.apk_path)
+ cls.adb.check_run(args)
+ cls.adb_root = adb_root
+ cls.compiled = False
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.adb.check_run(["uninstall", cls.package_name])
+
+ @classmethod
+ def cleanupTestFiles(cls):
+ cls.remove("binary_cache")
+ cls.remove("annotated_files")
+ cls.remove("perf.data")
+ cls.remove("report.txt")
+ cls.remove("pprof.profile")
+
+ def run_cmd(self, args, return_output=False):
+ args = [self.python_path] + args
+ try:
+ if not return_output:
+ returncode = subprocess.call(args)
+ else:
+ subproc = subprocess.Popen(args, stdout=subprocess.PIPE)
+ (output_data, _) = subproc.communicate()
+ returncode = subproc.returncode
+ except:
+ returncode = None
+ self.assertEqual(returncode, 0, msg="failed to run cmd: %s" % args)
+ if return_output:
+ return output_data
+
+ def run_app_profiler(self, record_arg = "-g --duration 3 -e cpu-cycles:u",
+ build_binary_cache=True, skip_compile=False, start_activity=True,
+ native_lib_dir=None):
+ args = ["app_profiler.py", "--app", self.package_name, "--apk", self.apk_path,
+ "-a", self.activity_name, "-r", record_arg, "-o", "perf.data"]
+ if not build_binary_cache:
+ args.append("-nb")
+ if skip_compile or self.__class__.compiled:
+ args.append("-nc")
+ if start_activity:
+ args += ["-a", self.activity_name]
+ if native_lib_dir:
+ args += ["-lib", native_lib_dir]
+ if not self.adb_root:
+ args.append("--disable_adb_root")
+ self.run_cmd(args)
+ self.check_exist(file="perf.data")
+ if build_binary_cache:
+ self.check_exist(dir="binary_cache")
+ if not skip_compile:
+ self.__class__.compiled = True
+
+ @classmethod
+ def remove(cls, dir):
+ shutil.rmtree(dir, ignore_errors=True)
+
+ def check_exist(self, file=None, dir=None):
+ if file:
+ self.assertTrue(os.path.isfile(file), file)
+ if dir:
+ self.assertTrue(os.path.isdir(dir), dir)
+
+ def check_file_under_dir(self, dir, file):
+ self.check_exist(dir=dir)
+ for _, _, files in os.walk(dir):
+ for f in files:
+ if f == file:
+ return
+ self.fail("Failed to call check_file_under_dir(dir=%s, file=%s)" % (dir, file))
+
+
+ def check_strings_in_file(self, file, strings):
+ self.check_exist(file=file)
+ with open(file, 'r') as fh:
+ self.check_strings_in_content(fh.read(), strings)
+
+ def check_strings_in_content(self, content, strings):
+ for s in strings:
+ self.assertNotEqual(content.find(s), -1, "s: %s, content: %s" % (s, content))
+
+ def check_annotation_summary(self, summary_file, check_entries):
+ """ check_entries is a list of (name, accumulated_period, period).
+ This function checks for each entry, if the line containing [name]
+ has at least required accumulated_period and period.
+ """
+ self.check_exist(file=summary_file)
+ with open(summary_file, 'r') as fh:
+ summary = fh.read()
+ fulfilled = [False for x in check_entries]
+ if not hasattr(self, "summary_check_re"):
+ self.summary_check_re = re.compile(r'accumulated_period:\s*([\d.]+)%.*period:\s*([\d.]+)%')
+ for line in summary.split('\n'):
+ for i in range(len(check_entries)):
+ (name, need_acc_period, need_period) = check_entries[i]
+ if not fulfilled[i] and line.find(name) != -1:
+ m = self.summary_check_re.search(line)
+ if m:
+ acc_period = float(m.group(1))
+ period = float(m.group(2))
+ if acc_period >= need_acc_period and period >= need_period:
+ fulfilled[i] = True
+ self.assertEqual(len(fulfilled), sum([int(x) for x in fulfilled]), fulfilled)
+
+ def common_test_app_profiler(self):
+ self.run_cmd(["app_profiler.py", "-h"])
+ self.remove("binary_cache")
+ self.run_app_profiler(build_binary_cache=False)
+ self.assertFalse(os.path.isdir("binary_cache"))
+ args = ["binary_cache_builder.py"]
+ if not self.adb_root:
+ args.append("--disable_adb_root")
+ self.run_cmd(args)
+ self.check_exist(dir="binary_cache")
+ self.remove("binary_cache")
+ self.run_app_profiler(build_binary_cache=True)
+ self.run_app_profiler(skip_compile=True)
+ self.run_app_profiler(start_activity=False)
+
+
+ def common_test_report(self):
+ self.run_cmd(["report.py", "-h"])
+ self.run_app_profiler(build_binary_cache=False)
+ self.run_cmd(["report.py"])
+ self.run_cmd(["report.py", "-i", "perf.data"])
+ self.run_cmd(["report.py", "-g"])
+ self.run_cmd(["report.py", "--self-kill-for-testing", "-g", "--gui"])
+
+ def common_test_annotate(self):
+ self.run_cmd(["annotate.py", "-h"])
+ self.run_app_profiler()
+ self.remove("annotated_files")
+ self.run_cmd(["annotate.py", "-s", self.example_path])
+ self.check_exist(dir="annotated_files")
+
+ def common_test_report_sample(self, check_strings):
+ self.run_cmd(["report_sample.py", "-h"])
+ self.remove("binary_cache")
+ self.run_app_profiler(build_binary_cache=False)
+ self.run_cmd(["report_sample.py"])
+ output = self.run_cmd(["report_sample.py", "perf.data"], return_output=True)
+ self.check_strings_in_content(output, check_strings)
+ self.run_app_profiler(record_arg="-g --duration 3 -e cpu-cycles:u, --no-dump-symbols")
+ output = self.run_cmd(["report_sample.py", "--symfs", "binary_cache"], return_output=True)
+ self.check_strings_in_content(output, check_strings)
+
+ def common_test_pprof_proto_generator(self, check_strings_with_lines,
+ check_strings_without_lines):
+ if not has_google_protobuf:
+ log_info('Skip test for pprof_proto_generator because google.protobuf is missing')
+ return
+ self.run_cmd(["pprof_proto_generator.py", "-h"])
+ self.run_app_profiler()
+ self.run_cmd(["pprof_proto_generator.py"])
+ self.remove("pprof.profile")
+ self.run_cmd(["pprof_proto_generator.py", "-i", "perf.data", "-o", "pprof.profile"])
+ self.check_exist(file="pprof.profile")
+ self.run_cmd(["pprof_proto_generator.py", "--show"])
+ output = self.run_cmd(["pprof_proto_generator.py", "--show", "pprof.profile"],
+ return_output=True)
+ self.check_strings_in_content(output, check_strings_with_lines +
+ ["has_line_numbers: True"])
+ self.remove("binary_cache")
+ self.run_cmd(["pprof_proto_generator.py"])
+ output = self.run_cmd(["pprof_proto_generator.py", "--show", "pprof.profile"],
+ return_output=True)
+ self.check_strings_in_content(output, check_strings_without_lines +
+ ["has_line_numbers: False"])
+
+
+class TestExamplePureJava(TestExamplesBase):
+ @classmethod
+ def setUpClass(cls):
+ cls.prepare("SimpleperfExamplePureJava",
+ "com.example.simpleperf.simpleperfexamplepurejava",
+ ".MainActivity")
+
+ def test_app_profiler(self):
+ self.common_test_app_profiler()
+
+ def test_report(self):
+ self.common_test_report()
+ self.run_cmd(["report.py", "-g", "-o", "report.txt"])
+ self.check_strings_in_file("report.txt",
+ ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()",
+ "__start_thread"])
+
+ def test_annotate(self):
+ self.common_test_annotate()
+ self.check_file_under_dir("annotated_files", "MainActivity.java")
+ summary_file = os.path.join("annotated_files", "summary")
+ self.check_annotation_summary(summary_file,
+ [("MainActivity.java", 80, 80),
+ ("run", 80, 0),
+ ("callFunction", 0, 0),
+ ("line 24", 80, 0)])
+
+ def test_report_sample(self):
+ self.common_test_report_sample(
+ ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()",
+ "__start_thread"])
+
+ def test_pprof_proto_generator(self):
+ self.common_test_pprof_proto_generator(
+ check_strings_with_lines=
+ ["com/example/simpleperf/simpleperfexamplepurejava/MainActivity.java",
+ "run"],
+ check_strings_without_lines=
+ ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()"])
+
+
+class TestExamplePureJavaRoot(TestExamplesBase):
+ @classmethod
+ def setUpClass(cls):
+ cls.prepare("SimpleperfExamplePureJava",
+ "com.example.simpleperf.simpleperfexamplepurejava",
+ ".MainActivity",
+ adb_root=True)
+
+ def test_app_profiler(self):
+ self.common_test_app_profiler()
+
+
+class TestExampleWithNative(TestExamplesBase):
+ @classmethod
+ def setUpClass(cls):
+ cls.prepare("SimpleperfExampleWithNative",
+ "com.example.simpleperf.simpleperfexamplewithnative",
+ ".MainActivity")
+
+ def test_app_profiler(self):
+ self.common_test_app_profiler()
+ self.remove("binary_cache")
+ self.run_app_profiler(native_lib_dir=self.example_path)
+
+ def test_report(self):
+ self.common_test_report()
+ self.run_cmd(["report.py", "-g", "-o", "report.txt"])
+ self.check_strings_in_file("report.txt",
+ ["BusyLoopThread",
+ "__start_thread"])
+
+ def test_annotate(self):
+ self.common_test_annotate()
+ self.check_file_under_dir("annotated_files", "native-lib.cpp")
+ summary_file = os.path.join("annotated_files", "summary")
+ self.check_annotation_summary(summary_file,
+ [("native-lib.cpp", 20, 0),
+ ("BusyLoopThread", 20, 0),
+ ("line 46", 20, 0)])
+
+ def test_report_sample(self):
+ self.common_test_report_sample(
+ ["BusyLoopThread",
+ "__start_thread"])
+
+ def test_pprof_proto_generator(self):
+ self.common_test_pprof_proto_generator(
+ check_strings_with_lines=
+ ["native-lib.cpp",
+ "BusyLoopThread"],
+ check_strings_without_lines=
+ ["BusyLoopThread"])
+
+
+class TestExampleWithNativeRoot(TestExamplesBase):
+ @classmethod
+ def setUpClass(cls):
+ cls.prepare("SimpleperfExampleWithNative",
+ "com.example.simpleperf.simpleperfexamplewithnative",
+ ".MainActivity",
+ adb_root=True)
+
+ def test_app_profiler(self):
+ self.common_test_app_profiler()
+ self.remove("binary_cache")
+ self.run_app_profiler(native_lib_dir=self.example_path)
+
+
+class TestExampleWithNativeForceArm(TestExampleWithNative):
+ @classmethod
+ def setUpClass(cls):
+ cls.prepare("SimpleperfExampleWithNative",
+ "com.example.simpleperf.simpleperfexamplewithnative",
+ ".MainActivity",
+ abi="armeabi-v7a")
+
+
+class TestExampleWithNativeForceArmRoot(TestExampleWithNativeRoot):
+ @classmethod
+ def setUpClass(cls):
+ cls.prepare("SimpleperfExampleWithNative",
+ "com.example.simpleperf.simpleperfexamplewithnative",
+ ".MainActivity",
+ abi="armeabi-v7a",
+ adb_root=False)
+
+
+class TestExampleOfKotlin(TestExamplesBase):
+ @classmethod
+ def setUpClass(cls):
+ cls.prepare("SimpleperfExampleOfKotlin",
+ "com.example.simpleperf.simpleperfexampleofkotlin",
+ ".MainActivity")
+
+ def test_app_profiler(self):
+ self.common_test_app_profiler()
+
+ def test_report(self):
+ self.common_test_report()
+ self.run_cmd(["report.py", "-g", "-o", "report.txt"])
+ self.check_strings_in_file("report.txt",
+ ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()",
+ "__start_thread"])
+
+ def test_annotate(self):
+ self.common_test_annotate()
+ self.check_file_under_dir("annotated_files", "MainActivity.kt")
+ summary_file = os.path.join("annotated_files", "summary")
+ self.check_annotation_summary(summary_file,
+ [("MainActivity.kt", 80, 80),
+ ("run", 80, 0),
+ ("callFunction", 0, 0),
+ ("line 19", 80, 0),
+ ("line 25", 0, 0)])
+
+ def test_report_sample(self):
+ self.common_test_report_sample(
+ ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()",
+ "__start_thread"])
+
+ def test_pprof_proto_generator(self):
+ self.common_test_pprof_proto_generator(
+ check_strings_with_lines=
+ ["com/example/simpleperf/simpleperfexampleofkotlin/MainActivity.kt",
+ "run"],
+ check_strings_without_lines=
+ ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()"])
+
+
+class TestExampleOfKotlinRoot(TestExamplesBase):
+ @classmethod
+ def setUpClass(cls):
+ cls.prepare("SimpleperfExampleOfKotlin",
+ "com.example.simpleperf.simpleperfexampleofkotlin",
+ ".MainActivity",
+ adb_root=True)
+
+ def test_app_profiler(self):
+ self.common_test_app_profiler()
+
+
+if __name__ == '__main__':
+ test_program = unittest.main(failfast=True, exit=False)
+ if test_program.result.wasSuccessful():
+ TestExamplesBase.cleanupTestFiles()
\ No newline at end of file
--- /dev/null
+package com.example.simpleperf.simpleperfexampleofkotlin
+
+import android.support.v7.app.AppCompatActivity
+import android.os.Bundle
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ createBusyThread()
+ }
+
+ fun createBusyThread() {
+ object : Thread() {
+ var i = 0
+
+ override fun run() {
+ while (true) {
+ i = callFunction(i)
+ }
+ }
+
+ fun callFunction(i: Int): Int {
+ return i + 1
+ }
+ }.start()
+ }
+}
--- /dev/null
+package com.example.simpleperf.simpleperfexamplepurejava;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ createBusyThread();
+ }
+
+ void createBusyThread() {
+ new Thread(new Runnable() {
+
+ volatile int i = 0;
+
+ @Override
+ public void run() {
+ while (true) {
+ i = callFunction(i);
+ }
+ }
+
+ private int callFunction(int a) {
+ return a+1;
+ }
+ }).start();
+ }
+}
--- /dev/null
+#include <jni.h>
+
+#include <pthread.h>
+#include <stdlib.h>
+
+#include <string>
+
+
+
+extern "C"
+JNIEXPORT jstring JNICALL
+Java_com_example_simpleperf_simpleperfexamplewithnative_MainActivity_stringFromJNI(
+ JNIEnv *env,
+ jobject /* this */) {
+ std::string hello = "Hello from C++";
+ return env->NewStringUTF(hello.c_str());
+}
+
+static void ThrowErrnoException(JNIEnv* env, const char* function_name, int err) {
+ jclass cls = env->FindClass("android/system/ErrnoException");
+ if (cls == nullptr) {
+ return;
+ }
+ jmethodID cid = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;I)V");
+ if (cid == nullptr) {
+ return;
+ }
+ jstring msg = env->NewStringUTF(function_name);
+ if (msg == nullptr) {
+ return;
+ }
+ jthrowable obj = (jthrowable)env->NewObject(cls, cid, msg, err);
+ if (obj == nullptr) {
+ return;
+ }
+ env->Throw(obj);
+}
+
+int CallFunction(int a) {
+ return a + atoi("1");
+}
+
+static void* BusyLoopThread(void*) {
+ volatile int i = 0;
+ while (true) {
+ i = CallFunction(i);
+ }
+ return nullptr;
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_example_simpleperf_simpleperfexamplewithnative_MainActivity_createBusyThreadFromJNI(
+ JNIEnv *env,
+ jobject /* this */) {
+ pthread_t thread;
+ int ret = pthread_create(&thread, nullptr, BusyLoopThread, nullptr);
+ if (ret) {
+ ThrowErrnoException(env, "pthread_create", ret);
+ return;
+ }
+}
--- /dev/null
+package com.example.simpleperf.simpleperfexamplewithnative;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.widget.TextView;
+
+public class MainActivity extends AppCompatActivity {
+
+ // Used to load the 'native-lib' library on application startup.
+ static {
+ System.loadLibrary("native-lib");
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ // Example of a call to a native method
+ TextView tv = (TextView) findViewById(R.id.sample_text);
+ tv.setText(stringFromJNI());
+
+ createBusyThreadFromJNI();
+ }
+
+ /**
+ * A native method that is implemented by the 'native-lib' native library,
+ * which is packaged with this application.
+ */
+ public native String stringFromJNI();
+
+ private native void createBusyThreadFromJNI();
+}
import os.path
import subprocess
import sys
+import time
def get_script_dir():
return os.path.dirname(os.path.realpath(__file__))
class AdbHelper(object):
- def __init__(self):
+ def __init__(self, enable_switch_to_root=True):
adb_path = find_tool_path('adb')
if not adb_path:
log_exit("Can't find adb in PATH environment.")
self.adb_path = adb_path
+ self.enable_switch_to_root = enable_switch_to_root
def run(self, adb_args):
return self.run_and_return_output(adb_args)[0]
- def run_and_return_output(self, adb_args, stdout_file=None):
+ def run_and_return_output(self, adb_args, stdout_file=None, log_output=True):
adb_args = [self.adb_path] + adb_args
log_debug('run adb cmd: %s' % adb_args)
if stdout_file:
result = (returncode == 0)
if stdoutdata and adb_args[1] != 'push' and adb_args[1] != 'pull':
stdoutdata = bytes_to_str(stdoutdata)
- log_debug(stdoutdata)
+ if log_output:
+ log_debug(stdoutdata)
log_debug('run adb cmd: %s [result %s]' % (adb_args, result))
return (result, stdoutdata)
self.check_run_and_return_output(adb_args)
- def check_run_and_return_output(self, adb_args, stdout_file=None):
- result, stdoutdata = self.run_and_return_output(adb_args, stdout_file)
+ def check_run_and_return_output(self, adb_args, stdout_file=None, log_output=True):
+ result, stdoutdata = self.run_and_return_output(adb_args, stdout_file, log_output)
if not result:
log_exit('run "adb %s" failed' % adb_args)
return stdoutdata
+ def _unroot(self):
+ result, stdoutdata = self.run_and_return_output(['shell', 'whoami'])
+ if not result:
+ return
+ if stdoutdata.find('root') == -1:
+ return
+ log_info('unroot adb')
+ self.run(['unroot'])
+ self.run(['wait-for-device'])
+ time.sleep(1)
+
+
def switch_to_root(self):
+ if not self.enable_switch_to_root:
+ self._unroot()
+ return False
result, stdoutdata = self.run_and_return_output(['shell', 'whoami'])
if not result:
return False
if build_type == 'user':
return False
self.run(['root'])
+ time.sleep(1)
+ self.run(['wait-for-device'])
result, stdoutdata = self.run_and_return_output(['shell', 'whoami'])
if result and stdoutdata.find('root') != -1:
return True