Home | History | Annotate | Download | only in chrome
      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.debug('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