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
10import logging
11from operator import attrgetter
12
13from telemetry.timeline import async_slice as async_slice_module
14from telemetry.timeline import bounds
15from telemetry.timeline import event_container
16from telemetry.timeline import inspector_importer
17from telemetry.timeline import process as process_module
18from telemetry.timeline import slice as slice_module
19from telemetry.timeline import surface_flinger_importer
20from telemetry.timeline import tab_id_importer
21from telemetry.timeline import trace_event_importer
22from tracing.trace_data import trace_data as trace_data_module
23
24
25# Register importers for data
26
27_IMPORTERS = [
28    inspector_importer.InspectorTimelineImporter,
29    tab_id_importer.TabIdImporter,
30    trace_event_importer.TraceEventTimelineImporter,
31    surface_flinger_importer.SurfaceFlingerTimelineImporter
32]
33
34
35class MarkerMismatchError(Exception):
36  def __init__(self):
37    super(MarkerMismatchError, self).__init__(
38        'Number or order of timeline markers does not match provided labels')
39
40
41class MarkerOverlapError(Exception):
42  def __init__(self):
43    super(MarkerOverlapError, self).__init__(
44        'Overlapping timeline markers found')
45
46
47def IsSliceOrAsyncSlice(t):
48  if t == async_slice_module.AsyncSlice:
49    return True
50  return t == slice_module.Slice
51
52
53class TimelineModel(event_container.TimelineEventContainer):
54  def __init__(self, trace_data=None, shift_world_to_zero=True):
55    """ Initializes a TimelineModel.
56
57    Args:
58        trace_data: trace_data.TraceData containing events to import
59        shift_world_to_zero: If true, the events will be shifted such that the
60            first event starts at time 0.
61    """
62    super(TimelineModel, self).__init__(name='TimelineModel', parent=None)
63    self._bounds = bounds.Bounds()
64    self._thread_time_bounds = {}
65    self._processes = {}
66    self._browser_process = None
67    self._gpu_process = None
68    self._surface_flinger_process = None
69    self._frozen = False
70    self._tab_ids_to_renderer_threads_map = {}
71    self.import_errors = []
72    self.metadata = []
73    self.flow_events = []
74    self._global_memory_dumps = None
75    if trace_data is not None:
76      self.ImportTraces(trace_data, shift_world_to_zero=shift_world_to_zero)
77
78  def SetGlobalMemoryDumps(self, global_memory_dumps):
79    """Populates the model with a sequence of GlobalMemoryDump objects."""
80    assert not self._frozen and self._global_memory_dumps is None
81    # Keep dumps sorted in chronological order.
82    self._global_memory_dumps = tuple(sorted(global_memory_dumps,
83                                             key=lambda dump: dump.start))
84
85  def IterGlobalMemoryDumps(self):
86    """Iterate over the memory dump events of this model."""
87    return iter(self._global_memory_dumps or [])
88
89  def IterChildContainers(self):
90    for process in self._processes.itervalues():
91      yield process
92
93  def GetAllProcesses(self):
94    return self._processes.values()
95
96  def GetAllThreads(self):
97    threads = []
98    for process in self._processes.values():
99      threads.extend(process.threads.values())
100    return threads
101
102  @property
103  def bounds(self):
104    return self._bounds
105
106  @property
107  def processes(self):
108    return self._processes
109
110  @property
111  def browser_process(self):
112    return self._browser_process
113
114  @browser_process.setter
115  def browser_process(self, browser_process):
116    self._browser_process = browser_process
117
118  @property
119  def gpu_process(self):
120    return self._gpu_process
121
122  @gpu_process.setter
123  def gpu_process(self, gpu_process):
124    self._gpu_process = gpu_process
125
126  @property
127  def surface_flinger_process(self):
128    return self._surface_flinger_process
129
130  @surface_flinger_process.setter
131  def surface_flinger_process(self, surface_flinger_process):
132    self._surface_flinger_process = surface_flinger_process
133
134  def AddMappingFromTabIdToRendererThread(self, tab_id, renderer_thread):
135    if self._frozen:
136      raise Exception('Cannot add mapping from tab id to renderer thread once '
137                      'trace is imported')
138    self._tab_ids_to_renderer_threads_map[tab_id] = renderer_thread
139
140  def ImportTraces(self, trace_data, shift_world_to_zero=True):
141    """Populates the model with the provided trace data.
142
143    trace_data must be an instance of TraceData.
144
145    Passing shift_world_to_zero=True causes the events to be shifted such that
146    the first event starts at time 0.
147    """
148    if self._frozen:
149      raise Exception("Cannot add events once trace is imported")
150    assert isinstance(trace_data, trace_data_module.TraceData)
151
152    importers = self._CreateImporters(trace_data)
153
154    for importer in importers:
155      # TODO: catch exceptions here and add it to error list
156      importer.ImportEvents()
157    self.FinalizeImport(shift_world_to_zero, importers)
158
159  def FinalizeImport(self, shift_world_to_zero=False, importers=None):
160    if importers == None:
161      importers = []
162    self.UpdateBounds()
163    if not self.bounds.is_empty:
164      for process in self._processes.itervalues():
165        process.AutoCloseOpenSlices(self.bounds.max,
166                                    self._thread_time_bounds)
167
168    for importer in importers:
169      importer.FinalizeImport()
170
171    for process in self.processes.itervalues():
172      process.FinalizeImport()
173
174    if shift_world_to_zero:
175      self.ShiftWorldToZero()
176    self.UpdateBounds()
177
178    # Because of FinalizeImport, it would probably be a good idea
179    # to prevent the timeline from from being modified.
180    self._frozen = True
181
182  def ShiftWorldToZero(self):
183    self.UpdateBounds()
184    if self._bounds.is_empty:
185      return
186    shift_amount = self._bounds.min
187    for event in self.IterAllEvents():
188      event.start -= shift_amount
189
190  def UpdateBounds(self):
191    self._bounds.Reset()
192    for event in self.IterAllEvents():
193      self._bounds.AddValue(event.start)
194      self._bounds.AddValue(event.end)
195
196    self._thread_time_bounds = {}
197    for thread in self.GetAllThreads():
198      self._thread_time_bounds[thread] = bounds.Bounds()
199      for event in thread.IterEventsInThisContainer(
200          event_type_predicate=lambda t: True,
201          event_predicate=lambda e: True):
202        if event.thread_start != None:
203          self._thread_time_bounds[thread].AddValue(event.thread_start)
204        if event.thread_end != None:
205          self._thread_time_bounds[thread].AddValue(event.thread_end)
206
207  def GetOrCreateProcess(self, pid):
208    if pid not in self._processes:
209      assert not self._frozen
210      self._processes[pid] = process_module.Process(self, pid)
211    return self._processes[pid]
212
213  def FindTimelineMarkers(self, timeline_marker_names):
214    """Find the timeline events with the given names.
215
216    If the number and order of events found does not match the names,
217    raise an error.
218    """
219    # Make sure names are in a list and remove all None names
220    if not isinstance(timeline_marker_names, list):
221      timeline_marker_names = [timeline_marker_names]
222    names = [x for x in timeline_marker_names if x is not None]
223
224    # Gather all events that match the names and sort them.
225    events = []
226    name_set = set()
227    for name in names:
228      name_set.add(name)
229
230    def IsEventNeeded(event):
231      if event.parent_slice != None:
232        return
233      return event.name in name_set
234
235    events = list(self.IterAllEvents(
236      recursive=True,
237      event_type_predicate=IsSliceOrAsyncSlice,
238      event_predicate=IsEventNeeded))
239    events.sort(key=attrgetter('start'))
240
241    # Check if the number and order of events matches the provided names,
242    # and that the events don't overlap.
243    if len(events) != len(names):
244      raise MarkerMismatchError()
245    for (i, event) in enumerate(events):
246      if event.name != names[i]:
247        raise MarkerMismatchError()
248    for i in xrange(0, len(events)):
249      for j in xrange(i+1, len(events)):
250        if events[j].start < events[i].start + events[i].duration:
251          raise MarkerOverlapError()
252
253    return events
254
255  def GetRendererProcessFromTabId(self, tab_id):
256    renderer_thread = self.GetRendererThreadFromTabId(tab_id)
257    if renderer_thread:
258      return renderer_thread.parent
259    return None
260
261  def GetRendererThreadFromTabId(self, tab_id):
262    return self._tab_ids_to_renderer_threads_map.get(tab_id, None)
263
264  def _CreateImporters(self, trace_data):
265    def FindImporterClassForPart(part):
266      for importer_class in _IMPORTERS:
267        if importer_class.GetSupportedPart() == part:
268          return importer_class
269
270    importers = []
271    for part in trace_data.active_parts:
272      importer_class = FindImporterClassForPart(part)
273      if not importer_class:
274        logging.warning('No importer found for %s' % repr(part))
275      else:
276        importers.append(importer_class(self, trace_data))
277        importers.sort(key=lambda k: k.import_order)
278
279    return importers
280