1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from telemetry.internal.util import atexit_with_log 6import json 7import logging 8import os 9import shutil 10import stat 11import sys 12import tempfile 13import traceback 14 15from py_trace_event import trace_time 16from telemetry.internal.platform import tracing_agent 17from telemetry.internal.platform.tracing_agent import ( 18 chrome_tracing_devtools_manager) 19 20_DESKTOP_OS_NAMES = ['linux', 'mac', 'win'] 21_STARTUP_TRACING_OS_NAMES = _DESKTOP_OS_NAMES + ['android', 'chromeos'] 22 23# The trace config file path should be the same as specified in 24# src/components/tracing/trace_config_file.[h|cc] 25_CHROME_TRACE_CONFIG_DIR_ANDROID = '/data/local/' 26_CHROME_TRACE_CONFIG_DIR_CROS = '/tmp/' 27_CHROME_TRACE_CONFIG_FILE_NAME = 'chrome-trace-config.json' 28 29 30def ClearStarupTracingStateIfNeeded(platform_backend): 31 # Trace config file has fixed path on Android and temporary path on desktop. 32 if platform_backend.GetOSName() == 'android': 33 trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_ANDROID, 34 _CHROME_TRACE_CONFIG_FILE_NAME) 35 platform_backend.device.RunShellCommand( 36 ['rm', '-f', trace_config_file], check_return=True, as_root=True) 37 38 39class ChromeTracingStartedError(Exception): 40 pass 41 42 43class ChromeTracingStoppedError(Exception): 44 pass 45 46 47class ChromeClockSyncError(Exception): 48 pass 49 50 51class ChromeTracingAgent(tracing_agent.TracingAgent): 52 def __init__(self, platform_backend): 53 super(ChromeTracingAgent, self).__init__(platform_backend) 54 self._trace_config = None 55 self._trace_config_file = None 56 self._previously_responsive_devtools = [] 57 58 @property 59 def trace_config(self): 60 # Trace config is also used to check if Chrome tracing is running or not. 61 return self._trace_config 62 63 @property 64 def trace_config_file(self): 65 return self._trace_config_file 66 67 @classmethod 68 def IsStartupTracingSupported(cls, platform_backend): 69 if platform_backend.GetOSName() in _STARTUP_TRACING_OS_NAMES: 70 return True 71 else: 72 return False 73 74 @classmethod 75 def IsSupported(cls, platform_backend): 76 if cls.IsStartupTracingSupported(platform_backend): 77 return True 78 else: 79 return chrome_tracing_devtools_manager.IsSupported(platform_backend) 80 81 def _StartStartupTracing(self, config): 82 if not self.IsStartupTracingSupported(self._platform_backend): 83 return False 84 self._CreateTraceConfigFile(config) 85 return True 86 87 def _StartDevToolsTracing(self, config, timeout): 88 if not chrome_tracing_devtools_manager.IsSupported(self._platform_backend): 89 return False 90 devtools_clients = (chrome_tracing_devtools_manager 91 .GetActiveDevToolsClients(self._platform_backend)) 92 if not devtools_clients: 93 return False 94 for client in devtools_clients: 95 if client.is_tracing_running: 96 raise ChromeTracingStartedError( 97 'Tracing is already running on devtools at port %s on platform' 98 'backend %s.' % (client.remote_port, self._platform_backend)) 99 client.StartChromeTracing(config, timeout) 100 return True 101 102 def StartAgentTracing(self, config, timeout): 103 if not config.enable_chrome_trace: 104 return False 105 106 if self._trace_config: 107 raise ChromeTracingStartedError( 108 'Tracing is already running on platform backend %s.' 109 % self._platform_backend) 110 111 if (config.enable_android_graphics_memtrack and 112 self._platform_backend.GetOSName() == 'android'): 113 self._platform_backend.SetGraphicsMemoryTrackingEnabled(True) 114 115 # Chrome tracing Agent needs to start tracing for chrome browsers that are 116 # not yet started, and for the ones that already are. For the former, we 117 # first setup the trace_config_file, which allows browsers that starts after 118 # this point to use it for enabling tracing upon browser startup. For the 119 # latter, we invoke start tracing command through devtools for browsers that 120 # are already started and tracked by chrome_tracing_devtools_manager. 121 started_startup_tracing = self._StartStartupTracing(config) 122 started_devtools_tracing = self._StartDevToolsTracing(config, timeout) 123 if started_startup_tracing or started_devtools_tracing: 124 self._trace_config = config 125 return True 126 return False 127 128 def SupportsExplicitClockSync(self): 129 return True 130 131 def _RecordClockSyncMarkerDevTools( 132 self, sync_id, record_controller_clock_sync_marker_callback, 133 devtools_clients): 134 has_clock_synced = False 135 for client in devtools_clients: 136 try: 137 timestamp = trace_time.Now() 138 client.RecordChromeClockSyncMarker(sync_id) 139 # We only need one successful clock sync. 140 has_clock_synced = True 141 break 142 except Exception: 143 logging.exception('Failed to record clock sync marker with sync_id=%r ' 144 'via DevTools client %r:' % (sync_id, client)) 145 if not has_clock_synced: 146 raise ChromeClockSyncError( 147 'Failed to issue clock sync to devtools client') 148 record_controller_clock_sync_marker_callback(sync_id, timestamp) 149 150 def _RecordClockSyncMarkerAsyncEvent( 151 self, sync_id, record_controller_clock_sync_marker_callback): 152 has_clock_synced = False 153 for backend in self._IterInspectorBackends(): 154 try: 155 timestamp = trace_time.Now() 156 event = 'ClockSyncEvent.%s' % sync_id 157 backend.EvaluateJavaScript( 158 "console.time({{ event }});", event=event) 159 backend.EvaluateJavaScript( 160 "console.timeEnd({{ event }});", event=event) 161 has_clock_synced = True 162 break 163 except Exception: 164 logging.exception('Failed to record clock sync marker with sync_id=%r ' 165 'via inspector backend %r:' % (sync_id, backend)) 166 if not has_clock_synced: 167 raise ChromeClockSyncError( 168 'Failed to issue clock sync to devtools client') 169 record_controller_clock_sync_marker_callback(sync_id, timestamp) 170 171 def RecordClockSyncMarker(self, sync_id, 172 record_controller_clock_sync_marker_callback): 173 devtools_clients = (chrome_tracing_devtools_manager 174 .GetActiveDevToolsClients(self._platform_backend)) 175 if not devtools_clients: 176 raise ChromeClockSyncError('Cannot issue clock sync. No devtools clients') 177 version = None 178 for client in devtools_clients: 179 version = client.GetChromeBranchNumber() 180 break 181 logging.info('Chrome version: %s', version) 182 # Note, we aren't sure whether 2744 is the correct cut-off point which 183 # Chrome will support clock sync marker, however we verified that 2743 does 184 # not support clock sync (catapult/issues/2804) hence we use it here. 185 # On the next update of Chrome ref build, if testTBM2ForSmoke still fails, 186 # the cut-off branch number will need to be bumped up again. 187 if version and int(version) > 2743: 188 self._RecordClockSyncMarkerDevTools( 189 sync_id, record_controller_clock_sync_marker_callback, 190 devtools_clients) 191 else: # TODO(rnephew): Remove once chrome stable is past branch 2743. 192 self._RecordClockSyncMarkerAsyncEvent( 193 sync_id, record_controller_clock_sync_marker_callback) 194 195 def StopAgentTracing(self): 196 if not self._trace_config: 197 raise ChromeTracingStoppedError( 198 'Tracing is not running on platform backend %s.' 199 % self._platform_backend) 200 201 if self.IsStartupTracingSupported(self._platform_backend): 202 self._RemoveTraceConfigFile() 203 204 # We get all DevTools clients including the stale ones, so that we get an 205 # exception if there is a stale client. This is because we will potentially 206 # lose data if there is a stale client. 207 devtools_clients = (chrome_tracing_devtools_manager 208 .GetDevToolsClients(self._platform_backend)) 209 raised_exception_messages = [] 210 assert len(self._previously_responsive_devtools) == 0 211 for client in devtools_clients: 212 try: 213 client.StopChromeTracing() 214 self._previously_responsive_devtools.append(client) 215 216 except Exception: 217 raised_exception_messages.append( 218 'Error when trying to stop Chrome tracing on devtools at port %s:\n%s' 219 % (client.remote_port, 220 ''.join(traceback.format_exception(*sys.exc_info())))) 221 222 if (self._trace_config.enable_android_graphics_memtrack and 223 self._platform_backend.GetOSName() == 'android'): 224 self._platform_backend.SetGraphicsMemoryTrackingEnabled(False) 225 226 self._trace_config = None 227 if raised_exception_messages: 228 raise ChromeTracingStoppedError( 229 'Exceptions raised when trying to stop Chrome devtool tracing:\n' + 230 '\n'.join(raised_exception_messages)) 231 232 def CollectAgentTraceData(self, trace_data_builder, timeout=None): 233 raised_exception_messages = [] 234 for client in self._previously_responsive_devtools: 235 try: 236 client.CollectChromeTracingData(trace_data_builder) 237 except Exception: 238 raised_exception_messages.append( 239 'Error when collecting Chrome tracing on devtools at port %s:\n%s' 240 % (client.remote_port, 241 ''.join(traceback.format_exception(*sys.exc_info())))) 242 self._previously_responsive_devtools = [] 243 244 if raised_exception_messages: 245 raise ChromeTracingStoppedError( 246 'Exceptions raised when trying to collect Chrome devtool tracing:\n' + 247 '\n'.join(raised_exception_messages)) 248 249 def _CreateTraceConfigFileString(self, config): 250 # See src/components/tracing/trace_config_file.h for the format 251 result = { 252 'trace_config': 253 config.chrome_trace_config.GetChromeTraceConfigForStartupTracing() 254 } 255 return json.dumps(result, sort_keys=True) 256 257 def _CreateTraceConfigFile(self, config): 258 assert not self._trace_config_file 259 if self._platform_backend.GetOSName() == 'android': 260 self._trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_ANDROID, 261 _CHROME_TRACE_CONFIG_FILE_NAME) 262 self._platform_backend.device.WriteFile(self._trace_config_file, 263 self._CreateTraceConfigFileString(config), as_root=True) 264 # The config file has fixed path on Android. We need to ensure it is 265 # always cleaned up. 266 atexit_with_log.Register(self._RemoveTraceConfigFile) 267 elif self._platform_backend.GetOSName() == 'chromeos': 268 self._trace_config_file = os.path.join(_CHROME_TRACE_CONFIG_DIR_CROS, 269 _CHROME_TRACE_CONFIG_FILE_NAME) 270 cri = self._platform_backend.cri 271 cri.PushContents(self._CreateTraceConfigFileString(config), 272 self._trace_config_file) 273 cri.Chown(self._trace_config_file) 274 # The config file has fixed path on CrOS. We need to ensure it is 275 # always cleaned up. 276 atexit_with_log.Register(self._RemoveTraceConfigFile) 277 elif self._platform_backend.GetOSName() in _DESKTOP_OS_NAMES: 278 self._trace_config_file = os.path.join(tempfile.mkdtemp(), 279 _CHROME_TRACE_CONFIG_FILE_NAME) 280 with open(self._trace_config_file, 'w') as f: 281 trace_config_string = self._CreateTraceConfigFileString(config) 282 logging.info('Trace config file string: %s', trace_config_string) 283 f.write(trace_config_string) 284 os.chmod(self._trace_config_file, 285 os.stat(self._trace_config_file).st_mode | stat.S_IROTH) 286 else: 287 raise NotImplementedError 288 289 def _RemoveTraceConfigFile(self): 290 if not self._trace_config_file: 291 return 292 if self._platform_backend.GetOSName() == 'android': 293 self._platform_backend.device.RunShellCommand( 294 ['rm', '-f', self._trace_config_file], check_return=True, 295 as_root=True) 296 elif self._platform_backend.GetOSName() == 'chromeos': 297 self._platform_backend.cri.RmRF(self._trace_config_file) 298 elif self._platform_backend.GetOSName() in _DESKTOP_OS_NAMES: 299 if os.path.exists(self._trace_config_file): 300 os.remove(self._trace_config_file) 301 shutil.rmtree(os.path.dirname(self._trace_config_file)) 302 else: 303 raise NotImplementedError 304 self._trace_config_file = None 305 306 def SupportsFlushingAgentTracing(self): 307 return True 308 309 def FlushAgentTracing(self, config, timeout, trace_data_builder): 310 if not self._trace_config: 311 raise ChromeTracingStoppedError( 312 'Tracing is not running on platform backend %s.' 313 % self._platform_backend) 314 315 for backend in self._IterInspectorBackends(): 316 backend.EvaluateJavaScript("console.time('flush-tracing');") 317 318 self.StopAgentTracing() 319 self.CollectAgentTraceData(trace_data_builder) 320 self.StartAgentTracing(config, timeout) 321 322 for backend in self._IterInspectorBackends(): 323 backend.EvaluateJavaScript("console.timeEnd('flush-tracing');") 324 325 def _IterInspectorBackends(self): 326 for client in chrome_tracing_devtools_manager.GetDevToolsClients( 327 self._platform_backend): 328 context_map = client.GetUpdatedInspectableContexts() 329 for context in context_map.contexts: 330 if context['type'] in ['iframe', 'page', 'webview']: 331 yield context_map.GetInspectorBackend(context['id']) 332