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