• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2013 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
7import base64
8import gzip
9import logging
10import optparse
11import os
12import re
13import shutil
14import sys
15import threading
16import time
17import webbrowser
18import zipfile
19import zlib
20
21from pylib import android_commands
22from pylib import cmd_helper
23from pylib import constants
24from pylib import pexpect
25
26
27_TRACE_VIEWER_TEMPLATE = """<!DOCTYPE html>
28<html>
29  <head>
30    <title>%(title)s</title>
31    <style>
32      %(timeline_css)s
33    </style>
34    <style>
35      .view {
36        overflow: hidden;
37        position: absolute;
38        top: 0;
39        bottom: 0;
40        left: 0;
41        right: 0;
42      }
43    </style>
44    <script>
45      %(timeline_js)s
46    </script>
47    <script>
48      document.addEventListener('DOMContentLoaded', function() {
49        var trace_data = window.atob('%(trace_data_base64)s');
50        var m = new tracing.TraceModel(trace_data);
51        var timelineViewEl = document.querySelector('.view');
52        ui.decorate(timelineViewEl, tracing.TimelineView);
53        timelineViewEl.model = m;
54        timelineViewEl.tabIndex = 1;
55        timelineViewEl.timeline.focusElement = timelineViewEl;
56      });
57    </script>
58  </head>
59  <body>
60    <div class="view"></view>
61  </body>
62</html>"""
63
64_DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES'
65
66
67def _GetTraceTimestamp():
68 return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
69
70
71def _PackageTraceAsHtml(trace_file_name, html_file_name):
72  trace_viewer_root = os.path.join(constants.DIR_SOURCE_ROOT,
73                                   'third_party', 'trace-viewer')
74  build_dir = os.path.join(trace_viewer_root, 'build')
75  src_dir = os.path.join(trace_viewer_root, 'src')
76  if not build_dir in sys.path:
77    sys.path.append(build_dir)
78  generate = __import__('generate', {}, {})
79  parse_deps = __import__('parse_deps', {}, {})
80
81  basename = os.path.splitext(trace_file_name)[0]
82  load_sequence = parse_deps.calc_load_sequence(
83      ['tracing/standalone_timeline_view.js'], [src_dir])
84
85  with open(trace_file_name) as trace_file:
86    trace_data = base64.b64encode(trace_file.read())
87    with open(html_file_name, 'w') as html_file:
88      html = _TRACE_VIEWER_TEMPLATE % {
89        'title': os.path.basename(os.path.splitext(trace_file_name)[0]),
90        'timeline_js': generate.generate_js(load_sequence),
91        'timeline_css': generate.generate_css(load_sequence),
92        'trace_data_base64': trace_data
93      }
94      html_file.write(html)
95
96
97class ChromeTracingController(object):
98  def __init__(self, adb, package_info, categories, ring_buffer):
99    self._adb = adb
100    self._package_info = package_info
101    self._categories = categories
102    self._ring_buffer = ring_buffer
103    self._trace_file = None
104    self._trace_interval = None
105    self._trace_start_re = \
106       re.compile(r'Logging performance trace to file: (.*)')
107    self._trace_finish_re = \
108       re.compile(r'Profiler finished[.] Results are in (.*)[.]')
109    self._adb.StartMonitoringLogcat(clear=False)
110
111  def __str__(self):
112    return 'chrome trace'
113
114  def StartTracing(self, interval):
115    self._trace_interval = interval
116    self._adb.SyncLogCat()
117    self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_START',
118                              '-e categories "%s"' % ','.join(self._categories),
119                              '-e continuous' if self._ring_buffer else '')
120    # Chrome logs two different messages related to tracing:
121    #
122    # 1. "Logging performance trace to file [...]"
123    # 2. "Profiler finished. Results are in [...]"
124    #
125    # The first one is printed when tracing starts and the second one indicates
126    # that the trace file is ready to be pulled.
127    try:
128      self._trace_file = self._adb.WaitForLogMatch(self._trace_start_re,
129                                                   None,
130                                                   timeout=5).group(1)
131    except pexpect.TIMEOUT:
132      raise RuntimeError('Trace start marker not found. Is the correct version '
133                         'of the browser running?')
134
135  def StopTracing(self):
136    if not self._trace_file:
137      return
138    self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP')
139    self._adb.WaitForLogMatch(self._trace_finish_re, None, timeout=120)
140
141  def PullTrace(self):
142    # Wait a bit for the browser to finish writing the trace file.
143    time.sleep(self._trace_interval / 4 + 1)
144
145    trace_file = self._trace_file.replace('/storage/emulated/0/', '/sdcard/')
146    host_file = os.path.join(os.path.curdir, os.path.basename(trace_file))
147    self._adb.PullFileFromDevice(trace_file, host_file)
148    return host_file
149
150
151_SYSTRACE_OPTIONS = [
152    # Compress the trace before sending it over USB.
153    '-z',
154    # Use a large trace buffer to increase the polling interval.
155    '-b', '16384'
156]
157
158# Interval in seconds for sampling systrace data.
159_SYSTRACE_INTERVAL = 15
160
161
162class SystraceController(object):
163  def __init__(self, adb, categories, ring_buffer):
164    self._adb = adb
165    self._categories = categories
166    self._ring_buffer = ring_buffer
167    self._done = threading.Event()
168    self._thread = None
169    self._trace_data = None
170
171  def __str__(self):
172    return 'systrace'
173
174  @staticmethod
175  def GetCategories(adb):
176    return adb.RunShellCommand('atrace --list_categories')
177
178  def StartTracing(self, interval):
179    self._thread = threading.Thread(target=self._CollectData)
180    self._thread.start()
181
182  def StopTracing(self):
183    self._done.set()
184
185  def PullTrace(self):
186    self._thread.join()
187    self._thread = None
188    if self._trace_data:
189      output_name = 'systrace-%s' % _GetTraceTimestamp()
190      with open(output_name, 'w') as out:
191        out.write(self._trace_data)
192      return output_name
193
194  def _RunATraceCommand(self, command):
195    # We use a separate interface to adb because the one from AndroidCommands
196    # isn't re-entrant.
197    device = ['-s', self._adb.GetDevice()] if self._adb.GetDevice() else []
198    cmd = ['adb'] + device + ['shell', 'atrace', '--%s' % command] + \
199        _SYSTRACE_OPTIONS + self._categories
200    return cmd_helper.GetCmdOutput(cmd)
201
202  def _CollectData(self):
203    trace_data = []
204    self._RunATraceCommand('async_start')
205    try:
206      while not self._done.is_set():
207        self._done.wait(_SYSTRACE_INTERVAL)
208        if not self._ring_buffer or self._done.is_set():
209          trace_data.append(
210              self._DecodeTraceData(self._RunATraceCommand('async_dump')))
211    finally:
212      trace_data.append(
213          self._DecodeTraceData(self._RunATraceCommand('async_stop')))
214    self._trace_data = ''.join([zlib.decompress(d) for d in trace_data])
215
216  @staticmethod
217  def _DecodeTraceData(trace_data):
218    try:
219      trace_start = trace_data.index('TRACE:')
220    except ValueError:
221      raise RuntimeError('Systrace start marker not found')
222    trace_data = trace_data[trace_start + 6:]
223
224    # Collapse CRLFs that are added by adb shell.
225    if trace_data.startswith('\r\n'):
226      trace_data = trace_data.replace('\r\n', '\n')
227
228    # Skip the initial newline.
229    return trace_data[1:]
230
231
232def _GetSupportedBrowsers():
233  # Add aliases for backwards compatibility.
234  supported_browsers = {
235    'stable': constants.PACKAGE_INFO['chrome_stable'],
236    'beta': constants.PACKAGE_INFO['chrome_beta'],
237    'dev': constants.PACKAGE_INFO['chrome_dev'],
238    'build': constants.PACKAGE_INFO['chrome'],
239  }
240  supported_browsers.update(constants.PACKAGE_INFO)
241  unsupported_browsers = ['content_browsertests', 'gtest', 'legacy_browser']
242  for browser in unsupported_browsers:
243    del supported_browsers[browser]
244  return supported_browsers
245
246
247def _CompressFile(host_file, output):
248  with gzip.open(output, 'wb') as out:
249    with open(host_file, 'rb') as input_file:
250      out.write(input_file.read())
251  os.unlink(host_file)
252
253
254def _ArchiveFiles(host_files, output):
255  with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as z:
256    for host_file in host_files:
257      z.write(host_file)
258      os.unlink(host_file)
259
260
261def _PrintMessage(heading, eol='\n'):
262  sys.stdout.write('%s%s' % (heading, eol))
263  sys.stdout.flush()
264
265
266def _StartTracing(controllers, interval):
267  for controller in controllers:
268    controller.StartTracing(interval)
269
270
271def _StopTracing(controllers):
272  for controller in controllers:
273    controller.StopTracing()
274
275
276def _PullTraces(controllers, output, compress, write_html):
277  _PrintMessage('Downloading...', eol='')
278  trace_files = []
279  for controller in controllers:
280    trace_files.append(controller.PullTrace())
281
282  if compress and len(trace_files) == 1:
283    result = output or trace_files[0] + '.gz'
284    _CompressFile(trace_files[0], result)
285  elif len(trace_files) > 1:
286    result = output or 'chrome-combined-trace-%s.zip' % _GetTraceTimestamp()
287    _ArchiveFiles(trace_files, result)
288  elif output:
289    result = output
290    shutil.move(trace_files[0], result)
291  else:
292    result = trace_files[0]
293
294  if write_html:
295    result, trace_file = os.path.splitext(result)[0] + '.html', result
296    _PackageTraceAsHtml(trace_file, result)
297    if trace_file != result:
298      os.unlink(trace_file)
299
300  _PrintMessage('done')
301  _PrintMessage('Trace written to %s' % os.path.abspath(result))
302  return result
303
304
305def _CaptureAndPullTrace(controllers, interval, output, compress, write_html):
306  trace_type = ' + '.join(map(str, controllers))
307  try:
308    _StartTracing(controllers, interval)
309    if interval:
310      _PrintMessage('Capturing %d-second %s. Press Ctrl-C to stop early...' % \
311          (interval, trace_type), eol='')
312      time.sleep(interval)
313    else:
314      _PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='')
315      raw_input()
316  except KeyboardInterrupt:
317    _PrintMessage('\nInterrupted...', eol='')
318  finally:
319    _StopTracing(controllers)
320  if interval:
321    _PrintMessage('done')
322
323  return _PullTraces(controllers, output, compress, write_html)
324
325
326def _ComputeChromeCategories(options):
327  categories = []
328  if options.trace_cc:
329    categories.append('disabled-by-default-cc.debug*')
330  if options.trace_gpu:
331    categories.append('disabled-by-default-gpu.debug*')
332  if options.chrome_categories:
333    categories += options.chrome_categories.split(',')
334  return categories
335
336
337def _ComputeSystraceCategories(options):
338  if not options.systrace_categories:
339    return []
340  return options.systrace_categories.split(',')
341
342
343def main():
344  parser = optparse.OptionParser(description='Record about://tracing profiles '
345                                 'from Android browsers. See http://dev.'
346                                 'chromium.org/developers/how-tos/trace-event-'
347                                 'profiling-tool for detailed instructions for '
348                                 'profiling.')
349
350  timed_options = optparse.OptionGroup(parser, 'Timed tracing')
351  timed_options.add_option('-t', '--time', help='Profile for N seconds and '
352                          'download the resulting trace.', metavar='N',
353                           type='float')
354  parser.add_option_group(timed_options)
355
356  cont_options = optparse.OptionGroup(parser, 'Continuous tracing')
357  cont_options.add_option('--continuous', help='Profile continuously until '
358                          'stopped.', action='store_true')
359  cont_options.add_option('--ring-buffer', help='Use the trace buffer as a '
360                          'ring buffer and save its contents when stopping '
361                          'instead of appending events into one long trace.',
362                          action='store_true')
363  parser.add_option_group(cont_options)
364
365  categories = optparse.OptionGroup(parser, 'Trace categories')
366  categories.add_option('-c', '--categories', help='Select Chrome tracing '
367                        'categories with comma-delimited wildcards, '
368                        'e.g., "*", "cat1*,-cat1a". Omit this option to trace '
369                        'Chrome\'s default categories. Chrome tracing can be '
370                        'disabled with "--categories=\'\'".',
371                        metavar='CHROME_CATEGORIES', dest='chrome_categories',
372                        default=_DEFAULT_CHROME_CATEGORIES)
373  categories.add_option('-s', '--systrace', help='Capture a systrace with the '
374                        'chosen comma-delimited systrace categories. You can '
375                        'also capture a combined Chrome + systrace by enabling '
376                        'both types of categories. Use "list" to see the '
377                        'available categories. Systrace is disabled by '
378                        'default.', metavar='SYS_CATEGORIES',
379                        dest='systrace_categories', default='')
380  categories.add_option('--trace-cc', help='Enable extra trace categories for '
381                        'compositor frame viewer data.', action='store_true')
382  categories.add_option('--trace-gpu', help='Enable extra trace categories for '
383                        'GPU data.', action='store_true')
384  parser.add_option_group(categories)
385
386  output_options = optparse.OptionGroup(parser, 'Output options')
387  output_options.add_option('-o', '--output', help='Save trace output to file.')
388  output_options.add_option('--html', help='Package trace into a standalone '
389                            'html file.', action='store_true')
390  output_options.add_option('--view', help='Open resulting trace file in a '
391                            'browser.', action='store_true')
392  parser.add_option_group(output_options)
393
394  browsers = sorted(_GetSupportedBrowsers().keys())
395  parser.add_option('-b', '--browser', help='Select among installed browsers. '
396                    'One of ' + ', '.join(browsers) + ', "stable" is used by '
397                    'default.', type='choice', choices=browsers,
398                    default='stable')
399  parser.add_option('-v', '--verbose', help='Verbose logging.',
400                    action='store_true')
401  parser.add_option('-z', '--compress', help='Compress the resulting trace '
402                    'with gzip. ', action='store_true')
403  options, args = parser.parse_args()
404
405  if options.verbose:
406    logging.getLogger().setLevel(logging.DEBUG)
407
408  adb = android_commands.AndroidCommands()
409  if options.systrace_categories in ['list', 'help']:
410    _PrintMessage('\n'.join(SystraceController.GetCategories(adb)))
411    return 0
412
413  if not options.time and not options.continuous:
414    _PrintMessage('Time interval or continuous tracing should be specified.')
415    return 1
416
417  chrome_categories = _ComputeChromeCategories(options)
418  systrace_categories = _ComputeSystraceCategories(options)
419  package_info = _GetSupportedBrowsers()[options.browser]
420
421  if chrome_categories and 'webview' in systrace_categories:
422    logging.warning('Using the "webview" category in systrace together with '
423                    'Chrome tracing results in duplicate trace events.')
424
425  controllers = []
426  if chrome_categories:
427    controllers.append(ChromeTracingController(adb,
428                                               package_info,
429                                               chrome_categories,
430                                               options.ring_buffer))
431  if systrace_categories:
432    controllers.append(SystraceController(adb,
433                                          systrace_categories,
434                                          options.ring_buffer))
435
436  if not controllers:
437    _PrintMessage('No trace categories enabled.')
438    return 1
439
440  result = _CaptureAndPullTrace(controllers,
441                                options.time if not options.continuous else 0,
442                                options.output,
443                                options.compress,
444                                options.html)
445  if options.view:
446    webbrowser.open(result)
447
448
449if __name__ == '__main__':
450  sys.exit(main())
451