diff options
author | alokp@chromium.org <alokp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-07-18 21:36:55 +0000 |
---|---|---|
committer | alokp@chromium.org <alokp@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-07-18 21:36:55 +0000 |
commit | 46d8f4c5dc5a1081a7e842ad28e4deea9c1736b7 (patch) | |
tree | 6143d61ccab14b5f3ac105e7bea2ffb108a97a16 /tools/profile_chrome | |
parent | 415038a6f024d502c4d30dd8ca6b8bac03d2faca (diff) | |
download | chromium_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__.py | 11 | ||||
-rw-r--r-- | tools/profile_chrome/chrome_controller.py | 103 | ||||
-rw-r--r-- | tools/profile_chrome/chrome_controller_unittest.py | 46 | ||||
-rw-r--r-- | tools/profile_chrome/controllers.py | 17 | ||||
-rw-r--r-- | tools/profile_chrome/controllers_unittest.py | 24 | ||||
-rwxr-xr-x | tools/profile_chrome/main.py | 250 | ||||
-rw-r--r-- | tools/profile_chrome/perf_controller.py | 187 | ||||
-rw-r--r-- | tools/profile_chrome/perf_controller_unittest.py | 50 | ||||
-rw-r--r-- | tools/profile_chrome/profiler.py | 87 | ||||
-rw-r--r-- | tools/profile_chrome/profiler_unittest.py | 78 | ||||
-rwxr-xr-x | tools/profile_chrome/run_tests | 3 | ||||
-rw-r--r-- | tools/profile_chrome/systrace_controller.py | 95 | ||||
-rw-r--r-- | tools/profile_chrome/systrace_controller_unittest.py | 36 | ||||
-rw-r--r-- | tools/profile_chrome/trace_packager.py | 94 | ||||
-rw-r--r-- | tools/profile_chrome/trace_packager_unittest.py | 35 | ||||
-rw-r--r-- | tools/profile_chrome/ui.py | 27 | ||||
-rw-r--r-- | tools/profile_chrome/util.py | 8 |
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()) |