1#!/usr/bin/env python2.6
2#
3# Copyright (C) 2011 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18#
19# Remotely controls an OProfile session on an Android device.
20#
21
22import os
23import sys
24import subprocess
25import getopt
26import re
27import shutil
28
29
30# Find oprofile binaries (compiled on the host)
31try:
32  oprofile_bin_dir = os.environ['OPROFILE_BIN_DIR']
33except:
34  try:
35    android_host_out = os.environ['ANDROID_HOST_OUT']
36  except:
37    print "Either OPROFILE_BIN_DIR or ANDROID_HOST_OUT must be set. Run \". envsetup.sh\" first"
38    sys.exit(1)
39  oprofile_bin_dir = os.path.join(android_host_out, 'bin')
40
41opimport_bin = os.path.join(oprofile_bin_dir, 'opimport')
42opreport_bin = os.path.join(oprofile_bin_dir, 'opreport')
43opannotate_bin = os.path.join(oprofile_bin_dir, 'opannotate')
44
45
46# Find symbol directories
47try:
48  android_product_out = os.environ['ANDROID_PRODUCT_OUT']
49except:
50  print "ANDROID_PRODUCT_OUT must be set. Run \". envsetup.sh\" first"
51  sys.exit(1)
52
53symbols_dir = os.path.join(android_product_out, 'symbols')
54system_dir = os.path.join(android_product_out, 'system')
55
56
57def execute(command, echo=True):
58  if echo:
59    print ' '.join(command)
60  popen = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
61  output = ''
62  while True:
63    stdout, stderr = popen.communicate()
64    if echo and len(stdout) != 0:
65      print stdout
66    if echo and len(stderr) != 0:
67      print stderr
68    output += stdout
69    output += stderr
70    rc = popen.poll()
71    if rc is not None:
72      break
73  if echo:
74    print 'exit code: %d' % rc
75  return rc, output
76
77# ADB wrapper
78class Adb:
79  def __init__(self, serial_number):
80    self._base_args = ['adb']
81    if serial_number != None:
82      self._base_args.append('-s')
83      self._base_args.append(serial_number)
84
85  def shell(self, command_args, echo=True):
86    return self._adb('shell', command_args, echo)
87
88  def pull(self, source, dest, echo=True):
89    return self._adb('pull', [source, dest], echo)
90
91  def _adb(self, command, command_args, echo):
92    return execute(self._base_args + [command] + command_args, echo)
93
94
95# The tool program itself
96class Tool:
97  def __init__(self, argv):
98    self.argv = argv
99    self.verbose = False
100    self.session_dir = '/tmp/oprofile'
101
102  def usage(self):
103    print "Usage: " + self.argv[0] + " [options] <command> [command args]"
104    print
105    print "  Options:"
106    print
107    print "    -h, --help            : show this help text"
108    print "    -s, --serial=number   : the serial number of the device being profiled"
109    print "    -v, --verbose         : show verbose output"
110    print "    -d, --dir=path        : directory to store oprofile session on the host, default: /tmp/oprofile"
111    print
112    print "  Commands:"
113    print
114    print "    setup [args]          : setup profiler with specified arguments to 'opcontrol --setup'"
115    print "      -t, --timer             : enable timer based profiling"
116    print "      -e, --event=[spec]      : specify an event type to profile, eg. --event=CPU_CYCLES:100000"
117    print "                                (not supported on all devices)"
118    print "      -c, --callgraph=[depth] : specify callgraph capture depth, default is none"
119    print "                                (not supported in timer mode)"
120    print "      -k, --kernel-image      : specifies the location of a kernel image relative to the symbols directory"
121    print "                                (and turns on kernel profiling). This need not be the same as the"
122    print "                                location of the kernel on the actual device."
123    print
124    print "    shutdown              : shutdown profiler"
125    print
126    print "    start                 : start profiling"
127    print
128    print "    stop                  : stop profiling"
129    print
130    print "    status                : show profiler status"
131    print
132    print "    import                : dump samples and pull session directory from the device"
133    print "      -f, --force             : remove existing session directory before import" 
134    print
135    print "    report [args]         : generate report with specified arguments to 'opreport'"
136    print "      -l, --symbols           : show symbols"
137    print "      -c, --callgraph         : show callgraph"
138    print "      --help                  : show help for additional opreport options"
139    print
140    print "    annotate [args]       : generate annotation with specified arguments to 'annotation'"
141    print "      -s, --source            : show source"
142    print "      -a, --assembly          : show assembly"
143    print "      --help                  : show help for additional opannotate options"
144    print
145
146  def main(self):
147    rc = self.do_main()
148    if rc == 2:
149      print
150      self.usage()
151    return rc
152
153  def do_main(self):
154    try:
155      opts, args = getopt.getopt(self.argv[1:],
156        'hs:vd', ['help', 'serial=', 'dir=', 'verbose'])
157    except getopt.GetoptError, e:
158      print str(e)
159      return 2
160
161    serial_number = None
162    for o, a in opts:
163      if o in ('-h', '--help'):
164        self.usage()
165        return 0
166      elif o in ('-s', '--serial'):
167        serial_number = a
168      elif o in ('-d', '--dir'):
169        self.session_dir = a
170      elif o in ('-v', '--verbose'):
171        self.verbose = True
172
173    if len(args) == 0:
174      print '* A command must be specified.'
175      return 2
176
177    command = args[0]
178    command_args = args[1:]
179
180    self.adb = Adb(serial_number)
181
182    if command == 'setup':
183      rc = self.do_setup(command_args)
184    elif command == 'shutdown':
185      rc = self.do_shutdown(command_args)
186    elif command == 'start':
187      rc = self.do_start(command_args)
188    elif command == 'stop':
189      rc = self.do_stop(command_args)
190    elif command == 'status':
191      rc = self.do_status(command_args)
192    elif command == 'import':
193      rc = self.do_import(command_args)
194    elif command == 'report':
195      rc = self.do_report(command_args)
196    elif command == 'annotate':
197      rc = self.do_annotate(command_args)
198    else:
199      print '* Unknown command: ' + command
200      return 2
201
202    return rc
203
204  def do_setup(self, command_args):
205    events = []
206    timer = False
207    kernel = False
208    kernel_image = ''
209    callgraph = None
210
211    try:
212      opts, args = getopt.getopt(command_args,
213        'te:c:k:', ['timer', 'event=', 'callgraph=', 'kernel='])
214    except getopt.GetoptError, e:
215      print '* Unsupported setup command arguments:', str(e)
216      return 2
217
218    for o, a in opts:
219      if o in ('-t', '--timer'):
220        timer = True
221      elif o in ('-e', '--event'):
222        events.append('--event=' + a)
223      elif o in ('-c', '--callgraph'):
224        callgraph = a
225      elif o in ('-k', '--kernel'):
226        kernel = True
227        kernel_image = a
228
229    if len(args) != 0:
230      print '* Unsupported setup command arguments: %s' % (' '.join(args))
231      return 2
232
233    if not timer and len(events) == 0:
234      print '* Must specify --timer or at least one --event argument.'
235      return 2
236
237    if timer and len(events) != 0:
238      print '* --timer and --event cannot be used together.'
239      return 2
240
241    opcontrol_args = events
242    if timer:
243      opcontrol_args.append('--timer')
244    if callgraph is not None:
245      opcontrol_args.append('--callgraph=' + callgraph)
246    if kernel and len(kernel_image) != 0:
247      opcontrol_args.append('--vmlinux=' + kernel_image)
248
249    # Get kernal VMA range.
250    rc, output = self.adb.shell(['cat', '/proc/kallsyms'], echo=False)
251    if rc != 0:
252      print '* Failed to determine kernel VMA range.'
253      print output
254      return 1
255    vma_start = re.search('([0-9a-fA-F]{8}) T _text', output).group(1)
256    vma_end = re.search('([0-9a-fA-F]{8}) A _etext', output).group(1)
257
258    # Setup the profiler.
259    rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [
260      '--reset',
261      '--kernel-range=' + vma_start + ',' + vma_end] + opcontrol_args + [
262      '--setup',
263      '--status', '--verbose-log=all'])
264    if rc != 0:
265      print '* Failed to setup profiler.'
266      return 1
267    return 0
268
269  def do_shutdown(self, command_args):
270    if len(command_args) != 0:
271      print '* Unsupported shutdown command arguments: %s' % (' '.join(command_args))
272      return 2
273
274    rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [
275      '--shutdown'])
276    if rc != 0:
277      print '* Failed to shutdown.'
278      return 1
279    return 0
280
281  def do_start(self, command_args):
282    if len(command_args) != 0:
283      print '* Unsupported start command arguments: %s' % (' '.join(command_args))
284      return 2
285
286    rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [
287      '--start', '--status'])
288    if rc != 0:
289      print '* Failed to start profiler.'
290      return 1
291    return 0
292
293  def do_stop(self, command_args):
294    if len(command_args) != 0:
295      print '* Unsupported stop command arguments: %s' % (' '.join(command_args))
296      return 2
297
298    rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [
299      '--stop', '--status'])
300    if rc != 0:
301      print '* Failed to stop profiler.'
302      return 1
303    return 0
304
305  def do_status(self, command_args):
306    if len(command_args) != 0:
307      print '* Unsupported status command arguments: %s' % (' '.join(command_args))
308      return 2
309
310    rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [
311      '--status'])
312    if rc != 0:
313      print '* Failed to get profiler status.'
314      return 1
315    return 0
316
317  def do_import(self, command_args):
318    force = False
319
320    try:
321      opts, args = getopt.getopt(command_args,
322        'f', ['force'])
323    except getopt.GetoptError, e:
324      print '* Unsupported import command arguments:', str(e)
325      return 2
326
327    for o, a in opts:
328      if o in ('-f', '--force'):
329        force = True
330
331    if len(args) != 0:
332      print '* Unsupported import command arguments: %s' % (' '.join(args))
333      return 2
334
335    # Create session directory.
336    print 'Creating session directory.'
337    if os.path.exists(self.session_dir):
338      if not force:
339        print "* Session directory already exists: %s" % (self.session_dir)
340        print "* Use --force to remove and recreate the session directory."
341        return 1
342
343      try:
344        shutil.rmtree(self.session_dir)
345      except e:
346        print "* Failed to remove existing session directory: %s" % (self.session_dir)
347        print e
348        return 1
349
350    try:
351      os.makedirs(self.session_dir)
352    except e:
353      print "* Failed to create session directory: %s" % (self.session_dir)
354      print e
355      return 1
356
357    raw_samples_dir = os.path.join(self.session_dir, 'raw_samples')
358    samples_dir = os.path.join(self.session_dir, 'samples')
359    abi_file = os.path.join(self.session_dir, 'abi')
360
361    # Dump samples.
362    print 'Dumping samples.'
363    rc, output = self.adb.shell(['/system/xbin/opcontrol'] + self._opcontrol_verbose_arg() + [
364      '--dump', '--status'])
365    if rc != 0:
366      print '* Failed to dump samples.'
367      print output
368      return 1
369
370    # Pull samples.
371    print 'Pulling samples from device.'
372    rc, output = self.adb.pull('/data/oprofile/samples/', raw_samples_dir + '/', echo=False)
373    if rc != 0:
374      print '* Failed to pull samples from the device.'
375      print output
376      return 1
377
378    # Pull ABI.
379    print 'Pulling ABI information from device.'
380    rc, output = self.adb.pull('/data/oprofile/abi', abi_file, echo=False)
381    if rc != 0:
382      print '* Failed to pull abi information from the device.'
383      print output
384      return 1
385
386    # Invoke opimport on each sample file to convert it from the device ABI (ARM)
387    # to the host ABI (x86).
388    print 'Importing samples.'
389    for dirpath, dirnames, filenames in os.walk(raw_samples_dir):
390      for filename in filenames:
391        if not re.match('^.*\.log$', filename):
392          in_path = os.path.join(dirpath, filename)
393          out_path = os.path.join(samples_dir, os.path.relpath(in_path, raw_samples_dir))
394          out_dir = os.path.dirname(out_path)
395          try:
396            os.makedirs(out_dir)
397          except e:
398            print "* Failed to create sample directory: %s" % (out_dir)
399            print e
400            return 1
401
402          rc, output = execute([opimport_bin, '-a', abi_file, '-o', out_path, in_path], echo=False)
403          if rc != 0:
404            print '* Failed to import samples.'
405            print output
406            return 1
407
408    # Generate a short summary report.
409    rc, output = self._execute_opreport([])
410    if rc != 0:
411      print '* Failed to generate summary report.'
412      return 1
413    return 0
414
415  def do_report(self, command_args):
416    rc, output = self._execute_opreport(command_args)
417    if rc != 0:
418      print '* Failed to generate report.'
419      return 1
420    return 0
421
422  def do_annotate(self, command_args):
423    rc, output = self._execute_opannotate(command_args)
424    if rc != 0:
425      print '* Failed to generate annotation.'
426      return 1
427    return 0
428
429  def _opcontrol_verbose_arg(self):
430    if self.verbose:
431      return ['--verbose']
432    else:
433      return []
434
435  def _execute_opreport(self, args):
436    return execute([opreport_bin,
437      '--session-dir=' + self.session_dir,
438      '--image-path=' + symbols_dir + ',' + system_dir] + args)
439
440  def _execute_opannotate(self, command_args):
441    try:
442      opts, args = getopt.getopt(command_args, 'sap:',
443                                 ['source', 'assembly', 'help', 'image-path='])
444    except getopt.GetoptError, e:
445      print '* Unsupported opannotate command arguments:', str(e)
446      return 2
447
448    # Start with the default symbols directory
449    symbols_dirs = symbols_dir
450
451    anno_flag = []
452    for o, a in opts:
453      if o in ('-s', '--source'):
454        anno_flag.append('-s')
455      if o in ('-a', '--assembly'):
456        anno_flag.append('-a')
457        anno_flag.append('--objdump-params=-Cd')
458      if o in ('--help'):
459        anno_flag.append('--help')
460      if o in ('p', '--image-path'):
461        symbols_dirs = a + ',' + symbols_dir
462
463    return execute([opannotate_bin,
464      '--session-dir=' + self.session_dir,
465      '--image-path=' + symbols_dirs + ',' + system_dir] + anno_flag + args)
466
467# Main entry point
468tool = Tool(sys.argv)
469rc = tool.main()
470sys.exit(rc)
471