- add sources.
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / backends / chrome / tracing_backend.py
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
5 import cStringIO
6 import json
7 import logging
8 import socket
9 import threading
10
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
15
16
17 class TracingUnsupportedException(Exception):
18   pass
19
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
27
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.
35     if len(d) == 0:
36       pass
37     elif len(d) == 1:
38       f.write(d[0])
39     else:
40       f.write(d[0])
41       for i in range(1, len(d)):
42         f.write(',')
43         f.write(d[i])
44     f.write(']}')
45
46   def AsTimelineModel(self):
47     f = cStringIO.StringIO()
48     self.Serialize(f)
49     return model.TimelineModel(
50       event_data=f.getvalue(),
51       shift_world_to_zero=False)
52
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
58
59   def Serialize(self, f):
60     f.write('{"traceEvents":')
61     json.dump(self._tracing_data, f)
62     f.write('}')
63
64   def AsTimelineModel(self):
65     return model.TimelineModel(self._tracing_data)
66
67 class CategoryFilter(object):
68   def __init__(self, filter_string):
69     self.excluded = set()
70     self.included = set()
71     self.disabled = set()
72     self.contains_wildcards = False
73
74     if not filter_string:
75       return
76
77     if '*' in filter_string or '?' in filter_string:
78       self.contains_wildcards = True
79
80     filter_set = set(filter_string.split(','))
81     for category in filter_set:
82       if category == '':
83         continue
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)
89       else:
90         self.included.add(category)
91
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.
96     """
97     # We don't handle filters with wildcards in this test.
98     if self.contains_wildcards or other.contains_wildcards:
99       return None
100
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
103     # disabled set.
104     if len(self.disabled):
105       if self.disabled > other.disabled:
106         return False
107
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:
112         return False
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)):
117         return False
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.
121       return None
122     else:
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:
126         return False
127
128     return True
129
130 class TracingBackend(object):
131   def __init__(self, devtools_port):
132     self._conn = websocket_browser_connection.WebSocketBrowserConnection(
133         devtools_port)
134     self._thread = None
135     self._category_filter = None
136     self._nesting = 0
137     self._tracing_data = []
138
139   def _IsTracing(self):
140     return self._thread != None
141
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.
145     """
146     self._nesting += 1
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.')
154       return False
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
165     self._thread.start()
166     return True
167
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.
172     """
173     self._nesting -= 1
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.')
181       self._thread = None
182     if self._nesting == 0:
183       self._category_filter = None
184       return self._GetTraceResultAndReset()
185     else:
186       return self._GetTraceResult()
187
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)
192     else:
193       result_impl = RawTraceResultImpl(self._tracing_data)
194     return trace_result.TraceResult(result_impl)
195
196   def _GetTraceResultAndReset(self):
197     result = self._GetTraceResult()
198     self._tracing_data = []
199     return result
200
201   def _TracingReader(self):
202     while self._conn.socket:
203       try:
204         data = self._conn.socket.recv()
205         if not data:
206           break
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)
215           else:
216             logging.warning('Unexpected type in tracing data')
217         elif 'Tracing.tracingComplete' == res.get('method'):
218           break
219       except websocket.WebSocketTimeoutException as e:
220         logging.warning('Unusual timeout waiting for tracing response (%s).',
221                         e)
222       except (socket.error, websocket.WebSocketException) as e:
223         logging.warning('Unrecoverable exception when reading tracing response '
224                         '(%s, %s).', type(e), e)
225         raise
226
227   def Close(self):
228     self._conn.Close()
229
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')
237     elif 'error' in res:
238       return