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 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 if not build_id: 110 continue 111 for binary in paths: 112 expected_build_id = self.binaries.get(binary) 113 if expected_build_id == build_id: 114 self._copy_to_binary_cache(os.path.join(root, filename), 115 expected_build_id, binary) 116 break 117 118 119 def _copy_to_binary_cache(self, from_path, expected_build_id, target_file): 120 if target_file[0] == '/': 121 target_file = target_file[1:] 122 target_file = target_file.replace('/', os.sep) 123 target_file = os.path.join(self.binary_cache_dir, target_file) 124 if not self._need_to_copy(from_path, target_file, expected_build_id): 125 # The existing file in binary_cache can provide more information, so no need to copy. 126 return 127 target_dir = os.path.dirname(target_file) 128 if not os.path.isdir(target_dir): 129 os.makedirs(target_dir) 130 log_info('copy to binary_cache: %s to %s' % (from_path, target_file)) 131 shutil.copy(from_path, target_file) 132 133 134 def _need_to_copy(self, source_file, target_file, expected_build_id): 135 if not os.path.isfile(target_file): 136 return True 137 if self._read_build_id(target_file) != expected_build_id: 138 return True 139 return self._get_file_stripped_level(source_file) < self._get_file_stripped_level( 140 target_file) 141 142 143 def _get_file_stripped_level(self, file_path): 144 """Return stripped level of an ELF file. Larger value means more stripped.""" 145 sections = self.readelf.get_sections(file_path) 146 if '.debug_line' in sections: 147 return 0 148 if '.symtab' in sections: 149 return 1 150 return 2 151 152 153 def _pull_binaries_from_device(self): 154 """pull binaries needed in perf.data to binary_cache.""" 155 for binary in self.binaries: 156 build_id = self.binaries[binary] 157 if not binary.startswith('/') or binary == "//anon" or binary.startswith("/dev/"): 158 # [kernel.kallsyms] or unknown, or something we can't find binary. 159 continue 160 binary_cache_file = binary[1:].replace('/', os.sep) 161 binary_cache_file = os.path.join(self.binary_cache_dir, binary_cache_file) 162 self._check_and_pull_binary(binary, build_id, binary_cache_file) 163 164 165 def _check_and_pull_binary(self, binary, expected_build_id, binary_cache_file): 166 """If the binary_cache_file exists and has the expected_build_id, there 167 is no need to pull the binary from device. Otherwise, pull it. 168 """ 169 need_pull = True 170 if os.path.isfile(binary_cache_file): 171 need_pull = False 172 if expected_build_id: 173 build_id = self._read_build_id(binary_cache_file) 174 if expected_build_id != build_id: 175 need_pull = True 176 if need_pull: 177 target_dir = os.path.dirname(binary_cache_file) 178 if not os.path.isdir(target_dir): 179 os.makedirs(target_dir) 180 if os.path.isfile(binary_cache_file): 181 os.remove(binary_cache_file) 182 log_info('pull file to binary_cache: %s to %s' % (binary, binary_cache_file)) 183 self._pull_file_from_device(binary, binary_cache_file) 184 else: 185 log_info('use current file in binary_cache: %s' % binary_cache_file) 186 187 188 def _read_build_id(self, file_path): 189 """read build id of a binary on host.""" 190 return self.readelf.get_build_id(file_path) 191 192 193 def _pull_file_from_device(self, device_path, host_path): 194 if self.adb.run(['pull', device_path, host_path]): 195 return True 196 # In non-root device, we can't pull /data/app/XXX/base.odex directly. 197 # Instead, we can first copy the file to /data/local/tmp, then pull it. 198 filename = device_path[device_path.rfind('/')+1:] 199 if (self.adb.run(['shell', 'cp', device_path, '/data/local/tmp']) and 200 self.adb.run(['pull', '/data/local/tmp/' + filename, host_path])): 201 self.adb.run(['shell', 'rm', '/data/local/tmp/' + filename]) 202 return True 203 log_warning('failed to pull %s from device' % device_path) 204 return False 205 206 207 def _pull_kernel_symbols(self): 208 file_path = os.path.join(self.binary_cache_dir, 'kallsyms') 209 if os.path.isfile(file_path): 210 os.remove(file_path) 211 if self.adb.switch_to_root(): 212 self.adb.run(['shell', '"echo 0 >/proc/sys/kernel/kptr_restrict"']) 213 self.adb.run(['pull', '/proc/kallsyms', file_path]) 214 215 216def main(): 217 parser = argparse.ArgumentParser(description=""" 218 Pull binaries needed by perf.data from device to binary_cache directory.""") 219 parser.add_argument('-i', '--perf_data_path', default='perf.data', type=extant_file, help=""" 220 The path of profiling data.""") 221 parser.add_argument('-lib', '--native_lib_dir', type=extant_dir, nargs='+', help=""" 222 Path to find debug version of native shared libraries used in the app.""", action='append') 223 parser.add_argument('--disable_adb_root', action='store_true', help=""" 224 Force adb to run in non root mode.""") 225 parser.add_argument('--ndk_path', nargs=1, help='Find tools in the ndk path.') 226 args = parser.parse_args() 227 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