OSDN Git Service

simpleperf: use app_profiler.py in inferno.
authorYabin Cui <yabinc@google.com>
Tue, 8 Aug 2017 00:53:33 +0000 (17:53 -0700)
committerYabin Cui <yabinc@google.com>
Tue, 8 Aug 2017 01:03:43 +0000 (18:03 -0700)
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

simpleperf/scripts/inferno/adb.py [deleted file]
simpleperf/scripts/inferno/adb_non_root.py [deleted file]
simpleperf/scripts/inferno/adb_root.py [deleted file]
simpleperf/scripts/inferno/inferno.py
simpleperf/scripts/test.py

diff --git a/simpleperf/scripts/inferno/adb.py b/simpleperf/scripts/inferno/adb.py
deleted file mode 100644 (file)
index be0a41e..0000000
+++ /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 (file)
index f187c28..0000000
+++ /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 (file)
index 4958643..0000000
+++ /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
index a6d950c..7510cb2 100644 (file)
 
 """
 
-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("<html>")
     f.write("<body style='font-family: Monospace;' onload='init()'>")
-    f.write('<style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;} </style>')
+    f.write("""<style type="text/css"> .s { stroke:black; stroke-width:0.5; cursor:pointer;}
+            </style>""")
     f.write('<style type="text/css"> .t:hover { cursor:pointer; } </style>')
     f.write('<img height="180" alt = "Embedded Image" src ="data')
     f.write(get_local_asset_content("inferno.b64"))
     f.write('"/>')
-    f.write("<div style='display:inline-block;'> \
-    <font size='8'>\
-    Inferno Flamegraph Report</font><br/><br/> \
-    Process : %s (%d)<br/>\
-    Date&nbsp;&nbsp;&nbsp;&nbsp;: %s<br/>\
-    Threads : %d <br/>\
-    Samples : %d</br>\
-    Duration: %s seconds<br/>\
-    Machine : %s (%s) by %s<br/>\
-    Capture : %s<br/><br/></div>"
-            % (
+    f.write("""<div style='display:inline-block;'>
+                  <font size='8'>
+                  Inferno Flamegraph Report</font><br/><br/>
+                  Process : %s (%d)<br/>
+                  Date&nbsp;&nbsp;&nbsp;&nbsp;: %s<br/>
+                  Threads : %d <br/>
+                  Samples : %d</br>
+                  Duration: %s seconds<br/>""" % (
                 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("<br/><br/><div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div>")
+                process.args.capture_duration))
+    if 'ro.product.model' in process.props:
+        f.write("Machine : %s (%s) by %s<br/>" % (process.props["ro.product.model"],
+                process.props["ro.product.name"], process.props["ro.product.manufacturer"]))
+    if process.cmd:
+        f.write("Capture : %s<br/><br/>" % process.cmd)
+    f.write("</div>")
+    f.write("""<br/><br/>
+            <div>Navigate with WASD, zoom in with SPACE, zoom out with BACKSPACE.</div>""")
     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("<br/><br/><b>Main Thread %d (%d samples):</b><br/>\n\n\n\n" % (thread.tid, thread.num_samples))
+        f.write("<br/><br/><b>Main Thread %d (%d samples):</b><br/>\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("<br/><br/><b>Thread %d (%d samples):</b><br/>\n\n\n\n" % (thread.tid, thread.num_samples))
+        f.write("<br/><br/><b>Thread %d (%d samples):</b><br/>\n\n\n\n" % (
+                thread.tid, thread.num_samples))
         renderSVG(thread.flamegraph, f, process.args.color, process.args.svg_width)
 
     f.write("</body>")
@@ -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
index a4f2ebc..1a97ab1 100644 (file)
@@ -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()