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.
21 from pylib import android_commands
22 from pylib import cmd_helper
23 from pylib import constants
24 from pylib import pexpect
26 _TRACE_VIEWER_ROOT = os.path.join(constants.DIR_SOURCE_ROOT,
27 'third_party', 'trace-viewer')
28 sys.path.append(_TRACE_VIEWER_ROOT)
29 from trace_viewer.build import trace2html # pylint: disable=F0401
31 _DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES'
34 def _GetTraceTimestamp():
35 return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
38 class ChromeTracingController(object):
39 def __init__(self, adb, package_info, categories, ring_buffer):
41 self._package_info = package_info
42 self._categories = categories
43 self._ring_buffer = ring_buffer
44 self._trace_file = None
45 self._trace_interval = None
46 self._trace_start_re = \
47 re.compile(r'Logging performance trace to file: (.*)')
48 self._trace_finish_re = \
49 re.compile(r'Profiler finished[.] Results are in (.*)[.]')
50 self._adb.StartMonitoringLogcat(clear=False)
55 def StartTracing(self, interval):
56 self._trace_interval = interval
57 self._adb.SyncLogCat()
58 self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_START',
59 '-e categories "%s"' % ','.join(self._categories),
60 '-e continuous' if self._ring_buffer else '')
61 # Chrome logs two different messages related to tracing:
63 # 1. "Logging performance trace to file [...]"
64 # 2. "Profiler finished. Results are in [...]"
66 # The first one is printed when tracing starts and the second one indicates
67 # that the trace file is ready to be pulled.
69 self._trace_file = self._adb.WaitForLogMatch(self._trace_start_re,
72 except pexpect.TIMEOUT:
73 raise RuntimeError('Trace start marker not found. Is the correct version '
74 'of the browser running?')
76 def StopTracing(self):
77 if not self._trace_file:
79 self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP')
80 self._adb.WaitForLogMatch(self._trace_finish_re, None, timeout=120)
83 # Wait a bit for the browser to finish writing the trace file.
84 time.sleep(self._trace_interval / 4 + 1)
86 trace_file = self._trace_file.replace('/storage/emulated/0/', '/sdcard/')
87 host_file = os.path.join(os.path.curdir, os.path.basename(trace_file))
88 self._adb.PullFileFromDevice(trace_file, host_file)
93 # Compress the trace before sending it over USB.
95 # Use a large trace buffer to increase the polling interval.
99 # Interval in seconds for sampling systrace data.
100 _SYSTRACE_INTERVAL = 15
103 class SystraceController(object):
104 def __init__(self, adb, categories, ring_buffer):
106 self._categories = categories
107 self._ring_buffer = ring_buffer
108 self._done = threading.Event()
110 self._trace_data = None
116 def GetCategories(adb):
117 return adb.RunShellCommand('atrace --list_categories')
119 def StartTracing(self, _):
120 self._thread = threading.Thread(target=self._CollectData)
123 def StopTracing(self):
130 output_name = 'systrace-%s' % _GetTraceTimestamp()
131 with open(output_name, 'w') as out:
132 out.write(self._trace_data)
135 def _RunATraceCommand(self, command):
136 # We use a separate interface to adb because the one from AndroidCommands
138 device = ['-s', self._adb.GetDevice()] if self._adb.GetDevice() else []
139 cmd = ['adb'] + device + ['shell', 'atrace', '--%s' % command] + \
140 _SYSTRACE_OPTIONS + self._categories
141 return cmd_helper.GetCmdOutput(cmd)
143 def _CollectData(self):
145 self._RunATraceCommand('async_start')
147 while not self._done.is_set():
148 self._done.wait(_SYSTRACE_INTERVAL)
149 if not self._ring_buffer or self._done.is_set():
151 self._DecodeTraceData(self._RunATraceCommand('async_dump')))
154 self._DecodeTraceData(self._RunATraceCommand('async_stop')))
155 self._trace_data = ''.join([zlib.decompress(d) for d in trace_data])
158 def _DecodeTraceData(trace_data):
160 trace_start = trace_data.index('TRACE:')
162 raise RuntimeError('Systrace start marker not found')
163 trace_data = trace_data[trace_start + 6:]
165 # Collapse CRLFs that are added by adb shell.
166 if trace_data.startswith('\r\n'):
167 trace_data = trace_data.replace('\r\n', '\n')
169 # Skip the initial newline.
170 return trace_data[1:]
173 def _GetSupportedBrowsers():
174 # Add aliases for backwards compatibility.
175 supported_browsers = {
176 'stable': constants.PACKAGE_INFO['chrome_stable'],
177 'beta': constants.PACKAGE_INFO['chrome_beta'],
178 'dev': constants.PACKAGE_INFO['chrome_dev'],
179 'build': constants.PACKAGE_INFO['chrome'],
181 supported_browsers.update(constants.PACKAGE_INFO)
182 unsupported_browsers = ['content_browsertests', 'gtest', 'legacy_browser']
183 for browser in unsupported_browsers:
184 del supported_browsers[browser]
185 return supported_browsers
188 def _CompressFile(host_file, output):
189 with gzip.open(output, 'wb') as out:
190 with open(host_file, 'rb') as input_file:
191 out.write(input_file.read())
195 def _ArchiveFiles(host_files, output):
196 with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as z:
197 for host_file in host_files:
202 def _PackageTracesAsHtml(trace_files, html_file):
203 with open(html_file, 'w') as f:
204 trace2html.WriteHTMLForTracesToFile(trace_files, f)
205 for trace_file in trace_files:
206 os.unlink(trace_file)
209 def _PrintMessage(heading, eol='\n'):
210 sys.stdout.write('%s%s' % (heading, eol))
214 def _WaitForEnter(timeout):
215 select.select([sys.stdin], [], [], timeout)
218 def _StartTracing(controllers, interval):
219 for controller in controllers:
220 controller.StartTracing(interval)
223 def _StopTracing(controllers):
224 for controller in controllers:
225 controller.StopTracing()
228 def _PullTraces(controllers, output, compress, write_json):
229 _PrintMessage('Downloading...', eol='')
231 for controller in controllers:
232 trace_files.append(controller.PullTrace())
235 html_file = os.path.splitext(trace_files[0])[0] + '.html'
236 _PackageTracesAsHtml(trace_files, html_file)
237 trace_files = [html_file]
239 if compress and len(trace_files) == 1:
240 result = output or trace_files[0] + '.gz'
241 _CompressFile(trace_files[0], result)
242 elif len(trace_files) > 1:
243 result = output or 'chrome-combined-trace-%s.zip' % _GetTraceTimestamp()
244 _ArchiveFiles(trace_files, result)
247 shutil.move(trace_files[0], result)
249 result = trace_files[0]
251 _PrintMessage('done')
252 _PrintMessage('Trace written to file://%s' % os.path.abspath(result))
256 def _CaptureAndPullTrace(controllers, interval, output, compress, write_json):
257 trace_type = ' + '.join(map(str, controllers))
259 _StartTracing(controllers, interval)
261 _PrintMessage('Capturing %d-second %s. Press Enter to stop early...' % \
262 (interval, trace_type), eol='')
263 _WaitForEnter(interval)
265 _PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='')
268 _StopTracing(controllers)
270 _PrintMessage('done')
272 return _PullTraces(controllers, output, compress, write_json)
275 def _ComputeChromeCategories(options):
277 if options.trace_frame_viewer:
278 categories.append('disabled-by-default-cc.debug')
279 if options.trace_ubercompositor:
280 categories.append('disabled-by-default-cc.debug*')
281 if options.trace_gpu:
282 categories.append('disabled-by-default-gpu.debug*')
283 if options.trace_flow:
284 categories.append('disabled-by-default-toplevel.flow')
285 if options.chrome_categories:
286 categories += options.chrome_categories.split(',')
290 def _ComputeSystraceCategories(options):
291 if not options.systrace_categories:
293 return options.systrace_categories.split(',')
297 parser = optparse.OptionParser(description='Record about://tracing profiles '
298 'from Android browsers. See http://dev.'
299 'chromium.org/developers/how-tos/trace-event-'
300 'profiling-tool for detailed instructions for '
303 timed_options = optparse.OptionGroup(parser, 'Timed tracing')
304 timed_options.add_option('-t', '--time', help='Profile for N seconds and '
305 'download the resulting trace.', metavar='N',
307 parser.add_option_group(timed_options)
309 cont_options = optparse.OptionGroup(parser, 'Continuous tracing')
310 cont_options.add_option('--continuous', help='Profile continuously until '
311 'stopped.', action='store_true')
312 cont_options.add_option('--ring-buffer', help='Use the trace buffer as a '
313 'ring buffer and save its contents when stopping '
314 'instead of appending events into one long trace.',
316 parser.add_option_group(cont_options)
318 categories = optparse.OptionGroup(parser, 'Trace categories')
319 categories.add_option('-c', '--categories', help='Select Chrome tracing '
320 'categories with comma-delimited wildcards, '
321 'e.g., "*", "cat1*,-cat1a". Omit this option to trace '
322 'Chrome\'s default categories. Chrome tracing can be '
323 'disabled with "--categories=\'\'".',
324 metavar='CHROME_CATEGORIES', dest='chrome_categories',
325 default=_DEFAULT_CHROME_CATEGORIES)
326 categories.add_option('-s', '--systrace', help='Capture a systrace with the '
327 'chosen comma-delimited systrace categories. You can '
328 'also capture a combined Chrome + systrace by enabling '
329 'both types of categories. Use "list" to see the '
330 'available categories. Systrace is disabled by '
331 'default.', metavar='SYS_CATEGORIES',
332 dest='systrace_categories', default='')
333 categories.add_option('--trace-cc',
334 help='Deprecated, use --trace-frame-viewer.',
336 categories.add_option('--trace-frame-viewer',
337 help='Enable enough trace categories for '
338 'compositor frame viewing.', action='store_true')
339 categories.add_option('--trace-ubercompositor',
340 help='Enable enough trace categories for '
341 'ubercompositor frame data.', action='store_true')
342 categories.add_option('--trace-gpu', help='Enable extra trace categories for '
343 'GPU data.', action='store_true')
344 categories.add_option('--trace-flow', help='Enable extra trace categories '
345 'for IPC message flows.', action='store_true')
346 parser.add_option_group(categories)
348 output_options = optparse.OptionGroup(parser, 'Output options')
349 output_options.add_option('-o', '--output', help='Save trace output to file.')
350 output_options.add_option('--json', help='Save trace as raw JSON instead of '
351 'HTML.', action='store_true')
352 output_options.add_option('--view', help='Open resulting trace file in a '
353 'browser.', action='store_true')
354 parser.add_option_group(output_options)
356 browsers = sorted(_GetSupportedBrowsers().keys())
357 parser.add_option('-b', '--browser', help='Select among installed browsers. '
358 'One of ' + ', '.join(browsers) + ', "stable" is used by '
359 'default.', type='choice', choices=browsers,
361 parser.add_option('-v', '--verbose', help='Verbose logging.',
363 parser.add_option('-z', '--compress', help='Compress the resulting trace '
364 'with gzip. ', action='store_true')
365 options, _args = parser.parse_args()
367 parser.parse_error("""--trace-cc is deprecated.
369 For basic jank busting uses, use --trace-frame-viewer
370 For detailed study of ubercompositor, pass --trace-ubercompositor.
372 When in doubt, just try out --trace-frame-viewer.
376 logging.getLogger().setLevel(logging.DEBUG)
378 adb = android_commands.AndroidCommands()
379 if options.systrace_categories in ['list', 'help']:
380 _PrintMessage('\n'.join(SystraceController.GetCategories(adb)))
383 if not options.time and not options.continuous:
384 _PrintMessage('Time interval or continuous tracing should be specified.')
387 chrome_categories = _ComputeChromeCategories(options)
388 systrace_categories = _ComputeSystraceCategories(options)
389 package_info = _GetSupportedBrowsers()[options.browser]
391 if chrome_categories and 'webview' in systrace_categories:
392 logging.warning('Using the "webview" category in systrace together with '
393 'Chrome tracing results in duplicate trace events.')
396 if chrome_categories:
397 controllers.append(ChromeTracingController(adb,
400 options.ring_buffer))
401 if systrace_categories:
402 controllers.append(SystraceController(adb,
404 options.ring_buffer))
407 _PrintMessage('No trace categories enabled.')
411 options.output = os.path.expanduser(options.output)
412 result = _CaptureAndPullTrace(controllers,
413 options.time if not options.continuous else 0,
418 if sys.platform == 'darwin':
419 os.system('/usr/bin/open %s' % os.path.abspath(result))
421 webbrowser.open(result)
424 if __name__ == '__main__':