#!/usr/bin/env python2.6 # # Copyright (C) 2011 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. # # # Remotely controls an OProfile session on an Android device. # import os import sys import subprocess import getopt import re import shutil # Find oprofile binaries (compiled on the host) try: oprofile_bin_dir = os.environ['OPROFILE_BIN_DIR'] except: try: android_host_out = os.environ['ANDROID_HOST_OUT'] except: print "Either OPROFILE_BIN_DIR or ANDROID_HOST_OUT must be set. Run \". envsetup.sh\" first" sys.exit(1) oprofile_bin_dir = os.path.join(android_host_out, 'bin') opimport_bin = os.path.join(oprofile_bin_dir, 'opimport') opreport_bin = os.path.join(oprofile_bin_dir, 'opreport') opannotate_bin = os.path.join(oprofile_bin_dir, 'opannotate') # Find symbol directories try: android_product_out = os.environ['ANDROID_PRODUCT_OUT'] except: print "ANDROID_PRODUCT_OUT must be set. Run \". envsetup.sh\" first" sys.exit(1) symbols_dir = os.path.join(android_product_out, 'symbols') system_dir = os.path.join(android_product_out, 'system') def execute(command, echo=True): if echo: print ' '.join(command) popen = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output = '' while True: stdout, stderr = popen.communicate() if echo and len(stdout) != 0: print stdout if echo and len(stderr) != 0: print stderr output += stdout output += stderr rc = popen.poll() if rc is not None: break if echo: print 'exit code: %d' % rc return rc, output # ADB wrapper class Adb: def __init__(self, serial_number): self._base_args = ['adb'] if serial_number != None: self._base_args.append('-s') self._base_args.append(serial_number) def shell(self, command_args, echo=True): return self._adb('shell', command_args, echo) def pull(self, source, dest, echo=True): return self._adb('pull', [source, dest], echo) def _adb(self, command, command_args, echo): return execute(self._base_args + [command] + command_args, echo) # The tool program itself class Tool: def __init__(self, argv): self.argv = argv self.verbose = False self.session_dir = '/tmp/oprofile' def usage(self): print "Usage: " + self.argv[0] + " [options] <command> [command args]" print print " Options:" print print " -h, --help : show this help text" print " -s, --serial=number : the serial number of the device being profiled" print " -v, --verbose : show verbose output" print " -d, --dir=path : directory to store oprofile session on the host, default: /tmp/oprofile" print print " Commands:" print print " setup [args] : setup profiler with specified arguments to 'opcontrol --setup'" print " -t, --timer : enable timer based profiling" print " -e, --event=[spec] : specify an event type to profile, eg. --event=CPU_CYCLES:100000" print " (not supported on all devices)" print " -c, --callgraph=[depth] : specify callgraph capture depth, default is none" print " (not supported in timer mode)" print " -k, --kernel-image : specifies the location of a kernel image relative to the symbols directory" print " (and turns on kernel profiling). This need not be the same as the" print " location of the kernel on the actual device." print print " shutdown : shutdown profiler" print print " start : start profiling" print print " stop : stop profiling" print print " status : show profiler status" print print " import : dump samples and pull session directory from the device" print " -f, --force : remove existing session directory before import" print print " report [args] : generate report with specified arguments to 'opreport'" print " -l, --symbols : show symbols" print " -c, --callgraph : show callgraph" print " --help : show help for additional opreport options" print print " annotate [args] : generate annotation with specified arguments to 'annotation'" print " -s, --source : show source" print " -a, --assembly : show assembly" print " --help : show help for additional opannotate options" print def main(self): rc = self.do_main() if rc == 2: print self.usage() return rc def do_main(self): try: opts, args = getopt.getopt(self.argv[1:], 'hs:vd', ['help', 'serial=', 'dir=', 'verbose']) except getopt.GetoptError, e: print str(e) return 2 serial_number = None for o, a in opts: if o in ('-h', '--help'): self.usage() return 0 elif o in ('-s', '--serial'): serial_number = a elif o in ('-d', '--dir'): self.session_dir = a elif o in ('-v', '--verbose'): self.verbose = True if len(args) == 0: print '* A command must be specified.' return 2 command = args[0] command_args = args[1:] self.adb = Adb(serial_number) if command == 'setup': rc = self.do_setup(command_args) elif command == 'shutdown': rc = self.do_shutdown(command_args) elif command == 'start': rc = self.do_start(command_args) elif command == 'stop': rc = self.do_stop(command_args) elif command == 'status': rc = self.do_status(command_args) elif command == 'import': rc = self.do_import(command_args) elif command == 'report': rc = self.do_report(command_args) elif command == 'annotate': rc = self.do_annotate(command_args) else: print '* Unknown command: ' + command return 2 return rc def do_setup(self, command_args): events = [] timer = False kernel = False kernel_image = '' callgraph = None try: opts, args = getopt.getopt(command_args, 'te:c:k:', ['timer', 'event=', 'callgraph=', 'kernel=']) except getopt.GetoptError, e: print '* Unsupported setup command arguments:', str(e) return 2 for o, a in opts: if o in ('-t', '--timer'): timer = True elif o in ('-e', '--event'): events.append('--event=' + a) elif o in ('-c', '--callgraph'): callgraph = a elif o in ('-k', '--kernel'): kernel = True kernel_image = a if len(args) != 0: print '* Unsupported setup command arguments: %s' % (' '.join(args)) return 2 if not timer and len(events) == 0: print '* Must specify --timer or at least one --event argument.' return 2 if timer and len(events) != 0: print '* --timer and --event cannot be used together.' return 2 opcontrol_args = events if timer: opcontrol_args.append('--timer') if callgraph is not None: opcontrol_args.append('--callgraph=' + callgraph) if kernel and len(kernel_image) != 0: opcontrol_args.append('--vmlinux=' + kernel_image) # Get kernal VMA range. rc, output = self.adb.shell(['cat', '/proc/kallsyms'], echo=False) if rc != 0: print '* Failed to determine kernel VMA range.' print output return 1 vma_start = re.search('([0-9a-fA-F]{8}) T _text', output).group(1) vma_end = re.search('([0-9a-fA-F]{8}) A _etext', output).group(1) # Setup the profiler. rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [ '--reset', '--kernel-range=' + vma_start + ',' + vma_end] + opcontrol_args + [ '--setup', '--status', '--verbose-log=all']) if rc != 0: print '* Failed to setup profiler.' return 1 return 0 def do_shutdown(self, command_args): if len(command_args) != 0: print '* Unsupported shutdown command arguments: %s' % (' '.join(command_args)) return 2 rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [ '--shutdown']) if rc != 0: print '* Failed to shutdown.' return 1 return 0 def do_start(self, command_args): if len(command_args) != 0: print '* Unsupported start command arguments: %s' % (' '.join(command_args)) return 2 rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [ '--start', '--status']) if rc != 0: print '* Failed to start profiler.' return 1 return 0 def do_stop(self, command_args): if len(command_args) != 0: print '* Unsupported stop command arguments: %s' % (' '.join(command_args)) return 2 rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [ '--stop', '--status']) if rc != 0: print '* Failed to stop profiler.' return 1 return 0 def do_status(self, command_args): if len(command_args) != 0: print '* Unsupported status command arguments: %s' % (' '.join(command_args)) return 2 rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [ '--status']) if rc != 0: print '* Failed to get profiler status.' return 1 return 0 def do_import(self, command_args): force = False try: opts, args = getopt.getopt(command_args, 'f', ['force']) except getopt.GetoptError, e: print '* Unsupported import command arguments:', str(e) return 2 for o, a in opts: if o in ('-f', '--force'): force = True if len(args) != 0: print '* Unsupported import command arguments: %s' % (' '.join(args)) return 2 # Create session directory. print 'Creating session directory.' if os.path.exists(self.session_dir): if not force: print "* Session directory already exists: %s" % (self.session_dir) print "* Use --force to remove and recreate the session directory." return 1 try: shutil.rmtree(self.session_dir) except e: print "* Failed to remove existing session directory: %s" % (self.session_dir) print e return 1 try: os.makedirs(self.session_dir) except e: print "* Failed to create session directory: %s" % (self.session_dir) print e return 1 raw_samples_dir = os.path.join(self.session_dir, 'raw_samples') samples_dir = os.path.join(self.session_dir, 'samples') abi_file = os.path.join(self.session_dir, 'abi') # Dump samples. print 'Dumping samples.' rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [ '--dump', '--status']) if rc != 0: print '* Failed to dump samples.' print output return 1 # Pull samples. print 'Pulling samples from device.' rc, output = self.adb.pull('/data/oprofile/samples/', raw_samples_dir + '/', echo=False) if rc != 0: print '* Failed to pull samples from the device.' print output return 1 # Pull ABI. print 'Pulling ABI information from device.' rc, output = self.adb.pull('/data/oprofile/abi', abi_file, echo=False) if rc != 0: print '* Failed to pull abi information from the device.' print output return 1 # Invoke opimport on each sample file to convert it from the device ABI (ARM) # to the host ABI (x86). print 'Importing samples.' for dirpath, dirnames, filenames in os.walk(raw_samples_dir): for filename in filenames: if not re.match('^.*\.log$', filename): in_path = os.path.join(dirpath, filename) out_path = os.path.join(samples_dir, os.path.relpath(in_path, raw_samples_dir)) out_dir = os.path.dirname(out_path) try: os.makedirs(out_dir) except e: print "* Failed to create sample directory: %s" % (out_dir) print e return 1 rc, output = execute([opimport_bin, '-a', abi_file, '-o', out_path, in_path], echo=False) if rc != 0: print '* Failed to import samples.' print output return 1 # Generate a short summary report. rc, output = self._execute_opreport([]) if rc != 0: print '* Failed to generate summary report.' return 1 return 0 def do_report(self, command_args): rc, output = self._execute_opreport(command_args) if rc != 0: print '* Failed to generate report.' return 1 return 0 def do_annotate(self, command_args): rc, output = self._execute_opannotate(command_args) if rc != 0: print '* Failed to generate annotation.' return 1 return 0 def _opcontrol_verbose_arg(self): if self.verbose: return ['--verbose'] else: return [] def _execute_opreport(self, args): return execute([opreport_bin, '--session-dir=' + self.session_dir, '--image-path=' + symbols_dir + ',' + system_dir] + args) def _execute_opannotate(self, command_args): try: opts, args = getopt.getopt(command_args, 'sap:', ['source', 'assembly', 'help', 'image-path=']) except getopt.GetoptError, e: print '* Unsupported opannotate command arguments:', str(e) return 2 # Start with the default symbols directory symbols_dirs = symbols_dir anno_flag = [] for o, a in opts: if o in ('-s', '--source'): anno_flag.append('-s') if o in ('-a', '--assembly'): anno_flag.append('-a') anno_flag.append('--objdump-params=-Cd') if o in ('--help'): anno_flag.append('--help') if o in ('p', '--image-path'): symbols_dirs = a + ',' + symbols_dir return execute([opannotate_bin, '--session-dir=' + self.session_dir, '--image-path=' + symbols_dirs + ',' + system_dir] + anno_flag + args) # Main entry point tool = Tool(sys.argv) rc = tool.main() sys.exit(rc)