summaryrefslogtreecommitdiffstats
path: root/tools/profile_chrome
diff options
context:
space:
mode:
authoralokp@chromium.org <alokp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-07-18 21:36:55 +0000
committeralokp@chromium.org <alokp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-07-18 21:36:55 +0000
commit46d8f4c5dc5a1081a7e842ad28e4deea9c1736b7 (patch)
tree6143d61ccab14b5f3ac105e7bea2ffb108a97a16 /tools/profile_chrome
parent415038a6f024d502c4d30dd8ca6b8bac03d2faca (diff)
downloadchromium_src-46d8f4c5dc5a1081a7e842ad28e4deea9c1736b7.zip
chromium_src-46d8f4c5dc5a1081a7e842ad28e4deea9c1736b7.tar.gz
chromium_src-46d8f4c5dc5a1081a7e842ad28e4deea9c1736b7.tar.bz2
Move adb_profile_chrome to profile_chrome.
This is being done with the intention to port this tool to other platforms. The first step is to move the existing code to a common location. Review URL: https://codereview.chromium.org/402803005 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@284207 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'tools/profile_chrome')
-rw-r--r--tools/profile_chrome/__init__.py11
-rw-r--r--tools/profile_chrome/chrome_controller.py103
-rw-r--r--tools/profile_chrome/chrome_controller_unittest.py46
-rw-r--r--tools/profile_chrome/controllers.py17
-rw-r--r--tools/profile_chrome/controllers_unittest.py24
-rwxr-xr-xtools/profile_chrome/main.py250
-rw-r--r--tools/profile_chrome/perf_controller.py187
-rw-r--r--tools/profile_chrome/perf_controller_unittest.py50
-rw-r--r--tools/profile_chrome/profiler.py87
-rw-r--r--tools/profile_chrome/profiler_unittest.py78
-rwxr-xr-xtools/profile_chrome/run_tests3
-rw-r--r--tools/profile_chrome/systrace_controller.py95
-rw-r--r--tools/profile_chrome/systrace_controller_unittest.py36
-rw-r--r--tools/profile_chrome/trace_packager.py94
-rw-r--r--tools/profile_chrome/trace_packager_unittest.py35
-rw-r--r--tools/profile_chrome/ui.py27
-rw-r--r--tools/profile_chrome/util.py8
17 files changed, 1151 insertions, 0 deletions
diff --git a/tools/profile_chrome/__init__.py b/tools/profile_chrome/__init__.py
new file mode 100644
index 0000000..760546f
--- /dev/null
+++ b/tools/profile_chrome/__init__.py
@@ -0,0 +1,11 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import sys
+
+
+sys.path.append(os.path.join(os.path.dirname(__file__),
+ os.pardir, os.pardir,
+ 'build', 'android'))
diff --git a/tools/profile_chrome/chrome_controller.py b/tools/profile_chrome/chrome_controller.py
new file mode 100644
index 0000000..1bfb37c
--- /dev/null
+++ b/tools/profile_chrome/chrome_controller.py
@@ -0,0 +1,103 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import json
+import os
+import re
+import time
+
+from profile_chrome import controllers
+
+from pylib import pexpect
+from pylib.device import intent
+
+
+_HEAP_PROFILE_MMAP_PROPERTY = 'heapprof.mmap'
+
+class ChromeTracingController(controllers.BaseController):
+ def __init__(self, device, package_info,
+ categories, ring_buffer, trace_memory=False):
+ controllers.BaseController.__init__(self)
+ self._device = device
+ self._package_info = package_info
+ self._categories = categories
+ self._ring_buffer = ring_buffer
+ self._trace_file = None
+ self._trace_interval = None
+ self._trace_memory = trace_memory
+ self._trace_start_re = \
+ re.compile(r'Logging performance trace to file')
+ self._trace_finish_re = \
+ re.compile(r'Profiler finished[.] Results are in (.*)[.]')
+ self._device.old_interface.StartMonitoringLogcat(clear=False)
+
+ def __repr__(self):
+ return 'chrome trace'
+
+ @staticmethod
+ def GetCategories(device, package_info):
+ device.BroadcastIntent(intent.Intent(
+ action='%s.GPU_PROFILER_LIST_CATEGORIES' % package_info.package))
+ try:
+ json_category_list = device.old_interface.WaitForLogMatch(
+ re.compile(r'{"traceCategoriesList(.*)'), None, timeout=5).group(0)
+ except pexpect.TIMEOUT:
+ raise RuntimeError('Performance trace category list marker not found. '
+ 'Is the correct version of the browser running?')
+
+ record_categories = []
+ disabled_by_default_categories = []
+ json_data = json.loads(json_category_list)['traceCategoriesList']
+ for item in json_data:
+ if item.startswith('disabled-by-default'):
+ disabled_by_default_categories.append(item)
+ else:
+ record_categories.append(item)
+
+ return record_categories, disabled_by_default_categories
+
+ def StartTracing(self, interval):
+ self._trace_interval = interval
+ self._device.old_interface.SyncLogCat()
+ start_extras = {'categories': ','.join(self._categories)}
+ if self._ring_buffer:
+ start_extras['continuous'] = None
+ self._device.BroadcastIntent(intent.Intent(
+ action='%s.GPU_PROFILER_START' % self._package_info.package,
+ extras=start_extras))
+
+ if self._trace_memory:
+ self._device.old_interface.EnableAdbRoot()
+ self._device.SetProp(_HEAP_PROFILE_MMAP_PROPERTY, 1)
+
+ # Chrome logs two different messages related to tracing:
+ #
+ # 1. "Logging performance trace to file"
+ # 2. "Profiler finished. Results are in [...]"
+ #
+ # The first one is printed when tracing starts and the second one indicates
+ # that the trace file is ready to be pulled.
+ try:
+ self._device.old_interface.WaitForLogMatch(
+ self._trace_start_re, None, timeout=5)
+ except pexpect.TIMEOUT:
+ raise RuntimeError('Trace start marker not found. Is the correct version '
+ 'of the browser running?')
+
+ def StopTracing(self):
+ self._device.BroadcastIntent(intent.Intent(
+ action='%s.GPU_PROFILER_STOP' % self._package_info.package))
+ self._trace_file = self._device.old_interface.WaitForLogMatch(
+ self._trace_finish_re, None, timeout=120).group(1)
+ if self._trace_memory:
+ self._device.SetProp(_HEAP_PROFILE_MMAP_PROPERTY, 0)
+
+ def PullTrace(self):
+ # Wait a bit for the browser to finish writing the trace file.
+ time.sleep(self._trace_interval / 4 + 1)
+
+ trace_file = self._trace_file.replace('/storage/emulated/0/', '/sdcard/')
+ host_file = os.path.join(os.path.curdir, os.path.basename(trace_file))
+ self._device.PullFile(trace_file, host_file)
+ return host_file
diff --git a/tools/profile_chrome/chrome_controller_unittest.py b/tools/profile_chrome/chrome_controller_unittest.py
new file mode 100644
index 0000000..aec4e77
--- /dev/null
+++ b/tools/profile_chrome/chrome_controller_unittest.py
@@ -0,0 +1,46 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import json
+
+from profile_chrome import chrome_controller
+from profile_chrome import controllers_unittest
+
+
+class ChromeControllerTest(controllers_unittest.BaseControllerTest):
+ def testGetCategories(self):
+ # Not supported on stable yet.
+ # TODO(skyostil): Remove this once category queries roll into stable.
+ if self.browser == 'stable':
+ return
+
+ categories = \
+ chrome_controller.ChromeTracingController.GetCategories(
+ self.device, self.package_info)
+
+ self.assertEquals(len(categories), 2)
+ self.assertTrue(categories[0])
+ self.assertTrue(categories[1])
+
+ def testTracing(self):
+ categories = '*'
+ ring_buffer = False
+ controller = chrome_controller.ChromeTracingController(self.device,
+ self.package_info,
+ categories,
+ ring_buffer)
+
+ interval = 1
+ try:
+ controller.StartTracing(interval)
+ finally:
+ controller.StopTracing()
+
+ result = controller.PullTrace()
+ try:
+ with open(result) as f:
+ json.loads(f.read())
+ finally:
+ os.remove(result)
diff --git a/tools/profile_chrome/controllers.py b/tools/profile_chrome/controllers.py
new file mode 100644
index 0000000..2a25a97
--- /dev/null
+++ b/tools/profile_chrome/controllers.py
@@ -0,0 +1,17 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import exceptions
+
+
+# pylint: disable=R0201
+class BaseController(object):
+ def StartTracing(self, _):
+ raise exceptions.NotImplementError
+
+ def StopTracing(self):
+ raise exceptions.NotImplementError
+
+ def PullTrace(self):
+ raise exceptions.NotImplementError
diff --git a/tools/profile_chrome/controllers_unittest.py b/tools/profile_chrome/controllers_unittest.py
new file mode 100644
index 0000000..3d64810
--- /dev/null
+++ b/tools/profile_chrome/controllers_unittest.py
@@ -0,0 +1,24 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+from profile_chrome import profiler
+
+from pylib import android_commands
+from pylib.device import device_utils
+from pylib.device import intent
+
+
+class BaseControllerTest(unittest.TestCase):
+ def setUp(self):
+ devices = android_commands.GetAttachedDevices()
+ self.browser = 'stable'
+ self.package_info = profiler.GetSupportedBrowsers()[self.browser]
+ self.device = device_utils.DeviceUtils(devices[0])
+
+ self.device.StartActivity(
+ intent.Intent(activity=self.package_info.activity,
+ package=self.package_info.package),
+ blocking=True)
diff --git a/tools/profile_chrome/main.py b/tools/profile_chrome/main.py
new file mode 100755
index 0000000..32476fb
--- /dev/null
+++ b/tools/profile_chrome/main.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python
+#
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import optparse
+import os
+import sys
+import webbrowser
+
+from profile_chrome import chrome_controller
+from profile_chrome import perf_controller
+from profile_chrome import profiler
+from profile_chrome import systrace_controller
+from profile_chrome import ui
+
+from pylib import android_commands
+from pylib.device import device_utils
+
+
+_DEFAULT_CHROME_CATEGORIES = '_DEFAULT_CHROME_CATEGORIES'
+
+
+def _ComputeChromeCategories(options):
+ categories = []
+ if options.trace_frame_viewer:
+ categories.append('disabled-by-default-cc.debug')
+ if options.trace_ubercompositor:
+ categories.append('disabled-by-default-cc.debug*')
+ if options.trace_gpu:
+ categories.append('disabled-by-default-gpu.debug*')
+ if options.trace_flow:
+ categories.append('disabled-by-default-toplevel.flow')
+ if options.trace_memory:
+ categories.append('disabled-by-default-memory')
+ if options.chrome_categories:
+ categories += options.chrome_categories.split(',')
+ return categories
+
+
+def _ComputeSystraceCategories(options):
+ if not options.systrace_categories:
+ return []
+ return options.systrace_categories.split(',')
+
+
+def _ComputePerfCategories(options):
+ if not options.perf_categories:
+ return []
+ return options.perf_categories.split(',')
+
+
+def _OptionalValueCallback(default_value):
+ def callback(option, _, __, parser):
+ value = default_value
+ if parser.rargs and not parser.rargs[0].startswith('-'):
+ value = parser.rargs.pop(0)
+ setattr(parser.values, option.dest, value)
+ return callback
+
+
+def _CreateOptionParser():
+ parser = optparse.OptionParser(description='Record about://tracing profiles '
+ 'from Android browsers. See http://dev.'
+ 'chromium.org/developers/how-tos/trace-event-'
+ 'profiling-tool for detailed instructions for '
+ 'profiling.')
+
+ timed_options = optparse.OptionGroup(parser, 'Timed tracing')
+ timed_options.add_option('-t', '--time', help='Profile for N seconds and '
+ 'download the resulting trace.', metavar='N',
+ type='float')
+ parser.add_option_group(timed_options)
+
+ cont_options = optparse.OptionGroup(parser, 'Continuous tracing')
+ cont_options.add_option('--continuous', help='Profile continuously until '
+ 'stopped.', action='store_true')
+ cont_options.add_option('--ring-buffer', help='Use the trace buffer as a '
+ 'ring buffer and save its contents when stopping '
+ 'instead of appending events into one long trace.',
+ action='store_true')
+ parser.add_option_group(cont_options)
+
+ chrome_opts = optparse.OptionGroup(parser, 'Chrome tracing options')
+ chrome_opts.add_option('-c', '--categories', help='Select Chrome tracing '
+ 'categories with comma-delimited wildcards, '
+ 'e.g., "*", "cat1*,-cat1a". Omit this option to trace '
+ 'Chrome\'s default categories. Chrome tracing can be '
+ 'disabled with "--categories=\'\'". Use "list" to '
+ 'see the available categories.',
+ metavar='CHROME_CATEGORIES', dest='chrome_categories',
+ default=_DEFAULT_CHROME_CATEGORIES)
+ chrome_opts.add_option('--trace-cc',
+ help='Deprecated, use --trace-frame-viewer.',
+ action='store_true')
+ chrome_opts.add_option('--trace-frame-viewer',
+ help='Enable enough trace categories for '
+ 'compositor frame viewing.', action='store_true')
+ chrome_opts.add_option('--trace-ubercompositor',
+ help='Enable enough trace categories for '
+ 'ubercompositor frame data.', action='store_true')
+ chrome_opts.add_option('--trace-gpu', help='Enable extra trace categories '
+ 'for GPU data.', action='store_true')
+ chrome_opts.add_option('--trace-flow', help='Enable extra trace categories '
+ 'for IPC message flows.', action='store_true')
+ chrome_opts.add_option('--trace-memory', help='Enable extra trace categories '
+ 'for memory profile. (tcmalloc required)',
+ action='store_true')
+ parser.add_option_group(chrome_opts)
+
+ systrace_opts = optparse.OptionGroup(parser, 'Systrace tracing options')
+ systrace_opts.add_option('-s', '--systrace', help='Capture a systrace with '
+ 'the chosen comma-delimited systrace categories. You '
+ 'can also capture a combined Chrome + systrace by '
+ 'enable both types of categories. Use "list" to see '
+ 'the available categories. Systrace is disabled by '
+ 'default.', metavar='SYS_CATEGORIES',
+ dest='systrace_categories', default='')
+ parser.add_option_group(systrace_opts)
+
+ if perf_controller.PerfProfilerController.IsSupported():
+ perf_opts = optparse.OptionGroup(parser, 'Perf profiling options')
+ perf_opts.add_option('-p', '--perf', help='Capture a perf profile with '
+ 'the chosen comma-delimited event categories. '
+ 'Samples CPU cycles by default. Use "list" to see '
+ 'the available sample types.', action='callback',
+ default='', callback=_OptionalValueCallback('cycles'),
+ metavar='PERF_CATEGORIES', dest='perf_categories')
+ parser.add_option_group(perf_opts)
+
+ output_options = optparse.OptionGroup(parser, 'Output options')
+ output_options.add_option('-o', '--output', help='Save trace output to file.')
+ output_options.add_option('--json', help='Save trace as raw JSON instead of '
+ 'HTML.', action='store_true')
+ output_options.add_option('--view', help='Open resulting trace file in a '
+ 'browser.', action='store_true')
+ parser.add_option_group(output_options)
+
+ browsers = sorted(profiler.GetSupportedBrowsers().keys())
+ parser.add_option('-b', '--browser', help='Select among installed browsers. '
+ 'One of ' + ', '.join(browsers) + ', "stable" is used by '
+ 'default.', type='choice', choices=browsers,
+ default='stable')
+ parser.add_option('-v', '--verbose', help='Verbose logging.',
+ action='store_true')
+ parser.add_option('-z', '--compress', help='Compress the resulting trace '
+ 'with gzip. ', action='store_true')
+ return parser
+
+
+def main():
+ parser = _CreateOptionParser()
+ options, _args = parser.parse_args()
+ if options.trace_cc:
+ parser.parse_error("""--trace-cc is deprecated.
+
+For basic jank busting uses, use --trace-frame-viewer
+For detailed study of ubercompositor, pass --trace-ubercompositor.
+
+When in doubt, just try out --trace-frame-viewer.
+""")
+
+ if options.verbose:
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ devices = android_commands.GetAttachedDevices()
+ if len(devices) != 1:
+ parser.error('Exactly 1 device must be attached.')
+ device = device_utils.DeviceUtils(devices[0])
+ package_info = profiler.GetSupportedBrowsers()[options.browser]
+
+ if options.chrome_categories in ['list', 'help']:
+ ui.PrintMessage('Collecting record categories list...', eol='')
+ record_categories = []
+ disabled_by_default_categories = []
+ record_categories, disabled_by_default_categories = \
+ chrome_controller.ChromeTracingController.GetCategories(
+ device, package_info)
+
+ ui.PrintMessage('done')
+ ui.PrintMessage('Record Categories:')
+ ui.PrintMessage('\n'.join('\t%s' % item \
+ for item in sorted(record_categories)))
+
+ ui.PrintMessage('\nDisabled by Default Categories:')
+ ui.PrintMessage('\n'.join('\t%s' % item \
+ for item in sorted(disabled_by_default_categories)))
+
+ return 0
+
+ if options.systrace_categories in ['list', 'help']:
+ ui.PrintMessage('\n'.join(
+ systrace_controller.SystraceController.GetCategories(device)))
+ return 0
+
+ if options.perf_categories in ['list', 'help']:
+ ui.PrintMessage('\n'.join(
+ perf_controller.PerfProfilerController.GetCategories(device)))
+ return 0
+
+ if not options.time and not options.continuous:
+ ui.PrintMessage('Time interval or continuous tracing should be specified.')
+ return 1
+
+ chrome_categories = _ComputeChromeCategories(options)
+ systrace_categories = _ComputeSystraceCategories(options)
+ perf_categories = _ComputePerfCategories(options)
+
+ if chrome_categories and 'webview' in systrace_categories:
+ logging.warning('Using the "webview" category in systrace together with '
+ 'Chrome tracing results in duplicate trace events.')
+
+ enabled_controllers = []
+ if chrome_categories:
+ enabled_controllers.append(
+ chrome_controller.ChromeTracingController(device,
+ package_info,
+ chrome_categories,
+ options.ring_buffer,
+ options.trace_memory))
+ if systrace_categories:
+ enabled_controllers.append(
+ systrace_controller.SystraceController(device,
+ systrace_categories,
+ options.ring_buffer))
+
+ if perf_categories:
+ enabled_controllers.append(
+ perf_controller.PerfProfilerController(device,
+ perf_categories))
+
+ if not enabled_controllers:
+ ui.PrintMessage('No trace categories enabled.')
+ return 1
+
+ if options.output:
+ options.output = os.path.expanduser(options.output)
+ result = profiler.CaptureProfile(
+ enabled_controllers,
+ options.time if not options.continuous else 0,
+ output=options.output,
+ compress=options.compress,
+ write_json=options.json)
+ if options.view:
+ if sys.platform == 'darwin':
+ os.system('/usr/bin/open %s' % os.path.abspath(result))
+ else:
+ webbrowser.open(result)
diff --git a/tools/profile_chrome/perf_controller.py b/tools/profile_chrome/perf_controller.py
new file mode 100644
index 0000000..245435e
--- /dev/null
+++ b/tools/profile_chrome/perf_controller.py
@@ -0,0 +1,187 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import os
+import subprocess
+import sys
+import tempfile
+
+from profile_chrome import controllers
+from profile_chrome import ui
+
+from pylib import android_commands
+from pylib import constants
+from pylib.perf import perf_control
+
+sys.path.append(os.path.join(constants.DIR_SOURCE_ROOT,
+ 'tools',
+ 'telemetry'))
+try:
+ # pylint: disable=F0401
+ from telemetry.core.platform.profiler import android_profiling_helper
+ from telemetry.util import support_binaries
+except ImportError:
+ android_profiling_helper = None
+ support_binaries = None
+
+
+_PERF_OPTIONS = [
+ # Sample across all processes and CPUs to so that the current CPU gets
+ # recorded to each sample.
+ '--all-cpus',
+ # In perf 3.13 --call-graph requires an argument, so use the -g short-hand
+ # which does not.
+ '-g',
+ # Increase priority to avoid dropping samples. Requires root.
+ '--realtime', '80',
+ # Record raw samples to get CPU information.
+ '--raw-samples',
+ # Increase sampling frequency for better coverage.
+ '--freq', '2000',
+]
+
+
+class _PerfProfiler(object):
+ def __init__(self, device, perf_binary, categories):
+ self._device = device
+ self._output_file = android_commands.DeviceTempFile(
+ self._device.old_interface, prefix='perf_output')
+ self._log_file = tempfile.TemporaryFile()
+
+ device_param = (['-s', self._device.old_interface.GetDevice()]
+ if self._device.old_interface.GetDevice() else [])
+ cmd = ['adb'] + device_param + \
+ ['shell', perf_binary, 'record',
+ '--output', self._output_file.name] + _PERF_OPTIONS
+ if categories:
+ cmd += ['--event', ','.join(categories)]
+ self._perf_control = perf_control.PerfControl(self._device)
+ self._perf_control.SetPerfProfilingMode()
+ self._perf_process = subprocess.Popen(cmd,
+ stdout=self._log_file,
+ stderr=subprocess.STDOUT)
+
+ def SignalAndWait(self):
+ self._device.KillAll('perf', signum=signal.SIGINT)
+ self._perf_process.wait()
+ self._perf_control.SetDefaultPerfMode()
+
+ def _FailWithLog(self, msg):
+ self._log_file.seek(0)
+ log = self._log_file.read()
+ raise RuntimeError('%s. Log output:\n%s' % (msg, log))
+
+ def PullResult(self, output_path):
+ if not self._device.FileExists(self._output_file.name):
+ self._FailWithLog('Perf recorded no data')
+
+ perf_profile = os.path.join(output_path,
+ os.path.basename(self._output_file.name))
+ self._device.PullFile(self._output_file.name, perf_profile)
+ if not os.stat(perf_profile).st_size:
+ os.remove(perf_profile)
+ self._FailWithLog('Perf recorded a zero-sized file')
+
+ self._log_file.close()
+ self._output_file.close()
+ return perf_profile
+
+
+class PerfProfilerController(controllers.BaseController):
+ def __init__(self, device, categories):
+ controllers.BaseController.__init__(self)
+ self._device = device
+ self._categories = categories
+ self._perf_binary = self._PrepareDevice(device)
+ self._perf_instance = None
+
+ def __repr__(self):
+ return 'perf profile'
+
+ @staticmethod
+ def IsSupported():
+ return bool(android_profiling_helper)
+
+ @staticmethod
+ def _PrepareDevice(device):
+ if not 'BUILDTYPE' in os.environ:
+ os.environ['BUILDTYPE'] = 'Release'
+ return android_profiling_helper.PrepareDeviceForPerf(device)
+
+ @classmethod
+ def GetCategories(cls, device):
+ perf_binary = cls._PrepareDevice(device)
+ return device.RunShellCommand('%s list' % perf_binary)
+
+ def StartTracing(self, _):
+ self._perf_instance = _PerfProfiler(self._device,
+ self._perf_binary,
+ self._categories)
+
+ def StopTracing(self):
+ if not self._perf_instance:
+ return
+ self._perf_instance.SignalAndWait()
+
+ @staticmethod
+ def _GetInteractivePerfCommand(perfhost_path, perf_profile, symfs_dir,
+ required_libs, kallsyms):
+ cmd = '%s report -n -i %s --symfs %s --kallsyms %s' % (
+ os.path.relpath(perfhost_path, '.'), perf_profile, symfs_dir, kallsyms)
+ for lib in required_libs:
+ lib = os.path.join(symfs_dir, lib[1:])
+ if not os.path.exists(lib):
+ continue
+ objdump_path = android_profiling_helper.GetToolchainBinaryPath(
+ lib, 'objdump')
+ if objdump_path:
+ cmd += ' --objdump %s' % os.path.relpath(objdump_path, '.')
+ break
+ return cmd
+
+ def PullTrace(self):
+ symfs_dir = os.path.join(tempfile.gettempdir(),
+ os.path.expandvars('$USER-perf-symfs'))
+ if not os.path.exists(symfs_dir):
+ os.makedirs(symfs_dir)
+ required_libs = set()
+
+ # Download the recorded perf profile.
+ perf_profile = self._perf_instance.PullResult(symfs_dir)
+ required_libs = \
+ android_profiling_helper.GetRequiredLibrariesForPerfProfile(
+ perf_profile)
+ if not required_libs:
+ logging.warning('No libraries required by perf trace. Most likely there '
+ 'are no samples in the trace.')
+
+ # Build a symfs with all the necessary libraries.
+ kallsyms = android_profiling_helper.CreateSymFs(self._device,
+ symfs_dir,
+ required_libs,
+ use_symlinks=False)
+ perfhost_path = os.path.abspath(support_binaries.FindPath(
+ 'perfhost', 'linux'))
+
+ ui.PrintMessage('\nNote: to view the profile in perf, run:')
+ ui.PrintMessage(' ' + self._GetInteractivePerfCommand(perfhost_path,
+ perf_profile, symfs_dir, required_libs, kallsyms))
+
+ # Convert the perf profile into JSON.
+ perf_script_path = os.path.join(constants.DIR_SOURCE_ROOT,
+ 'tools', 'telemetry', 'telemetry', 'core', 'platform', 'profiler',
+ 'perf_vis', 'perf_to_tracing.py')
+ json_file_name = os.path.basename(perf_profile)
+ with open(os.devnull, 'w') as dev_null, \
+ open(json_file_name, 'w') as json_file:
+ cmd = [perfhost_path, 'script', '-s', perf_script_path, '-i',
+ perf_profile, '--symfs', symfs_dir, '--kallsyms', kallsyms]
+ if subprocess.call(cmd, stdout=json_file, stderr=dev_null):
+ logging.warning('Perf data to JSON conversion failed. The result will '
+ 'not contain any perf samples. You can still view the '
+ 'perf data manually as shown above.')
+ return None
+
+ return json_file_name
diff --git a/tools/profile_chrome/perf_controller_unittest.py b/tools/profile_chrome/perf_controller_unittest.py
new file mode 100644
index 0000000..fbc226f
--- /dev/null
+++ b/tools/profile_chrome/perf_controller_unittest.py
@@ -0,0 +1,50 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import json
+
+from profile_chrome import controllers_unittest
+from profile_chrome import perf_controller
+from profile_chrome import ui
+
+from pylib import constants
+
+
+class PerfProfilerControllerTest(controllers_unittest.BaseControllerTest):
+ def testGetCategories(self):
+ if not perf_controller.PerfProfilerController.IsSupported():
+ return
+ categories = \
+ perf_controller.PerfProfilerController.GetCategories(self.device)
+ assert 'cycles' in ' '.join(categories)
+
+ def testTracing(self):
+ if not perf_controller.PerfProfilerController.IsSupported():
+ return
+ ui.EnableTestMode()
+ categories = ['cycles']
+ controller = perf_controller.PerfProfilerController(self.device,
+ categories)
+
+ interval = 1
+ try:
+ controller.StartTracing(interval)
+ finally:
+ controller.StopTracing()
+
+ result = controller.PullTrace()
+ # Perf-to-JSON conversion can fail if dependencies are missing.
+ if not result:
+ perf_script_path = os.path.join(constants.DIR_SOURCE_ROOT,
+ 'tools', 'telemetry', 'telemetry', 'core', 'platform', 'profiler',
+ 'perf_vis', 'perf_to_tracing.py')
+ assert not os.path.exists(perf_script_path)
+ return
+
+ try:
+ with open(result) as f:
+ json.loads(f.read())
+ finally:
+ os.remove(result)
diff --git a/tools/profile_chrome/profiler.py b/tools/profile_chrome/profiler.py
new file mode 100644
index 0000000..bdc869a
--- /dev/null
+++ b/tools/profile_chrome/profiler.py
@@ -0,0 +1,87 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+
+from profile_chrome import trace_packager
+from profile_chrome import ui
+
+from pylib import constants
+
+
+def _StartTracing(controllers, interval):
+ for controller in controllers:
+ controller.StartTracing(interval)
+
+
+def _StopTracing(controllers):
+ for controller in controllers:
+ controller.StopTracing()
+
+
+def _PullTraces(controllers, output, compress, write_json):
+ ui.PrintMessage('Downloading...', eol='')
+ trace_files = [controller.PullTrace() for controller in controllers]
+ trace_files = [trace for trace in trace_files if trace]
+ if not trace_files:
+ ui.PrintMessage('No results')
+ return []
+ result = trace_packager.PackageTraces(trace_files,
+ output=output,
+ compress=compress,
+ write_json=write_json)
+ ui.PrintMessage('done')
+ ui.PrintMessage('Trace written to file://%s' % os.path.abspath(result))
+ return result
+
+
+def GetSupportedBrowsers():
+ """Returns the package names of all supported browsers."""
+ # Add aliases for backwards compatibility.
+ supported_browsers = {
+ 'stable': constants.PACKAGE_INFO['chrome_stable'],
+ 'beta': constants.PACKAGE_INFO['chrome_beta'],
+ 'dev': constants.PACKAGE_INFO['chrome_dev'],
+ 'build': constants.PACKAGE_INFO['chrome'],
+ }
+ supported_browsers.update(constants.PACKAGE_INFO)
+ unsupported_browsers = ['content_browsertests', 'gtest', 'legacy_browser']
+ for browser in unsupported_browsers:
+ del supported_browsers[browser]
+ return supported_browsers
+
+
+def CaptureProfile(controllers, interval, output=None, compress=False,
+ write_json=False):
+ """Records a profiling trace saves the result to a file.
+
+ Args:
+ controllers: List of tracing controllers.
+ interval: Time interval to capture in seconds. An interval of None (or 0)
+ continues tracing until stopped by the user.
+ output: Output file name or None to use an automatically generated name.
+ compress: If True, the result will be compressed either with gzip or zip
+ depending on the number of captured subtraces.
+ write_json: If True, prefer JSON output over HTML.
+
+ Returns:
+ Path to saved profile.
+ """
+ trace_type = ' + '.join(map(str, controllers))
+ try:
+ _StartTracing(controllers, interval)
+ if interval:
+ ui.PrintMessage('Capturing %d-second %s. Press Enter to stop early...' % \
+ (interval, trace_type), eol='')
+ ui.WaitForEnter(interval)
+ else:
+ ui.PrintMessage('Capturing %s. Press Enter to stop...' % \
+ trace_type, eol='')
+ raw_input()
+ finally:
+ _StopTracing(controllers)
+ if interval:
+ ui.PrintMessage('done')
+
+ return _PullTraces(controllers, output, compress, write_json)
diff --git a/tools/profile_chrome/profiler_unittest.py b/tools/profile_chrome/profiler_unittest.py
new file mode 100644
index 0000000..ef55d70
--- /dev/null
+++ b/tools/profile_chrome/profiler_unittest.py
@@ -0,0 +1,78 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import tempfile
+import unittest
+import zipfile
+
+from profile_chrome import profiler
+from profile_chrome import ui
+
+
+class FakeController(object):
+ def __init__(self, contents='fake-contents'):
+ self.contents = contents
+ self.interval = None
+ self.stopped = False
+ self.filename = None
+
+ def StartTracing(self, interval):
+ self.interval = interval
+
+ def StopTracing(self):
+ self.stopped = True
+
+ def PullTrace(self):
+ with tempfile.NamedTemporaryFile(delete=False) as f:
+ self.filename = f.name
+ f.write(self.contents)
+ return f.name
+
+ def __repr__(self):
+ return 'faketrace'
+
+
+class ProfilerTest(unittest.TestCase):
+ def setUp(self):
+ ui.EnableTestMode()
+
+ def testCaptureBasicProfile(self):
+ controller = FakeController()
+ interval = 1.5
+ result = profiler.CaptureProfile([controller], interval)
+
+ try:
+ self.assertEquals(controller.interval, interval)
+ self.assertTrue(controller.stopped)
+ self.assertTrue(os.path.exists(result))
+ self.assertFalse(os.path.exists(controller.filename))
+ self.assertTrue(result.endswith('.html'))
+ finally:
+ os.remove(result)
+
+ def testCaptureJsonProfile(self):
+ controller = FakeController()
+ result = profiler.CaptureProfile([controller], 1, write_json=True)
+
+ try:
+ self.assertFalse(result.endswith('.html'))
+ with open(result) as f:
+ self.assertEquals(f.read(), controller.contents)
+ finally:
+ os.remove(result)
+
+ def testCaptureMultipleProfiles(self):
+ controllers = [FakeController('c1'), FakeController('c2')]
+ result = profiler.CaptureProfile(controllers, 1, write_json=True)
+
+ try:
+ self.assertTrue(result.endswith('.zip'))
+ self.assertTrue(zipfile.is_zipfile(result))
+ with zipfile.ZipFile(result) as f:
+ self.assertEquals(
+ f.namelist(),
+ [controllers[0].filename[1:], controllers[1].filename[1:]])
+ finally:
+ os.remove(result)
diff --git a/tools/profile_chrome/run_tests b/tools/profile_chrome/run_tests
new file mode 100755
index 0000000..6ae1854
--- /dev/null
+++ b/tools/profile_chrome/run_tests
@@ -0,0 +1,3 @@
+#!/bin/sh
+cd $(dirname $0)/../
+exec python -m unittest discover profile_chrome '*_unittest.py' $@
diff --git a/tools/profile_chrome/systrace_controller.py b/tools/profile_chrome/systrace_controller.py
new file mode 100644
index 0000000..387ea9f
--- /dev/null
+++ b/tools/profile_chrome/systrace_controller.py
@@ -0,0 +1,95 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import threading
+import zlib
+
+from profile_chrome import controllers
+from profile_chrome import util
+
+from pylib import cmd_helper
+
+
+_SYSTRACE_OPTIONS = [
+ # Compress the trace before sending it over USB.
+ '-z',
+ # Use a large trace buffer to increase the polling interval.
+ '-b', '16384'
+]
+
+# Interval in seconds for sampling systrace data.
+_SYSTRACE_INTERVAL = 15
+
+
+class SystraceController(controllers.BaseController):
+ def __init__(self, device, categories, ring_buffer):
+ controllers.BaseController.__init__(self)
+ self._device = device
+ self._categories = categories
+ self._ring_buffer = ring_buffer
+ self._done = threading.Event()
+ self._thread = None
+ self._trace_data = None
+
+ def __repr__(self):
+ return 'systrace'
+
+ @staticmethod
+ def GetCategories(device):
+ return device.RunShellCommand('atrace --list_categories')
+
+ def StartTracing(self, _):
+ self._thread = threading.Thread(target=self._CollectData)
+ self._thread.start()
+
+ def StopTracing(self):
+ self._done.set()
+
+ def PullTrace(self):
+ self._thread.join()
+ self._thread = None
+ if self._trace_data:
+ output_name = 'systrace-%s' % util.GetTraceTimestamp()
+ with open(output_name, 'w') as out:
+ out.write(self._trace_data)
+ return output_name
+
+ def _RunATraceCommand(self, command):
+ # TODO(jbudorick) can this be made work with DeviceUtils?
+ # We use a separate interface to adb because the one from AndroidCommands
+ # isn't re-entrant.
+ device_param = (['-s', self._device.old_interface.GetDevice()]
+ if self._device.old_interface.GetDevice() else [])
+ cmd = ['adb'] + device_param + ['shell', 'atrace', '--%s' % command] + \
+ _SYSTRACE_OPTIONS + self._categories
+ return cmd_helper.GetCmdOutput(cmd)
+
+ def _CollectData(self):
+ trace_data = []
+ self._RunATraceCommand('async_start')
+ try:
+ while not self._done.is_set():
+ self._done.wait(_SYSTRACE_INTERVAL)
+ if not self._ring_buffer or self._done.is_set():
+ trace_data.append(
+ self._DecodeTraceData(self._RunATraceCommand('async_dump')))
+ finally:
+ trace_data.append(
+ self._DecodeTraceData(self._RunATraceCommand('async_stop')))
+ self._trace_data = ''.join([zlib.decompress(d) for d in trace_data])
+
+ @staticmethod
+ def _DecodeTraceData(trace_data):
+ try:
+ trace_start = trace_data.index('TRACE:')
+ except ValueError:
+ raise RuntimeError('Systrace start marker not found')
+ trace_data = trace_data[trace_start + 6:]
+
+ # Collapse CRLFs that are added by adb shell.
+ if trace_data.startswith('\r\n'):
+ trace_data = trace_data.replace('\r\n', '\n')
+
+ # Skip the initial newline.
+ return trace_data[1:]
diff --git a/tools/profile_chrome/systrace_controller_unittest.py b/tools/profile_chrome/systrace_controller_unittest.py
new file mode 100644
index 0000000..30b22c1
--- /dev/null
+++ b/tools/profile_chrome/systrace_controller_unittest.py
@@ -0,0 +1,36 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+
+from profile_chrome import controllers_unittest
+from profile_chrome import systrace_controller
+
+
+class SystraceControllerTest(controllers_unittest.BaseControllerTest):
+ def testGetCategories(self):
+ categories = \
+ systrace_controller.SystraceController.GetCategories(self.device)
+ self.assertTrue(categories)
+ assert 'gfx' in ' '.join(categories)
+
+ def testTracing(self):
+ categories = ['gfx', 'input', 'view']
+ ring_buffer = False
+ controller = systrace_controller.SystraceController(self.device,
+ categories,
+ ring_buffer)
+
+ interval = 1
+ try:
+ controller.StartTracing(interval)
+ finally:
+ controller.StopTracing()
+
+ result = controller.PullTrace()
+ try:
+ with open(result) as f:
+ self.assertTrue('CPU#' in f.read())
+ finally:
+ os.remove(result)
diff --git a/tools/profile_chrome/trace_packager.py b/tools/profile_chrome/trace_packager.py
new file mode 100644
index 0000000..d880f7b
--- /dev/null
+++ b/tools/profile_chrome/trace_packager.py
@@ -0,0 +1,94 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import gzip
+import json
+import os
+import shutil
+import sys
+import zipfile
+
+from profile_chrome import util
+
+from pylib import constants
+
+sys.path.append(os.path.join(constants.DIR_SOURCE_ROOT,
+ 'third_party',
+ 'trace-viewer'))
+# pylint: disable=F0401
+from trace_viewer.build import trace2html
+
+
+def _PackageTracesAsHtml(trace_files, html_file):
+ with open(html_file, 'w') as f:
+ trace2html.WriteHTMLForTracesToFile(trace_files, f)
+ for trace_file in trace_files:
+ os.unlink(trace_file)
+
+
+def _CompressFile(host_file, output):
+ with gzip.open(output, 'wb') as out, \
+ open(host_file, 'rb') as input_file:
+ out.write(input_file.read())
+ os.unlink(host_file)
+
+
+def _ArchiveFiles(host_files, output):
+ with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as z:
+ for host_file in host_files:
+ z.write(host_file)
+ os.unlink(host_file)
+
+
+def _MergeTracesIfNeeded(trace_files):
+ if len(trace_files) <= 1:
+ return trace_files
+ merge_candidates = []
+ for trace_file in trace_files:
+ with open(trace_file) as f:
+ # Try to detect a JSON file cheaply since that's all we can merge.
+ if f.read(1) != '{':
+ continue
+ f.seek(0)
+ try:
+ json_data = json.load(f)
+ except ValueError:
+ continue
+ merge_candidates.append((trace_file, json_data))
+ if len(merge_candidates) <= 1:
+ return trace_files
+
+ other_files = [f for f in trace_files
+ if not f in [c[0] for c in merge_candidates]]
+ merged_file, merged_data = merge_candidates[0]
+ for trace_file, json_data in merge_candidates[1:]:
+ for key, value in json_data.items():
+ if not merged_data.get(key) or json_data[key]:
+ merged_data[key] = value
+ os.unlink(trace_file)
+
+ with open(merged_file, 'w') as f:
+ json.dump(merged_data, f)
+ return [merged_file] + other_files
+
+
+def PackageTraces(trace_files, output=None, compress=False, write_json=False):
+ trace_files = _MergeTracesIfNeeded(trace_files)
+ if not write_json:
+ html_file = os.path.splitext(trace_files[0])[0] + '.html'
+ _PackageTracesAsHtml(trace_files, html_file)
+ trace_files = [html_file]
+
+ if compress and len(trace_files) == 1:
+ result = output or trace_files[0] + '.gz'
+ _CompressFile(trace_files[0], result)
+ elif len(trace_files) > 1:
+ result = output or 'chrome-combined-trace-%s.zip' % util.GetTraceTimestamp()
+ _ArchiveFiles(trace_files, result)
+ elif output:
+ result = output
+ shutil.move(trace_files[0], result)
+ else:
+ result = trace_files[0]
+ return result
diff --git a/tools/profile_chrome/trace_packager_unittest.py b/tools/profile_chrome/trace_packager_unittest.py
new file mode 100644
index 0000000..97c1332
--- /dev/null
+++ b/tools/profile_chrome/trace_packager_unittest.py
@@ -0,0 +1,35 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import json
+import tempfile
+import unittest
+
+from profile_chrome import trace_packager
+
+
+class TracePackagerTest(unittest.TestCase):
+ def testJsonTraceMerging(self):
+ t1 = {'traceEvents': [{'ts': 123, 'ph': 'b'}]}
+ t2 = {'traceEvents': [], 'stackFrames': ['blah']}
+
+ # Both trace files will be merged to a third file and will get deleted in
+ # the process, so there's no need for NamedTemporaryFile to do the
+ # deletion.
+ with tempfile.NamedTemporaryFile(delete=False) as f1, \
+ tempfile.NamedTemporaryFile(delete=False) as f2:
+ f1.write(json.dumps(t1))
+ f2.write(json.dumps(t2))
+ f1.flush()
+ f2.flush()
+
+ with tempfile.NamedTemporaryFile() as output:
+ trace_packager.PackageTraces([f1.name, f2.name],
+ output.name,
+ compress=False,
+ write_json=True)
+ with open(output.name) as output:
+ output = json.load(output)
+ self.assertEquals(output['traceEvents'], t1['traceEvents'])
+ self.assertEquals(output['stackFrames'], t2['stackFrames'])
diff --git a/tools/profile_chrome/ui.py b/tools/profile_chrome/ui.py
new file mode 100644
index 0000000..bc95d48
--- /dev/null
+++ b/tools/profile_chrome/ui.py
@@ -0,0 +1,27 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import select
+import sys
+
+
+def PrintMessage(heading, eol='\n'):
+ sys.stdout.write('%s%s' % (heading, eol))
+ sys.stdout.flush()
+
+
+def WaitForEnter(timeout):
+ select.select([sys.stdin], [], [], timeout)
+
+
+def EnableTestMode():
+ def NoOp(*_, **__):
+ pass
+ # pylint: disable=W0601
+ global PrintMessage
+ global WaitForEnter
+ PrintMessage = NoOp
+ WaitForEnter = NoOp
+ logging.getLogger().disabled = True
diff --git a/tools/profile_chrome/util.py b/tools/profile_chrome/util.py
new file mode 100644
index 0000000..75ef1b6
--- /dev/null
+++ b/tools/profile_chrome/util.py
@@ -0,0 +1,8 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import time
+
+def GetTraceTimestamp():
+ return time.strftime('%Y-%m-%d-%H%M%S', time.localtime())