def start_recording(args): adb = AdbHelper() device_arch = adb.get_device_arch() simpleperf_binary = get_target_binary_path(device_arch, 'simpleperf') adb.check_run(['push', simpleperf_binary, '/data/local/tmp']) adb.check_run(['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf']) adb.check_run([ 'shell', 'rm', '-rf', '/data/local/tmp/perf.data', '/data/local/tmp/simpleperf_output' ]) shell_cmd = 'cd /data/local/tmp && nohup ./simpleperf record ' + args.record_options if args.app: shell_cmd += ' --app ' + args.app if args.size_limit: shell_cmd += ' --size-limit ' + args.size_limit shell_cmd += ' >/data/local/tmp/simpleperf_output 2>&1' print('shell_cmd: %s' % shell_cmd) subproc = subprocess.Popen([adb.adb_path, 'shell', shell_cmd]) # Wait 2 seconds to see if the simpleperf command fails to start. time.sleep(2) if subproc.poll() is None: print( 'Simpleperf recording has started. Please unplug the usb cable and run the app.' ) print('After that, run `%s stop` to get recording result.' % sys.argv[0]) else: adb.run(['shell', 'cat', '/data/local/tmp/simpleperf_output']) sys.exit(subproc.returncode)
def start_recording(args): adb = AdbHelper() device_arch = adb.get_device_arch() simpleperf_binary = get_target_binary_path(device_arch, 'simpleperf') adb.check_run(['push', simpleperf_binary, '/data/local/tmp']) adb.check_run(['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf']) adb.check_run(['shell', 'rm', '-rf', '/data/local/tmp/perf.data', '/data/local/tmp/simpleperf_output']) shell_cmd = 'cd /data/local/tmp && nohup ./simpleperf record ' + args.record_options if args.app: shell_cmd += ' --app ' + args.app if args.size_limit: shell_cmd += ' --size-limit ' + args.size_limit shell_cmd += ' >/data/local/tmp/simpleperf_output 2>&1' print('shell_cmd: %s' % shell_cmd) subproc = subprocess.Popen([adb.adb_path, 'shell', shell_cmd]) # Wait 2 seconds to see if the simpleperf command fails to start. time.sleep(2) if subproc.poll() is None: print('Simpleperf recording has started. Please unplug the usb cable and run the app.') print('After that, run `%s stop` to get recording result.' % sys.argv[0]) else: adb.run(['shell', 'cat', '/data/local/tmp/simpleperf_output']) sys.exit(subproc.returncode)
def stop_recording(args): adb = AdbHelper() result = adb.run(['shell', 'pidof', 'simpleperf']) if not result: log_warning('No simpleperf process on device. The recording has ended.') else: adb.run(['shell', 'pkill', '-l', '2', 'simpleperf']) print('Waiting for simpleperf process to finish...') while adb.run(['shell', 'pidof', 'simpleperf']): time.sleep(1) adb.run(['shell', 'cat', '/data/local/tmp/simpleperf_output']) adb.check_run(['pull', '/data/local/tmp/perf.data', args.perf_data_path]) print('The recording data has been collected in %s.' % args.perf_data_path)
def stop_recording(args): adb = AdbHelper() result = adb.run(['shell', 'pidof', 'simpleperf']) if not result: log_warning( 'No simpleperf process on device. The recording has ended.') else: adb.run(['shell', 'pkill', '-l', '2', 'simpleperf']) print('Waiting for simpleperf process to finish...') while adb.run(['shell', 'pidof', 'simpleperf']): time.sleep(1) adb.run(['shell', 'cat', '/data/local/tmp/simpleperf_output']) adb.check_run(['pull', '/data/local/tmp/perf.data', args.perf_data_path]) print('The recording data has been collected in %s.' % args.perf_data_path)
class BinaryCacheBuilder(object): """Collect all binaries needed by perf.data in binary_cache.""" def __init__(self, ndk_path, disable_adb_root): self.adb = AdbHelper(enable_switch_to_root=not disable_adb_root) self.readelf = ReadElf(ndk_path) self.binary_cache_dir = 'binary_cache' if not os.path.isdir(self.binary_cache_dir): os.makedirs(self.binary_cache_dir) self.binaries = {} def build_binary_cache(self, perf_data_path, symfs_dirs): self._collect_used_binaries(perf_data_path) self.copy_binaries_from_symfs_dirs(symfs_dirs) self._pull_binaries_from_device() self._pull_kernel_symbols() def _collect_used_binaries(self, perf_data_path): """read perf.data, collect all used binaries and their build id (if available).""" # A dict mapping from binary name to build_id binaries = {} lib = ReportLib() lib.SetRecordFile(perf_data_path) lib.SetLogSeverity('error') while True: sample = lib.GetNextSample() if sample is None: lib.Close() break symbols = [lib.GetSymbolOfCurrentSample()] callchain = lib.GetCallChainOfCurrentSample() for i in range(callchain.nr): symbols.append(callchain.entries[i].symbol) for symbol in symbols: dso_name = symbol.dso_name if dso_name not in binaries: if is_jit_symfile(dso_name): continue binaries[dso_name] = lib.GetBuildIdForPath(dso_name) self.binaries = binaries def copy_binaries_from_symfs_dirs(self, symfs_dirs): """collect all files in symfs_dirs.""" if not symfs_dirs: return # It is possible that the path of the binary in symfs_dirs doesn't match # the one recorded in perf.data. For example, a file in symfs_dirs might # be "debug/arm/obj/armeabi-v7a/libsudo-game-jni.so", but the path in # perf.data is "/data/app/xxxx/lib/arm/libsudo-game-jni.so". So we match # binaries if they have the same filename (like libsudo-game-jni.so) # and same build_id. # Map from filename to binary paths. filename_dict = {} for binary in self.binaries: index = binary.rfind('/') filename = binary[index+1:] paths = filename_dict.get(filename) if paths is None: filename_dict[filename] = paths = [] paths.append(binary) # Walk through all files in symfs_dirs, and copy matching files to build_cache. for symfs_dir in symfs_dirs: for root, _, files in os.walk(symfs_dir): for filename in files: paths = filename_dict.get(filename) if not paths: continue build_id = self._read_build_id(os.path.join(root, filename)) if not build_id: continue for binary in paths: expected_build_id = self.binaries.get(binary) if expected_build_id == build_id: self._copy_to_binary_cache(os.path.join(root, filename), expected_build_id, binary) break def _copy_to_binary_cache(self, from_path, expected_build_id, target_file): if target_file[0] == '/': target_file = target_file[1:] target_file = target_file.replace('/', os.sep) target_file = os.path.join(self.binary_cache_dir, target_file) if not self._need_to_copy(from_path, target_file, expected_build_id): # The existing file in binary_cache can provide more information, so no need to copy. return target_dir = os.path.dirname(target_file) if not os.path.isdir(target_dir): os.makedirs(target_dir) log_info('copy to binary_cache: %s to %s' % (from_path, target_file)) shutil.copy(from_path, target_file) def _need_to_copy(self, source_file, target_file, expected_build_id): if not os.path.isfile(target_file): return True if self._read_build_id(target_file) != expected_build_id: return True return self._get_file_stripped_level(source_file) < self._get_file_stripped_level( target_file) def _get_file_stripped_level(self, file_path): """Return stripped level of an ELF file. Larger value means more stripped.""" sections = self.readelf.get_sections(file_path) if '.debug_line' in sections: return 0 if '.symtab' in sections: return 1 return 2 def _pull_binaries_from_device(self): """pull binaries needed in perf.data to binary_cache.""" for binary in self.binaries: build_id = self.binaries[binary] if not binary.startswith('/') or binary == "//anon" or binary.startswith("/dev/"): # [kernel.kallsyms] or unknown, or something we can't find binary. continue binary_cache_file = binary[1:].replace('/', os.sep) binary_cache_file = os.path.join(self.binary_cache_dir, binary_cache_file) self._check_and_pull_binary(binary, build_id, binary_cache_file) def _check_and_pull_binary(self, binary, expected_build_id, binary_cache_file): """If the binary_cache_file exists and has the expected_build_id, there is no need to pull the binary from device. Otherwise, pull it. """ need_pull = True if os.path.isfile(binary_cache_file): need_pull = False if expected_build_id: build_id = self._read_build_id(binary_cache_file) if expected_build_id != build_id: need_pull = True if need_pull: target_dir = os.path.dirname(binary_cache_file) if not os.path.isdir(target_dir): os.makedirs(target_dir) if os.path.isfile(binary_cache_file): os.remove(binary_cache_file) log_info('pull file to binary_cache: %s to %s' % (binary, binary_cache_file)) self._pull_file_from_device(binary, binary_cache_file) else: log_info('use current file in binary_cache: %s' % binary_cache_file) def _read_build_id(self, file_path): """read build id of a binary on host.""" return self.readelf.get_build_id(file_path) def _pull_file_from_device(self, device_path, host_path): if self.adb.run(['pull', device_path, host_path]): return True # In non-root device, we can't pull /data/app/XXX/base.odex directly. # Instead, we can first copy the file to /data/local/tmp, then pull it. filename = device_path[device_path.rfind('/')+1:] if (self.adb.run(['shell', 'cp', device_path, '/data/local/tmp']) and self.adb.run(['pull', '/data/local/tmp/' + filename, host_path])): self.adb.run(['shell', 'rm', '/data/local/tmp/' + filename]) return True log_warning('failed to pull %s from device' % device_path) return False def _pull_kernel_symbols(self): file_path = os.path.join(self.binary_cache_dir, 'kallsyms') if os.path.isfile(file_path): os.remove(file_path) if self.adb.switch_to_root(): self.adb.run(['shell', '"echo 0 >/proc/sys/kernel/kptr_restrict"']) self.adb.run(['pull', '/proc/kallsyms', file_path])
class ProfilerBase(object): """Base class of all Profilers.""" def __init__(self, args): self.args = args self.adb = AdbHelper(enable_switch_to_root=not args.disable_adb_root) self.is_root_device = self.adb.switch_to_root() self.android_version = self.adb.get_android_version() if self.android_version < 7: log_exit( """app_profiler.py isn't supported on Android < N, please switch to use simpleperf binary directly.""") self.device_arch = self.adb.get_device_arch() self.record_subproc = None def profile(self): log_info('prepare profiling') self.prepare() log_info('start profiling') self.start() self.wait_profiling() log_info('collect profiling data') self.collect_profiling_data() log_info('profiling is finished.') def prepare(self): """Prepare recording. """ self.download_simpleperf() if self.args.native_lib_dir: self.download_libs() def download_simpleperf(self): simpleperf_binary = get_target_binary_path(self.device_arch, 'simpleperf') self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp']) self.adb.check_run( ['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf']) def download_libs(self): downloader = NativeLibDownloader(self.args.ndk_path, self.device_arch, self.adb) downloader.collect_native_libs_on_host(self.args.native_lib_dir) downloader.collect_native_libs_on_device() downloader.sync_native_libs_on_device() def start(self): raise NotImplementedError def start_profiling(self, target_args): """Start simpleperf reocrd process on device.""" args = [ '/data/local/tmp/simpleperf', 'record', '-o', '/data/local/tmp/perf.data', self.args.record_options ] if self.adb.run( ['shell', 'ls', NATIVE_LIBS_DIR_ON_DEVICE, '>/dev/null', '2>&1']): args += ['--symfs', NATIVE_LIBS_DIR_ON_DEVICE] args += ['--log', self.args.log] args += target_args adb_args = [self.adb.adb_path, 'shell'] + args log_info('run adb cmd: %s' % adb_args) self.record_subproc = subprocess.Popen(adb_args) def wait_profiling(self): """Wait until profiling finishes, or stop profiling when user presses Ctrl-C.""" returncode = None try: returncode = self.record_subproc.wait() except KeyboardInterrupt: self.stop_profiling() self.record_subproc = None # Don't check return value of record_subproc. Because record_subproc also # receives Ctrl-C, and always returns non-zero. returncode = 0 log_debug('profiling result [%s]' % (returncode == 0)) if returncode != 0: log_exit('Failed to record profiling data.') def stop_profiling(self): """Stop profiling by sending SIGINT to simpleperf, and wait until it exits to make sure perf.data is completely generated.""" has_killed = False while True: (result, _) = self.adb.run_and_return_output( ['shell', 'pidof', 'simpleperf']) if not result: break if not has_killed: has_killed = True self.adb.run_and_return_output( ['shell', 'pkill', '-l', '2', 'simpleperf']) time.sleep(1) def collect_profiling_data(self): self.adb.check_run_and_return_output( ['pull', '/data/local/tmp/perf.data', self.args.perf_data_path]) if not self.args.skip_collect_binaries: binary_cache_args = [ sys.executable, os.path.join(get_script_dir(), 'binary_cache_builder.py') ] binary_cache_args += [ '-i', self.args.perf_data_path, '--log', self.args.log ] if self.args.native_lib_dir: binary_cache_args += ['-lib', self.args.native_lib_dir] if self.args.disable_adb_root: binary_cache_args += ['--disable_adb_root'] if self.args.ndk_path: binary_cache_args += ['--ndk_path', self.args.ndk_path] subprocess.check_call(binary_cache_args)
class ProfilerBase(object): """Base class of all Profilers.""" def __init__(self, args): self.args = args self.adb = AdbHelper(enable_switch_to_root=not args.disable_adb_root) self.is_root_device = self.adb.switch_to_root() self.android_version = self.adb.get_android_version() if self.android_version < 7: log_exit("""app_profiler.py isn't supported on Android < N, please switch to use simpleperf binary directly.""") self.device_arch = self.adb.get_device_arch() self.record_subproc = None def profile(self): log_info('prepare profiling') self.prepare() log_info('start profiling') self.start() self.wait_profiling() log_info('collect profiling data') self.collect_profiling_data() log_info('profiling is finished.') def prepare(self): """Prepare recording. """ self.download_simpleperf() if self.args.native_lib_dir: self.download_libs() def download_simpleperf(self): simpleperf_binary = get_target_binary_path(self.device_arch, 'simpleperf') self.adb.check_run(['push', simpleperf_binary, '/data/local/tmp']) self.adb.check_run(['shell', 'chmod', 'a+x', '/data/local/tmp/simpleperf']) def download_libs(self): downloader = NativeLibDownloader(self.args.ndk_path, self.device_arch, self.adb) downloader.collect_native_libs_on_host(self.args.native_lib_dir) downloader.collect_native_libs_on_device() downloader.sync_natives_libs_on_device() def start(self): raise NotImplementedError def start_profiling(self, target_args): """Start simpleperf reocrd process on device.""" args = ['/data/local/tmp/simpleperf', 'record', '-o', '/data/local/tmp/perf.data', self.args.record_options] if self.adb.run(['shell', 'ls', NATIVE_LIBS_DIR_ON_DEVICE]): args += ['--symfs', NATIVE_LIBS_DIR_ON_DEVICE] args += target_args adb_args = [self.adb.adb_path, 'shell'] + args log_debug('run adb cmd: %s' % adb_args) self.record_subproc = subprocess.Popen(adb_args) def wait_profiling(self): """Wait until profiling finishes, or stop profiling when user presses Ctrl-C.""" returncode = None try: returncode = self.record_subproc.wait() except KeyboardInterrupt: self.stop_profiling() self.record_subproc = None # Don't check return value of record_subproc. Because record_subproc also # receives Ctrl-C, and always returns non-zero. returncode = 0 log_debug('profiling result [%s]' % (returncode == 0)) if returncode != 0: log_exit('Failed to record profiling data.') def stop_profiling(self): """Stop profiling by sending SIGINT to simpleperf, and wait until it exits to make sure perf.data is completely generated.""" has_killed = False while True: (result, _) = self.adb.run_and_return_output(['shell', 'pidof', 'simpleperf']) if not result: break if not has_killed: has_killed = True self.adb.run_and_return_output(['shell', 'pkill', '-l', '2', 'simpleperf']) time.sleep(1) def collect_profiling_data(self): self.adb.check_run_and_return_output(['pull', '/data/local/tmp/perf.data', self.args.perf_data_path]) if not self.args.skip_collect_binaries: binary_cache_args = [sys.executable, os.path.join(get_script_dir(), 'binary_cache_builder.py')] binary_cache_args += ['-i', self.args.perf_data_path] if self.args.native_lib_dir: binary_cache_args += ['-lib', self.args.native_lib_dir] if self.args.disable_adb_root: binary_cache_args += ['--disable_adb_root'] if self.args.ndk_path: binary_cache_args += ['--ndk_path', self.args.ndk_path] subprocess.check_call(binary_cache_args)