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.
12 from telemetry.core.backends.chrome import tracing_timeline_data
13 from telemetry.core.backends.chrome import websocket
14 from telemetry.core.backends.chrome import websocket_browser_connection
17 class TracingUnsupportedException(Exception):
21 class CategoryFilter(object):
22 def __init__(self, filter_string):
26 self.synthetic_delays = set()
27 self.contains_wildcards = False
32 if '*' in filter_string or '?' in filter_string:
33 self.contains_wildcards = True
35 filter_set = set(filter_string.split(','))
36 delay_re = re.compile(r'DELAY[(][A-Za-z0-9._;]+[)]')
37 for category in filter_set:
40 if delay_re.match(category):
41 self.synthetic_delays.add(category)
42 elif category[0] == '-':
43 category = category[1:]
44 self.excluded.add(category)
45 elif category.startswith('disabled-by-default-'):
46 self.disabled.add(category)
48 self.included.add(category)
50 def IsSubset(self, other):
51 """ Determine if filter A (self) is a subset of filter B (other).
52 Returns True if A is a subset of B, False if A is not a subset of B,
53 and None if we can't tell for sure.
55 # We don't handle filters with wildcards in this test.
56 if self.contains_wildcards or other.contains_wildcards:
59 # Disabled categories get into a trace if and only if they are contained in
60 # the 'disabled' set. Return False if A's disabled set is not a subset of
62 if not self.disabled <= other.disabled:
65 # If A defines more or different synthetic delays than B, then A is not a
67 if not self.synthetic_delays <= other.synthetic_delays:
70 if self.included and other.included:
71 # A and B have explicit include lists. If A includes something that B
72 # doesn't, return False.
73 if not self.included <= other.included:
76 # Only A has an explicit include list. If A includes something that B
77 # excludes, return False.
78 if self.included.intersection(other.excluded):
81 # Only B has an explicit include list. We don't know which categories are
82 # contained in the default list, so return None.
85 # None of the filter have explicit include list. If B excludes categories
86 # that A doesn't exclude, return False.
87 if not other.excluded <= self.excluded:
92 class TracingBackend(object):
93 def __init__(self, devtools_port):
94 self._conn = websocket_browser_connection.WebSocketBrowserConnection(
97 self._category_filter = None
99 self._tracing_data = []
100 # Use a WeakKeyDictionary, because an ordinary dictionary could keep
101 # references to Tab objects around until it gets garbage collected.
102 # This would prevent telemetry from navigating to another page.
103 self._tab_to_marker_mapping = weakref.WeakKeyDictionary()
106 def is_tracing_running(self):
107 return self._thread != None
109 def AddTabToMarkerMapping(self, tab, marker):
110 self._tab_to_marker_mapping[tab] = marker
112 def StartTracing(self, custom_categories=None, timeout=10):
113 """ Starts tracing on the first nested call and returns True. Returns False
114 and does nothing on subsequent nested calls.
117 if self.is_tracing_running:
118 new_category_filter = CategoryFilter(custom_categories)
119 is_subset = new_category_filter.IsSubset(self._category_filter)
120 assert(is_subset != False)
121 if is_subset == None:
122 logging.warning('Cannot determine if category filter of nested ' +
123 'StartTracing call is subset of current filter.')
125 self._CheckNotificationSupported()
126 req = {'method': 'Tracing.start'}
127 self._category_filter = CategoryFilter(custom_categories)
128 if custom_categories:
129 req['params'] = {'categories': custom_categories}
130 self._conn.SyncRequest(req, timeout)
131 # Tracing.start will send asynchronous notifications containing trace
132 # data, until Tracing.end is called.
133 self._thread = threading.Thread(target=self._TracingReader)
134 self._thread.daemon = True
138 def StopTracing(self):
139 """ Stops tracing on the innermost (!) nested call, because we cannot get
140 results otherwise. Resets _tracing_data on the outermost nested call.
141 Returns the result of the trace, as TracingTimelineData object.
144 assert self._nesting >= 0
145 if self.is_tracing_running:
146 req = {'method': 'Tracing.end'}
147 self._conn.SendRequest(req)
148 self._thread.join(timeout=30)
149 if self._thread.is_alive():
150 raise RuntimeError('Timed out waiting for tracing thread to join.')
152 if self._nesting == 0:
153 self._category_filter = None
154 return self._GetTraceResultAndReset()
156 return self._GetTraceResult()
158 def _GetTraceResult(self):
159 assert not self.is_tracing_running
160 return tracing_timeline_data.TracingTimelineData(
161 self._tracing_data, self._tab_to_marker_mapping)
163 def _GetTraceResultAndReset(self):
164 result = self._GetTraceResult()
165 # Reset tab to marker mapping for the next tracing run. Don't use clear(),
166 # because a TraceResult may still hold a reference to the dictionary object.
167 self._tab_to_marker_mapping = weakref.WeakKeyDictionary()
168 self._tracing_data = []
171 def _TracingReader(self):
172 while self._conn.socket:
174 data = self._conn.socket.recv()
177 res = json.loads(data)
178 logging.debug('got [%s]', data)
179 if 'Tracing.dataCollected' == res.get('method'):
180 value = res.get('params', {}).get('value')
181 if type(value) in [str, unicode]:
182 self._tracing_data.append(value)
183 elif type(value) is list:
184 self._tracing_data.extend(value)
186 logging.warning('Unexpected type in tracing data')
187 elif 'Tracing.tracingComplete' == res.get('method'):
189 except websocket.WebSocketTimeoutException as e:
190 logging.warning('Unusual timeout waiting for tracing response (%s).',
192 except (socket.error, websocket.WebSocketException) as e:
193 logging.warning('Unrecoverable exception when reading tracing response '
194 '(%s, %s).', type(e), e)
200 def _CheckNotificationSupported(self):
201 """Ensures we're running against a compatible version of chrome."""
202 req = {'method': 'Tracing.hasCompleted'}
203 res = self._conn.SyncRequest(req)
204 if res.get('response'):
205 raise TracingUnsupportedException(
206 'Tracing not supported for this browser')