| Index: tools/telemetry/telemetry/core/platform/profiler/perfvis_profiler.py
|
| diff --git a/tools/telemetry/telemetry/core/platform/profiler/perfvis_profiler.py b/tools/telemetry/telemetry/core/platform/profiler/perfvis_profiler.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..8092454265abffe6b80d6d0101bf90fbc9b883b2
|
| --- /dev/null
|
| +++ b/tools/telemetry/telemetry/core/platform/profiler/perfvis_profiler.py
|
| @@ -0,0 +1,315 @@
|
| +# Copyright (c) 2013 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 re
|
| +import signal
|
| +import subprocess
|
| +import sys
|
| +import tempfile
|
| +import pdb
|
| +import json
|
| +from datetime import date
|
| +
|
| +from telemetry.core import util
|
| +from telemetry.core.platform import profiler
|
| +from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
|
| +from telemetry.util import support_binaries
|
| +
|
| +util.AddDirToPythonPath(util.GetChromiumSrcDir(), 'build', 'android')
|
| +from pylib.perf import perf_control # pylint: disable=F0401
|
| +
|
| +timeline_metadata = {}
|
| +
|
| +class _AllProcessPerfProfiler(object):
|
| + """An internal class for using perf for a given process.
|
| +
|
| + On android, this profiler uses pre-built binaries from AOSP.
|
| + See more details in prebuilt/android/README.txt.
|
| + """
|
| + def __init__(self, pid, output_file, browser_backend, platform_backend):
|
| + self._pid = pid
|
| + self._browser_backend = browser_backend
|
| + self._platform_backend = platform_backend
|
| + self._output_file = output_file
|
| + self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
|
| + self._is_android = platform_backend.GetOSName() == 'android'
|
| + cmd_prefix = []
|
| + if self._is_android:
|
| + perf_binary = android_prebuilt_profiler_helper.GetDevicePath(
|
| + 'perf')
|
| + cmd_prefix = ['adb', '-s', browser_backend.adb.device_serial(), 'shell',
|
| + perf_binary]
|
| + output_file = os.path.join('/sdcard', 'perf_profiles',
|
| + os.path.basename(output_file))
|
| + self._device_output_file = output_file
|
| + browser_backend.adb.RunShellCommand(
|
| + 'mkdir -p ' + os.path.dirname(self._device_output_file))
|
| + else:
|
| + cmd_prefix = ['perf']
|
| + # In perf 3.13 --call-graph requires an argument, so use
|
| + # the -g short-hand which does not.
|
| + proc_cmd = cmd_prefix +[
|
| + 'record', '-g', '-r', '80', '-e', 'cycles', '-F', '2000',
|
| + '-a', '-R', '--output', output_file]
|
| +
|
| + print "Start perf:", proc_cmd
|
| + self._proc = subprocess.Popen(proc_cmd,
|
| + stdout=self._tmp_output_file, stderr=subprocess.STDOUT)
|
| +
|
| + def StopProfile(self):
|
| + if ('renderer' in self._output_file and
|
| + not self._is_android and
|
| + not self._platform_backend.GetCommandLine(self._pid)):
|
| + logging.warning('Renderer was swapped out during profiling. '
|
| + 'To collect a full profile rerun with '
|
| + '"--extra-browser-args=--single-process"')
|
| + if self._is_android:
|
| + device = self._browser_backend.adb.device()
|
| + perf_pids = device.old_interface.ExtractPid('perf')
|
| + device.old_interface.RunShellCommand(
|
| + 'kill -SIGINT ' + ' '.join(perf_pids))
|
| + util.WaitFor(lambda: not device.old_interface.ExtractPid('perf'),
|
| + timeout=2)
|
| + self._proc.send_signal(signal.SIGINT)
|
| + exit_code = self._proc.wait()
|
| + try:
|
| + if exit_code == 128:
|
| + raise Exception(
|
| + """perf failed with exit code 128.
|
| +Try rerunning this script under sudo or setting
|
| +/proc/sys/kernel/perf_event_paranoid to "-1".\nOutput:\n%s""" %
|
| + self._GetStdOut())
|
| + elif exit_code not in (0, -2):
|
| + raise Exception(
|
| + 'perf failed with exit code %d. Output:\n%s' % (exit_code,
|
| + self._GetStdOut()))
|
| + finally:
|
| + self._tmp_output_file.close()
|
| +
|
| + def CollectProfile(self):
|
| + perfvis_path = os.path.abspath(os.path.dirname(__file__)) + '/perf_vis/'
|
| + perfhost_path = \
|
| + os.path.abspath(support_binaries.FindPath('perfhost', 'linux'))
|
| + # Make perfhost executable.
|
| + os.chmod(perfhost_path, 0755)
|
| +
|
| + symfs = []
|
| + if self._is_android:
|
| + print 'On Android, assuming $CHROMIUM_OUT_DIR/Release/lib has a fresh'
|
| + print 'symbolized library matching the one on device.'
|
| + objdump_path = os.path.join(os.environ.get('ANDROID_TOOLCHAIN',
|
| + '$ANDROID_TOOLCHAIN'),
|
| + 'arm-linux-androideabi-objdump')
|
| + print 'If you have recent version of perf (3.10+), append the following '
|
| + print 'to see annotated source code (by pressing the \'a\' key): '
|
| + print ' --objdump %s' % objdump_path
|
| +
|
| + # Pull perf output file.
|
| + device = self._browser_backend.adb.device()
|
| + device.old_interface.Adb().Pull(
|
| + self._device_output_file, self._output_file)
|
| +
|
| + # Parse required symbol libs.
|
| + required_libs = []
|
| + with open(os.devnull, 'w') as null_file:
|
| + repcmd = [perfhost_path, 'script', '-s',
|
| + perfvis_path + 'perf_to_tracing.py', '-i', self._output_file]
|
| + print "Parsing required libs...", repcmd
|
| + proc = subprocess.Popen(
|
| + repcmd, stdout=null_file, stderr=subprocess.PIPE)
|
| + regex = re.compile('Failed to open ([^,]+), continuing.*')
|
| + required_libs = [
|
| + m.group(1) for m in [
|
| + regex.match(line) for line in proc.stderr.readlines()] if m]
|
| + proc.wait()
|
| +
|
| + symfs = self._PrepareAndroidSymfs(required_libs)
|
| +
|
| + # Output bottom-up perf report
|
| + with open(self._output_file + '.json', 'w') as report_file:
|
| + repcmd = [perfhost_path, 'script', '-s', perfvis_path +
|
| + 'perf_to_tracing.py', '-i', self._output_file] + symfs
|
| + print 'Generating tracing json with: ' + ' '.join(repcmd)
|
| + print ' ->', self._output_file + '.json'
|
| + subprocess.call(repcmd, stdout=report_file, stderr=subprocess.PIPE)
|
| +
|
| + with open(self._output_file + 'meta.json', 'w') as meta_file:
|
| + json.dump(timeline_metadata, meta_file, indent=1)
|
| +
|
| + device = self._browser_backend.adb.device()
|
| + max_freq = int(device.old_interface.GetFileContents(
|
| + '/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq')[0]) * 1000
|
| +
|
| + today = date.today()
|
| + output_ext = '_%02d%02d%02d.html' % (today.day, today.month, today.year)
|
| +
|
| + repcmd = [perfvis_path + 'perf_vis.py',
|
| + '-m', self._output_file + 'meta.json',
|
| + '-o', self._output_file + output_ext,
|
| + '-c', str(max_freq),
|
| + self._output_file + '.json']
|
| + print 'Generating perf-vis with: ' + ' '.join(repcmd)
|
| + subprocess.call(
|
| + repcmd, stderr=subprocess.STDOUT)
|
| +
|
| + return self._output_file
|
| +
|
| + def _PrepareAndroidSymfs(self, required_libs):
|
| + """Create a symfs directory using an Android device.
|
| +
|
| + Create a symfs directory by pulling the necessary files from an Android
|
| + device.
|
| +
|
| + Returns:
|
| + List of arguments to be passed to perf to point it to the created symfs.
|
| + """
|
| + assert self._is_android
|
| + device = self._browser_backend.adb.device()
|
| + device.old_interface.Adb().Pull(self._device_output_file, self._output_file)
|
| + symfs_dir = os.path.dirname(self._output_file)
|
| + host_app_symfs = os.path.join(symfs_dir, 'data', 'app-lib')
|
| + if not os.path.exists(host_app_symfs):
|
| + print "starts with", self._browser_backend.package
|
| + os.makedirs(host_app_symfs)
|
| + os.makedirs(os.path.join(symfs_dir, 'data', 'app'))
|
| + # On Android, the --symfs parameter needs to map a directory structure
|
| + # similar to the device, that is:
|
| + # --symfs=/tmp/foobar and then inside foobar there'll be something like
|
| + # /tmp/foobar/data/app-lib/$PACKAGE/libname.so
|
| + # Assume the symbolized library under out/Release/lib is equivalent to
|
| + # the one in the device, and symlink it in the host to match --symfs.
|
| +
|
| + # Also pull copies of required libraries from the device so perf can
|
| + # resolve their symbols.
|
| +
|
| + for lib in required_libs:
|
| + #print "Lib", lib
|
| + if lib.startswith('/system/lib/') or lib.startswith('/vendor/lib/'):
|
| + print 'Pulling', lib
|
| + device.old_interface.Adb().Pull('/%s' % lib,
|
| + symfs_dir + '/%s' % lib)
|
| + elif lib.startswith('/data/') and self._browser_backend.package in lib:
|
| + # Assume the symbolized library under out/Release/lib is equivalent to
|
| + # the one in the device, and symlink it in the host to match --symfs.
|
| + chromepath = os.path.abspath(symfs_dir + '/%s' % lib)
|
| + chromedir = os.path.dirname(chromepath)
|
| + lib_basename = os.path.basename(chromepath)
|
| + os.makedirs(chromedir)
|
| + print "Symlink '%s' lib to" % lib_basename, chromepath
|
| + os.symlink(os.path.abspath(
|
| + os.path.join(util.GetChromiumSrcDir(),
|
| + os.environ.get('CHROMIUM_OUT_DIR', 'out'),
|
| + 'Release', 'lib', lib_basename)),
|
| + chromepath)
|
| + # Pull a copy of the kernel symbols.
|
| + host_kallsyms = os.path.join(symfs_dir, 'kallsyms')
|
| + if not os.path.exists(host_kallsyms):
|
| + device.old_interface.Adb().Pull('/proc/kallsyms', host_kallsyms)
|
| + return ['--kallsyms', host_kallsyms, '--symfs', symfs_dir]
|
| +
|
| + def _GetStdOut(self):
|
| + self._tmp_output_file.flush()
|
| + try:
|
| + with open(self._tmp_output_file.name) as f:
|
| + return f.read()
|
| + except IOError:
|
| + return ''
|
| +
|
| +
|
| +class PerfVisProfiler(profiler.Profiler):
|
| +
|
| + def __init__(self, browser_backend, platform_backend, output_path, state):
|
| + super(PerfVisProfiler, self).__init__(
|
| + browser_backend, platform_backend, output_path, state)
|
| + print "New PerfVisProfiler..."
|
| + self._perf_control = None
|
| + if platform_backend.GetOSName() == 'android':
|
| + print " ...installing 'perf' to device."
|
| +
|
| + device = browser_backend.adb.device()
|
| + android_prebuilt_profiler_helper.InstallOnDevice(
|
| + device, 'perf')
|
| + # Make sure kernel pointers are not hidden.
|
| + browser_backend.adb.device().old_interface.SetProtectedFileContents(
|
| + '/proc/sys/kernel/kptr_restrict', '0')
|
| +
|
| + self._perf_control = perf_control.PerfControl(device)
|
| + self._perf_control.SetPerfProfilingMode()
|
| +
|
| + process_output_file_map = self._GetProcessOutputFileMap()
|
| + self._process_profilers = []
|
| + self._browser_backend = browser_backend
|
| + self._platform_backend = platform_backend
|
| + for pid, output_file in process_output_file_map.iteritems():
|
| + if output_file.find('browser0') >= 0:
|
| + self._process_profilers.append(
|
| + _AllProcessPerfProfiler(
|
| + pid, output_file, browser_backend, platform_backend))
|
| + break
|
| +
|
| + @classmethod
|
| + def name(cls):
|
| + return 'perfvis'
|
| +
|
| + @classmethod
|
| + def is_supported(cls, browser_type):
|
| + if sys.platform != 'linux2':
|
| + return False
|
| + if browser_type.startswith('cros'):
|
| + return False
|
| + return True
|
| +
|
| + @classmethod
|
| + def CustomizeBrowserOptions(cls, browser_type, options):
|
| + options.AppendExtraBrowserArgs([
|
| + '--no-sandbox',
|
| + '--allow-sandbox-debugging',
|
| + ])
|
| +
|
| + @classmethod
|
| + def WillProfile(cls, browser_backend, platform_backend):
|
| + pass
|
| +
|
| + def StopProfile(self):
|
| + # Stop all process profilers together.
|
| + for single_process in self._process_profilers:
|
| + single_process.StopProfile()
|
| +
|
| + if self._perf_control:
|
| + self._perf_control.SetDefaultPerfMode()
|
| +
|
| + def CollectProfile(self):
|
| + output_files = []
|
| + for single_process in self._process_profilers:
|
| + output_files.append(single_process.CollectProfile())
|
| + return output_files
|
| +
|
| + @classmethod
|
| + def GetTopSamples(cls, file_name, number):
|
| + """Parses the perf generated profile in |file_name| and returns a
|
| + {function: period} dict of the |number| hottests functions.
|
| + """
|
| + assert os.path.exists(file_name)
|
| + with open(os.devnull, 'w') as devnull:
|
| + report = subprocess.Popen(
|
| + ['perf', 'report', '--show-total-period', '-U', '-t', '^', '-i',
|
| + file_name],
|
| + stdout=subprocess.PIPE, stderr=devnull).communicate()[0]
|
| + period_by_function = {}
|
| + for line in report.split('\n'):
|
| + if not line or line.startswith('#'):
|
| + continue
|
| + fields = line.split('^')
|
| + if len(fields) != 5:
|
| + continue
|
| + period = int(fields[1])
|
| + function = fields[4].partition(' ')[2]
|
| + function = re.sub('<.*>', '', function) # Strip template params.
|
| + function = re.sub('[(].*[)]', '', function) # Strip function params.
|
| + period_by_function[function] = period
|
| + if len(period_by_function) == number:
|
| + break
|
| + return period_by_function
|
|
|