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.
22 from pylib import android_commands
23 from pylib import cmd_helper
24 from pylib import constants
25 from pylib import pexpect
28 _TRACE_VIEWER_TEMPLATE = """<!DOCTYPE html>
31 <title>%(title)s</title>
49 document.addEventListener('DOMContentLoaded', function() {
50 var trace_data = window.atob('%(trace_data_base64)s');
51 var m = new tracing.TraceModel(trace_data);
52 var timelineViewEl = document.querySelector('.view');
53 ui.decorate(timelineViewEl, tracing.TimelineView);
54 timelineViewEl.model = m;
55 timelineViewEl.tabIndex = 1;
56 timelineViewEl.timeline.focusElement = timelineViewEl;
61 <div class="view"></view>
65 _DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES'
68 def _GetTraceTimestamp():
69 return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
72 def _PackageTraceAsHtml(trace_file_name, html_file_name):
73 trace_viewer_root = os.path.join(constants.DIR_SOURCE_ROOT,
74 'third_party', 'trace-viewer')
75 build_dir = os.path.join(trace_viewer_root, 'build')
76 src_dir = os.path.join(trace_viewer_root, 'src')
77 if not build_dir in sys.path:
78 sys.path.append(build_dir)
79 generate = __import__('generate', {}, {})
80 parse_deps = __import__('parse_deps', {}, {})
82 basename = os.path.splitext(trace_file_name)[0]
83 load_sequence = parse_deps.calc_load_sequence(
84 ['tracing/standalone_timeline_view.js'], [src_dir])
86 with open(trace_file_name) as trace_file:
87 trace_data = base64.b64encode(trace_file.read())
88 with open(html_file_name, 'w') as html_file:
89 html = _TRACE_VIEWER_TEMPLATE % {
90 'title': os.path.basename(os.path.splitext(trace_file_name)[0]),
91 'timeline_js': generate.generate_js(load_sequence),
92 'timeline_css': generate.generate_css(load_sequence),
93 'trace_data_base64': trace_data
98 class ChromeTracingController(object):
99 def __init__(self, adb, package_info, categories, ring_buffer):
101 self._package_info = package_info
102 self._categories = categories
103 self._ring_buffer = ring_buffer
104 self._trace_file = None
105 self._trace_interval = None
106 self._trace_start_re = \
107 re.compile(r'Logging performance trace to file: (.*)')
108 self._trace_finish_re = \
109 re.compile(r'Profiler finished[.] Results are in (.*)[.]')
110 self._adb.StartMonitoringLogcat(clear=False)
113 return 'chrome trace'
115 def StartTracing(self, interval):
116 self._trace_interval = interval
117 self._adb.SyncLogCat()
118 self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_START',
119 '-e categories "%s"' % ','.join(self._categories),
120 '-e continuous' if self._ring_buffer else '')
121 # Chrome logs two different messages related to tracing:
123 # 1. "Logging performance trace to file [...]"
124 # 2. "Profiler finished. Results are in [...]"
126 # The first one is printed when tracing starts and the second one indicates
127 # that the trace file is ready to be pulled.
129 self._trace_file = self._adb.WaitForLogMatch(self._trace_start_re,
132 except pexpect.TIMEOUT:
133 raise RuntimeError('Trace start marker not found. Is the correct version '
134 'of the browser running?')
136 def StopTracing(self):
137 if not self._trace_file:
139 self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP')
140 self._adb.WaitForLogMatch(self._trace_finish_re, None, timeout=120)
143 # Wait a bit for the browser to finish writing the trace file.
144 time.sleep(self._trace_interval / 4 + 1)
146 trace_file = self._trace_file.replace('/storage/emulated/0/', '/sdcard/')
147 host_file = os.path.join(os.path.curdir, os.path.basename(trace_file))
148 self._adb.PullFileFromDevice(trace_file, host_file)
152 _SYSTRACE_OPTIONS = [
153 # Compress the trace before sending it over USB.
155 # Use a large trace buffer to increase the polling interval.
159 # Interval in seconds for sampling systrace data.
160 _SYSTRACE_INTERVAL = 15
163 class SystraceController(object):
164 def __init__(self, adb, categories, ring_buffer):
166 self._categories = categories
167 self._ring_buffer = ring_buffer
168 self._done = threading.Event()
170 self._trace_data = None
176 def GetCategories(adb):
177 return adb.RunShellCommand('atrace --list_categories')
179 def StartTracing(self, interval):
180 self._thread = threading.Thread(target=self._CollectData)
183 def StopTracing(self):
190 output_name = 'systrace-%s' % _GetTraceTimestamp()
191 with open(output_name, 'w') as out:
192 out.write(self._trace_data)
195 def _RunATraceCommand(self, command):
196 # We use a separate interface to adb because the one from AndroidCommands
198 device = ['-s', self._adb.GetDevice()] if self._adb.GetDevice() else []
199 cmd = ['adb'] + device + ['shell', 'atrace', '--%s' % command] + \
200 _SYSTRACE_OPTIONS + self._categories
201 return cmd_helper.GetCmdOutput(cmd)
203 def _CollectData(self):
205 self._RunATraceCommand('async_start')
207 while not self._done.is_set():
208 self._done.wait(_SYSTRACE_INTERVAL)
209 if not self._ring_buffer or self._done.is_set():
211 self._DecodeTraceData(self._RunATraceCommand('async_dump')))
214 self._DecodeTraceData(self._RunATraceCommand('async_stop')))
215 self._trace_data = ''.join([zlib.decompress(d) for d in trace_data])
218 def _DecodeTraceData(trace_data):
220 trace_start = trace_data.index('TRACE:')
222 raise RuntimeError('Systrace start marker not found')
223 trace_data = trace_data[trace_start + 6:]
225 # Collapse CRLFs that are added by adb shell.
226 if trace_data.startswith('\r\n'):
227 trace_data = trace_data.replace('\r\n', '\n')
229 # Skip the initial newline.
230 return trace_data[1:]
233 def _GetSupportedBrowsers():
234 # Add aliases for backwards compatibility.
235 supported_browsers = {
236 'stable': constants.PACKAGE_INFO['chrome_stable'],
237 'beta': constants.PACKAGE_INFO['chrome_beta'],
238 'dev': constants.PACKAGE_INFO['chrome_dev'],
239 'build': constants.PACKAGE_INFO['chrome'],
241 supported_browsers.update(constants.PACKAGE_INFO)
242 unsupported_browsers = ['content_browsertests', 'gtest', 'legacy_browser']
243 for browser in unsupported_browsers:
244 del supported_browsers[browser]
245 return supported_browsers
248 def _CompressFile(host_file, output):
249 with gzip.open(output, 'wb') as out:
250 with open(host_file, 'rb') as input_file:
251 out.write(input_file.read())
255 def _ArchiveFiles(host_files, output):
256 with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as z:
257 for host_file in host_files:
262 def _PrintMessage(heading, eol='\n'):
263 sys.stdout.write('%s%s' % (heading, eol))
267 def _WaitForEnter(timeout):
268 select.select([sys.stdin], [], [], timeout)
271 def _StartTracing(controllers, interval):
272 for controller in controllers:
273 controller.StartTracing(interval)
276 def _StopTracing(controllers):
277 for controller in controllers:
278 controller.StopTracing()
281 def _PullTraces(controllers, output, compress, write_html):
282 _PrintMessage('Downloading...', eol='')
284 for controller in controllers:
285 trace_files.append(controller.PullTrace())
287 if compress and len(trace_files) == 1:
288 result = output or trace_files[0] + '.gz'
289 _CompressFile(trace_files[0], result)
290 elif len(trace_files) > 1:
291 result = output or 'chrome-combined-trace-%s.zip' % _GetTraceTimestamp()
292 _ArchiveFiles(trace_files, result)
295 shutil.move(trace_files[0], result)
297 result = trace_files[0]
300 result, trace_file = os.path.splitext(result)[0] + '.html', result
301 _PackageTraceAsHtml(trace_file, result)
302 if trace_file != result:
303 os.unlink(trace_file)
305 _PrintMessage('done')
306 _PrintMessage('Trace written to %s' % os.path.abspath(result))
310 def _CaptureAndPullTrace(controllers, interval, output, compress, write_html):
311 trace_type = ' + '.join(map(str, controllers))
313 _StartTracing(controllers, interval)
315 _PrintMessage('Capturing %d-second %s. Press Enter to stop early...' % \
316 (interval, trace_type), eol='')
317 _WaitForEnter(interval)
319 _PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='')
322 _StopTracing(controllers)
324 _PrintMessage('done')
326 return _PullTraces(controllers, output, compress, write_html)
329 def _ComputeChromeCategories(options):
331 if options.trace_frame_viewer:
332 categories.append('disabled-by-default-cc.debug')
333 if options.trace_ubercompositor:
334 categories.append('disabled-by-default-cc.debug*')
335 if options.trace_gpu:
336 categories.append('disabled-by-default-gpu.debug*')
337 if options.chrome_categories:
338 categories += options.chrome_categories.split(',')
342 def _ComputeSystraceCategories(options):
343 if not options.systrace_categories:
345 return options.systrace_categories.split(',')
349 parser = optparse.OptionParser(description='Record about://tracing profiles '
350 'from Android browsers. See http://dev.'
351 'chromium.org/developers/how-tos/trace-event-'
352 'profiling-tool for detailed instructions for '
355 timed_options = optparse.OptionGroup(parser, 'Timed tracing')
356 timed_options.add_option('-t', '--time', help='Profile for N seconds and '
357 'download the resulting trace.', metavar='N',
359 parser.add_option_group(timed_options)
361 cont_options = optparse.OptionGroup(parser, 'Continuous tracing')
362 cont_options.add_option('--continuous', help='Profile continuously until '
363 'stopped.', action='store_true')
364 cont_options.add_option('--ring-buffer', help='Use the trace buffer as a '
365 'ring buffer and save its contents when stopping '
366 'instead of appending events into one long trace.',
368 parser.add_option_group(cont_options)
370 categories = optparse.OptionGroup(parser, 'Trace categories')
371 categories.add_option('-c', '--categories', help='Select Chrome tracing '
372 'categories with comma-delimited wildcards, '
373 'e.g., "*", "cat1*,-cat1a". Omit this option to trace '
374 'Chrome\'s default categories. Chrome tracing can be '
375 'disabled with "--categories=\'\'".',
376 metavar='CHROME_CATEGORIES', dest='chrome_categories',
377 default=_DEFAULT_CHROME_CATEGORIES)
378 categories.add_option('-s', '--systrace', help='Capture a systrace with the '
379 'chosen comma-delimited systrace categories. You can '
380 'also capture a combined Chrome + systrace by enabling '
381 'both types of categories. Use "list" to see the '
382 'available categories. Systrace is disabled by '
383 'default.', metavar='SYS_CATEGORIES',
384 dest='systrace_categories', default='')
385 categories.add_option('--trace-cc',
386 help='Deprecated, use --trace-frame-viewer.',
388 categories.add_option('--trace-frame-viewer',
389 help='Enable enough trace categories for '
390 'compositor frame viewing.', action='store_true')
391 categories.add_option('--trace-ubercompositor',
392 help='Enable enough trace categories for '
393 'ubercompositor frame data.', action='store_true')
394 categories.add_option('--trace-gpu', help='Enable extra trace categories for '
395 'GPU data.', action='store_true')
396 parser.add_option_group(categories)
398 output_options = optparse.OptionGroup(parser, 'Output options')
399 output_options.add_option('-o', '--output', help='Save trace output to file.')
400 output_options.add_option('--html', help='Package trace into a standalone '
401 'html file.', action='store_true')
402 output_options.add_option('--view', help='Open resulting trace file in a '
403 'browser.', action='store_true')
404 parser.add_option_group(output_options)
406 browsers = sorted(_GetSupportedBrowsers().keys())
407 parser.add_option('-b', '--browser', help='Select among installed browsers. '
408 'One of ' + ', '.join(browsers) + ', "stable" is used by '
409 'default.', type='choice', choices=browsers,
411 parser.add_option('-v', '--verbose', help='Verbose logging.',
413 parser.add_option('-z', '--compress', help='Compress the resulting trace '
414 'with gzip. ', action='store_true')
415 options, args = parser.parse_args()
417 parser.parse_error("""--trace-cc is deprecated.
419 For basic jank busting uses, use --trace-frame-viewer
420 For detailed study of ubercompositor, pass --trace-ubercompositor.
422 When in doubt, just try out --trace-frame-viewer.
426 logging.getLogger().setLevel(logging.DEBUG)
428 adb = android_commands.AndroidCommands()
429 if options.systrace_categories in ['list', 'help']:
430 _PrintMessage('\n'.join(SystraceController.GetCategories(adb)))
433 if not options.time and not options.continuous:
434 _PrintMessage('Time interval or continuous tracing should be specified.')
437 chrome_categories = _ComputeChromeCategories(options)
438 systrace_categories = _ComputeSystraceCategories(options)
439 package_info = _GetSupportedBrowsers()[options.browser]
441 if chrome_categories and 'webview' in systrace_categories:
442 logging.warning('Using the "webview" category in systrace together with '
443 'Chrome tracing results in duplicate trace events.')
446 if chrome_categories:
447 controllers.append(ChromeTracingController(adb,
450 options.ring_buffer))
451 if systrace_categories:
452 controllers.append(SystraceController(adb,
454 options.ring_buffer))
457 _PrintMessage('No trace categories enabled.')
460 result = _CaptureAndPullTrace(controllers,
461 options.time if not options.continuous else 0,
466 webbrowser.open(result)
469 if __name__ == '__main__':