1#!/usr/bin/env python 2# 3# Copyright (C) 2016 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18"""binary_cache_builder.py: read perf.data, collect binaries needed by 19 it, and put them in binary_cache. 20""" 21 22from __future__ import print_function 23import argparse 24import os 25import os.path 26import shutil 27 28from simpleperf_report_lib import ReportLib 29from utils import AdbHelper, extant_dir, extant_file, flatten_arg_list, log_info, log_warning 30from utils import ReadElf, set_log_level 31 32def is_jit_symfile(dso_name): 33 return dso_name.split('/')[-1].startswith('TemporaryFile') 34 35class BinaryCacheBuilder(object): 36 """Collect all binaries needed by perf.data in binary_cache.""" 37 def __init__(self, ndk_path, disable_adb_root): 38 self.adb = AdbHelper(enable_switch_to_root=not disable_adb_root) 39 self.readelf = ReadElf(ndk_path) 40 self.binary_cache_dir = 'binary_cache' 41 if not os.path.isdir(self.binary_cache_dir): 42 os.makedirs(self.binary_cache_dir) 43 self.binaries = {} 44 45 46 def build_binary_cache(self, perf_data_path, symfs_dirs): 47 self._collect_used_binaries(perf_data_path) 48 self.copy_binaries_from_symfs_dirs(symfs_dirs) 49 self.pull_binaries_from_device() 50 self._pull_kernel_symbols() 51 52 53 def _collect_used_binaries(self, perf_data_path): 54 """read perf.data, collect all used binaries and their build id (if available).""" 55 # A dict mapping from binary name to build_id 56 binaries = {} 57 lib = ReportLib() 58 lib.SetRecordFile(perf_data_path) 59 lib.SetLogSeverity('error') 60 while True: 61 sample = lib.GetNextSample() 62 if sample is None: 63 lib.Close() 64 break 65 symbols = [lib.GetSymbolOfCurrentSample()] 66 callchain = lib.GetCallChainOfCurrentSample() 67 for i in range(callchain.nr): 68 symbols.append(callchain.entries[i].symbol) 69 70 for symbol in symbols: 71 dso_name = symbol.dso_name 72 if dso_name not in binaries: 73 if is_jit_symfile(dso_name): 74 continue 75 binaries[dso_name] = lib.GetBuildIdForPath(dso_name) 76 self.binaries = binaries 77 78 79 def copy_binaries_from_symfs_dirs(self, symfs_dirs): 80 """collect all files in symfs_dirs.""" 81 if not symfs_dirs: 82 return 83 84 # It is possible that the path of the binary in symfs_dirs doesn't match 85 # the one recorded in perf.data. For example, a file in symfs_dirs might 86 # be "debug/arm/obj/armeabi-v7a/libsudo-game-jni.so", but the path in 87 # perf.data is "/data/app/xxxx/lib/arm/libsudo-game-jni.so". So we match 88 # binaries if they have the same filename (like libsudo-game-jni.so) 89 # and same build_id. 90 91 # Map from filename to binary paths. 92 filename_dict = {} 93 for binary in self.binaries: 94 index = binary.rfind('/') 95 filename = binary[index+1:] 96 paths = filename_dict.get(filename) 97 if paths is None: 98 filename_dict[filename] = paths = [] 99 paths.append(binary) 100 101 # Walk through all files in symfs_dirs, and copy matching files to build_cache. 102 for symfs_dir in symfs_dirs: 103 for root, _, files in os.walk(symfs_dir): 104 for filename in files: 105 paths = filename_dict.get(filename) 106 if not paths: 107 continue 108 build_id = self._read_build_id(os.path.join(root, filename)) 109 for binary in paths: 110 expected_build_id = self.binaries.get(binary) 111 if expected_build_id == build_id: 112 self._copy_to_binary_cache(os.path.join(root, filename), 113 expected_build_id, binary) 114 break 115 116 117 def _copy_to_binary_cache(self, from_path, expected_build_id, target_file): 118 if target_file[0] == '/': 119 target_file = target_file[1:] 120 target_file = target_file.replace('/', os.sep) 121 target_file = os.path.join(self.binary_cache_dir, target_file) 122 if not self._need_to_copy(from_path, target_file, expected_build_id): 123 # The existing file in binary_cache can provide more information, so no need to copy. 124 return 125 target_dir = os.path.dirname(target_file) 126 if not os.path.isdir(target_dir): 127 os.makedirs(target_dir) 128 log_info('copy to binary_cache: %s to %s' % (from_path, target_file)) 129 shutil.copy(from_path, target_file) 130 131 132 def _need_to_copy(self, source_file, target_file, expected_build_id): 133 if not os.path.isfile(target_file): 134 return True 135 if self._read_build_id(target_file) != expected_build_id: 136 return True 137 return self._get_file_stripped_level(source_file) < self._get_file_stripped_level( 138 target_file) 139 140 141 def _get_file_stripped_level(self, file_path): 142 """Return stripped level of an ELF file. Larger value means more stripped.""" 143 sections = self.readelf.get_sections(file_path) 144 if '.debug_line' in sections: 145 return 0 146 if '.symtab' in sections: 147 return 1 148 return 2 149 150 151 def pull_binaries_from_device(self): 152 """pull binaries needed in perf.data to binary_cache.""" 153 for binary in self.binaries: 154 build_id = self.binaries[binary] 155 if not binary.startswith('/') or binary == "//anon" or binary.startswith("/dev/"): 156 # [kernel.kallsyms] or unknown, or something we can't find binary. 157 continue 158 binary_cache_file = binary[1:].replace('/', os.sep) 159 binary_cache_file = os.path.join(self.binary_cache_dir, binary_cache_file) 160 self._check_and_pull_binary(binary, build_id, binary_cache_file) 161 162 163 def _check_and_pull_binary(self, binary, expected_build_id, binary_cache_file): 164 """If the binary_cache_file exists and has the expected_build_id, there 165 is no need to pull the binary from device. Otherwise, pull it. 166 """ 167 need_pull = True 168 if os.path.isfile(binary_cache_file): 169 need_pull = False 170 if expected_build_id: 171 build_id = self._read_build_id(binary_cache_file) 172 if expected_build_id != build_id: 173 need_pull = True 174 if need_pull: 175 target_dir = os.path.dirname(binary_cache_file) 176 if not os.path.isdir(target_dir): 177 os.makedirs(target_dir) 178 if os.path.isfile(binary_cache_file): 179 os.remove(binary_cache_file) 180 log_info('pull file to binary_cache: %s to %s' % (binary, binary_cache_file)) 181 self._pull_file_from_device(binary, binary_cache_file) 182 else: 183 log_info('use current file in binary_cache: %s' % binary_cache_file) 184 185 186 def _read_build_id(self, file_path): 187 """read build id of a binary on host.""" 188 return self.readelf.get_build_id(file_path) 189 190 191 def _pull_file_from_device(self, device_path, host_path): 192 if self.adb.run(['pull', device_path, host_path]): 193 return True 194 # In non-root device, we can't pull /data/app/XXX/base.odex directly. 195 # Instead, we can first copy the file to /data/local/tmp, then pull it. 196 filename = device_path[device_path.rfind('/')+1:] 197 if (self.adb.run(['shell', 'cp', device_path, '/data/local/tmp']) and 198 self.adb.run(['pull', '/data/local/tmp/' + filename, host_path])): 199 self.adb.run(['shell', 'rm', '/data/local/tmp/' + filename]) 200 return True 201 log_warning('failed to pull %s from device' % device_path) 202 return False 203 204 205 def _pull_kernel_symbols(self): 206 file_path = os.path.join(self.binary_cache_dir, 'kallsyms') 207 if os.path.isfile(file_path): 208 os.remove(file_path) 209 if self.adb.switch_to_root(): 210 self.adb.run(['shell', 'echo', '0', '>/proc/sys/kernel/kptr_restrict']) 211 self.adb.run(['pull', '/proc/kallsyms', file_path]) 212 213 214def main(): 215 parser = argparse.ArgumentParser(description=""" 216 Pull binaries needed by perf.data from device to binary_cache directory.""") 217 parser.add_argument('-i', '--perf_data_path', default='perf.data', type=extant_file, help=""" 218 The path of profiling data.""") 219 parser.add_argument('-lib', '--native_lib_dir', type=extant_dir, nargs='+', help=""" 220 Path to find debug version of native shared libraries used in the app.""", action='append') 221 parser.add_argument('--disable_adb_root', action='store_true', help=""" 222 Force adb to run in non root mode.""") 223 parser.add_argument('--ndk_path', nargs=1, help='Find tools in the ndk path.') 224 parser.add_argument( 225 '--log', choices=['debug', 'info', 'warning'], default='info', help='set log level') 226 args = parser.parse_args() 227 set_log_level(args.log) 228 ndk_path = None if not args.ndk_path else args.ndk_path[0] 229 builder = BinaryCacheBuilder(ndk_path, args.disable_adb_root) 230 symfs_dirs = flatten_arg_list(args.native_lib_dir) 231 builder.build_binary_cache(args.perf_data_path, symfs_dirs) 232 233 234if __name__ == '__main__': 235 main() 236