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 16import os 17import tempfile 18import shutil 19import subprocess 20import logging 21 22PATH_SYSTRACE_SCRIPT = os.path.join('tools/external/chromium-trace', 23 'systrace.py') 24EXPECTED_START_STDOUT = 'Starting tracing' 25 26 27class 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.error('systrace output file does not exist: %s', 96 self._path_output) 97 return False 98 return True 99 100 def Start(self): 101 '''Start systrace process. 102 103 Use shell command to start a python systrace script 104 105 Returns: 106 True if successfully started systrace; False otherwise. 107 ''' 108 self._subprocess = None 109 self._path_output = None 110 111 if not self.is_valid: 112 logging.error( 113 'Cannot start systrace: configuration is not correct for %s.', 114 self.process_name) 115 return False 116 117 # TODO: check target device for compatibility (e.g. has systrace hooks) 118 process_name_arg = '' 119 if self.process_name: 120 process_name_arg = '-a %s' % self.process_name 121 122 device_serial_arg = '' 123 if self._device_serial: 124 device_serial_arg = '--serial=%s' % self._device_serial 125 126 tmp_dir = tempfile.mkdtemp() 127 tmp_filename = self.process_name if self.process_name else 'systrace' 128 self._path_output = str(os.path.join(tmp_dir, tmp_filename + '.html')) 129 130 cmd = ('python -u {script} hal sched ' 131 '{process_name_arg} {serial} -o {output}').format( 132 script=self._path_systrace_script, 133 process_name_arg=process_name_arg, 134 serial=device_serial_arg, 135 output=self._path_output) 136 process = subprocess.Popen( 137 str(cmd), 138 shell=True, 139 stdin=subprocess.PIPE, 140 stdout=subprocess.PIPE, 141 stderr=subprocess.PIPE) 142 143 line = '' 144 success = False 145 while process.poll() is None: 146 line += process.stdout.read(1) 147 148 if not line: 149 break 150 elif EXPECTED_START_STDOUT in line: 151 success = True 152 break 153 154 if not success: 155 logging.error('Failed to start systrace on process %s', 156 self.process_name) 157 stdout, stderr = process.communicate() 158 logging.error('stdout: %s', line + stdout) 159 logging.error('stderr: %s', stderr) 160 logging.error('ret_code: %s', process.returncode) 161 return False 162 163 self._subprocess = process 164 logging.info('Systrace started for %s', self.process_name) 165 return True 166 167 def Stop(self): 168 '''Stop systrace process. 169 170 Returns: 171 True if successfully stopped systrace or systrace already stopped; 172 False otherwise. 173 ''' 174 if not self.is_valid: 175 logging.warn( 176 'Cannot stop systrace: systrace was not started for %s.', 177 self.process_name) 178 return False 179 180 if not self._subprocess: 181 logging.debug('Systrace already stopped.') 182 return True 183 184 # Press enter to stop systrace script 185 self._subprocess.stdin.write('\n') 186 self._subprocess.stdin.flush() 187 # Wait for output to be written down 188 # TODO: use subprocess.TimeoutExpired after upgrading to python >3.3 189 out, err = self._subprocess.communicate() 190 logging.info('Systrace stopped for %s', self.process_name) 191 logging.debug('Systrace stdout: %s', out) 192 if err.strip(): 193 logging.error('Systrace stderr: %s', err) 194 195 self._subprocess = None 196 197 return True 198 199 def ReadLastOutput(self): 200 '''Read systrace output html. 201 202 Returns: 203 string, data of systrace html output. None if failed to read. 204 ''' 205 if not self.is_valid or not self._subprocess: 206 logging.warn( 207 'Cannot read output: systrace was not started for %s.', 208 self.process_name) 209 return None 210 211 if not self.has_output: 212 logging.error( 213 'systrace did not started/ended correctly. Output is empty.') 214 return False 215 216 try: 217 with open(self._path_output, 'r') as f: 218 data = f.read() 219 logging.debug('Systrace output length for %s: %s', 220 process_name, len(data)) 221 return data 222 except Exception as e: 223 logging.error('Cannot read output: file open failed, %s', e) 224 return None 225 226 def SaveLastOutput(self, report_path=None): 227 if not report_path: 228 logging.error('report path supplied is None') 229 return False 230 report_path = str(report_path) 231 232 if not self.has_output: 233 logging.error( 234 'systrace did not started/ended correctly. Output is empty.') 235 return False 236 237 parent_dir = os.path.dirname(report_path) 238 if not os.path.exists(parent_dir): 239 try: 240 os.makedirs(parent_dir) 241 except Exception as e: 242 logging.error('error happened while creating directory: %s', e) 243 return False 244 245 try: 246 shutil.copy(self._path_output, report_path) 247 except Exception as e: # TODO(yuexima): more specific error catch 248 logging.error('failed to copy output to report path: %s', e) 249 return False 250 251 return True 252 253 def ClearLastOutput(self): 254 '''Clear systrace output html. 255 256 Since output are created in temp directories, this step is optional. 257 258 Returns: 259 True if successfully deleted temp output file; False otherwise. 260 ''' 261 262 if self._path_output: 263 try: 264 shutil.rmtree(os.path.basename(self._path_output)) 265 except Exception as e: 266 logging.error('failed to remove systrace output file. %s', e) 267 return False 268 finally: 269 self._path_output = None 270 271 return True 272