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
5from telemetry.core.backends.chrome import timeline_recorder
6from telemetry.timeline import inspector_timeline_data
7
8
9class TabBackendException(Exception):
10  """An exception which indicates an error response from devtools inspector."""
11  pass
12
13
14class InspectorTimeline(timeline_recorder.TimelineRecorder):
15  """Implementation of dev tools timeline."""
16
17  class Recorder(object):
18    """Utility class to Start and Stop recording timeline.
19
20    Example usage:
21
22        with inspector_timeline.InspectorTimeline.Recorder(tab):
23          # Something to run while the timeline is recording.
24
25    This is an alternative to directly calling the Start and Stop methods below.
26    """
27    def __init__(self, tab):
28      self._tab = tab
29
30    def __enter__(self):
31      self._tab.StartTimelineRecording()
32
33    def __exit__(self, *args):
34      self._tab.StopTimelineRecording()
35
36  def __init__(self, inspector_backend):
37    super(InspectorTimeline, self).__init__()
38    self._inspector_backend = inspector_backend
39    self._is_recording = False
40    self._raw_events = None
41
42  @property
43  def is_timeline_recording_running(self):
44    return self._is_recording
45
46  def Start(self):
47    """Starts recording."""
48    assert not self._is_recording, 'Start should only be called once.'
49    self._raw_events = None
50    self._is_recording = True
51    self._inspector_backend.RegisterDomain(
52        'Timeline', self._OnNotification, self._OnClose)
53    # The 'bufferEvents' parameter below means that events should not be sent
54    # individually as messages, but instead all at once when a Timeline.stop
55    # request is sent.
56    request = {
57        'method': 'Timeline.start',
58        'params': {'bufferEvents': True},
59    }
60    self._SendSyncRequest(request)
61
62  def Stop(self):
63    """Stops recording and returns timeline event data."""
64    if not self._is_recording:
65      return None
66    request = {'method': 'Timeline.stop'}
67    result = self._SendSyncRequest(request)
68    self._inspector_backend.UnregisterDomain('Timeline')
69    self._is_recording = False
70
71    # TODO: Backward compatibility. Needs to be removed when
72    # M38 becomes stable.
73    if 'events' in result:
74      raw_events = result['events']
75    else:  # In M38 events will arrive via Timeline.stopped event.
76      raw_events = self._raw_events
77      self._raw_events = None
78    return inspector_timeline_data.InspectorTimelineData(raw_events)
79
80  def _SendSyncRequest(self, request, timeout=60):
81    """Sends a devtools remote debugging protocol request.
82
83    The types of request that are valid is determined by protocol.json:
84    https://src.chromium.org/viewvc/blink/trunk/Source/devtools/protocol.json
85
86    Args:
87      request: Request dict, may contain the keys 'method' and 'params'.
88      timeout: Number of seconds to wait for a response.
89
90    Returns:
91      The result given in the response message.
92
93    Raises:
94      TabBackendException: The response indicates an error occurred.
95    """
96    response = self._inspector_backend.SyncRequest(request, timeout)
97    if 'error' in response:
98      raise TabBackendException(response['error']['message'])
99    return response['result']
100
101  def _OnNotification(self, msg):
102    """Handler called when a message is received."""
103    # Since 'Timeline.start' was invoked with the 'bufferEvents' parameter,
104    # the events will arrive in Timeline.stopped event.
105    if msg['method'] == 'Timeline.stopped' and 'events' in msg['params']:
106      self._raw_events = msg['params']['events']
107
108  def _OnClose(self):
109    """Handler called when a domain is unregistered."""
110    pass
111