1 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
13 from telemetry.core import util
14 from telemetry.core.platform import profiler
15 from telemetry.core.platform.profiler import android_prebuilt_profiler_helper
18 class _SingleProcessPerfProfiler(object):
19 """An internal class for using perf for a given process.
21 On android, this profiler uses pre-built binaries from AOSP.
22 See more details in prebuilt/android/README.txt.
24 def __init__(self, pid, output_file, browser_backend, platform_backend):
26 self._browser_backend = browser_backend
27 self._platform_backend = platform_backend
28 self._output_file = output_file
29 self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
30 self._is_android = platform_backend.GetOSName() == 'android'
33 perf_binary = android_prebuilt_profiler_helper.GetDevicePath(
35 cmd_prefix = ['adb', '-s', browser_backend.adb.device_serial(), 'shell',
37 output_file = os.path.join('/sdcard', 'perf_profiles',
38 os.path.basename(output_file))
39 self._device_output_file = output_file
40 browser_backend.adb.RunShellCommand(
41 'mkdir -p ' + os.path.dirname(self._device_output_file))
44 # In perf 3.13 --call-graph requires an argument, so use
45 # the -g short-hand which does not.
46 self._proc = subprocess.Popen(cmd_prefix +
48 '--pid', str(pid), '--output', output_file],
49 stdout=self._tmp_output_file, stderr=subprocess.STDOUT)
51 def CollectProfile(self):
52 if ('renderer' in self._output_file and
53 not self._is_android and
54 not self._platform_backend.GetCommandLine(self._pid)):
55 logging.warning('Renderer was swapped out during profiling. '
56 'To collect a full profile rerun with '
57 '"--extra-browser-args=--single-process"')
59 device = self._browser_backend.adb.device()
60 perf_pids = device.old_interface.ExtractPid('perf')
61 device.old_interface.RunShellCommand(
62 'kill -SIGINT ' + ' '.join(perf_pids))
63 util.WaitFor(lambda: not device.old_interface.ExtractPid('perf'),
65 self._proc.send_signal(signal.SIGINT)
66 exit_code = self._proc.wait()
70 """perf failed with exit code 128.
71 Try rerunning this script under sudo or setting
72 /proc/sys/kernel/perf_event_paranoid to "-1".\nOutput:\n%s""" %
74 elif exit_code not in (0, -2):
76 'perf failed with exit code %d. Output:\n%s' % (exit_code,
79 self._tmp_output_file.close()
80 cmd = 'perf report -n -i %s' % self._output_file
82 print 'On Android, assuming $CHROMIUM_OUT_DIR/Release/lib has a fresh'
83 print 'symbolized library matching the one on device.'
84 objdump_path = os.path.join(os.environ.get('ANDROID_TOOLCHAIN',
85 '$ANDROID_TOOLCHAIN'),
86 'arm-linux-androideabi-objdump')
87 print 'If you have recent version of perf (3.10+), append the following '
88 print 'to see annotated source code (by pressing the \'a\' key): '
89 print ' --objdump %s' % objdump_path
90 cmd += ' ' + ' '.join(self._PrepareAndroidSymfs())
91 print 'To view the profile, run:'
93 return self._output_file
95 def _PrepareAndroidSymfs(self):
96 """Create a symfs directory using an Android device.
98 Create a symfs directory by pulling the necessary files from an Android
102 List of arguments to be passed to perf to point it to the created symfs.
104 assert self._is_android
105 device = self._browser_backend.adb.device()
106 device.old_interface.Adb().Pull(self._device_output_file, self._output_file)
107 symfs_dir = os.path.dirname(self._output_file)
108 host_app_symfs = os.path.join(symfs_dir, 'data', 'app-lib')
109 if not os.path.exists(host_app_symfs):
110 os.makedirs(host_app_symfs)
111 # On Android, the --symfs parameter needs to map a directory structure
112 # similar to the device, that is:
113 # --symfs=/tmp/foobar and then inside foobar there'll be something like
114 # /tmp/foobar/data/app-lib/$PACKAGE/libname.so
115 # Assume the symbolized library under out/Release/lib is equivalent to
116 # the one in the device, and symlink it in the host to match --symfs.
118 lambda app_lib: app_lib.startswith(self._browser_backend.package),
119 device.old_interface.RunShellCommand('ls /data/app-lib'))
120 os.symlink(os.path.abspath(
121 os.path.join(util.GetChromiumSrcDir(),
122 os.environ.get('CHROMIUM_OUT_DIR', 'out'),
124 os.path.join(host_app_symfs, device_dir[0]))
126 # Also pull copies of common system libraries from the device so perf can
127 # resolve their symbols. Only copy a subset of libraries to make this
129 # TODO(skyostil): Find a way to pull in all system libraries without being
131 host_system_symfs = os.path.join(symfs_dir, 'system', 'lib')
132 if not os.path.exists(host_system_symfs):
133 os.makedirs(host_system_symfs)
134 common_system_libs = [
148 for lib in common_system_libs:
149 device.old_interface.Adb().Pull('/system/lib/%s' % lib,
151 # Pull a copy of the kernel symbols.
152 host_kallsyms = os.path.join(symfs_dir, 'kallsyms')
153 if not os.path.exists(host_kallsyms):
154 device.old_interface.Adb().Pull('/proc/kallsyms', host_kallsyms)
155 return ['--kallsyms', host_kallsyms, '--symfs', symfs_dir]
157 def _GetStdOut(self):
158 self._tmp_output_file.flush()
160 with open(self._tmp_output_file.name) as f:
166 class PerfProfiler(profiler.Profiler):
168 def __init__(self, browser_backend, platform_backend, output_path, state):
169 super(PerfProfiler, self).__init__(
170 browser_backend, platform_backend, output_path, state)
171 process_output_file_map = self._GetProcessOutputFileMap()
172 self._process_profilers = []
173 if platform_backend.GetOSName() == 'android':
174 android_prebuilt_profiler_helper.InstallOnDevice(
175 browser_backend.adb.device(), 'perf')
176 # Make sure kernel pointers are not hidden.
177 browser_backend.adb.device().old_interface.SetProtectedFileContents(
178 '/proc/sys/kernel/kptr_restrict', '0')
179 for pid, output_file in process_output_file_map.iteritems():
180 if 'zygote' in output_file:
182 self._process_profilers.append(
183 _SingleProcessPerfProfiler(
184 pid, output_file, browser_backend, platform_backend))
191 def is_supported(cls, browser_type):
192 if sys.platform != 'linux2':
194 if browser_type.startswith('cros'):
196 return cls._CheckLinuxPerf() or browser_type.startswith('android')
199 def _CheckLinuxPerf(cls):
201 with open(os.devnull, 'w') as devnull:
202 return not subprocess.Popen(['perf', '--version'],
204 stdout=devnull).wait()
209 def CustomizeBrowserOptions(cls, browser_type, options):
210 options.AppendExtraBrowserArgs([
212 '--allow-sandbox-debugging',
215 def CollectProfile(self):
217 for single_process in self._process_profilers:
218 output_files.append(single_process.CollectProfile())
222 def GetTopSamples(cls, file_name, number):
223 """Parses the perf generated profile in |file_name| and returns a
224 {function: period} dict of the |number| hottests functions.
226 assert os.path.exists(file_name)
227 with open(os.devnull, 'w') as devnull:
228 report = subprocess.Popen(
229 ['perf', 'report', '--show-total-period', '-U', '-t', '^', '-i',
231 stdout=subprocess.PIPE, stderr=devnull).communicate()[0]
232 period_by_function = {}
233 for line in report.split('\n'):
234 if not line or line.startswith('#'):
236 fields = line.split('^')
239 period = int(fields[1])
240 function = fields[4].partition(' ')[2]
241 function = re.sub('<.*>', '', function) # Strip template params.
242 function = re.sub('[(].*[)]', '', function) # Strip function params.
243 period_by_function[function] = period
244 if len(period_by_function) == number:
246 return period_by_function