Home | History | Annotate | Download | only in systrace
      1 #
      2 # Copyright (C) 2016 The Android Open Source Project
      3 #
      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 
     16 import os
     17 import tempfile
     18 import shutil
     19 import subprocess
     20 import logging
     21 
     22 PATH_SYSTRACE_SCRIPT = os.path.join('tools/external/chromium-trace',
     23                                     'systrace.py')
     24 EXPECTED_START_STDOUT = 'Starting tracing'
     25 
     26 
     27 class SystraceController(object):
     28     '''A util to start/stop systrace through shell command.
     29 
     30     Attributes:
     31         _android_vts_path: string, path to android-vts
     32         _path_output: string, systrace temporally output path
     33         _path_systrace_script: string, path to systrace controller python script
     34         _device_serial: string, device serial string
     35         _subprocess: subprocess.Popen, a subprocess objects of systrace shell command
     36         is_valid: boolean, whether the current environment setting for
     37                   systrace is valid
     38         process_name: string, process name to trace. The value can be empty.
     39     '''
     40 
     41     def __init__(self, android_vts_path, device_serial, process_name=''):
     42         self._android_vts_path = android_vts_path
     43         self._path_output = None
     44         self._subprocess = None
     45         self._device_serial = device_serial
     46         if not device_serial:
     47             logging.warning(
     48                 'Device serial is not provided for systrace. '
     49                 'Tool will not start if multiple devices are connected.')
     50         self.process_name = process_name
     51         self._path_systrace_script = os.path.join(android_vts_path,
     52                                                   PATH_SYSTRACE_SCRIPT)
     53         self.is_valid = os.path.exists(self._path_systrace_script)
     54         if not self.is_valid:
     55             logging.error('invalid systrace script path: %s',
     56                           self._path_systrace_script)
     57 
     58     @property
     59     def is_valid(self):
     60         ''''returns whether the current environment setting is valid'''
     61         return self._is_valid
     62 
     63     @is_valid.setter
     64     def is_valid(self, is_valid):
     65         ''''Set valid status'''
     66         self._is_valid = is_valid
     67 
     68     @property
     69     def process_name(self):
     70         ''''returns process name'''
     71         return self._process_name
     72 
     73     @process_name.setter
     74     def process_name(self, process_name):
     75         ''''Set process name'''
     76         self._process_name = process_name
     77 
     78     @property
     79     def has_output(self):
     80         ''''returns whether output file exists and not empty.
     81 
     82         Returns:
     83             False if output path is not specified, or output file doesn't exist, or output
     84             file size is zero; True otherwise.
     85         '''
     86         if not self._path_output:
     87             logging.warning('systrace output path is empty.')
     88             return False
     89 
     90         try:
     91             if os.path.getsize(self._path_output) == 0:
     92                 logging.warning('systrace output file is empty.')
     93                 return False
     94         except OSError:
     95             logging.info('systrace output file does not exist.')
     96             return False
     97         return True
     98 
     99     def Start(self):
    100         '''Start systrace process.
    101 
    102         Use shell command to start a python systrace script
    103 
    104         Returns:
    105             True if successfully started systrace; False otherwise.
    106         '''
    107         self._subprocess = None
    108         self._path_output = None
    109 
    110         if not self.is_valid:
    111             logging.error(
    112                 'Cannot start systrace: configuration is not correct for %s.',
    113                 self.process_name)
    114             return False
    115 
    116         # TODO: check target device for compatibility (e.g. has systrace hooks)
    117         process_name_arg = ''
    118         if self.process_name:
    119             process_name_arg = '-a %s' % self.process_name
    120 
    121         device_serial_arg = ''
    122         if self._device_serial:
    123             device_serial_arg = '--serial=%s' % self._device_serial
    124 
    125         tmp_dir = tempfile.mkdtemp()
    126         tmp_filename = self.process_name if self.process_name else 'systrace'
    127         self._path_output = str(os.path.join(tmp_dir, tmp_filename + '.html'))
    128 
    129         cmd = ('python -u {script} hal sched '
    130                '{process_name_arg} {serial} -o {output}').format(
    131                    script=self._path_systrace_script,
    132                    process_name_arg=process_name_arg,
    133                    serial=device_serial_arg,
    134                    output=self._path_output)
    135         process = subprocess.Popen(
    136             str(cmd),
    137             shell=True,
    138             stdin=subprocess.PIPE,
    139             stdout=subprocess.PIPE,
    140             stderr=subprocess.PIPE)
    141 
    142         line = ''
    143         success = False
    144         while process.poll() is None:
    145             line += process.stdout.read(1)
    146 
    147             if not line:
    148                 break
    149             elif EXPECTED_START_STDOUT in line:
    150                 success = True
    151                 break
    152 
    153         if not success:
    154             logging.error('Failed to start systrace on process %s',
    155                           self.process_name)
    156             stdout, stderr = process.communicate()
    157             logging.error('stdout: %s', line + stdout)
    158             logging.error('stderr: %s', stderr)
    159             logging.error('ret_code: %s', process.returncode)
    160             return False
    161 
    162         self._subprocess = process
    163         logging.info('Systrace started for %s', self.process_name)
    164         return True
    165 
    166     def Stop(self):
    167         '''Stop systrace process.
    168 
    169         Returns:
    170             True if successfully stopped systrace or systrace already stopped;
    171             False otherwise.
    172         '''
    173         if not self.is_valid:
    174             logging.warn(
    175                 'Cannot stop systrace: systrace was not started for %s.',
    176                 self.process_name)
    177             return False
    178 
    179         if not self._subprocess:
    180             logging.info('Systrace already stopped.')
    181             return True
    182 
    183         # Press enter to stop systrace script
    184         self._subprocess.stdin.write('\n')
    185         self._subprocess.stdin.flush()
    186         # Wait for output to be written down
    187         # TODO: use subprocess.TimeoutExpired after upgrading to python >3.3
    188         out, err = self._subprocess.communicate()
    189         logging.info('Systrace stopped for %s', self.process_name)
    190         logging.info('Systrace stdout: %s', out)
    191         logging.info('Systrace stderr: %s', err)
    192 
    193         self._subprocess = None
    194 
    195         return True
    196 
    197     def ReadLastOutput(self):
    198         '''Read systrace output html.
    199 
    200         Returns:
    201             string, data of systrace html output. None if failed to read.
    202         '''
    203         if not self.is_valid or not self._subprocess:
    204             logging.warn(
    205                 'Cannot read output: systrace was not started for %s.',
    206                 self.process_name)
    207             return None
    208 
    209         if not self.has_output:
    210             logging.error(
    211                 'systrace did not started/ended correctly. Output is empty.')
    212             return False
    213 
    214         try:
    215             with open(self._path_output, 'r') as f:
    216                 data = f.read()
    217                 logging.info('Systrace output length for %s: %s', process_name,
    218                              len(data))
    219                 return data
    220         except Exception as e:
    221             logging.error('Cannot read output: file open failed, %s', e)
    222             return None
    223 
    224     def SaveLastOutput(self, report_path=None):
    225         if not report_path:
    226             logging.error('report path supplied is None')
    227             return False
    228         report_path = str(report_path)
    229 
    230         if not self.has_output:
    231             logging.error(
    232                 'systrace did not started/ended correctly. Output is empty.')
    233             return False
    234 
    235         parent_dir = os.path.dirname(report_path)
    236         if not os.path.exists(parent_dir):
    237             try:
    238                 os.makedirs(parent_dir)
    239             except Exception as e:
    240                 logging.error('error happened while creating directory: %s', e)
    241                 return False
    242 
    243         try:
    244             shutil.copy(self._path_output, report_path)
    245         except Exception as e:  # TODO(yuexima): more specific error catch
    246             logging.error('failed to copy output to report path: %s', e)
    247             return False
    248 
    249         return True
    250 
    251     def ClearLastOutput(self):
    252         '''Clear systrace output html.
    253 
    254         Since output are created in temp directories, this step is optional.
    255 
    256         Returns:
    257             True if successfully deleted temp output file; False otherwise.
    258         '''
    259 
    260         if self._path_output:
    261             try:
    262                 shutil.rmtree(os.path.basename(self._path_output))
    263             except Exception as e:
    264                 logging.error('failed to remove systrace output file. %s', e)
    265                 return False
    266             finally:
    267                 self._path_output = None
    268 
    269         return True
    270