1 # Copyright 2013 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 5 import logging 6 import os 7 import sys 8 9 from telemetry import decorators 10 from telemetry.core import bitmap 11 from telemetry.core import exceptions 12 from telemetry.core import util 13 from telemetry.core.backends.chrome import inspector_console 14 from telemetry.core.backends.chrome import inspector_memory 15 from telemetry.core.backends.chrome import inspector_network 16 from telemetry.core.backends.chrome import inspector_page 17 from telemetry.core.backends.chrome import inspector_runtime 18 from telemetry.core.backends.chrome import inspector_timeline 19 from telemetry.core.backends.chrome import inspector_websocket 20 from telemetry.core.backends.chrome import websocket 21 from telemetry.core.heap import model 22 from telemetry.timeline import model as timeline_model 23 from telemetry.timeline import recording_options 24 25 26 class InspectorException(Exception): 27 pass 28 29 30 class InspectorBackend(inspector_websocket.InspectorWebsocket): 31 def __init__(self, browser_backend, context, timeout=60): 32 super(InspectorBackend, self).__init__(self._HandleNotification, 33 self._HandleError) 34 35 self._browser_backend = browser_backend 36 self._context = context 37 self._domain_handlers = {} 38 39 logging.debug('InspectorBackend._Connect() to %s', self.debugger_url) 40 try: 41 self.Connect(self.debugger_url) 42 except (websocket.WebSocketException, util.TimeoutException): 43 err_msg = sys.exc_info()[1] 44 if not self._browser_backend.IsBrowserRunning(): 45 raise exceptions.BrowserGoneException(self.browser, err_msg) 46 elif not self._browser_backend.HasBrowserFinishedLaunching(): 47 raise exceptions.BrowserConnectionGoneException(self.browser, err_msg) 48 else: 49 raise exceptions.TabCrashException(self.browser, err_msg) 50 51 self._console = inspector_console.InspectorConsole(self) 52 self._memory = inspector_memory.InspectorMemory(self) 53 self._page = inspector_page.InspectorPage(self, timeout=timeout) 54 self._runtime = inspector_runtime.InspectorRuntime(self) 55 self._timeline = inspector_timeline.InspectorTimeline(self) 56 self._network = inspector_network.InspectorNetwork(self) 57 self._timeline_model = None 58 59 def __del__(self): 60 self.Disconnect() 61 62 def Disconnect(self): 63 for _, handlers in self._domain_handlers.items(): 64 _, will_close_handler = handlers 65 will_close_handler() 66 self._domain_handlers = {} 67 68 super(InspectorBackend, self).Disconnect() 69 70 @property 71 def browser(self): 72 return self._browser_backend.browser 73 74 @property 75 def url(self): 76 for c in self._browser_backend.ListInspectableContexts(): 77 if c['id'] == self._context['id']: 78 return c['url'] 79 return None 80 81 @property 82 def id(self): 83 return self.debugger_url 84 85 @property 86 def debugger_url(self): 87 return self._context['webSocketDebuggerUrl'] 88 89 # Public methods implemented in JavaScript. 90 91 @property 92 @decorators.Cache 93 def screenshot_supported(self): 94 if (self.browser.platform.GetOSName() == 'linux' and ( 95 os.getenv('DISPLAY') not in [':0', ':0.0'])): 96 # Displays other than 0 mean we are likely running in something like 97 # xvfb where screenshotting doesn't work. 98 return False 99 return not self.EvaluateJavaScript(""" 100 window.chrome.gpuBenchmarking === undefined || 101 window.chrome.gpuBenchmarking.beginWindowSnapshotPNG === undefined 102 """) 103 104 def Screenshot(self, timeout): 105 assert self.screenshot_supported, 'Browser does not support screenshotting' 106 107 self.EvaluateJavaScript(""" 108 if(!window.__telemetry) { 109 window.__telemetry = {} 110 } 111 window.__telemetry.snapshotComplete = false; 112 window.__telemetry.snapshotData = null; 113 window.chrome.gpuBenchmarking.beginWindowSnapshotPNG( 114 function(snapshot) { 115 window.__telemetry.snapshotData = snapshot; 116 window.__telemetry.snapshotComplete = true; 117 } 118 ); 119 """) 120 121 def IsSnapshotComplete(): 122 return self.EvaluateJavaScript( 123 'window.__telemetry.snapshotComplete') 124 125 util.WaitFor(IsSnapshotComplete, timeout) 126 127 snap = self.EvaluateJavaScript(""" 128 (function() { 129 var data = window.__telemetry.snapshotData; 130 delete window.__telemetry.snapshotComplete; 131 delete window.__telemetry.snapshotData; 132 return data; 133 })() 134 """) 135 if snap: 136 return bitmap.Bitmap.FromBase64Png(snap['data']) 137 return None 138 139 # Console public methods. 140 141 @property 142 def message_output_stream(self): # pylint: disable=E0202 143 return self._console.message_output_stream 144 145 @message_output_stream.setter 146 def message_output_stream(self, stream): # pylint: disable=E0202 147 self._console.message_output_stream = stream 148 149 # Memory public methods. 150 151 def GetDOMStats(self, timeout): 152 dom_counters = self._memory.GetDOMCounters(timeout) 153 return { 154 'document_count': dom_counters['documents'], 155 'node_count': dom_counters['nodes'], 156 'event_listener_count': dom_counters['jsEventListeners'] 157 } 158 159 # Page public methods. 160 161 def WaitForNavigate(self, timeout): 162 self._page.WaitForNavigate(timeout) 163 164 def Navigate(self, url, script_to_evaluate_on_commit, timeout): 165 self._page.Navigate(url, script_to_evaluate_on_commit, timeout) 166 167 def GetCookieByName(self, name, timeout): 168 return self._page.GetCookieByName(name, timeout) 169 170 # Runtime public methods. 171 172 def ExecuteJavaScript(self, expr, context_id=None, timeout=60): 173 self._runtime.Execute(expr, context_id, timeout) 174 175 def EvaluateJavaScript(self, expr, context_id=None, timeout=60): 176 return self._runtime.Evaluate(expr, context_id, timeout) 177 178 def EnableAllContexts(self): 179 return self._runtime.EnableAllContexts() 180 181 # Timeline public methods. 182 183 @property 184 def timeline_model(self): 185 return self._timeline_model 186 187 def StartTimelineRecording(self, options=None): 188 if not options: 189 options = recording_options.TimelineRecordingOptions() 190 if options.record_timeline: 191 self._timeline.Start() 192 if options.record_network: 193 self._network.timeline_recorder.Start() 194 195 def StopTimelineRecording(self): 196 data = [] 197 timeline_data = self._timeline.Stop() 198 if timeline_data: 199 data.append(timeline_data) 200 network_data = self._network.timeline_recorder.Stop() 201 if network_data: 202 data.append(network_data) 203 if data: 204 self._timeline_model = timeline_model.TimelineModel( 205 timeline_data=data, shift_world_to_zero=False) 206 else: 207 self._timeline_model = None 208 209 @property 210 def is_timeline_recording_running(self): 211 return self._timeline.is_timeline_recording_running 212 213 # Network public methods. 214 215 def ClearCache(self): 216 self._network.ClearCache() 217 218 # Methods used internally by other backends. 219 220 def _IsInspectable(self): 221 contexts = self._browser_backend.ListInspectableContexts() 222 return self._context['id'] in [c['id'] for c in contexts] 223 224 def _HandleNotification(self, res): 225 if (res['method'] == 'Inspector.detached' and 226 res.get('params', {}).get('reason', '') == 'replaced_with_devtools'): 227 self._WaitForInspectorToGoAwayAndReconnect() 228 return 229 if res['method'] == 'Inspector.targetCrashed': 230 raise exceptions.TabCrashException(self.browser) 231 232 mname = res['method'] 233 dot_pos = mname.find('.') 234 domain_name = mname[:dot_pos] 235 if domain_name in self._domain_handlers: 236 try: 237 self._domain_handlers[domain_name][0](res) 238 except Exception: 239 import traceback 240 traceback.print_exc() 241 else: 242 logging.warn('Unhandled inspector message: %s', res) 243 244 def _HandleError(self, elapsed_time): 245 if self._IsInspectable(): 246 raise util.TimeoutException( 247 'Received a socket error in the browser connection and the tab ' 248 'still exists, assuming it timed out. ' 249 'Elapsed=%ds Error=%s' % (elapsed_time, sys.exc_info()[1])) 250 raise exceptions.TabCrashException(self.browser, 251 'Received a socket error in the browser connection and the tab no ' 252 'longer exists, assuming it crashed. Error=%s' % sys.exc_info()[1]) 253 254 def _WaitForInspectorToGoAwayAndReconnect(self): 255 sys.stderr.write('The connection to Chrome was lost to the Inspector UI.\n') 256 sys.stderr.write('Telemetry is waiting for the inspector to be closed...\n') 257 super(InspectorBackend, self).Disconnect() 258 self._socket.close() 259 self._socket = None 260 def IsBack(): 261 if not self._IsInspectable(): 262 return False 263 try: 264 self.Connect(self.debugger_url) 265 except exceptions.TabCrashException, ex: 266 if ex.message.message.find('Handshake Status 500') == 0: 267 return False 268 raise 269 return True 270 util.WaitFor(IsBack, 512) 271 sys.stderr.write('\n') 272 sys.stderr.write('Inspector\'s UI closed. Telemetry will now resume.\n') 273 274 def RegisterDomain(self, 275 domain_name, notification_handler, will_close_handler): 276 """Registers a given domain for handling notification methods. 277 278 For example, given inspector_backend: 279 def OnConsoleNotification(msg): 280 if msg['method'] == 'Console.messageAdded': 281 print msg['params']['message'] 282 return 283 def OnConsoleClose(self): 284 pass 285 inspector_backend.RegisterDomain('Console', 286 OnConsoleNotification, OnConsoleClose) 287 """ 288 assert domain_name not in self._domain_handlers 289 self._domain_handlers[domain_name] = (notification_handler, 290 will_close_handler) 291 292 def UnregisterDomain(self, domain_name): 293 """Unregisters a previously registered domain.""" 294 assert domain_name in self._domain_handlers 295 self._domain_handlers.pop(domain_name) 296 297 def CollectGarbage(self): 298 self._page.CollectGarbage() 299 300 def TakeJSHeapSnapshot(self, timeout=120): 301 snapshot = [] 302 303 def OnNotification(res): 304 if res['method'] == 'HeapProfiler.addHeapSnapshotChunk': 305 snapshot.append(res['params']['chunk']) 306 307 def OnClose(): 308 pass 309 310 self.RegisterDomain('HeapProfiler', OnNotification, OnClose) 311 312 self.SyncRequest({'method': 'Page.getResourceTree'}, timeout) 313 self.SyncRequest({'method': 'Debugger.enable'}, timeout) 314 self.SyncRequest({'method': 'HeapProfiler.takeHeapSnapshot'}, timeout) 315 snapshot = ''.join(snapshot) 316 317 self.UnregisterDomain('HeapProfiler') 318 return model.Model(snapshot) 319