1# Copyright 2018 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. 4 5import contextlib 6import os 7import shutil 8import subprocess 9import sys 10import tempfile 11 12from devil import devil_env 13from devil.android import device_signal 14from devil.android.sdk import version_codes 15from pylib import constants 16 17 18def _ProcessType(proc): 19 _, _, suffix = proc.name.partition(':') 20 if not suffix: 21 return 'browser' 22 if suffix.startswith('sandboxed_process'): 23 return 'renderer' 24 if suffix.startswith('privileged_process'): 25 return 'gpu' 26 return None 27 28 29def _GetSpecifiedPID(device, package_name, process_specifier): 30 if process_specifier is None: 31 return None 32 33 # Check for numeric PID 34 try: 35 pid = int(process_specifier) 36 return pid 37 except ValueError: 38 pass 39 40 # Check for exact process name; can be any of these formats: 41 # <package>:<process name>, i.e. 'org.chromium.chrome:sandboxed_process0' 42 # :<process name>, i.e. ':sandboxed_process0' 43 # <process name>, i.e. 'sandboxed_process0' 44 full_process_name = process_specifier 45 if process_specifier.startswith(':'): 46 full_process_name = package_name + process_specifier 47 elif ':' not in process_specifier: 48 full_process_name = '%s:%s' % (package_name, process_specifier) 49 matching_processes = device.ListProcesses(full_process_name) 50 if len(matching_processes) == 1: 51 return matching_processes[0].pid 52 if len(matching_processes) > 1: 53 raise RuntimeError('Found %d processes with name "%s".' % ( 54 len(matching_processes), process_specifier)) 55 56 # Check for process type (i.e. 'renderer') 57 package_processes = device.ListProcesses(package_name) 58 matching_processes = [p for p in package_processes if ( 59 _ProcessType(p) == process_specifier)] 60 if process_specifier == 'renderer' and len(matching_processes) > 1: 61 raise RuntimeError('Found %d renderer processes; please re-run with only ' 62 'one open tab.' % len(matching_processes)) 63 if len(matching_processes) != 1: 64 raise RuntimeError('Found %d processes of type "%s".' % ( 65 len(matching_processes), process_specifier)) 66 return matching_processes[0].pid 67 68 69def _ThreadsForProcess(device, pid): 70 # The thread list output format for 'ps' is the same regardless of version. 71 # Here's the column headers, and a sample line for a thread belonging to 72 # pid 12345 (note that the last few columns are not aligned with headers): 73 # 74 # USER PID TID PPID VSZ RSS WCHAN ADDR S CMD 75 # u0_i101 12345 24680 567 1357902 97531 futex_wait_queue_me e85acd9c S \ 76 # CrRendererMain 77 if device.build_version_sdk >= version_codes.OREO: 78 pid_regex = ( 79 r'^[[:graph:]]\{1,\}[[:blank:]]\{1,\}%d[[:blank:]]\{1,\}' % pid) 80 ps_cmd = "ps -T -e | grep '%s'" % pid_regex 81 ps_output_lines = device.RunShellCommand( 82 ps_cmd, shell=True, check_return=True) 83 else: 84 ps_cmd = ['ps', '-p', str(pid), '-t'] 85 ps_output_lines = device.RunShellCommand(ps_cmd, check_return=True) 86 result = [] 87 for l in ps_output_lines: 88 fields = l.split() 89 # fields[2] is tid, fields[-1] is thread name. Output may include an entry 90 # for the process itself with tid=pid; omit that one. 91 if fields[2] == str(pid): 92 continue 93 result.append((int(fields[2]), fields[-1])) 94 return result 95 96 97def _ThreadType(thread_name): 98 if not thread_name: 99 return 'unknown' 100 if (thread_name.startswith('Chrome_ChildIO') or 101 thread_name.startswith('Chrome_IO')): 102 return 'io' 103 if thread_name.startswith('Compositor'): 104 return 'compositor' 105 if (thread_name.startswith('ChildProcessMai') or 106 thread_name.startswith('CrGpuMain') or 107 thread_name.startswith('CrRendererMain')): 108 return 'main' 109 if thread_name.startswith('RenderThread'): 110 return 'render' 111 112 113def _GetSpecifiedTID(device, pid, thread_specifier): 114 if thread_specifier is None: 115 return None 116 117 # Check for numeric TID 118 try: 119 tid = int(thread_specifier) 120 return tid 121 except ValueError: 122 pass 123 124 # Check for thread type 125 if pid is not None: 126 matching_threads = [t for t in _ThreadsForProcess(device, pid) if ( 127 _ThreadType(t[1]) == thread_specifier)] 128 if len(matching_threads) != 1: 129 raise RuntimeError('Found %d threads of type "%s".' % ( 130 len(matching_threads), thread_specifier)) 131 return matching_threads[0][0] 132 133 return None 134 135 136def PrepareDevice(device): 137 if device.build_version_sdk < version_codes.NOUGAT: 138 raise RuntimeError('Simpleperf profiling is only supported on Android N ' 139 'and later.') 140 141 # Necessary for profiling 142 # https://android-review.googlesource.com/c/platform/system/sepolicy/+/234400 143 device.SetProp('security.perf_harden', '0') 144 145 146def InstallSimpleperf(device, package_name): 147 package_arch = device.GetPackageArchitecture(package_name) or 'armeabi-v7a' 148 host_simpleperf_path = devil_env.config.LocalPath('simpleperf', package_arch) 149 if not host_simpleperf_path: 150 raise Exception('Could not get path to simpleperf executable on host.') 151 device_simpleperf_path = '/'.join( 152 ('/data/local/tmp/profilers', package_arch, 'simpleperf')) 153 device.PushChangedFiles([(host_simpleperf_path, device_simpleperf_path)]) 154 return device_simpleperf_path 155 156 157@contextlib.contextmanager 158def RunSimpleperf(device, device_simpleperf_path, package_name, 159 process_specifier, thread_specifier, profiler_args, 160 host_out_path): 161 pid = _GetSpecifiedPID(device, package_name, process_specifier) 162 tid = _GetSpecifiedTID(device, pid, thread_specifier) 163 if pid is None and tid is None: 164 raise RuntimeError('Could not find specified process/thread running on ' 165 'device. Make sure the apk is already running before ' 166 'attempting to profile.') 167 profiler_args = list(profiler_args) 168 if profiler_args and profiler_args[0] == 'record': 169 profiler_args.pop(0) 170 if '--call-graph' not in profiler_args and '-g' not in profiler_args: 171 profiler_args.append('-g') 172 if '-f' not in profiler_args: 173 profiler_args.extend(('-f', '1000')) 174 device_out_path = '/data/local/tmp/perf.data' 175 if '-o' in profiler_args: 176 device_out_path = profiler_args[profiler_args.index('-o') + 1] 177 else: 178 profiler_args.extend(('-o', device_out_path)) 179 180 if tid: 181 profiler_args.extend(('-t', str(tid))) 182 else: 183 profiler_args.extend(('-p', str(pid))) 184 185 adb_shell_simpleperf_process = device.adb.StartShell( 186 [device_simpleperf_path, 'record'] + profiler_args) 187 188 completed = False 189 try: 190 yield 191 completed = True 192 193 finally: 194 device.KillAll('simpleperf', signum=device_signal.SIGINT, blocking=True, 195 quiet=True) 196 if completed: 197 adb_shell_simpleperf_process.wait() 198 device.PullFile(device_out_path, host_out_path) 199 200 201def ConvertSimpleperfToPprof(simpleperf_out_path, build_directory, 202 pprof_out_path): 203 # The simpleperf scripts require the unstripped libs to be installed in the 204 # same directory structure as the libs on the device. Much of the logic here 205 # is just figuring out and creating the necessary directory structure, and 206 # symlinking the unstripped shared libs. 207 208 # Get the set of libs that we can symbolize 209 unstripped_lib_dir = os.path.join(build_directory, 'lib.unstripped') 210 unstripped_libs = set( 211 f for f in os.listdir(unstripped_lib_dir) if f.endswith('.so')) 212 213 # report.py will show the directory structure above the shared libs; 214 # that is the directory structure we need to recreate on the host. 215 script_dir = devil_env.config.LocalPath('simpleperf_scripts') 216 report_path = os.path.join(script_dir, 'report.py') 217 report_cmd = [sys.executable, report_path, '-i', simpleperf_out_path] 218 device_lib_path = None 219 for line in subprocess.check_output( 220 report_cmd, stderr=subprocess.STDOUT).splitlines(): 221 fields = line.split() 222 if len(fields) < 5: 223 continue 224 shlib_path = fields[4] 225 shlib_dirname, shlib_basename = shlib_path.rpartition('/')[::2] 226 if shlib_basename in unstripped_libs: 227 device_lib_path = shlib_dirname 228 break 229 if not device_lib_path: 230 raise RuntimeError('No chrome-related symbols in profiling data in %s. ' 231 'Either the process was idle for the entire profiling ' 232 'period, or something went very wrong (and you should ' 233 'file a bug at crbug.com/new with component ' 234 'Speed>Tracing, and assign it to szager@chromium.org).' 235 % simpleperf_out_path) 236 237 # Recreate the directory structure locally, and symlink unstripped libs. 238 processing_dir = tempfile.mkdtemp() 239 try: 240 processing_lib_dir = os.path.join( 241 processing_dir, 'binary_cache', device_lib_path.lstrip('/')) 242 os.makedirs(processing_lib_dir) 243 for lib in unstripped_libs: 244 unstripped_lib_path = os.path.join(unstripped_lib_dir, lib) 245 processing_lib_path = os.path.join(processing_lib_dir, lib) 246 os.symlink(unstripped_lib_path, processing_lib_path) 247 248 # Run the script to annotate symbols and convert from simpleperf format to 249 # pprof format. 250 pprof_converter_script = os.path.join( 251 script_dir, 'pprof_proto_generator.py') 252 pprof_converter_cmd = [ 253 sys.executable, pprof_converter_script, '-i', simpleperf_out_path, '-o', 254 os.path.abspath(pprof_out_path), '--ndk_path', 255 constants.ANDROID_NDK_ROOT 256 ] 257 subprocess.check_output(pprof_converter_cmd, stderr=subprocess.STDOUT, 258 cwd=processing_dir) 259 finally: 260 shutil.rmtree(processing_dir, ignore_errors=True) 261