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
6
7from telemetry.core.backends.chrome import inspector_websocket
8from telemetry.core.platform import tracing_category_filter
9from telemetry.core.platform import tracing_options
10
11
12class TracingUnsupportedException(Exception):
13  pass
14
15
16class TracingTimeoutException(Exception):
17  pass
18
19
20class TracingBackend(object):
21  def __init__(self, devtools_port, chrome_browser_backend):
22    self._inspector_websocket = inspector_websocket.InspectorWebsocket(
23        self._NotificationHandler,
24        self._ErrorHandler)
25
26    self._inspector_websocket.Connect(
27        'ws://127.0.0.1:%i/devtools/browser' % devtools_port)
28    self._category_filter = None
29    self._nesting = 0
30    self._tracing_data = []
31    self._is_tracing_running = False
32    self._chrome_browser_backend = chrome_browser_backend
33
34  @property
35  def is_tracing_running(self):
36    return self._is_tracing_running
37
38  def StartTracing(self, trace_options, custom_categories=None, timeout=10):
39    """ Starts tracing on the first nested call and returns True. Returns False
40        and does nothing on subsequent nested calls.
41    """
42    self._nesting += 1
43    if self.is_tracing_running:
44      new_category_filter = tracing_category_filter.TracingCategoryFilter(
45          filter_string=custom_categories)
46      is_subset = new_category_filter.IsSubset(self._category_filter)
47      assert(is_subset != False)
48      if is_subset == None:
49        logging.warning('Cannot determine if category filter of nested ' +
50                        'StartTracing call is subset of current filter.')
51      return False
52    self._CheckNotificationSupported()
53    #TODO(nednguyen): remove this when the stable branch pass 2118.
54    if (trace_options.record_mode == tracing_options.RECORD_AS_MUCH_AS_POSSIBLE
55        and self._chrome_browser_backend.chrome_branch_number
56        and self._chrome_browser_backend.chrome_branch_number < 2118):
57      logging.warning(
58          'Cannot use %s tracing mode on chrome browser with branch version %i,'
59          ' (<2118) fallback to use %s tracing mode' % (
60              trace_options.record_mode,
61              self._chrome_browser_backend.chrome_branch_number,
62              tracing_options.RECORD_UNTIL_FULL))
63      trace_options.record_mode = tracing_options.RECORD_UNTIL_FULL
64    req = {'method': 'Tracing.start'}
65    req['params'] = {}
66    m = {tracing_options.RECORD_UNTIL_FULL: 'record-until-full',
67         tracing_options.RECORD_AS_MUCH_AS_POSSIBLE:
68         'record-as-much-as-possible'}
69    req['params']['options'] = m[trace_options.record_mode]
70    self._category_filter = tracing_category_filter.TracingCategoryFilter(
71        filter_string=custom_categories)
72    if custom_categories:
73      req['params']['categories'] = custom_categories
74    self._inspector_websocket.SyncRequest(req, timeout)
75    self._is_tracing_running = True
76    return True
77
78  def StopTracing(self, timeout=30):
79    """ Stops tracing on the innermost (!) nested call, because we cannot get
80        results otherwise. Resets _tracing_data on the outermost nested call.
81        Returns the result of the trace, as TracingTimelineData object.
82    """
83    self._nesting -= 1
84    assert self._nesting >= 0
85    if self.is_tracing_running:
86      req = {'method': 'Tracing.end'}
87      self._inspector_websocket.SendAndIgnoreResponse(req)
88      # After Tracing.end, chrome browser will send asynchronous notifications
89      # containing trace data. This is until Tracing.tracingComplete is sent,
90      # which means there is no trace buffers pending flush.
91      try:
92        self._inspector_websocket.DispatchNotificationsUntilDone(timeout)
93      except \
94          inspector_websocket.DispatchNotificationsUntilDoneTimeoutException \
95          as err:
96        raise TracingTimeoutException(
97            'Trace data was not fully received due to timeout after %s '
98            'seconds. If the trace data is big, you may want to increase the '
99            'time out amount.' % err.elapsed_time)
100      self._is_tracing_running = False
101    if self._nesting == 0:
102      self._category_filter = None
103      return self._GetTraceResultAndReset()
104    else:
105      return self._GetTraceResult()
106
107  def _GetTraceResult(self):
108    assert not self.is_tracing_running
109    return self._tracing_data
110
111  def _GetTraceResultAndReset(self):
112    result = self._GetTraceResult()
113
114    self._tracing_data = []
115    return result
116
117  def _ErrorHandler(self, elapsed):
118    logging.error('Unrecoverable error after %ds reading tracing response.',
119                  elapsed)
120    raise
121
122  def _NotificationHandler(self, res):
123    if 'Tracing.dataCollected' == res.get('method'):
124      value = res.get('params', {}).get('value')
125      if type(value) in [str, unicode]:
126        self._tracing_data.append(value)
127      elif type(value) is list:
128        self._tracing_data.extend(value)
129      else:
130        logging.warning('Unexpected type in tracing data')
131    elif 'Tracing.tracingComplete' == res.get('method'):
132      return True
133
134  def Close(self):
135    self._inspector_websocket.Disconnect()
136
137  def _CheckNotificationSupported(self):
138    """Ensures we're running against a compatible version of chrome."""
139    req = {'method': 'Tracing.hasCompleted'}
140    res = self._inspector_websocket.SyncRequest(req)
141    if res.get('response'):
142      raise TracingUnsupportedException(
143          'Tracing not supported for this browser')
144    elif 'error' in res:
145      return
146