1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# Copyright (c) 2021 Huawei Device Co., Ltd. 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16import sys 17import subprocess 18import argparse 19import time 20import os 21import shutil 22import platform 23from ctypes import c_char_p 24from ctypes import cdll 25 26IS_DEBUG = False 27HDC_NAME = "hdc" 28SYMBOL_FILES_DIR = '/data/local/tmp/local_libs/' 29BUILD_ID_FILE = "build_id_list" 30EXPECTED_TOOLS = { 31 HDC_NAME: { 32 'is_binutils': False, 33 'test_option': 'version', 34 'path_in_tool': '../platform-tools/hdc', 35 } 36} 37 38 39def get_lib(): 40 script_dir = os.path.dirname(os.path.realpath(__file__)) 41 system_type = platform.system().lower() 42 if system_type == "windows": 43 lib_path = os.path.join(script_dir, "bin", system_type, 44 "x86_64", "libhiperf_report.dll") 45 elif system_type == "linux": 46 lib_path = os.path.join(script_dir, "bin", system_type, 47 "x86_64", "libhiperf_report.so") 48 elif system_type == "darwin": 49 lib_path = os.path.join(script_dir, "bin", system_type, 50 "x86_64", "libhiperf_report.dylib") 51 else: 52 print("Un Support System Platform") 53 raise RuntimeError 54 if not os.path.exists(lib_path): 55 print("{} does not exist!".format(lib_path)) 56 raise RuntimeError 57 return cdll.LoadLibrary(lib_path) 58 59 60def remove(files): 61 if os.path.isfile(files): 62 os.remove(files) 63 elif os.path.isdir(files): 64 shutil.rmtree(files, ignore_errors=True) 65 66 67def is_elf_file(path): 68 if os.path.isfile(path): 69 with open(path, 'rb') as file_bin: 70 data = file_bin.read(4) 71 if len(data) == 4 and bytes_to_str(data) == '\x7fELF': 72 return True 73 return False 74 75 76def get_architecture(elf_path): 77 if is_elf_file(elf_path): 78 my_lib = get_lib() 79 my_lib.ReportGetElfArch.restype = c_char_p 80 ret = my_lib.ReportGetElfArch(elf_path.encode()) 81 return ret.decode() 82 return 'unknown' 83 84 85def get_build_id(elf_path): 86 if is_elf_file(elf_path): 87 my_lib = get_lib() 88 my_lib.ReportGetBuildId.restype = c_char_p 89 ret = my_lib.ReportGetBuildId(elf_path.encode()) 90 return ret.decode() 91 return '' 92 93 94def get_hiperf_binary_path(arch, binary_name): 95 if arch == 'aarch64': 96 arch = 'arm64' 97 script_dir = os.path.dirname(os.path.realpath(__file__)) 98 arch_dir = os.path.join(script_dir, "bin", "ohos", arch) 99 if not os.path.isdir(arch_dir): 100 raise Exception("can't find arch directory: %s" % arch_dir) 101 binary_path = os.path.join(arch_dir, binary_name) 102 if not os.path.isfile(binary_path): 103 raise Exception("can't find binary: %s" % binary_path) 104 return binary_path 105 106 107def str_to_bytes(str_value): 108 if not (sys.version_info >= (3, 0)): 109 return str_value 110 return str_value.encode('utf-8') 111 112 113def bytes_to_str(bytes_value): 114 if not bytes_value: 115 return '' 116 if not (sys.version_info >= (3, 0)): 117 return bytes_value 118 return bytes_value.decode('utf-8') 119 120 121def executable_file_available(executable, option='--help'): 122 try: 123 print([executable, option]) 124 subproc = subprocess.Popen([executable, option], 125 stdout=subprocess.PIPE, 126 stderr=subprocess.PIPE) 127 subproc.communicate(timeout=5) 128 return subproc.returncode == 0 129 except OSError: 130 return False 131 132 133def dir_check(arg): 134 path = os.path.realpath(arg) 135 if not os.path.isdir(path): 136 raise argparse.ArgumentTypeError('{} is not a directory.'.format(path)) 137 return path 138 139 140def file_check(arg): 141 path = os.path.realpath(arg) 142 if not os.path.isfile(path): 143 raise argparse.ArgumentTypeError('{} is not a file.'.format(path)) 144 return path 145 146 147def get_arg_list(arg_list): 148 res = [] 149 if arg_list: 150 for arg in arg_list: 151 res += arg 152 return res 153 154 155def get_arch(arch): 156 if 'aarch64' in arch: 157 return 'arm64' 158 if 'arm' in arch: 159 return 'arm' 160 if 'x86_64' in arch or "amd64" in arch: 161 return 'x86_64' 162 if '86' in arch: 163 return 'x86' 164 raise Exception('unsupported architecture: %s' % arch.strip()) 165 166 167def find_tool_path(tool, tool_path=None): 168 if tool not in EXPECTED_TOOLS: 169 return None 170 tool_info = EXPECTED_TOOLS[tool] 171 test_option = tool_info.get('test_option', '--help') 172 path_in_tool = tool_info['path_in_tool'] 173 path_in_tool = path_in_tool.replace('/', os.sep) 174 175 # 1. Find tool in the given tool path. 176 if tool_path: 177 path = os.path.join(tool_path, path_in_tool) 178 if executable_file_available(path, test_option): 179 return path 180 181 # 2. Find tool in the tool directory containing hiperf scripts. 182 path = os.path.join('../..', path_in_tool) 183 if executable_file_available(path, test_option): 184 return path 185 186 # 3. Find tool in $PATH. 187 if executable_file_available(tool, test_option): 188 return tool 189 190 return None 191 192 193class HdcInterface: 194 def __init__(self, root_authority=True): 195 hdc_path = find_tool_path(HDC_NAME) 196 if not hdc_path: 197 raise Exception("Can't find hdc in PATH environment.") 198 self.hdc_path = hdc_path 199 self.root_authority = root_authority 200 201 def run_hdc_cmd(self, hdc_args, log_output=True): 202 hdc_args = [self.hdc_path] + hdc_args 203 if IS_DEBUG: 204 print('run hdc cmd: %s' % hdc_args) 205 subproc = subprocess.Popen(hdc_args, stdout=subprocess.PIPE) 206 (out, _) = subproc.communicate() 207 out = bytes_to_str(out) 208 return_code = subproc.returncode 209 result = (return_code == 0) 210 if out and hdc_args[1] != 'file send' and \ 211 hdc_args[1] != 'file recv': 212 if log_output: 213 print(out) 214 if IS_DEBUG: 215 print('run hdc cmd: %s [result %s]' % (hdc_args, result)) 216 return result, out 217 218 def check_run(self, hdc_args): 219 result, out = self.run_hdc_cmd(hdc_args) 220 if not result: 221 raise Exception('run "hdc %s" failed' % hdc_args) 222 return out 223 224 def _not_use_root(self): 225 result, out = self.run_hdc_cmd(['shell', 'whoami']) 226 if not result or 'root' not in out: 227 return 228 print('unroot hdc') 229 self.run_hdc_cmd(['unroot']) 230 time.sleep(1) 231 self.run_hdc_cmd(['wait-for-device']) 232 233 def switch_root(self): 234 if not self.root_authority: 235 self._not_use_root() 236 return False 237 result, out = self.run_hdc_cmd(['shell', 'whoami']) 238 if not result: 239 return False 240 if 'root' in out: 241 return True 242 build_type = self.get_attribute('ro.build.type') 243 if build_type == 'user': 244 return False 245 self.run_hdc_cmd(['root']) 246 time.sleep(1) 247 self.run_hdc_cmd(['wait-for-device']) 248 result, out = self.run_hdc_cmd(['shell', 'whoami']) 249 return result and 'root' in out 250 251 def get_attribute(self, name): 252 result, out = self.run_hdc_cmd(['shell', 'getprop', 253 name]) 254 if result: 255 return out 256 257 def get_device_architecture(self): 258 output = self.check_run(['shell', 'uname', '-m']) 259 return get_arch(output) 260 261 262class ElfStruct: 263 def __init__(self, path, lib_name): 264 self.path = path 265 self.name = lib_name 266 267 268class LocalLibDownload: 269 270 def __init__(self, device_arch, hdc): 271 self.hdc = hdc 272 self.device_arch = device_arch 273 274 self.build_id_map_of_host = {} 275 self.build_id_map_of_device = {} 276 self.host_lib_count_map = {} 277 self.request_architectures = self.__get_need_architectures(device_arch) 278 279 def get_host_local_libs(self, local_lib_dir): 280 self.build_id_map_of_host.clear() 281 for root, dirs, files in os.walk(local_lib_dir): 282 for name in files: 283 if name.endswith('.so'): 284 self.__append_host_local_lib(os.path.join(root, name), 285 name) 286 287 def get_device_local_libs(self): 288 self.build_id_map_of_device.clear() 289 if os.path.exists(BUILD_ID_FILE): 290 remove(BUILD_ID_FILE) 291 self.hdc.check_run(['shell', 'mkdir', '-p', SYMBOL_FILES_DIR]) 292 self.hdc.run_hdc_cmd(['file recv', SYMBOL_FILES_DIR + BUILD_ID_FILE]) 293 if os.path.isfile(BUILD_ID_FILE): 294 with open(BUILD_ID_FILE, 'rb') as file_bin: 295 for line in file_bin.readlines(): 296 line = bytes_to_str(line).strip() 297 items = line.split('=') 298 if len(items) == 2: 299 self.build_id_map_of_device[items[0]] = items[1] 300 remove(BUILD_ID_FILE) 301 302 def update_device_local_libs(self): 303 # Send local libs to device. 304 for build_id in self.build_id_map_of_host: 305 if build_id not in self.build_id_map_of_device: 306 elf_struct = self.build_id_map_of_host[build_id] 307 self.hdc.check_run(['file send', elf_struct.path, 308 SYMBOL_FILES_DIR + elf_struct.name]) 309 310 # Remove Device lib while local libs not exist on host. 311 for build_id in self.build_id_map_of_device: 312 if build_id not in self.build_id_map_of_host: 313 name = self.build_id_map_of_device[build_id] 314 self.hdc.run_hdc_cmd(['shell', 'rm', SYMBOL_FILES_DIR + name]) 315 316 # Send new build_id_list to device. 317 with open(BUILD_ID_FILE, 'wb') as file_bin: 318 for build_id in self.build_id_map_of_host: 319 str_bytes = str_to_bytes('%s=%s\n' % (build_id, 320 self.build_id_map_of_host[ 321 build_id].name)) 322 file_bin.write(str_bytes) 323 324 self.hdc.check_run(['file send', BUILD_ID_FILE, 325 SYMBOL_FILES_DIR + BUILD_ID_FILE]) 326 remove(BUILD_ID_FILE) 327 328 def __append_host_local_lib(self, path, name): 329 build_id = get_build_id(path) 330 if not build_id: 331 return 332 arch = get_architecture(path) 333 if arch not in self.request_architectures: 334 return 335 336 elf_struct = self.build_id_map_of_host.get(build_id) 337 if not elf_struct: 338 count = self.host_lib_count_map.get(name, 0) 339 self.host_lib_count_map[name] = count + 1 340 if count == 0: 341 unique_name = name 342 else: 343 unique_name = name + '_' + count 344 self.build_id_map_of_host[build_id] = ElfStruct(path, 345 unique_name) 346 else: 347 elf_struct.path = path 348 349 @classmethod 350 def __get_need_architectures(self, device_arch): 351 if device_arch == 'x86_64': 352 return ['x86', 'x86_64'] 353 if device_arch == 'x86': 354 return ['x86'] 355 if device_arch == 'arm64': 356 return ['arm', 'arm64'] 357 if device_arch == 'arm': 358 return ['arm'] 359 return [] 360 361 362class PerformanceProfile: 363 """Class of all Profilers.""" 364 365 def __init__(self, args, control_module=""): 366 self.args = args 367 self.hdc = HdcInterface(root_authority=not args.not_hdc_root) 368 self.device_root = self.hdc.switch_root() 369 self.device_arch = self.hdc.get_device_architecture() 370 self.record_subproc = None 371 self.is_control = bool(control_module) 372 self.control_mode = control_module 373 374 def profile(self): 375 if not self.is_control or self.control_mode == "prepare": 376 print('prepare profiling') 377 self.download() 378 print('start profiling') 379 if not self.combine_args(): 380 return 381 else: 382 self.exec_control() 383 self.profiling() 384 385 if not self.is_control: 386 print('pull profiling data') 387 self.get_profiling_data() 388 if self.control_mode == "stop": 389 self.wait_data_generate_done() 390 print('profiling is finished.') 391 392 def download(self): 393 """Prepare recording. """ 394 if self.args.local_lib_dir: 395 self.download_libs() 396 397 def download_libs(self): 398 executor = LocalLibDownload(self.device_arch, self.hdc) 399 executor.get_host_local_libs(self.args.local_lib_dir) 400 executor.get_device_local_libs() 401 executor.update_device_local_libs() 402 403 def combine_args(self): 404 if self.args.package_name: 405 if self.args.ability: 406 self.kill_process() 407 self.start_profiling(['--app', self.args.package_name]) 408 if self.args.ability: 409 ability = self.args.package_name + '/' + self.args.ability 410 start_cmd = ['shell', 'aa', 'start', '-a', ability] 411 result = self.hdc.run_hdc_cmd(start_cmd) 412 if not result: 413 self.record_subproc.terminate() 414 print("Can't start ability %s" % ability) 415 return False 416 # else: no need to start an ability. 417 elif self.args.local_program: 418 pid = self.hdc.check_run(['shell', 'pidof', 419 self.args.local_program]) 420 if not pid: 421 print("Can't find pid of %s" % self.args.local_program) 422 return False 423 pid = int(pid) 424 self.start_profiling(['-p', str(pid)]) 425 elif self.args.cmd: 426 cmds = self.args.cmd.split(' ') 427 cmd = [cmd.replace("'", "") for cmd in cmds] 428 self.start_profiling(cmd) 429 elif self.args.pid: 430 self.start_profiling(['-p', ','.join(self.args.pid)]) 431 elif self.args.tid: 432 self.start_profiling(['-t', ','.join(self.args.tid)]) 433 elif self.args.system_wide: 434 self.start_profiling(['-a']) 435 return True 436 437 def kill_process(self): 438 if self.get_app_process(): 439 self.hdc.check_run(['shell', 'aa', 'force-stop', self.args.app]) 440 count = 0 441 while True: 442 time.sleep(1) 443 pid = self.get_app_process() 444 if not pid: 445 break 446 count += 1 447 # 3 seconds exec kill 448 if count >= 3: 449 self.run_in_app_dir(['kill', '-9', str(pid)]) 450 451 def get_app_process(self): 452 result, output = self.hdc.run_hdc_cmd( 453 ['shell', 'pidof', self.args.package_name]) 454 return int(output) if result else None 455 456 def run_in_app_dir(self, args): 457 if self.device_root: 458 hdc_args = ['shell', 'cd /data/data/' + self.args.package_name + 459 ' && ' + (' '.join(args))] 460 else: 461 hdc_args = ['shell', 'run-as', self.args.package_name] + args 462 return self.hdc.run_hdc_cmd(hdc_args, log_output=False) 463 464 def start_profiling(self, selected_args): 465 """Start hiperf reocrd process on device.""" 466 record_options = self.args.record_options.split(' ') 467 record_options = [cmd.replace("'", "") for cmd in record_options] 468 if self.is_control: 469 args = ['hiperf', 'record', 470 '--control', self.control_mode, '-o', 471 '/data/local/tmp/perf.data'] + record_options 472 else: 473 args = ['hiperf', 'record', '-o', 474 '/data/local/tmp/perf.data'] + record_options 475 if self.args.local_lib_dir and self.hdc.run_hdc_cmd( 476 ['shell', 'ls', SYMBOL_FILES_DIR]): 477 args += ['--symbol-dir', SYMBOL_FILES_DIR] 478 args += selected_args 479 hdc_args = [self.hdc.hdc_path, 'shell'] + args 480 print('run hdc cmd: %s' % hdc_args) 481 self.record_subproc = subprocess.Popen(hdc_args) 482 483 def profiling(self): 484 """ 485 Wait until profiling finishes, or stop profiling when user 486 presses Ctrl-C. 487 """ 488 489 try: 490 return_code = self.record_subproc.wait() 491 except KeyboardInterrupt: 492 self.end_profiling() 493 self.record_subproc = None 494 return_code = 0 495 print('profiling result [%s]' % (return_code == 0)) 496 if return_code != 0: 497 raise Exception('Failed to record profiling data.') 498 499 def end_profiling(self): 500 """ 501 Stop profiling by sending SIGINT to hiperf, and wait until it exits 502 to make sure perf.data is completely generated. 503 """ 504 has_killed = False 505 while True: 506 (result, out) = self.hdc.run_hdc_cmd( 507 ['shell', 'pidof', 'hiperf']) 508 if not out: 509 break 510 if not has_killed: 511 has_killed = True 512 self.hdc.run_hdc_cmd(['shell', 'pkill', 513 '-l', '2', 'hiperf']) 514 time.sleep(1) 515 516 def get_profiling_data(self): 517 current_path = os.getcwd() 518 full_path = os.path.join(current_path, self.args.output_perf_data) 519 self.hdc.check_run(['file recv', '/data/local/tmp/perf.data', 520 full_path]) 521 self.hdc.run_hdc_cmd(['shell', 'rm', 522 '/data/local/tmp/perf.data']) 523 524 def exec_control(self): 525 hdc_args = [self.hdc.hdc_path, 'shell', 526 'hiperf', 'record', 527 '--control', self.control_mode] 528 print('run hdc cmd: %s' % hdc_args) 529 self.record_subproc = subprocess.Popen(hdc_args) 530 531 def wait_data_generate_done(self): 532 last_size = 0 533 while True: 534 (result, out) = self.hdc.run_hdc_cmd( 535 ['shell', 'du', 'data/local/tmp/perf.data']) 536 if "du" not in out: 537 current_size = out.split(" ")[0] 538 if current_size == last_size: 539 self.get_profiling_data() 540 break 541 else: 542 last_size = current_size 543 else: 544 print("not generate perf.data") 545 break 546 547 time.sleep(1) 548