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 22 from __future__ import print_function 23 import argparse 24 import os 25 import os.path 26 import re 27 import shutil 28 import subprocess 29 import sys 30 import time 31 32 from simpleperf_report_lib import * 33 from utils import * 34 35 36 class BinaryCacheBuilder(object): 37 """Collect all binaries needed by perf.data in binary_cache.""" 38 def __init__(self, config): 39 config_names = ['perf_data_path', 'symfs_dirs', 'adb_path', 40 'readelf_path', 'binary_cache_dir'] 41 for name in config_names: 42 if not config.has_key(name): 43 log_fatal('config for "%s" is missing' % name) 44 45 self.perf_data_path = config.get('perf_data_path') 46 if not os.path.isfile(self.perf_data_path): 47 log_fatal("can't find file %s" % self.perf_data_path) 48 self.symfs_dirs = config.get('symfs_dirs') 49 for symfs_dir in self.symfs_dirs: 50 if not os.path.isdir(symfs_dir): 51 log_fatal("symfs_dir '%s' is not a directory" % symfs_dir) 52 self.adb = AdbHelper(config['adb_path']) 53 self.readelf_path = config['readelf_path'] 54 self.binary_cache_dir = config['binary_cache_dir'] 55 if not os.path.isdir(self.binary_cache_dir): 56 os.makedirs(self.binary_cache_dir) 57 58 59 def build_binary_cache(self): 60 self._collect_used_binaries() 61 self._copy_binaries_from_symfs_dirs() 62 self._pull_binaries_from_device() 63 self._pull_kernel_symbols() 64 65 66 def _collect_used_binaries(self): 67 """read perf.data, collect all used binaries and their build id (if available).""" 68 # A dict mapping from binary name to build_id 69 binaries = dict() 70 lib = ReportLib() 71 lib.SetRecordFile(self.perf_data_path) 72 lib.SetLogSeverity('error') 73 while True: 74 sample = lib.GetNextSample() 75 if sample is None: 76 lib.Close() 77 break 78 symbols = [lib.GetSymbolOfCurrentSample()] 79 callchain = lib.GetCallChainOfCurrentSample() 80 for i in range(callchain.nr): 81 symbols.append(callchain.entries[i].symbol) 82 83 for symbol in symbols: 84 dso_name = symbol.dso_name 85 if not binaries.has_key(dso_name): 86 binaries[dso_name] = lib.GetBuildIdForPath(dso_name) 87 self.binaries = binaries 88 89 90 def _copy_binaries_from_symfs_dirs(self): 91 """collect all files in symfs_dirs.""" 92 if not self.symfs_dirs: 93 return 94 95 # It is possible that the path of the binary in symfs_dirs doesn't match 96 # the one recorded in perf.data. For example, a file in symfs_dirs might 97 # be "debug/arm/obj/armeabi-v7a/libsudo-game-jni.so", but the path in 98 # perf.data is "/data/app/xxxx/lib/arm/libsudo-game-jni.so". So we match 99 # binaries if they have the same filename (like libsudo-game-jni.so) 100 # and same build_id. 101 102 # Map from filename to binary paths. 103 filename_dict = dict() 104 for binary in self.binaries: 105 index = binary.rfind('/') 106 filename = binary[index+1:] 107 paths = filename_dict.get(filename) 108 if paths is None: 109 filename_dict[filename] = paths = [] 110 paths.append(binary) 111 112 # Walk through all files in symfs_dirs, and copy matching files to build_cache. 113 for symfs_dir in self.symfs_dirs: 114 for root, _, files in os.walk(symfs_dir): 115 for file in files: 116 paths = filename_dict.get(file) 117 if paths is not None: 118 build_id = self._read_build_id(os.path.join(root, file)) 119 if not build_id: 120 continue 121 for binary in paths: 122 expected_build_id = self.binaries.get(binary) 123 if expected_build_id == build_id: 124 self._copy_to_binary_cache(os.path.join(root, file), 125 expected_build_id, binary) 126 127 128 def _copy_to_binary_cache(self, from_path, expected_build_id, target_file): 129 if target_file[0] == '/': 130 target_file = target_file[1:] 131 target_file = target_file.replace('/', os.sep) 132 target_file = os.path.join(self.binary_cache_dir, target_file) 133 if (os.path.isfile(target_file) and self._read_build_id(target_file) == expected_build_id 134 and self._file_has_symbol_table(target_file)): 135 # The existing file in binary_cache can provide more information, so no 136 # need to copy. 137 return 138 target_dir = os.path.dirname(target_file) 139 if not os.path.isdir(target_dir): 140 os.makedirs(target_dir) 141 log_info('copy to binary_cache: %s to %s' % (from_path, target_file)) 142 shutil.copy(from_path, target_file) 143 144 145 def _pull_binaries_from_device(self): 146 """pull binaries needed in perf.data to binary_cache.""" 147 for binary in self.binaries: 148 build_id = self.binaries[binary] 149 if binary[0] != '/' or binary == "//anon": 150 # [kernel.kallsyms] or unknown, or something we can't find binary. 151 continue 152 binary_cache_file = binary[1:].replace('/', os.sep) 153 binary_cache_file = os.path.join(self.binary_cache_dir, binary_cache_file) 154 self._check_and_pull_binary(binary, build_id, binary_cache_file) 155 156 157 def _check_and_pull_binary(self, binary, expected_build_id, binary_cache_file): 158 """If the binary_cache_file exists and has the expected_build_id, there 159 is no need to pull the binary from device. Otherwise, pull it. 160 """ 161 need_pull = True 162 if os.path.isfile(binary_cache_file): 163 need_pull = False 164 if expected_build_id: 165 build_id = self._read_build_id(binary_cache_file) 166 if expected_build_id != build_id: 167 need_pull = True 168 if need_pull: 169 target_dir = os.path.dirname(binary_cache_file) 170 if not os.path.isdir(target_dir): 171 os.makedirs(target_dir) 172 if os.path.isfile(binary_cache_file): 173 os.remove(binary_cache_file) 174 log_info('pull file to binary_cache: %s to %s' % (binary, binary_cache_file)) 175 self._pull_file_from_device(binary, binary_cache_file) 176 else: 177 log_info('use current file in binary_cache: %s' % binary_cache_file) 178 179 180 def _read_build_id(self, file): 181 """read build id of a binary on host.""" 182 if not self.readelf_path: 183 return "" 184 output = subprocess.check_output([self.readelf_path, '-n', file]) 185 result = re.search(r'Build ID:\s*(\S+)', output) 186 if result: 187 build_id = result.group(1) 188 if len(build_id) < 40: 189 build_id += '0' * (40 - len(build_id)) 190 build_id = '0x' + build_id 191 return build_id 192 return "" 193 194 195 def _file_has_symbol_table(self, file): 196 """Test if an elf file has symbol table section.""" 197 if not self.readelf_path: 198 return False 199 output = subprocess.check_output([self.readelf_path, '-S', file]) 200 if output.find('.symtab') != -1: 201 return True 202 return False 203 204 205 def _pull_file_from_device(self, device_path, host_path): 206 if self.adb.run(['pull', device_path, host_path]): 207 return True 208 # In non-root device, we can't pull /data/app/XXX/base.odex directly. 209 # Instead, we can first copy the file to /data/local/tmp, then pull it. 210 filename = device_path[device_path.rfind('/')+1:] 211 if (self.adb.run(['shell', 'cp', device_path, '/data/local/tmp']) and 212 self.adb.run(['pull', '/data/local/tmp/' + filename, host_path])): 213 self.adb.run(['shell', 'rm', '/data/local/tmp/' + filename]) 214 return True 215 log_warning('failed to pull %s from device' % device_path) 216 return False 217 218 219 def _pull_kernel_symbols(self): 220 file = os.path.join(self.binary_cache_dir, 'kallsyms') 221 if os.path.isfile(file): 222 os.remove(file) 223 if self.adb.switch_to_root(): 224 self.adb.run(['shell', '"echo 0 >/proc/sys/kernel/kptr_restrict"']) 225 self.adb.run(['pull', '/proc/kallsyms', file]) 226 227 228 if __name__ == '__main__': 229 parser = argparse.ArgumentParser( 230 description="Pull binaries needed by perf.data from device to binary_cache.") 231 parser.add_argument('--config', default='binary_cache_builder.config', 232 help='Set configuration file. Default is binary_cache_builder.config.') 233 args = parser.parse_args() 234 config = load_config(args.config) 235 builder = BinaryCacheBuilder(config) 236 builder.build_binary_cache()