a219bfe2cecdda2978daaf80349da4ca17262862
[platform/framework/web/crosswalk.git] / src / build / android / adb_profile_chrome.py
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
7 import gzip
8 import logging
9 import optparse
10 import os
11 import re
12 import select
13 import shutil
14 import sys
15 import threading
16 import time
17 import webbrowser
18 import zipfile
19 import zlib
20
21 from pylib import android_commands
22 from pylib import cmd_helper
23 from pylib import constants
24 from pylib import pexpect
25
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
30
31 _DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES'
32
33
34 def _GetTraceTimestamp():
35   return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
36
37
38 class ChromeTracingController(object):
39   def __init__(self, adb, package_info, categories, ring_buffer):
40     self._adb = adb
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)
51
52   def __str__(self):
53     return 'chrome trace'
54
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:
62     #
63     # 1. "Logging performance trace to file [...]"
64     # 2. "Profiler finished. Results are in [...]"
65     #
66     # The first one is printed when tracing starts and the second one indicates
67     # that the trace file is ready to be pulled.
68     try:
69       self._trace_file = self._adb.WaitForLogMatch(self._trace_start_re,
70                                                    None,
71                                                    timeout=5).group(1)
72     except pexpect.TIMEOUT:
73       raise RuntimeError('Trace start marker not found. Is the correct version '
74                          'of the browser running?')
75
76   def StopTracing(self):
77     if not self._trace_file:
78       return
79     self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP')
80     self._adb.WaitForLogMatch(self._trace_finish_re, None, timeout=120)
81
82   def PullTrace(self):
83     # Wait a bit for the browser to finish writing the trace file.
84     time.sleep(self._trace_interval / 4 + 1)
85
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)
89     return host_file
90
91
92 _SYSTRACE_OPTIONS = [
93     # Compress the trace before sending it over USB.
94     '-z',
95     # Use a large trace buffer to increase the polling interval.
96     '-b', '16384'
97 ]
98
99 # Interval in seconds for sampling systrace data.
100 _SYSTRACE_INTERVAL = 15
101
102
103 class SystraceController(object):
104   def __init__(self, adb, categories, ring_buffer):
105     self._adb = adb
106     self._categories = categories
107     self._ring_buffer = ring_buffer
108     self._done = threading.Event()
109     self._thread = None
110     self._trace_data = None
111
112   def __str__(self):
113     return 'systrace'
114
115   @staticmethod
116   def GetCategories(adb):
117     return adb.RunShellCommand('atrace --list_categories')
118
119   def StartTracing(self, _):
120     self._thread = threading.Thread(target=self._CollectData)
121     self._thread.start()
122
123   def StopTracing(self):
124     self._done.set()
125
126   def PullTrace(self):
127     self._thread.join()
128     self._thread = None
129     if self._trace_data:
130       output_name = 'systrace-%s' % _GetTraceTimestamp()
131       with open(output_name, 'w') as out:
132         out.write(self._trace_data)
133       return output_name
134
135   def _RunATraceCommand(self, command):
136     # We use a separate interface to adb because the one from AndroidCommands
137     # isn't re-entrant.
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)
142
143   def _CollectData(self):
144     trace_data = []
145     self._RunATraceCommand('async_start')
146     try:
147       while not self._done.is_set():
148         self._done.wait(_SYSTRACE_INTERVAL)
149         if not self._ring_buffer or self._done.is_set():
150           trace_data.append(
151               self._DecodeTraceData(self._RunATraceCommand('async_dump')))
152     finally:
153       trace_data.append(
154           self._DecodeTraceData(self._RunATraceCommand('async_stop')))
155     self._trace_data = ''.join([zlib.decompress(d) for d in trace_data])
156
157   @staticmethod
158   def _DecodeTraceData(trace_data):
159     try:
160       trace_start = trace_data.index('TRACE:')
161     except ValueError:
162       raise RuntimeError('Systrace start marker not found')
163     trace_data = trace_data[trace_start + 6:]
164
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')
168
169     # Skip the initial newline.
170     return trace_data[1:]
171
172
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'],
180   }
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
186
187
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())
192   os.unlink(host_file)
193
194
195 def _ArchiveFiles(host_files, output):
196   with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as z:
197     for host_file in host_files:
198       z.write(host_file)
199       os.unlink(host_file)
200
201
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)
207
208
209 def _PrintMessage(heading, eol='\n'):
210   sys.stdout.write('%s%s' % (heading, eol))
211   sys.stdout.flush()
212
213
214 def _WaitForEnter(timeout):
215   select.select([sys.stdin], [], [], timeout)
216
217
218 def _StartTracing(controllers, interval):
219   for controller in controllers:
220     controller.StartTracing(interval)
221
222
223 def _StopTracing(controllers):
224   for controller in controllers:
225     controller.StopTracing()
226
227
228 def _PullTraces(controllers, output, compress, write_json):
229   _PrintMessage('Downloading...', eol='')
230   trace_files = []
231   for controller in controllers:
232     trace_files.append(controller.PullTrace())
233
234   if not write_json:
235     html_file = os.path.splitext(trace_files[0])[0] + '.html'
236     _PackageTracesAsHtml(trace_files, html_file)
237     trace_files = [html_file]
238
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)
245   elif output:
246     result = output
247     shutil.move(trace_files[0], result)
248   else:
249     result = trace_files[0]
250
251   _PrintMessage('done')
252   _PrintMessage('Trace written to file://%s' % os.path.abspath(result))
253   return result
254
255
256 def _CaptureAndPullTrace(controllers, interval, output, compress, write_json):
257   trace_type = ' + '.join(map(str, controllers))
258   try:
259     _StartTracing(controllers, interval)
260     if interval:
261       _PrintMessage('Capturing %d-second %s. Press Enter to stop early...' % \
262           (interval, trace_type), eol='')
263       _WaitForEnter(interval)
264     else:
265       _PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='')
266       raw_input()
267   finally:
268     _StopTracing(controllers)
269   if interval:
270     _PrintMessage('done')
271
272   return _PullTraces(controllers, output, compress, write_json)
273
274
275 def _ComputeChromeCategories(options):
276   categories = []
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(',')
287   return categories
288
289
290 def _ComputeSystraceCategories(options):
291   if not options.systrace_categories:
292     return []
293   return options.systrace_categories.split(',')
294
295
296 def main():
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 '
301                                  'profiling.')
302
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',
306                            type='float')
307   parser.add_option_group(timed_options)
308
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.',
315                           action='store_true')
316   parser.add_option_group(cont_options)
317
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.',
335                         action='store_true')
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)
347
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)
355
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,
360                     default='stable')
361   parser.add_option('-v', '--verbose', help='Verbose logging.',
362                     action='store_true')
363   parser.add_option('-z', '--compress', help='Compress the resulting trace '
364                     'with gzip. ', action='store_true')
365   options, _args = parser.parse_args()
366   if options.trace_cc:
367     parser.parse_error("""--trace-cc is deprecated.
368
369 For basic jank busting uses, use  --trace-frame-viewer
370 For detailed study of ubercompositor, pass --trace-ubercompositor.
371
372 When in doubt, just try out --trace-frame-viewer.
373 """)
374
375   if options.verbose:
376     logging.getLogger().setLevel(logging.DEBUG)
377
378   adb = android_commands.AndroidCommands()
379   if options.systrace_categories in ['list', 'help']:
380     _PrintMessage('\n'.join(SystraceController.GetCategories(adb)))
381     return 0
382
383   if not options.time and not options.continuous:
384     _PrintMessage('Time interval or continuous tracing should be specified.')
385     return 1
386
387   chrome_categories = _ComputeChromeCategories(options)
388   systrace_categories = _ComputeSystraceCategories(options)
389   package_info = _GetSupportedBrowsers()[options.browser]
390
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.')
394
395   controllers = []
396   if chrome_categories:
397     controllers.append(ChromeTracingController(adb,
398                                                package_info,
399                                                chrome_categories,
400                                                options.ring_buffer))
401   if systrace_categories:
402     controllers.append(SystraceController(adb,
403                                           systrace_categories,
404                                           options.ring_buffer))
405
406   if not controllers:
407     _PrintMessage('No trace categories enabled.')
408     return 1
409
410   if options.output:
411     options.output = os.path.expanduser(options.output)
412   result = _CaptureAndPullTrace(controllers,
413                                 options.time if not options.continuous else 0,
414                                 options.output,
415                                 options.compress,
416                                 options.json)
417   if options.view:
418     if sys.platform == 'darwin':
419       os.system('/usr/bin/open %s' % os.path.abspath(result))
420     else:
421       webbrowser.open(result)
422
423
424 if __name__ == '__main__':
425   sys.exit(main())