• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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