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