blob: 4959ed7a0c4163e05e823574adc0d2efb85de4c6 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Monitor tracing events on chrome via chrome remote debugging."""
import bisect
import itertools
import devtools_monitor
class TracingTrack(devtools_monitor.Track):
"""Grabs and processes trace event messages.
See https://goo.gl/Qabkqk for details on the protocol.
"""
def __init__(self, connection, categories=None, fetch_stream=False):
"""Initialize this TracingTrack.
Args:
connection: a DevToolsConnection.
categories: None, or a string, or list of strings, of tracing categories
to filter.
fetch_stream: if true, use a websocket stream to fetch tracing data rather
than dataCollected events. It appears based on very limited testing that
a stream is slower than the default reporting as dataCollected events.
"""
super(TracingTrack, self).__init__(connection)
if connection:
connection.RegisterListener('Tracing.dataCollected', self)
params = {}
if categories:
params['categories'] = (categories if type(categories) is str
else ','.join(categories))
if fetch_stream:
params['transferMode'] = 'ReturnAsStream'
if connection:
connection.SyncRequestNoResponse('Tracing.start', params)
self._events = []
self._event_msec_index = None
self._event_lists = None
def Handle(self, method, event):
for e in event['params']['value']:
self._events.append(Event(e))
# Just invalidate our indices rather than trying to be fancy and
# incrementally update.
self._event_msec_index = None
self._event_lists = None
def GetEvents(self):
return self._events
def EventsAt(self, msec):
"""Gets events active at a timestamp.
Args:
msec: tracing milliseconds to query. Tracing milliseconds appears to be
since chrome startup (ie, arbitrary epoch).
Returns:
List of events active at that timestamp. Instantaneous (ie, instant,
sample and counter) events are never included. Event end times are
exclusive, so that an event ending at the usec parameter will not be
returned.
TODO(mattcary): currently live objects are included. If this is too big we
may break that out into a separate index.
"""
self._IndexEvents()
idx = bisect.bisect_right(self._event_msec_index, msec) - 1
if idx < 0:
return []
events = self._event_lists[idx]
assert events.start_msec <= msec
if not events or events.end_msec < msec:
return []
return events.event_list
def ToJsonDict(self):
return {'events': [e.ToJsonDict() for e in self._events]}
@classmethod
def FromJsonDict(cls, json_dict):
assert 'events' in json_dict
events = [Event(e) for e in json_dict['events']]
tracing_track = TracingTrack(None)
tracing_track._events = events
return tracing_track
def EventsEndingBetween(self, start_msec, end_msec):
"""Gets the list of events whose end lies in a range.
Args:
start_msec: the start of the range to query, in milliseconds, inclusive.
end_msec: the end of the range to query, in milliseconds, inclusive.
Returns:
List of events whose end time lies in the range. Note that although the
range is inclusive at both ends, an ending timestamp is considered to be
exclusive of the actual event. An event ending at 10 msec will be returned
for a range [10, 14] as well as [8, 10], though the event is considered to
end the instant before 10 msec. In practice this is only important when
considering how events overlap; an event ending at 10 msec does not
overlap with one starting at 10 msec and so may unambiguously share ids,
etc.
"""
self._IndexEvents()
low_idx = bisect.bisect_left(self._event_msec_index, start_msec) - 1
high_idx = bisect.bisect_right(self._event_msec_index, end_msec)
matched_events = []
for i in xrange(max(0, low_idx), high_idx):
if self._event_lists[i]:
for e in self._event_lists[i].event_list:
assert e.end_msec is not None
if e.end_msec >= start_msec and e.end_msec <= end_msec:
matched_events.append(e)
return matched_events
def _IndexEvents(self):
"""Computes index for in-flight events.
Creates a list of timestamps where events start or end, and tracks the
current set of in-flight events at the instant after each timestamp. To do
this we have to synthesize ending events for complete events, as well as
join and track the nesting of async, flow and other spanning events.
Events such as instant and counter events that aren't indexable are skipped.
"""
if self._event_msec_index is not None:
return # Already indexed.
if not self._events:
raise devtools_monitor.DevToolsConnectionException('No events to index')
self._event_msec_index = []
self._event_lists = []
synthetic_events = []
for e in self._events:
synthetic_events.extend(e.Synthesize())
synthetic_events.sort(key=lambda e: e.start_msec)
current_events = set()
next_idx = 0
spanning_events = self._SpanningEvents()
while next_idx < len(synthetic_events):
current_msec = synthetic_events[next_idx].start_msec
while next_idx < len(synthetic_events):
event = synthetic_events[next_idx]
assert event.IsIndexable()
if event.start_msec > current_msec:
break
matched_event = spanning_events.Match(event)
if matched_event is not None:
event = matched_event
if not event.synthetic and (
event.end_msec is None or event.end_msec >= current_msec):
current_events.add(event)
next_idx += 1
current_events -= set([
e for e in current_events
if e.end_msec is not None and e.end_msec <= current_msec])
self._event_msec_index.append(current_msec)
self._event_lists.append(self._EventList(current_events))
if spanning_events.HasPending():
raise devtools_monitor.DevToolsConnectionException(
'Pending spanning events: %s' %
'\n'.join([str(e) for e in spanning_events.PendingEvents()]))
class _SpanningEvents(object):
def __init__(self):
self._duration_stack = []
self._async_stacks = {}
self._objects = {}
self._MATCH_HANDLER = {
'B': self._DurationBegin,
'E': self._DurationEnd,
'b': self._AsyncStart,
'e': self._AsyncEnd,
'S': self._AsyncStart,
'F': self._AsyncEnd,
'N': self._ObjectCreated,
'D': self._ObjectDestroyed,
'X': self._Ignore,
None: self._Ignore,
}
def Match(self, event):
return self._MATCH_HANDLER.get(
event.type, self._Unsupported)(event)
def HasPending(self):
return (self._duration_stack or
self._async_stacks or
self._objects)
def PendingEvents(self):
return itertools.chain(
(e for e in self._duration_stack),
(o for o in self._objects),
itertools.chain.from_iterable((
(e for e in s) for s in self._async_stacks.itervalues())))
def _AsyncKey(self, event):
return (event.tracing_event['cat'], event.id)
def _Ignore(self, _event):
return None
def _Unsupported(self, event):
raise devtools_monitor.DevToolsConnectionException(
'Unsupported spanning event type: %s' % event)
def _DurationBegin(self, event):
self._duration_stack.append(event)
return None
def _DurationEnd(self, event):
if not self._duration_stack:
raise devtools_monitor.DevToolsConnectionException(
'Unmatched duration end: %s' % event)
start = self._duration_stack.pop()
start.SetClose(event)
return start
def _AsyncStart(self, event):
key = self._AsyncKey(event)
self._async_stacks.setdefault(key, []).append(event)
return None
def _AsyncEnd(self, event):
key = self._AsyncKey(event)
if key not in self._async_stacks:
raise devtools_monitor.DevToolsConnectionException(
'Unmatched async end %s: %s' % (key, event))
stack = self._async_stacks[key]
start = stack.pop()
if not stack:
del self._async_stacks[key]
start.SetClose(event)
return start
def _ObjectCreated(self, event):
# The tracing event format has object deletion timestamps being exclusive,
# that is the timestamp for a deletion my equal that of the next create at
# the same address. This asserts that does not happen in practice as it is
# inconvenient to handle that correctly here.
if event.id in self._objects:
raise devtools_monitor.DevToolsConnectionException(
'Multiple objects at same address: %s, %s' %
(event, self._objects[event.id]))
self._objects[event.id] = event
return None
def _ObjectDestroyed(self, event):
if event.id not in self._objects:
raise devtools_monitor.DevToolsConnectionException(
'Missing object creation for %s' % event)
start = self._objects[event.id]
del self._objects[event.id]
start.SetClose(event)
return start
class _EventList(object):
def __init__(self, events):
self._events = [e for e in events]
if self._events:
self._start_msec = min(e.start_msec for e in self._events)
# Event end times may be changed after this list is created so the end
# can't be cached.
else:
self._start_msec = self._end_msec = None
@property
def event_list(self):
return self._events
@property
def start_msec(self):
return self._start_msec
@property
def end_msec(self):
return max(e.end_msec for e in self._events)
def __nonzero__(self):
return bool(self._events)
class Event(object):
"""Wraps a tracing event."""
CLOSING_EVENTS = {'E': 'B',
'e': 'b',
'F': 'S',
'D': 'N'}
def __init__(self, tracing_event, synthetic=False):
"""Creates Event.
Intended to be created only by TracingTrack.
Args:
tracing_event: JSON tracing event, as defined in https://goo.gl/Qabkqk.
synthetic: True if the event is synthetic. This is only used for indexing
internal to TracingTrack.
"""
if not synthetic and tracing_event['ph'] in ['s', 't', 'f']:
raise devtools_monitor.DevToolsConnectionException(
'Unsupported event: %s' % tracing_event)
if not synthetic and tracing_event['ph'] in ['p']:
raise devtools_monitor.DevToolsConnectionException(
'Deprecated event: %s' % tracing_event)
self._tracing_event = tracing_event
# Note tracing event times are in microseconds.
self._start_msec = tracing_event['ts'] / 1000.0
self._end_msec = None
self._synthetic = synthetic
if self.type == 'X':
# Some events don't have a duration.
duration = (tracing_event['dur']
if 'dur' in tracing_event else tracing_event['tdur'])
self._end_msec = self.start_msec + duration / 1000.0
@property
def start_msec(self):
return self._start_msec
@property
def end_msec(self):
return self._end_msec
@property
def type(self):
if self._synthetic:
return None
return self._tracing_event['ph']
@property
def args(self):
return self._tracing_event.get('args', {})
@property
def id(self):
return self._tracing_event.get('id')
@property
def tracing_event(self):
return self._tracing_event
@property
def synthetic(self):
return self._synthetic
def __str__(self):
return ''.join([str(self._tracing_event),
'[%s,%s]' % (self.start_msec, self.end_msec)])
def IsIndexable(self):
"""True iff the event can be indexed by time."""
return self._synthetic or self.type not in [
'I', 'P', 'c', 'C',
'n', 'T', 'p', # TODO(mattcary): ?? instant types of async events.
'O', # TODO(mattcary): ?? object snapshot
]
def Synthesize(self):
"""Expand into synthetic events.
Returns:
A list of events, possibly some synthetic, whose start times are all
interesting for purposes of indexing. If the event is not indexable the
set may be empty.
"""
if not self.IsIndexable():
return []
if self.type == 'X':
# Tracing event timestamps are microseconds!
return [self, Event({'ts': self.end_msec * 1000}, synthetic=True)]
return [self]
def SetClose(self, closing):
"""Close a spanning event.
Args:
closing: The closing event.
Raises:
devtools_monitor.DevToolsConnectionException if closing can't property
close this event.
"""
if self.type != self.CLOSING_EVENTS.get(closing.type):
raise devtools_monitor.DevToolsConnectionException(
'Bad closing: %s --> %s' % (self, closing))
if self.type in ['b', 'S'] and (
self.tracing_event['cat'] != closing.tracing_event['cat'] or
self.id != closing.id):
raise devtools_monitor.DevToolsConnectionException(
'Bad async closing: %s --> %s' % (self, closing))
self._end_msec = closing.start_msec
if 'args' in closing.tracing_event:
self.tracing_event.setdefault(
'args', {}).update(closing.tracing_event['args'])
def ToJsonDict(self):
return self._tracing_event
@classmethod
def FromJsonDict(cls, json_dict):
return Event(json_dict)