19a72fef19aa8348c0948a9e13e0285aa5b65d14
[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 base64
8 import gzip
9 import logging
10 import optparse
11 import os
12 import re
13 import select
14 import shutil
15 import sys
16 import threading
17 import time
18 import webbrowser
19 import zipfile
20 import zlib
21
22 from pylib import android_commands
23 from pylib import cmd_helper
24 from pylib import constants
25 from pylib import pexpect
26
27
28 _TRACE_VIEWER_TEMPLATE = """<!DOCTYPE html>
29 <html>
30   <head>
31     <title>%(title)s</title>
32     <style>
33       %(timeline_css)s
34     </style>
35     <style>
36       .view {
37         overflow: hidden;
38         position: absolute;
39         top: 0;
40         bottom: 0;
41         left: 0;
42         right: 0;
43       }
44     </style>
45     <script>
46       %(timeline_js)s
47     </script>
48     <script>
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;
57       });
58     </script>
59   </head>
60   <body>
61     <div class="view"></view>
62   </body>
63 </html>"""
64
65 _DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES'
66
67
68 def _GetTraceTimestamp():
69  return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())
70
71
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', {}, {})
81
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])
85
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
94       }
95       html_file.write(html)
96
97
98 class ChromeTracingController(object):
99   def __init__(self, adb, package_info, categories, ring_buffer):
100     self._adb = adb
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)
111
112   def __str__(self):
113     return 'chrome trace'
114
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:
122     #
123     # 1. "Logging performance trace to file [...]"
124     # 2. "Profiler finished. Results are in [...]"
125     #
126     # The first one is printed when tracing starts and the second one indicates
127     # that the trace file is ready to be pulled.
128     try:
129       self._trace_file = self._adb.WaitForLogMatch(self._trace_start_re,
130                                                    None,
131                                                    timeout=5).group(1)
132     except pexpect.TIMEOUT:
133       raise RuntimeError('Trace start marker not found. Is the correct version '
134                          'of the browser running?')
135
136   def StopTracing(self):
137     if not self._trace_file:
138       return
139     self._adb.BroadcastIntent(self._package_info.package, 'GPU_PROFILER_STOP')
140     self._adb.WaitForLogMatch(self._trace_finish_re, None, timeout=120)
141
142   def PullTrace(self):
143     # Wait a bit for the browser to finish writing the trace file.
144     time.sleep(self._trace_interval / 4 + 1)
145
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)
149     return host_file
150
151
152 _SYSTRACE_OPTIONS = [
153     # Compress the trace before sending it over USB.
154     '-z',
155     # Use a large trace buffer to increase the polling interval.
156     '-b', '16384'
157 ]
158
159 # Interval in seconds for sampling systrace data.
160 _SYSTRACE_INTERVAL = 15
161
162
163 class SystraceController(object):
164   def __init__(self, adb, categories, ring_buffer):
165     self._adb = adb
166     self._categories = categories
167     self._ring_buffer = ring_buffer
168     self._done = threading.Event()
169     self._thread = None
170     self._trace_data = None
171
172   def __str__(self):
173     return 'systrace'
174
175   @staticmethod
176   def GetCategories(adb):
177     return adb.RunShellCommand('atrace --list_categories')
178
179   def StartTracing(self, interval):
180     self._thread = threading.Thread(target=self._CollectData)
181     self._thread.start()
182
183   def StopTracing(self):
184     self._done.set()
185
186   def PullTrace(self):
187     self._thread.join()
188     self._thread = None
189     if self._trace_data:
190       output_name = 'systrace-%s' % _GetTraceTimestamp()
191       with open(output_name, 'w') as out:
192         out.write(self._trace_data)
193       return output_name
194
195   def _RunATraceCommand(self, command):
196     # We use a separate interface to adb because the one from AndroidCommands
197     # isn't re-entrant.
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)
202
203   def _CollectData(self):
204     trace_data = []
205     self._RunATraceCommand('async_start')
206     try:
207       while not self._done.is_set():
208         self._done.wait(_SYSTRACE_INTERVAL)
209         if not self._ring_buffer or self._done.is_set():
210           trace_data.append(
211               self._DecodeTraceData(self._RunATraceCommand('async_dump')))
212     finally:
213       trace_data.append(
214           self._DecodeTraceData(self._RunATraceCommand('async_stop')))
215     self._trace_data = ''.join([zlib.decompress(d) for d in trace_data])
216
217   @staticmethod
218   def _DecodeTraceData(trace_data):
219     try:
220       trace_start = trace_data.index('TRACE:')
221     except ValueError:
222       raise RuntimeError('Systrace start marker not found')
223     trace_data = trace_data[trace_start + 6:]
224
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')
228
229     # Skip the initial newline.
230     return trace_data[1:]
231
232
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'],
240   }
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
246
247
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())
252   os.unlink(host_file)
253
254
255 def _ArchiveFiles(host_files, output):
256   with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as z:
257     for host_file in host_files:
258       z.write(host_file)
259       os.unlink(host_file)
260
261
262 def _PrintMessage(heading, eol='\n'):
263   sys.stdout.write('%s%s' % (heading, eol))
264   sys.stdout.flush()
265
266
267 def _WaitForEnter(timeout):
268   select.select([sys.stdin], [], [], timeout)
269
270
271 def _StartTracing(controllers, interval):
272   for controller in controllers:
273     controller.StartTracing(interval)
274
275
276 def _StopTracing(controllers):
277   for controller in controllers:
278     controller.StopTracing()
279
280
281 def _PullTraces(controllers, output, compress, write_html):
282   _PrintMessage('Downloading...', eol='')
283   trace_files = []
284   for controller in controllers:
285     trace_files.append(controller.PullTrace())
286
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)
293   elif output:
294     result = output
295     shutil.move(trace_files[0], result)
296   else:
297     result = trace_files[0]
298
299   if write_html:
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)
304
305   _PrintMessage('done')
306   _PrintMessage('Trace written to %s' % os.path.abspath(result))
307   return result
308
309
310 def _CaptureAndPullTrace(controllers, interval, output, compress, write_html):
311   trace_type = ' + '.join(map(str, controllers))
312   try:
313     _StartTracing(controllers, interval)
314     if interval:
315       _PrintMessage('Capturing %d-second %s. Press Enter to stop early...' % \
316           (interval, trace_type), eol='')
317       _WaitForEnter(interval)
318     else:
319       _PrintMessage('Capturing %s. Press Enter to stop...' % trace_type, eol='')
320       raw_input()
321   finally:
322     _StopTracing(controllers)
323   if interval:
324     _PrintMessage('done')
325
326   return _PullTraces(controllers, output, compress, write_html)
327
328
329 def _ComputeChromeCategories(options):
330   categories = []
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(',')
339   return categories
340
341
342 def _ComputeSystraceCategories(options):
343   if not options.systrace_categories:
344     return []
345   return options.systrace_categories.split(',')
346
347
348 def main():
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 '
353                                  'profiling.')
354
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',
358                            type='float')
359   parser.add_option_group(timed_options)
360
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.',
367                           action='store_true')
368   parser.add_option_group(cont_options)
369
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.',
387                         action='store_true')
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)
397
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)
405
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,
410                     default='stable')
411   parser.add_option('-v', '--verbose', help='Verbose logging.',
412                     action='store_true')
413   parser.add_option('-z', '--compress', help='Compress the resulting trace '
414                     'with gzip. ', action='store_true')
415   options, args = parser.parse_args()
416   if options.trace_cc:
417     parser.parse_error("""--trace-cc is deprecated.
418
419 For basic jank busting uses, use  --trace-frame-viewer
420 For detailed study of ubercompositor, pass --trace-ubercompositor.
421
422 When in doubt, just try out --trace-frame-viewer.
423 """)
424
425   if options.verbose:
426     logging.getLogger().setLevel(logging.DEBUG)
427
428   adb = android_commands.AndroidCommands()
429   if options.systrace_categories in ['list', 'help']:
430     _PrintMessage('\n'.join(SystraceController.GetCategories(adb)))
431     return 0
432
433   if not options.time and not options.continuous:
434     _PrintMessage('Time interval or continuous tracing should be specified.')
435     return 1
436
437   chrome_categories = _ComputeChromeCategories(options)
438   systrace_categories = _ComputeSystraceCategories(options)
439   package_info = _GetSupportedBrowsers()[options.browser]
440
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.')
444
445   controllers = []
446   if chrome_categories:
447     controllers.append(ChromeTracingController(adb,
448                                                package_info,
449                                                chrome_categories,
450                                                options.ring_buffer))
451   if systrace_categories:
452     controllers.append(SystraceController(adb,
453                                           systrace_categories,
454                                           options.ring_buffer))
455
456   if not controllers:
457     _PrintMessage('No trace categories enabled.')
458     return 1
459
460   result = _CaptureAndPullTrace(controllers,
461                                 options.time if not options.continuous else 0,
462                                 options.output,
463                                 options.compress,
464                                 options.html)
465   if options.view:
466     webbrowser.open(result)
467
468
469 if __name__ == '__main__':
470   sys.exit(main())