From: Yabin Cui Date: Tue, 8 Aug 2017 00:53:33 +0000 (-0700) Subject: simpleperf: use app_profiler.py in inferno. X-Git-Tag: android-x86-9.0-r1~59^2~129^2~22^2~15^2^2 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=69bce02a4c7ae10974a2140c864739d660e9ecd3;p=android-x86%2Fsystem-extras.git simpleperf: use app_profiler.py in inferno. Also add tests to check if we can run inferno successfully. More tests will be added to check report results of inferno. Bug: http://b/64035530 Test: run test.py. Change-Id: Ie5dd1f5cb47c8c7a2ae68f8614cf81b68e3c7dbf --- diff --git a/simpleperf/scripts/inferno/adb.py b/simpleperf/scripts/inferno/adb.py deleted file mode 100644 index be0a41eb..00000000 --- a/simpleperf/scripts/inferno/adb.py +++ /dev/null @@ -1,88 +0,0 @@ -import subprocess -import abc -import os - -BIN_PATH = "bin/android/%s/simpleperf" - -class Abi: - ARM = 1 - ARM_64 = 2 - X86 = 3 - X86_64 = 4 - - def __init__(self): - pass - -class Adb: - - def __init__(self): - pass - - - def delete_previous_data(self): - err = subprocess.call(["adb", "shell", "rm", "-f", "/data/local/tmp/perf.data"]) - - - def get_process_pid(self, process_name): - piof_output = subprocess.check_output(["adb", "shell", "pidof", process_name]) - try: - process_id = int(piof_output) - except ValueError: - process_id = 0 - return process_id - - - def pull_data(self): - err = subprocess.call(["adb", "pull", "/data/local/tmp/perf.data", "."]) - return err - - - @abc.abstractmethod - def collect_data(self, simpleperf_command): - raise NotImplementedError("%s.collect_data(str) is not implemented!" % self.__class__.__name__) - - - def get_props(self): - props = {} - output = subprocess.check_output(["adb", "shell", "getprop"]) - lines = output.split("\n") - for line in lines: - tokens = line.split(": ") - if len(tokens) < 2: - continue - key = tokens[0].replace("[", "").replace("]", "") - value = tokens[1].replace("[", "").replace("]", "") - props[key] = value - return props - - def parse_abi(self, str): - if str.find("arm64") != -1: - return Abi.ARM_64 - if str.find("arm") != -1: - return Abi.ARM - if str.find("x86_64") != -1: - return Abi.X86_64 - if str.find("x86") != -1: - return Abi.X86 - return Abi.ARM_64 - - def get_exec_path(self, abi): - folder_name = "arm64" - if abi == Abi.ARM: - folder_name = "arm" - if abi == Abi.X86: - folder_name = "x86" - if abi == Abi.X86_64: - folder_name = "x86_64" - return os.path.join(os.path.dirname(__file__), BIN_PATH % folder_name) - - def push_simpleperf_binary(self): - # Detect the ABI of the device - props = self.get_props() - abi_raw = props["ro.product.cpu.abi"] - abi = self.parse_abi(abi_raw) - exec_path = self.get_exec_path(abi) - - # Push simpleperf to the device - print "Pushing local '%s' to device." % exec_path - subprocess.call(["adb", "push", exec_path, "/data/local/tmp/simpleperf"]) diff --git a/simpleperf/scripts/inferno/adb_non_root.py b/simpleperf/scripts/inferno/adb_non_root.py deleted file mode 100644 index f187c287..00000000 --- a/simpleperf/scripts/inferno/adb_non_root.py +++ /dev/null @@ -1,33 +0,0 @@ -from adb import Adb -import subprocess -import time - -class AdbNonRoot(Adb): - # If adb cannot run as root, there is still a way to collect data but it is much more complicated. - # 1. Identify the platform abi, use getprop: ro.product.cpu.abi - # 2. Push the precompiled scripts/bin/android/[ABI]/simpleperf to device /data/local/tmp/simpleperf - # 4. Use run-as to copy /data/local/tmp/simplerperf -> /apps/installation_path/simpleperf - # 5. Use run-as to run: /apps/installation_path/simpleperf -p APP_PID -o /apps/installation_path/perf.data - # 6. Use run-as fork+pipe trick to copy /apps/installation_path/perf.data to /data/local/tmp/perf.data - def collect_data(self, process): - - if not process.args.skip_push_binary: - self.push_simpleperf_binary() - - # Copy simpleperf to the data - subprocess.check_output(["adb", "shell", "run-as %s" % process.name, "cp", "/data/local/tmp/simpleperf", "."]) - - # Patch command to run with path to data folder where simpleperf was written. - process.cmd = process.cmd.replace("/data/local/tmp/perf.data", "./perf.data") - - # Start collecting samples. - process.cmd = ("run-as %s " % process.name) + process.cmd - subprocess.call(["adb", "shell", process.cmd]) - - # Wait sampling_duration+1.5 seconds. - time.sleep(int(process.args.capture_duration) + 1) - - # Move data to a location where shell user can read it. - subprocess.call(["adb", "shell", "run-as %s cat perf.data | tee /data/local/tmp/perf.data >/dev/null" % (process.name)]) - - return True diff --git a/simpleperf/scripts/inferno/adb_root.py b/simpleperf/scripts/inferno/adb_root.py deleted file mode 100644 index 4958643b..00000000 --- a/simpleperf/scripts/inferno/adb_root.py +++ /dev/null @@ -1,9 +0,0 @@ -from adb import Adb -import subprocess - -class AdbRoot(Adb): - def collect_data(self, process): - if not process.args.skip_push_binary: - self.push_simpleperf_binary() - subprocess.call(["adb", "shell", "cd /data/local/tmp; " + process.cmd]) - return True \ No newline at end of file diff --git a/simpleperf/scripts/inferno/inferno.py b/simpleperf/scripts/inferno/inferno.py index a6d950cc..7510cb25 100644 --- a/simpleperf/scripts/inferno/inferno.py +++ b/simpleperf/scripts/inferno/inferno.py @@ -29,89 +29,57 @@ """ -from simpleperf_report_lib import ReportLib import argparse -from data_types import * -from svg_renderer import * import datetime import os import subprocess +import sys import webbrowser -from adb_non_root import AdbNonRoot -from adb_root import AdbRoot -def create_process(adb_client, args): - """ Retrieves target process pid and create a process contained. - - :param args: Argument as parsed by argparse - :return: Process objectk - """ - process_id = adb_client.get_process_pid(args.process_name) - process = Process(args.process_name, process_id) - return process +try: + from simpleperf_report_lib import ReportLib + from utils import log_exit, log_info, AdbHelper +except: + print("Please go to the parent directory, and run inferno.sh or inferno.bat.") + sys.exit(1) +from data_types import * +from svg_renderer import * -def collect_data(adb_client, process): - """ Start simpleperf on device and collect data. Pull perf.data into cwd. - - :param process: Process object - :return: Populated Process object - """ - - if process.args.dwarf_unwinding: - unwinding_parameter = "-g" - print "Unwinding with dwarf." +def collect_data(args, process): + app_profiler_args = [sys.executable, "app_profiler.py", "-nb"] + if args.app: + app_profiler_args += ["-p", args.app] + elif args.native_program: + app_profiler_args += ["-np", args.native_program] else: - unwinding_parameter = "--call-graph fp" - print "Unwinding with frame pointers." - - # Check whether sampling will be frequency based or event based. - sampling_parameter = "-f %s" % process.args.sample_frequency - if process.args.events: - tokens = process.args.events.split(" ") + log_exit("Please set profiling target with -p or -np option.") + if args.skip_recompile: + app_profiler_args.append("-nc") + if args.disable_adb_root: + app_profiler_args.append("--disable_adb_root") + record_arg_str = "" + if args.dwarf_unwinding: + record_arg_str += "-g " + else: + record_arg_str += "--call-graph fp " + if args.events: + tokens = args.events.split() if len(tokens) == 2: num_events = tokens[0] event_name = tokens[1] - sampling_parameter = "-c %s -e '%s'" % (num_events, event_name) + record_arg_str += "-c %s -e %s " % (num_events, event_name) else: - print "Event format string not recognized. Expected \"requency event_name\"." - print "Got : [" + ",".join(tokens) + "]" - return False - print "Using event sampling (%s)." % sampling_parameter + log_exit("Event format string of -e option cann't be recognized.") + log_info("Using event sampling (-c %s -e %s)." % (num_events, event_name)) else: - print "Using frequency sampling (%s)." % sampling_parameter - - process.cmd = "./simpleperf record \ - -o /data/local/tmp/perf.data \ - %s \ - -p %s \ - --duration %s \ - %s" % ( - unwinding_parameter, - process.pid, - process.args.capture_duration, - sampling_parameter) - - print("Process '%s' PID = %d" % (process.name, process.pid)) - - if process.args.skip_collection: - print("Skipping data collection, expecting perf.data in folder") - return True - - print("Sampling for %s seconds..." % process.args.capture_duration) - - - adb_client.delete_previous_data() - - success = adb_client.collect_data(process) - if not success: - return False - - err = adb_client.pull_data() - if err: - return False - - return True + record_arg_str += "-f %d " % args.sample_frequency + log_info("Using frequency sampling (-f %d)." % args.sample_frequency) + record_arg_str += "--duration %d " % args.capture_duration + process.cmd = " ".join(app_profiler_args) + ' -r "%s"' % record_arg_str + app_profiler_args += ["-r", record_arg_str] + returncode = subprocess.call(app_profiler_args) + return returncode == 0 def parse_samples(process, args): @@ -141,7 +109,7 @@ def parse_samples(process, args): process.get_thread(sample.tid).add_callchain(callchain, symbol, sample) process.num_samples += 1 - print("Parsed %s callchains." % process.num_samples) + log_info("Parsed %s callchains." % process.num_samples) def collapse_callgraphs(process): @@ -176,42 +144,46 @@ def output_report(process): filepath = os.path.realpath(f.name) f.write("") f.write("") - f.write('') + f.write("""""") f.write('') f.write('Embedded Image') - f.write("
\ - \ - Inferno Flamegraph Report

