1#!/usr/bin/env python
2
3# Copyright (c) 2011 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Android system-wide tracing utility.
8
9This is a tool for capturing a trace that includes data from both userland and
10the kernel.  It creates an HTML file for visualizing the trace.
11"""
12
13import errno, optparse, os, re, select, subprocess, sys, time, zlib
14
15flattened_css_file = 'style.css'
16flattened_js_file = 'script.js'
17
18class OptionParserIgnoreErrors(optparse.OptionParser):
19  def error(self, msg):
20    pass
21
22  def exit(self):
23    pass
24
25  def print_usage(self):
26    pass
27
28  def print_help(self):
29    pass
30
31  def print_version(self):
32    pass
33
34def get_device_sdk_version():
35  getprop_args = ['adb', 'shell', 'getprop', 'ro.build.version.sdk']
36
37  parser = OptionParserIgnoreErrors()
38  parser.add_option('-e', '--serial', dest='device_serial', type='string')
39  options, args = parser.parse_args()
40  if options.device_serial is not None:
41    getprop_args[1:1] = ['-s', options.device_serial]
42
43  adb = subprocess.Popen(getprop_args, stdout=subprocess.PIPE,
44                         stderr=subprocess.PIPE)
45  out, err = adb.communicate()
46  if adb.returncode != 0:
47    print >> sys.stderr, 'Error querying device SDK-version:'
48    print >> sys.stderr, err
49    sys.exit(1)
50
51  version = int(out)
52  return version
53
54def add_adb_serial(command, serial):
55  if serial is not None:
56    command.insert(1, serial)
57    command.insert(1, '-s')
58
59def main():
60  device_sdk_version = get_device_sdk_version()
61  if device_sdk_version < 18:
62    legacy_script = os.path.join(os.path.dirname(sys.argv[0]), 'systrace-legacy.py')
63    os.execv(legacy_script, sys.argv)
64
65  usage = "Usage: %prog [options] [category1 [category2 ...]]"
66  desc = "Example: %prog -b 32768 -t 15 gfx input view sched freq"
67  parser = optparse.OptionParser(usage=usage, description=desc)
68  parser.add_option('-o', dest='output_file', help='write HTML to FILE',
69                    default='trace.html', metavar='FILE')
70  parser.add_option('-t', '--time', dest='trace_time', type='int',
71                    help='trace for N seconds', metavar='N')
72  parser.add_option('-b', '--buf-size', dest='trace_buf_size', type='int',
73                    help='use a trace buffer size of N KB', metavar='N')
74  parser.add_option('-k', '--ktrace', dest='kfuncs', action='store',
75                    help='specify a comma-separated list of kernel functions to trace')
76  parser.add_option('-l', '--list-categories', dest='list_categories', default=False,
77                    action='store_true', help='list the available categories and exit')
78  parser.add_option('-a', '--app', dest='app_name', default=None, type='string',
79                    action='store', help='enable application-level tracing for comma-separated ' +
80                    'list of app cmdlines')
81  parser.add_option('--no-fix-threads', dest='fix_threads', default=True,
82                    action='store_false', help='don\'t fix missing or truncated thread names')
83
84  parser.add_option('--link-assets', dest='link_assets', default=False,
85                    action='store_true', help='link to original CSS or JS resources '
86                    'instead of embedding them')
87  parser.add_option('--from-file', dest='from_file', action='store',
88                    help='read the trace from a file (compressed) rather than running a live trace')
89  parser.add_option('--asset-dir', dest='asset_dir', default='trace-viewer',
90                    type='string', help='')
91  parser.add_option('-e', '--serial', dest='device_serial', type='string',
92                    help='adb device serial number')
93
94  options, args = parser.parse_args()
95
96  if options.list_categories:
97    atrace_args = ['adb', 'shell', 'atrace', '--list_categories']
98    expect_trace = False
99  elif options.from_file is not None:
100    atrace_args = ['cat', options.from_file]
101    expect_trace = True
102  else:
103    atrace_args = ['adb', 'shell', 'atrace', '-z']
104    expect_trace = True
105
106    if options.trace_time is not None:
107      if options.trace_time > 0:
108        atrace_args.extend(['-t', str(options.trace_time)])
109      else:
110        parser.error('the trace time must be a positive number')
111
112    if options.trace_buf_size is not None:
113      if options.trace_buf_size > 0:
114        atrace_args.extend(['-b', str(options.trace_buf_size)])
115      else:
116        parser.error('the trace buffer size must be a positive number')
117
118    if options.app_name is not None:
119      atrace_args.extend(['-a', options.app_name])
120
121    if options.kfuncs is not None:
122      atrace_args.extend(['-k', options.kfuncs])
123
124    atrace_args.extend(args)
125
126    if options.fix_threads:
127      atrace_args.extend([';', 'ps', '-t'])
128
129  if atrace_args[0] == 'adb':
130    add_adb_serial(atrace_args, options.device_serial)
131
132  script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
133
134  if options.link_assets:
135    src_dir = os.path.join(script_dir, options.asset_dir, 'src')
136    build_dir = os.path.join(script_dir, options.asset_dir, 'build')
137
138    js_files, js_flattenizer, css_files, templates = get_assets(src_dir, build_dir)
139
140    css = '\n'.join(linked_css_tag % (os.path.join(src_dir, f)) for f in css_files)
141    js = '<script language="javascript">\n%s</script>\n' % js_flattenizer
142    js += '\n'.join(linked_js_tag % (os.path.join(src_dir, f)) for f in js_files)
143
144  else:
145    css_filename = os.path.join(script_dir, flattened_css_file)
146    js_filename = os.path.join(script_dir, flattened_js_file)
147    css = compiled_css_tag % (open(css_filename).read())
148    js = compiled_js_tag % (open(js_filename).read())
149    templates = ''
150
151  html_filename = options.output_file
152
153  adb = subprocess.Popen(atrace_args, stdout=subprocess.PIPE,
154                         stderr=subprocess.PIPE)
155
156  result = None
157  data = []
158
159  # Read the text portion of the output and watch for the 'TRACE:' marker that
160  # indicates the start of the trace data.
161  while result is None:
162    ready = select.select([adb.stdout, adb.stderr], [], [adb.stdout, adb.stderr])
163    if adb.stderr in ready[0]:
164      err = os.read(adb.stderr.fileno(), 4096)
165      sys.stderr.write(err)
166      sys.stderr.flush()
167    if adb.stdout in ready[0]:
168      out = os.read(adb.stdout.fileno(), 4096)
169      parts = out.split('\nTRACE:', 1)
170
171      txt = parts[0].replace('\r', '')
172      if len(parts) == 2:
173        # The '\nTRACE:' match stole the last newline from the text, so add it
174        # back here.
175        txt += '\n'
176      sys.stdout.write(txt)
177      sys.stdout.flush()
178
179      if len(parts) == 2:
180        data.append(parts[1])
181        sys.stdout.write("downloading trace...")
182        sys.stdout.flush()
183        break
184
185    result = adb.poll()
186
187  # Read and buffer the data portion of the output.
188  while True:
189    ready = select.select([adb.stdout, adb.stderr], [], [adb.stdout, adb.stderr])
190    keepReading = False
191    if adb.stderr in ready[0]:
192      err = os.read(adb.stderr.fileno(), 4096)
193      if len(err) > 0:
194        keepReading = True
195        sys.stderr.write(err)
196        sys.stderr.flush()
197    if adb.stdout in ready[0]:
198      out = os.read(adb.stdout.fileno(), 4096)
199      if len(out) > 0:
200        keepReading = True
201        data.append(out)
202
203    if result is not None and not keepReading:
204      break
205
206    result = adb.poll()
207
208  if result == 0:
209    if expect_trace:
210      data = ''.join(data)
211
212      # Collapse CRLFs that are added by adb shell.
213      if data.startswith('\r\n'):
214        data = data.replace('\r\n', '\n')
215
216      # Skip the initial newline.
217      data = data[1:]
218
219      if not data:
220        print >> sys.stderr, ('No data was captured.  Output file was not ' +
221          'written.')
222        sys.exit(1)
223      else:
224        # Indicate to the user that the data download is complete.
225        print " done\n"
226
227      # Extract the thread list dumped by ps.
228      threads = {}
229      if options.fix_threads:
230        parts = data.split('USER     PID   PPID  VSIZE  RSS     WCHAN    PC        NAME', 1)
231        if len(parts) == 2:
232          data = parts[0]
233          for line in parts[1].splitlines():
234            cols = line.split(None, 8)
235            if len(cols) == 9:
236              tid = int(cols[1])
237              name = cols[8]
238              threads[tid] = name
239
240      # Decompress and preprocess the data.
241      out = zlib.decompress(data)
242      if options.fix_threads:
243        def repl(m):
244          tid = int(m.group(2))
245          if tid > 0:
246            name = threads.get(tid)
247            if name is None:
248              name = m.group(1)
249              if name == '<...>':
250                name = '<' + str(tid) + '>'
251              threads[tid] = name
252            return name + '-' + m.group(2)
253          else:
254            return m.group(0)
255        out = re.sub(r'^\s*(\S+)-(\d+)', repl, out, flags=re.MULTILINE)
256
257      html_prefix = read_asset(script_dir, 'prefix.html')
258      html_suffix = read_asset(script_dir, 'suffix.html')
259
260      html_file = open(html_filename, 'w')
261      html_file.write(html_prefix % (css, js, templates))
262      html_out = out.replace('\n', '\\n\\\n')
263      html_file.write(html_out)
264      html_file.write(html_suffix)
265      html_file.close()
266      print "\n    wrote file://%s\n" % os.path.abspath(options.output_file)
267
268  else: # i.e. result != 0
269    print >> sys.stderr, 'adb returned error code %d' % result
270    sys.exit(1)
271
272def read_asset(src_dir, filename):
273  return open(os.path.join(src_dir, filename)).read()
274
275def get_assets(src_dir, build_dir):
276  sys.path.append(build_dir)
277  gen = __import__('generate_standalone_timeline_view', {}, {})
278  parse_deps = __import__('parse_deps', {}, {})
279  gen_templates = __import__('generate_template_contents', {}, {})
280  filenames = gen._get_input_filenames()
281  load_sequence = parse_deps.calc_load_sequence(filenames, src_dir)
282
283  js_files = []
284  js_flattenizer = "window.FLATTENED = {};\n"
285  js_flattenizer += "window.FLATTENED_RAW_SCRIPTS = {};\n"
286  css_files = []
287
288  for module in load_sequence:
289    js_files.append(os.path.relpath(module.filename, src_dir))
290    js_flattenizer += "window.FLATTENED['%s'] = true;\n" % module.name
291    for dependent_raw_script_name in module.dependent_raw_script_names:
292      js_flattenizer += (
293        "window.FLATTENED_RAW_SCRIPTS['%s'] = true;\n" %
294        dependent_raw_script_name)
295
296    for style_sheet in module.style_sheets:
297      css_files.append(os.path.relpath(style_sheet.filename, src_dir))
298
299  templates = gen_templates.generate_templates()
300
301  sys.path.pop()
302
303  return (js_files, js_flattenizer, css_files, templates)
304
305
306compiled_css_tag = """<style type="text/css">%s</style>"""
307compiled_js_tag = """<script language="javascript">%s</script>"""
308
309linked_css_tag = """<link rel="stylesheet" href="%s"></link>"""
310linked_js_tag = """<script language="javascript" src="%s"></script>"""
311
312if __name__ == '__main__':
313  main()
314