1# Copyright 2015 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.
4import collections
5import math
6import sys
7
8from telemetry.timeline import model as model_module
9from telemetry.value import improvement_direction
10from telemetry.value import list_of_scalar_values
11from telemetry.value import scalar
12from telemetry.web_perf.metrics import timeline_based_metric
13
14TOPLEVEL_GL_CATEGORY = 'gpu_toplevel'
15TOPLEVEL_SERVICE_CATEGORY = 'disabled-by-default-gpu.service'
16TOPLEVEL_DEVICE_CATEGORY = 'disabled-by-default-gpu.device'
17
18SERVICE_FRAME_END_MARKER = (TOPLEVEL_SERVICE_CATEGORY, 'SwapBuffer')
19DEVICE_FRAME_END_MARKER = (TOPLEVEL_DEVICE_CATEGORY, 'SwapBuffer')
20
21TRACKED_GL_CONTEXT_NAME = {'RenderCompositor': 'render_compositor',
22                           'BrowserCompositor': 'browser_compositor',
23                           'Compositor': 'browser_compositor'}
24
25
26def _CalculateFrameTimes(events_per_frame, event_data_func):
27  """Given a list of events per frame and a function to extract event time data,
28     returns a list of frame times."""
29  times_per_frame = []
30  for event_list in events_per_frame:
31    event_times = [event_data_func(event) for event in event_list]
32    times_per_frame.append(sum(event_times))
33  return times_per_frame
34
35
36def _CPUFrameTimes(events_per_frame):
37  """Given a list of events per frame, returns a list of CPU frame times."""
38  # CPU event frames are calculated using the event thread duration.
39  # Some platforms do not support thread_duration, convert those to 0.
40  return _CalculateFrameTimes(events_per_frame,
41                              lambda event: event.thread_duration or 0)
42
43
44def _GPUFrameTimes(events_per_frame):
45  """Given a list of events per frame, returns a list of GPU frame times."""
46  # GPU event frames are asynchronous slices which use the event duration.
47  return _CalculateFrameTimes(events_per_frame,
48                              lambda event: event.duration)
49
50
51def TimelineName(name, source_type, value_type):
52  """Constructs the standard name given in the timeline.
53
54  Args:
55    name: The name of the timeline, for example "total", or "render_compositor".
56    source_type: One of "cpu", "gpu" or None. None is only used for total times.
57    value_type: the type of value. For example "mean", "stddev"...etc.
58  """
59  if source_type:
60    return '%s_%s_%s_time' % (name, value_type, source_type)
61  else:
62    return '%s_%s_time' % (name, value_type)
63
64
65class GPUTimelineMetric(timeline_based_metric.TimelineBasedMetric):
66  """Computes GPU based metrics."""
67
68  def __init__(self):
69    super(GPUTimelineMetric, self).__init__()
70
71  def AddResults(self, model, _, interaction_records, results):
72    self.VerifyNonOverlappedRecords(interaction_records)
73    service_times = self._CalculateGPUTimelineData(model)
74    for value_item, durations in service_times.iteritems():
75      count = len(durations)
76      avg = 0.0
77      stddev = 0.0
78      maximum = 0.0
79      if count:
80        avg = sum(durations) / count
81        stddev = math.sqrt(sum((d - avg) ** 2 for d in durations) / count)
82        maximum = max(durations)
83
84      name, src = value_item
85
86      if src:
87        frame_times_name = '%s_%s_frame_times' % (name, src)
88      else:
89        frame_times_name = '%s_frame_times' % (name)
90
91      if durations:
92        results.AddValue(list_of_scalar_values.ListOfScalarValues(
93            results.current_page, frame_times_name, 'ms', durations,
94            tir_label=interaction_records[0].label,
95            improvement_direction=improvement_direction.DOWN))
96
97      results.AddValue(scalar.ScalarValue(
98          results.current_page, TimelineName(name, src, 'max'), 'ms', maximum,
99          tir_label=interaction_records[0].label,
100          improvement_direction=improvement_direction.DOWN))
101      results.AddValue(scalar.ScalarValue(
102          results.current_page, TimelineName(name, src, 'mean'), 'ms', avg,
103          tir_label=interaction_records[0].label,
104          improvement_direction=improvement_direction.DOWN))
105      results.AddValue(scalar.ScalarValue(
106          results.current_page, TimelineName(name, src, 'stddev'), 'ms', stddev,
107          tir_label=interaction_records[0].label,
108          improvement_direction=improvement_direction.DOWN))
109
110  def _CalculateGPUTimelineData(self, model):
111    """Uses the model and calculates the times for various values for each
112       frame. The return value will be a dictionary of the following format:
113         {
114           (EVENT_NAME1, SRC1_TYPE): [FRAME0_TIME, FRAME1_TIME...etc.],
115           (EVENT_NAME2, SRC2_TYPE): [FRAME0_TIME, FRAME1_TIME...etc.],
116         }
117
118       Events:
119         swap - The time in milliseconds between each swap marker.
120         total - The amount of time spent in the renderer thread.
121         TRACKED_NAMES: Using the TRACKED_GL_CONTEXT_NAME dict, we
122                        include the traces per frame for the
123                        tracked name.
124       Source Types:
125         None - This will only be valid for the "swap" event.
126         cpu - For an event, the "cpu" source type signifies time spent on the
127               gpu thread using the CPU. This uses the "gpu.service" markers.
128         gpu - For an event, the "gpu" source type signifies time spent on the
129               gpu thread using the GPU. This uses the "gpu.device" markers.
130    """
131    all_service_events = []
132    current_service_frame_end = sys.maxint
133    current_service_events = []
134
135    all_device_events = []
136    current_device_frame_end = sys.maxint
137    current_device_events = []
138
139    tracked_events = {}
140    tracked_events.update(
141        dict([((value, 'cpu'), [])
142              for value in TRACKED_GL_CONTEXT_NAME.itervalues()]))
143    tracked_events.update(
144        dict([((value, 'gpu'), [])
145              for value in TRACKED_GL_CONTEXT_NAME.itervalues()]))
146
147    # These will track traces within the current frame.
148    current_tracked_service_events = collections.defaultdict(list)
149    current_tracked_device_events = collections.defaultdict(list)
150
151    event_iter = model.IterAllEvents(
152        event_type_predicate=model_module.IsSliceOrAsyncSlice)
153    for event in event_iter:
154      # Look for frame end markers
155      if (event.category, event.name) == SERVICE_FRAME_END_MARKER:
156        current_service_frame_end = event.end
157      elif (event.category, event.name) == DEVICE_FRAME_END_MARKER:
158        current_device_frame_end = event.end
159
160      # Track all other toplevel gl category markers
161      elif event.args.get('gl_category', None) == TOPLEVEL_GL_CATEGORY:
162        base_name = event.name
163        dash_index = base_name.rfind('-')
164        if dash_index != -1:
165          base_name = base_name[:dash_index]
166        tracked_name = TRACKED_GL_CONTEXT_NAME.get(base_name, None)
167
168        if event.category == TOPLEVEL_SERVICE_CATEGORY:
169          # Check if frame has ended.
170          if event.start >= current_service_frame_end:
171            if current_service_events:
172              all_service_events.append(current_service_events)
173              for value in TRACKED_GL_CONTEXT_NAME.itervalues():
174                tracked_events[(value, 'cpu')].append(
175                    current_tracked_service_events[value])
176            current_service_events = []
177            current_service_frame_end = sys.maxint
178            current_tracked_service_events.clear()
179
180          current_service_events.append(event)
181          if tracked_name:
182            current_tracked_service_events[tracked_name].append(event)
183
184        elif event.category == TOPLEVEL_DEVICE_CATEGORY:
185          # Check if frame has ended.
186          if event.start >= current_device_frame_end:
187            if current_device_events:
188              all_device_events.append(current_device_events)
189              for value in TRACKED_GL_CONTEXT_NAME.itervalues():
190                tracked_events[(value, 'gpu')].append(
191                    current_tracked_device_events[value])
192            current_device_events = []
193            current_device_frame_end = sys.maxint
194            current_tracked_device_events.clear()
195
196          current_device_events.append(event)
197          if tracked_name:
198            current_tracked_device_events[tracked_name].append(event)
199
200    # Append Data for Last Frame.
201    if current_service_events:
202      all_service_events.append(current_service_events)
203      for value in TRACKED_GL_CONTEXT_NAME.itervalues():
204        tracked_events[(value, 'cpu')].append(
205            current_tracked_service_events[value])
206    if current_device_events:
207      all_device_events.append(current_device_events)
208      for value in TRACKED_GL_CONTEXT_NAME.itervalues():
209        tracked_events[(value, 'gpu')].append(
210            current_tracked_device_events[value])
211
212    # Calculate Mean Frame Time for the CPU side.
213    frame_times = []
214    if all_service_events:
215      prev_frame_end = all_service_events[0][0].start
216      for event_list in all_service_events:
217        last_service_event_in_frame = event_list[-1]
218        frame_times.append(last_service_event_in_frame.end - prev_frame_end)
219        prev_frame_end = last_service_event_in_frame.end
220
221    # Create the timeline data dictionary for service side traces.
222    total_frame_value = ('swap', None)
223    cpu_frame_value = ('total', 'cpu')
224    gpu_frame_value = ('total', 'gpu')
225    timeline_data = {}
226    timeline_data[total_frame_value] = frame_times
227    timeline_data[cpu_frame_value] = _CPUFrameTimes(all_service_events)
228    for value in TRACKED_GL_CONTEXT_NAME.itervalues():
229      cpu_value = (value, 'cpu')
230      timeline_data[cpu_value] = _CPUFrameTimes(tracked_events[cpu_value])
231
232    # Add in GPU side traces if it was supported (IE. device traces exist).
233    if all_device_events:
234      timeline_data[gpu_frame_value] = _GPUFrameTimes(all_device_events)
235      for value in TRACKED_GL_CONTEXT_NAME.itervalues():
236        gpu_value = (value, 'gpu')
237        tracked_gpu_event = tracked_events[gpu_value]
238        timeline_data[gpu_value] = _GPUFrameTimes(tracked_gpu_event)
239
240    return timeline_data
241