\ - Process : %s (%d)
\ - Date    : %s
\ - Threads : %d
\ - Samples : %d
\ - Duration: %s seconds
\ - Machine : %s (%s) by %s
\ - Capture : %s

" - % ( + f.write("""
+ + Inferno Flamegraph Report

+ Process : %s (%d)
+ Date    : %s
+ Threads : %d
+ Samples : %d
+ Duration: %s seconds
""" % ( process.name,process.pid, datetime.datetime.now().strftime("%Y-%m-%d (%A) %H:%M:%S"), len(process.threads), process.num_samples, - process.args.capture_duration, - process.props["ro.product.model"], process.props["ro.product.name"], - process.props["ro.product.manufacturer"], - process.cmd)) - f.write("

Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.
") + process.args.capture_duration)) + if 'ro.product.model' in process.props: + f.write("Machine : %s (%s) by %s
" % (process.props["ro.product.model"], + process.props["ro.product.name"], process.props["ro.product.manufacturer"])) + if process.cmd: + f.write("Capture : %s

" % process.cmd) + f.write("
") + f.write("""

+
Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.
""") f.write(get_local_asset_content("script.js")) # Output tid == pid Thread first. main_thread = [x for _, x in process.threads.items() if x.tid == process.pid] for thread in main_thread: - f.write("

Main Thread %d (%d samples):
\n\n\n\n" % (thread.tid, thread.num_samples)) + f.write("

Main Thread %d (%d samples):
\n\n\n\n" % ( + thread.tid, thread.num_samples)) renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width) other_threads = [x for _, x in process.threads.items() if x.tid != process.pid] for thread in other_threads: - f.write("

Thread %d (%d samples):
\n\n\n\n" % (thread.tid, thread.num_samples)) + f.write("

Thread %d (%d samples):
\n\n\n\n" % ( + thread.tid, thread.num_samples)) renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width) f.write("") @@ -232,16 +204,13 @@ def generate_threads_offsets(process): generate_flamegraph_offsets(thread.flamegraph) -def collect_machine_info(adb_client, process): - process.props = adb_client.get_props() - +def collect_machine_info(process): + adb = AdbHelper() + process.props = {} + process.props['ro.product.model'] = adb.get_property('ro.product.model') + process.props['ro.product.name'] = adb.get_property('ro.product.name') + process.props['ro.product.manufacturer'] = adb.get_property('ro.product.manufacturer') -def setup_adb(): - err = subprocess.call(["adb", "root"]) - if err == 0: - return AdbRoot() - else: - return AdbNonRoot() def open_report_in_browser(report_path): # Try to open the report with Chrome @@ -257,52 +226,61 @@ def open_report_in_browser(report_path): def main(): parser = argparse.ArgumentParser(description='Report samples in perf.data.') - parser.add_argument('--symfs', help='Set the path to find binaries with symbols and debug info.') + parser.add_argument('--symfs', help="""Set the path to find binaries with symbols and debug + info.""") parser.add_argument('--kallsyms', help='Set the path to find kernel symbols.') parser.add_argument('--record_file', default='perf.data', help='Default is perf.data.') - parser.add_argument('-t', '--capture_duration', default=10, help='Capture duration in seconds.') - parser.add_argument('-p', '--process_name', default='surfaceflinger', help='Default is surfaceflinger.') + parser.add_argument('-t', '--capture_duration', type=int, default=10, + help="""Capture duration in seconds.""") + parser.add_argument('-p', '--app', help="""Profile an Android app, given the package name. + Like -p com.example.android.myapp.""") + parser.add_argument('-np', '--native_program', default="surfaceflinger", + help="""Profile a native program. The program should be running on the + device. Like -np surfaceflinger.""") parser.add_argument('-c', '--color', default='hot', choices=['hot', 'dso', 'legacy'], - help='Color theme: hot=percentage of samples, dso=callsite DSO name, legacy=brendan style') - parser.add_argument('-sc','--skip_collection', default=False, help='Skip data collection', action="store_true") - parser.add_argument('-f', '--sample_frequency', default=6000, help='Sample frequency') + help="""Color theme: hot=percentage of samples, dso=callsite DSO name, + legacy=brendan style""") + parser.add_argument('-sc','--skip_collection', default=False, help='Skip data collection', + action="store_true") + parser.add_argument('-nc', '--skip_recompile', action='store_true', help="""When profiling + an Android app, by default we recompile java bytecode to native + instructions to profile java code. It takes some time. You can skip it + if the code has been compiled or you don't need to profile java code.""") + parser.add_argument('-f', '--sample_frequency', type=int, default=6000, help='Sample frequency') parser.add_argument('-w', '--svg_width', type=int, default=1124) - parser.add_argument('-sb', '--skip_push_binary', help='Skip pushing simpleperf before profiling', - default=False, action="store_true") parser.add_argument('-du', '--dwarf_unwinding', help='Perform unwinding using dwarf instead of fp.', default=False, action='store_true') - parser.add_argument('-e', '--events', - help='Sample based on event occurences instead of frequency. ' - 'Format expected is "event_counts event_name". e.g: "10000 cpu-cyles". A few examples of \ - nmames: cpu-cycles, cache-references, cache-misses, branch-instructions, branch-misses', + parser.add_argument('-e', '--events', help="""Sample based on event occurences instead of + frequency. Format expected is "event_counts event_name". + e.g: "10000 cpu-cyles". A few examples of event_name: cpu-cycles, + cache-references, cache-misses, branch-instructions, branch-misses""", default="") + parser.add_argument('--disable_adb_root', action='store_true', help="""Force adb to run in non + root mode.""") args = parser.parse_args() - - # Since we may attempt to sample privileged process, let's try to be root. - adb_client = setup_adb() - - # Create a process object - process = create_process(adb_client, args) - if process.pid == 0: - print("Unable to retrive pid for process '%s'. Terminating." % process.name) - return + process_name = args.app or args.native_program + process = Process(process_name, 0) process.args = args + if not args.skip_collection: + log_info("Starting data collection stage for process '%s'." % process_name) + if not collect_data(args, process): + log_exit("Unable to collect data.") + try: + result, output = AdbHelper().run_and_return_output(['shell', 'pidof', process_name]) + if result: + process.pid = int(output) + except: + raise + collect_machine_info(process) - print("Starting data collection stage for process '%s'." % args.process_name) - success = collect_data(adb_client, process) - if not success: - print "Unable to collect data" - return - - collect_machine_info(adb_client, process) parse_samples(process, args) collapse_callgraphs(process) generate_threads_offsets(process) report_path = output_report(process) open_report_in_browser(report_path) - print "Report generated at '%s'." % report_path + log_info("Report generated at '%s'." % report_path) if __name__ == "__main__": main() \ No newline at end of file diff --git a/simpleperf/scripts/test.py b/simpleperf/scripts/test.py index a4f2ebcc..1a97ab1b 100644 --- a/simpleperf/scripts/test.py +++ b/simpleperf/scripts/test.py @@ -51,6 +51,8 @@ try: except: has_google_protobuf = False +inferno_script = "inferno.bat" if is_windows() else "./inferno.sh" + support_trace_offcpu = None def is_trace_offcpu_supported(): @@ -121,12 +123,13 @@ class TestExampleBase(unittest.TestCase): def cleanupTestFiles(cls): remove("binary_cache") remove("annotated_files") - remove("perf.data") + #remove("perf.data") remove("report.txt") remove("pprof.profile") def run_cmd(self, args, return_output=False): - args = [sys.executable] + args + if args[0].endswith('.py'): + args = [sys.executable] + args try: if not return_output: returncode = subprocess.call(args) @@ -274,6 +277,18 @@ class TestExampleBase(unittest.TestCase): self.check_strings_in_content(output, check_strings_without_lines + ["has_line_numbers: False"]) + def common_test_inferno(self): + self.run_cmd([inferno_script, "-h"]) + remove("perf.data") + append_args = [] if self.adb_root else ["--disable_adb_root"] + self.run_cmd([inferno_script, "-p", self.package_name, "-t", "3"] + append_args) + self.check_exist(file="perf.data") + self.run_cmd([inferno_script, "-p", self.package_name, "-f", "1000", "-du", "-t", "1", + "-nc"] + append_args) + self.run_cmd([inferno_script, "-p", self.package_name, "-e", "100000 cpu-cycles", + "-t", "1", "-nc"] + append_args) + self.run_cmd([inferno_script, "-sc"]) + class TestExamplePureJava(TestExampleBase): @classmethod @@ -328,6 +343,9 @@ class TestExamplePureJava(TestExampleBase): check_strings_without_lines= ["com.example.simpleperf.simpleperfexamplepurejava.MainActivity$1.run()"]) + def test_inferno(self): + self.common_test_inferno() + class TestExamplePureJavaRoot(TestExampleBase): @classmethod @@ -368,6 +386,7 @@ class TestExamplePureJavaTraceOffCpu(TestExampleBase): ("SleepFunction", 20, 0), ("line 24", 20, 0), ("line 32", 20, 0)]) + self.run_cmd([inferno_script, "-sc"]) class TestExampleWithNative(TestExampleBase): @@ -411,6 +430,9 @@ class TestExampleWithNative(TestExampleBase): check_strings_without_lines= ["BusyLoopThread"]) + def test_inferno(self): + self.common_test_inferno() + class TestExampleWithNativeRoot(TestExampleBase): @classmethod @@ -452,6 +474,7 @@ class TestExampleWithNativeTraceOffCpu(TestExampleBase): ("SleepFunction", 20, 0), ("line 73", 20, 0), ("line 83", 20, 0)]) + self.run_cmd([inferno_script, "-sc"]) class TestExampleWithNativeJniCall(TestExampleBase): @@ -480,6 +503,7 @@ class TestExampleWithNativeJniCall(TestExampleBase): ("line 26", 20, 0), ("native-lib.cpp", 10, 0), ("line 40", 10, 0)]) + self.run_cmd([inferno_script, "-sc"]) class TestExampleWithNativeForceArm(TestExampleWithNative): @@ -551,6 +575,9 @@ class TestExampleOfKotlin(TestExampleBase): check_strings_without_lines= ["com.example.simpleperf.simpleperfexampleofkotlin.MainActivity$createBusyThread$1.run()"]) + def test_inferno(self): + self.common_test_inferno() + class TestExampleOfKotlinRoot(TestExampleBase): @classmethod @@ -591,6 +618,7 @@ class TestExampleOfKotlinTraceOffCpu(TestExampleBase): ("SleepFunction", 20, 0), ("line 24", 20, 0), ("line 32", 20, 0)]) + self.run_cmd([inferno_script, "-sc"]) class TestProfilingNativeProgram(TestExampleBase): @@ -617,6 +645,16 @@ class TestProfilingCmd(TestExampleBase): self.run_cmd(["report.py", "-g", "-o", "report.txt"]) +class TestProfilingNativeProgram(TestExampleBase): + def test_smoke(self): + adb = AdbHelper() + if adb.switch_to_root(): + self.run_cmd(["app_profiler.py", "-np", "surfaceflinger"]) + self.run_cmd(["report.py", "-g", "-o", "report.txt"]) + self.run_cmd([inferno_script, "-sc"]) + self.run_cmd([inferno_script, "-np", "surfaceflinger"]) + + class TestReportLib(unittest.TestCase): def setUp(self): self.report_lib = ReportLib()