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