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
5import logging
6import os
7import sys
8
9from telemetry import decorators
10from telemetry.core import bitmap
11from telemetry.core import exceptions
12from telemetry.core import util
13from telemetry.core.backends.chrome import inspector_console
14from telemetry.core.backends.chrome import inspector_memory
15from telemetry.core.backends.chrome import inspector_network
16from telemetry.core.backends.chrome import inspector_page
17from telemetry.core.backends.chrome import inspector_runtime
18from telemetry.core.backends.chrome import inspector_timeline
19from telemetry.core.backends.chrome import inspector_websocket
20from telemetry.core.backends.chrome import websocket
21from telemetry.core.heap import model
22from telemetry.timeline import model as timeline_model
23from telemetry.timeline import recording_options
24
25
26class InspectorException(Exception):
27  pass
28
29
30class 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