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.
11 from telemetry.core.backends.chrome import trace_result
12 from telemetry.core.backends.chrome import websocket
13 from telemetry.core.backends.chrome import websocket_browser_connection
14 from telemetry.core.timeline import model
17 class TracingUnsupportedException(Exception):
20 # This class supports legacy format of trace presentation within DevTools
21 # protocol, where trace data were sent as JSON-serialized strings. DevTools
22 # now send the data as raw objects within the protocol message JSON, so there's
23 # no need in extra de-serialization. We might want to remove this in the future.
24 class TraceResultImpl(object):
25 def __init__(self, tracing_data):
26 self._tracing_data = tracing_data
28 def Serialize(self, f):
29 f.write('{"traceEvents": [')
30 d = self._tracing_data
31 # Note: we're not using ','.join here because the strings that are in the
32 # tracing data are typically many megabytes in size. In the fast case, f is
33 # just a file, so by skipping the in memory step we keep our memory
34 # footprint low and avoid additional processing.
41 for i in range(1, len(d)):
46 def AsTimelineModel(self):
47 f = cStringIO.StringIO()
49 return model.TimelineModel(
50 event_data=f.getvalue(),
51 shift_world_to_zero=False)
53 # RawTraceResultImpl differs from TraceResultImpl above in that
54 # data are kept as a list of dicts, not strings.
55 class RawTraceResultImpl(object):
56 def __init__(self, tracing_data):
57 self._tracing_data = tracing_data
59 def Serialize(self, f):
60 f.write('{"traceEvents":')
61 json.dump(self._tracing_data, f)
64 def AsTimelineModel(self):
65 return model.TimelineModel(self._tracing_data)
67 class CategoryFilter(object):
68 def __init__(self, filter_string):
72 self.contains_wildcards = False
77 if '*' in filter_string or '?' in filter_string:
78 self.contains_wildcards = True
80 filter_set = set(filter_string.split(','))
81 for category in filter_set:
84 if category[0] == '-':
85 category = category[1:]
86 self.excluded.add(category)
87 elif category.startswith('disabled-by-default-'):
88 self.disabled.add(category)
90 self.included.add(category)
92 def IsSubset(self, other):
93 """ Determine if filter A (self) is a subset of filter B (other).
94 Returns True if A is a subset of B, False if A is not a subset of B,
95 and None if we can't tell for sure.
97 # We don't handle filters with wildcards in this test.
98 if self.contains_wildcards or other.contains_wildcards:
101 # Disabled categories get into a trace if and only if they are contained in
102 # the 'disabled' set. Return False if A's disabled set is a superset of B's
104 if len(self.disabled):
105 if self.disabled > other.disabled:
108 if len(self.included) and len(other.included):
109 # A and B have explicit include lists. If A includes something that B
110 # doesn't, return False.
111 if not self.included <= other.included:
113 elif len(self.included):
114 # Only A has an explicit include list. If A includes something that B
115 # excludes, return False.
116 if len(self.included.intersection(other.excluded)):
118 elif len(other.included):
119 # Only B has an explicit include list. We don't know which categories are
120 # contained in the default list, so return None.
123 # None of the filter have explicit include list. If B excludes categories
124 # that A doesn't exclude, return False.
125 if not other.excluded <= self.excluded:
130 class TracingBackend(object):
131 def __init__(self, devtools_port):
132 self._conn = websocket_browser_connection.WebSocketBrowserConnection(
135 self._category_filter = None
137 self._tracing_data = []
139 def _IsTracing(self):
140 return self._thread != None
142 def StartTracing(self, custom_categories=None, timeout=10):
143 """ Starts tracing on the first nested call and returns True. Returns False
144 and does nothing on subsequent nested calls.
147 if self._IsTracing():
148 new_category_filter = CategoryFilter(custom_categories)
149 is_subset = new_category_filter.IsSubset(self._category_filter)
150 assert(is_subset != False)
151 if is_subset == None:
152 logging.warning('Cannot determine if category filter of nested ' +
153 'StartTracing call is subset of current filter.')
155 self._CheckNotificationSupported()
156 req = {'method': 'Tracing.start'}
157 self._category_filter = CategoryFilter(custom_categories)
158 if custom_categories:
159 req['params'] = {'categories': custom_categories}
160 self._conn.SendRequest(req, timeout)
161 # Tracing.start will send asynchronous notifications containing trace
162 # data, until Tracing.end is called.
163 self._thread = threading.Thread(target=self._TracingReader)
164 self._thread.daemon = True
168 def StopTracing(self):
169 """ Stops tracing on the innermost (!) nested call, because we cannot get
170 results otherwise. Resets _tracing_data on the outermost nested call.
171 Returns the result of the trace, as TraceResult object.
174 assert self._nesting >= 0
175 if self._IsTracing():
176 req = {'method': 'Tracing.end'}
177 self._conn.SendRequest(req)
178 self._thread.join(timeout=30)
179 if self._thread.is_alive():
180 raise RuntimeError('Timed out waiting for tracing thread to join.')
182 if self._nesting == 0:
183 self._category_filter = None
184 return self._GetTraceResultAndReset()
186 return self._GetTraceResult()
188 def _GetTraceResult(self):
189 assert not self._IsTracing()
190 if self._tracing_data and type(self._tracing_data[0]) in [str, unicode]:
191 result_impl = TraceResultImpl(self._tracing_data)
193 result_impl = RawTraceResultImpl(self._tracing_data)
194 return trace_result.TraceResult(result_impl)
196 def _GetTraceResultAndReset(self):
197 result = self._GetTraceResult()
198 self._tracing_data = []
201 def _TracingReader(self):
202 while self._conn.socket:
204 data = self._conn.socket.recv()
207 res = json.loads(data)
208 logging.debug('got [%s]', data)
209 if 'Tracing.dataCollected' == res.get('method'):
210 value = res.get('params', {}).get('value')
211 if type(value) in [str, unicode]:
212 self._tracing_data.append(value)
213 elif type(value) is list:
214 self._tracing_data.extend(value)
216 logging.warning('Unexpected type in tracing data')
217 elif 'Tracing.tracingComplete' == res.get('method'):
219 except websocket.WebSocketTimeoutException as e:
220 logging.warning('Unusual timeout waiting for tracing response (%s).',
222 except (socket.error, websocket.WebSocketException) as e:
223 logging.warning('Unrecoverable exception when reading tracing response '
224 '(%s, %s).', type(e), e)
230 def _CheckNotificationSupported(self):
231 """Ensures we're running against a compatible version of chrome."""
232 req = {'method': 'Tracing.hasCompleted'}
233 res = self._conn.SyncRequest(req)
234 if res.get('response'):
235 raise TracingUnsupportedException(
236 'Tracing not supported for this browser')