| # Copyright 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 |
| |
| from devil.android import device_errors # pylint: disable=import-error |
| |
| from telemetry.internal.util import binary_manager |
| from telemetry.core import platform |
| from telemetry.internal.platform import profiler |
| from telemetry.internal.platform.profiler import android_profiling_helper |
| |
| from devil.android.perf import perf_control # pylint: disable=import-error |
| |
| |
| _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', |
| # Record raw samples to get CPU information. |
| '--raw-samples', |
| # Increase sampling frequency for better coverage. |
| '--freq', '2000', |
| ] |
| |
| _PERF_OPTIONS_ANDROID = [ |
| # Increase priority to avoid dropping samples. Requires root. |
| '--realtime', '80', |
| ] |
| |
| |
| def _NicePath(path): |
| rel_path = os.path.relpath(path, os.curdir) |
| return rel_path if len(rel_path) < len(path) else path |
| |
| |
| def _PrepareHostForPerf(): |
| kptr_file = '/proc/sys/kernel/kptr_restrict' |
| with open(kptr_file) as f: |
| if f.read().strip() != '0': |
| logging.warning('Making kernel symbols unrestricted. You might have to ' |
| 'enter your password for "sudo".') |
| with tempfile.NamedTemporaryFile() as zero: |
| zero.write('0') |
| zero.flush() |
| subprocess.call(['/usr/bin/sudo', 'cp', zero.name, kptr_file]) |
| |
| |
| def _InstallPerfHost(): |
| perfhost_name = android_profiling_helper.GetPerfhostName() |
| host = platform.GetHostPlatform() |
| if not host.CanLaunchApplication(perfhost_name): |
| host.InstallApplication(perfhost_name) |
| return binary_manager.FetchPath(perfhost_name, 'x86_64', 'linux') |
| |
| |
| class _SingleProcessPerfProfiler(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, |
| perf_binary, perfhost_binary): |
| 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' |
| self._perf_binary = perf_binary |
| self._perfhost_binary = perfhost_binary |
| cmd_prefix = [] |
| perf_args = ['record', '--pid', str(pid)] |
| if self._is_android: |
| cmd_prefix = ['adb', '-s', browser_backend.device.adb.GetDeviceSerial(), |
| 'shell', perf_binary] |
| perf_args += _PERF_OPTIONS_ANDROID |
| output_file = os.path.join('/sdcard', 'perf_profiles', |
| os.path.basename(output_file)) |
| self._device_output_file = output_file |
| browser_backend.device.RunShellCommand( |
| 'mkdir -p ' + os.path.dirname(self._device_output_file)) |
| browser_backend.device.RunShellCommand( |
| 'rm -f ' + self._device_output_file) |
| else: |
| cmd_prefix = [perf_binary] |
| perf_args += ['--output', output_file] + _PERF_OPTIONS |
| self._proc = subprocess.Popen(cmd_prefix + perf_args, |
| stdout=self._tmp_output_file, stderr=subprocess.STDOUT) |
| |
| def CollectProfile(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.device |
| try: |
| binary_name = os.path.basename(self._perf_binary) |
| device.KillAll(binary_name, signum=signal.SIGINT, blocking=True, |
| quiet=True) |
| except device_errors.CommandFailedError: |
| logging.warning('The perf process could not be killed on the device.') |
| 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() |
| cmd = '%s report -n -i %s' % (_NicePath(self._perfhost_binary), |
| self._output_file) |
| if self._is_android: |
| device = self._browser_backend.device |
| try: |
| device.PullFile(self._device_output_file, self._output_file) |
| except: |
| logging.exception('New exception caused by DeviceUtils conversion') |
| raise |
| required_libs = \ |
| android_profiling_helper.GetRequiredLibrariesForPerfProfile( |
| self._output_file) |
| symfs_root = os.path.dirname(self._output_file) |
| kallsyms = android_profiling_helper.CreateSymFs(device, |
| symfs_root, |
| required_libs, |
| use_symlinks=True) |
| cmd += ' --symfs %s --kallsyms %s' % (symfs_root, kallsyms) |
| for lib in required_libs: |
| lib = os.path.join(symfs_root, lib[1:]) |
| if not os.path.exists(lib): |
| continue |
| objdump_path = android_profiling_helper.GetToolchainBinaryPath( |
| lib, 'objdump') |
| if objdump_path: |
| cmd += ' --objdump %s' % _NicePath(objdump_path) |
| break |
| |
| print 'To view the profile, run:' |
| print ' ', cmd |
| return self._output_file |
| |
| 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 PerfProfiler(profiler.Profiler): |
| |
| def __init__(self, browser_backend, platform_backend, output_path, state): |
| super(PerfProfiler, self).__init__( |
| browser_backend, platform_backend, output_path, state) |
| process_output_file_map = self._GetProcessOutputFileMap() |
| self._process_profilers = [] |
| self._perf_control = None |
| |
| perf_binary = perfhost_binary = _InstallPerfHost() |
| try: |
| if platform_backend.GetOSName() == 'android': |
| device = browser_backend.device |
| perf_binary = android_profiling_helper.PrepareDeviceForPerf(device) |
| self._perf_control = perf_control.PerfControl(device) |
| self._perf_control.SetPerfProfilingMode() |
| else: |
| _PrepareHostForPerf() |
| |
| for pid, output_file in process_output_file_map.iteritems(): |
| if 'zygote' in output_file: |
| continue |
| self._process_profilers.append( |
| _SingleProcessPerfProfiler( |
| pid, output_file, browser_backend, platform_backend, |
| perf_binary, perfhost_binary)) |
| except: |
| if self._perf_control: |
| self._perf_control.SetDefaultPerfMode() |
| raise |
| |
| @classmethod |
| def name(cls): |
| return 'perf' |
| |
| @classmethod |
| def is_supported(cls, browser_type): |
| if sys.platform != 'linux2': |
| return False |
| if platform.GetHostPlatform().GetOSName() == 'chromeos': |
| return False |
| return True |
| |
| @classmethod |
| def CustomizeBrowserOptions(cls, browser_type, options): |
| options.AppendExtraBrowserArgs([ |
| '--no-sandbox', |
| '--allow-sandbox-debugging', |
| ]) |
| |
| def CollectProfile(self): |
| if self._perf_control: |
| self._perf_control.SetDefaultPerfMode() |
| 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: |
| _InstallPerfHost() |
| report = subprocess.Popen( |
| [android_profiling_helper.GetPerfhostName(), |
| '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 |