1# Copyright 2014 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'''A container for timeline-based events and traces and can handle importing
5raw event data from different sources. This model closely resembles that in the
6trace_viewer project:
7https://code.google.com/p/trace-viewer/
8'''
9
10from operator import attrgetter
11
12import telemetry.timeline.process as process_module
13from telemetry.timeline import async_slice as async_slice_module
14from telemetry.timeline import slice as slice_module
15from telemetry.timeline import bounds
16from telemetry.timeline import empty_timeline_data_importer
17from telemetry.timeline import event_container
18from telemetry.timeline import inspector_importer
19from telemetry.timeline import trace_event_importer
20
21# Register importers for data
22
23_IMPORTERS = [
24    empty_timeline_data_importer.EmptyTimelineDataImporter,
25    inspector_importer.InspectorTimelineImporter,
26    trace_event_importer.TraceEventTimelineImporter
27]
28
29
30class MarkerMismatchError(Exception):
31  def __init__(self):
32    super(MarkerMismatchError, self).__init__(
33        'Number or order of timeline markers does not match provided labels')
34
35
36class MarkerOverlapError(Exception):
37  def __init__(self):
38    super(MarkerOverlapError, self).__init__(
39        'Overlapping timeline markers found')
40
41def IsSliceOrAsyncSlice(t):
42  if t == async_slice_module.AsyncSlice:
43    return True
44  return t == slice_module.Slice
45
46
47class TimelineModel(event_container.TimelineEventContainer):
48  def __init__(self, timeline_data=None, shift_world_to_zero=True):
49    """ Initializes a TimelineModel. timeline_data can be a single TimelineData
50    object, a list of TimelineData objects, or None. If timeline_data is not
51    None, all events from it will be imported into the model. The events will
52    be shifted such that the first event starts at time 0, if
53    shift_world_to_zero is True.
54    """
55    super(TimelineModel, self).__init__(name='TimelineModel', parent=None)
56    self._bounds = bounds.Bounds()
57    self._thread_time_bounds = {}
58    self._processes = {}
59    self._browser_process = None
60    self._frozen = False
61    self._tab_ids_to_renderer_threads_map = {}
62    self.import_errors = []
63    self.metadata = []
64    self.flow_events = []
65    if timeline_data is not None:
66      self.ImportTraces(timeline_data, shift_world_to_zero=shift_world_to_zero)
67
68  def IterChildContainers(self):
69    for process in self._processes.itervalues():
70      yield process
71
72  def GetAllProcesses(self):
73    return self._processes.values()
74
75  def GetAllThreads(self):
76    threads = []
77    for process in self._processes.values():
78      threads.extend(process.threads.values())
79    return threads
80
81  @property
82  def bounds(self):
83    return self._bounds
84
85  @property
86  def processes(self):
87    return self._processes
88
89  @property
90  #pylint: disable=E0202
91  def browser_process(self):
92    return self._browser_process
93
94  @browser_process.setter
95  #pylint: disable=E0202
96  def browser_process(self, browser_process):
97    self._browser_process = browser_process
98
99  def AddMappingFromTabIdToRendererThread(self, tab_id, renderer_thread):
100    if self._frozen:
101      raise Exception('Cannot add mapping from tab id to renderer thread once '
102                      'trace is imported')
103    self._tab_ids_to_renderer_threads_map[tab_id] = renderer_thread
104
105  def ImportTraces(self, timeline_data, shift_world_to_zero=True):
106    if self._frozen:
107      raise Exception("Cannot add events once trace is imported")
108
109    importers = []
110    if isinstance(timeline_data, list):
111      for item in timeline_data:
112        importers.append(self._CreateImporter(item))
113    else:
114      importers.append(self._CreateImporter(timeline_data))
115
116    importers.sort(cmp=lambda x, y: x.import_priority - y.import_priority)
117
118    for importer in importers:
119      # TODO: catch exceptions here and add it to error list
120      importer.ImportEvents()
121    self.FinalizeImport(shift_world_to_zero, importers)
122
123  def FinalizeImport(self, shift_world_to_zero=False, importers=None):
124    if importers == None:
125      importers = []
126    self.UpdateBounds()
127    if not self.bounds.is_empty:
128      for process in self._processes.itervalues():
129        process.AutoCloseOpenSlices(self.bounds.max,
130                                    self._thread_time_bounds)
131
132    for importer in importers:
133      importer.FinalizeImport()
134
135    for process in self.processes.itervalues():
136      process.FinalizeImport()
137
138    if shift_world_to_zero:
139      self.ShiftWorldToZero()
140    self.UpdateBounds()
141
142    # Because of FinalizeImport, it would probably be a good idea
143    # to prevent the timeline from from being modified.
144    self._frozen = True
145
146  def ShiftWorldToZero(self):
147    self.UpdateBounds()
148    if self._bounds.is_empty:
149      return
150    shift_amount = self._bounds.min
151    for event in self.IterAllEvents():
152      event.start -= shift_amount
153
154  def UpdateBounds(self):
155    self._bounds.Reset()
156    for event in self.IterAllEvents():
157      self._bounds.AddValue(event.start)
158      self._bounds.AddValue(event.end)
159
160    self._thread_time_bounds = {}
161    for thread in self.GetAllThreads():
162      self._thread_time_bounds[thread] = bounds.Bounds()
163      for event in thread.IterEventsInThisContainer(
164          event_type_predicate=lambda t: True,
165          event_predicate=lambda e: True):
166        if event.thread_start != None:
167          self._thread_time_bounds[thread].AddValue(event.thread_start)
168        if event.thread_end != None:
169          self._thread_time_bounds[thread].AddValue(event.thread_end)
170
171  def GetOrCreateProcess(self, pid):
172    if pid not in self._processes:
173      assert not self._frozen
174      self._processes[pid] = process_module.Process(self, pid)
175    return self._processes[pid]
176
177  def FindTimelineMarkers(self, timeline_marker_names):
178    """Find the timeline events with the given names.
179
180    If the number and order of events found does not match the names,
181    raise an error.
182    """
183    # Make sure names are in a list and remove all None names
184    if not isinstance(timeline_marker_names, list):
185      timeline_marker_names = [timeline_marker_names]
186    names = [x for x in timeline_marker_names if x is not None]
187
188    # Gather all events that match the names and sort them.
189    events = []
190    name_set = set()
191    for name in names:
192      name_set.add(name)
193
194    def IsEventNeeded(event):
195      if event.parent_slice != None:
196        return
197      return event.name in name_set
198
199    events = list(self.IterAllEvents(
200      recursive=True,
201      event_type_predicate=IsSliceOrAsyncSlice,
202      event_predicate=IsEventNeeded))
203    events.sort(key=attrgetter('start'))
204
205    # Check if the number and order of events matches the provided names,
206    # and that the events don't overlap.
207    if len(events) != len(names):
208      raise MarkerMismatchError()
209    for (i, event) in enumerate(events):
210      if event.name != names[i]:
211        raise MarkerMismatchError()
212    for i in xrange(0, len(events)):
213      for j in xrange(i+1, len(events)):
214        if (events[j].start < events[i].start + events[i].duration):
215          raise MarkerOverlapError()
216
217    return events
218
219  def GetRendererProcessFromTabId(self, tab_id):
220    renderer_thread = self.GetRendererThreadFromTabId(tab_id)
221    if renderer_thread:
222      return renderer_thread.parent
223    return None
224
225  def GetRendererThreadFromTabId(self, tab_id):
226    return self._tab_ids_to_renderer_threads_map.get(tab_id, None)
227
228  def _CreateImporter(self, event_data):
229    for importer_class in _IMPORTERS:
230      if importer_class.CanImport(event_data):
231        return importer_class(self, event_data)
232    raise ValueError("Could not find an importer for the provided event data")
233