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.
4from operator import attrgetter
5
6from telemetry.web_perf.metrics import rendering_frame
7
8# These are LatencyInfo component names indicating the various components
9# that the input event has travelled through.
10# This is when the input event first reaches chrome.
11UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT'
12# This is when the input event was originally created by OS.
13ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT'
14# This is when the input event was sent from browser to renderer.
15BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT'
16# This is when an input event is turned into a scroll update.
17BEGIN_SCROLL_UPDATE_COMP_NAME = (
18    'INPUT_EVENT_LATENCY_BEGIN_SCROLL_UPDATE_MAIN_COMPONENT')
19# This is when a scroll update is forwarded to the main thread.
20FORWARD_SCROLL_UPDATE_COMP_NAME = (
21    'INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT')
22# This is when the input event has reached swap buffer.
23END_COMP_NAME = 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT'
24
25# Name for a main thread scroll update latency event.
26SCROLL_UPDATE_EVENT_NAME = 'InputLatency:ScrollUpdate'
27# Name for a gesture scroll update latency event.
28GESTURE_SCROLL_UPDATE_EVENT_NAME  = 'InputLatency:GestureScrollUpdate'
29
30
31def GetInputLatencyEvents(process, timeline_range):
32  """Get input events' LatencyInfo from the process's trace buffer that are
33     within the timeline_range.
34
35  Input events dump their LatencyInfo into trace buffer as async trace event
36  with name "InputLatency". The trace event has a memeber 'data' containing
37  its latency history.
38
39  """
40  input_events = []
41  if not process:
42    return input_events
43  for event in process.IterAllAsyncSlicesOfName('InputLatency'):
44    if event.start >= timeline_range.min and event.end <= timeline_range.max:
45      for ss in event.sub_slices:
46        if 'data' in ss.args:
47          input_events.append(ss)
48  return input_events
49
50
51def ComputeInputEventLatencies(input_events):
52  """ Compute input event latencies.
53
54  Input event latency is the time from when the input event is created to
55  when its resulted page is swap buffered.
56  Input event on differnt platforms uses different LatencyInfo component to
57  record its creation timestamp. We go through the following component list
58  to find the creation timestamp:
59  1. INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT -- when event is created in OS
60  2. INPUT_EVENT_LATENCY_UI_COMPONENT -- when event reaches Chrome
61  3. INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT -- when event reaches RenderWidget
62
63  If the latency starts with a
64  INPUT_EVENT_LATENCY_BEGIN_SCROLL_UPDATE_MAIN_COMPONENT component, then it is
65  classified as a scroll update instead of a normal input latency measure.
66
67  Returns:
68    A list sorted by increasing start time of latencies which are tuples of
69    (input_event_name, latency_in_ms).
70  """
71  input_event_latencies = []
72  for event in input_events:
73    data = event.args['data']
74    if END_COMP_NAME in data:
75      end_time = data[END_COMP_NAME]['time']
76      if ORIGINAL_COMP_NAME in data:
77        start_time = data[ORIGINAL_COMP_NAME]['time']
78      elif UI_COMP_NAME in data:
79        start_time = data[UI_COMP_NAME]['time']
80      elif BEGIN_COMP_NAME in data:
81        start_time = data[BEGIN_COMP_NAME]['time']
82      elif BEGIN_SCROLL_UPDATE_COMP_NAME in data:
83        start_time = data[BEGIN_SCROLL_UPDATE_COMP_NAME]['time']
84      else:
85        raise ValueError, 'LatencyInfo has no begin component'
86      latency = (end_time - start_time) / 1000.0
87      input_event_latencies.append((start_time, event.name, latency))
88
89  input_event_latencies.sort()
90  return [(name, latency) for _, name, latency in input_event_latencies]
91
92
93def HasRenderingStats(process):
94  """ Returns True if the process contains at least one
95      BenchmarkInstrumentation::*RenderingStats event with a frame.
96  """
97  if not process:
98    return False
99  for event in process.IterAllSlicesOfName(
100      'BenchmarkInstrumentation::MainThreadRenderingStats'):
101    if 'data' in event.args and event.args['data']['frame_count'] == 1:
102      return True
103  for event in process.IterAllSlicesOfName(
104      'BenchmarkInstrumentation::ImplThreadRenderingStats'):
105    if 'data' in event.args and event.args['data']['frame_count'] == 1:
106      return True
107  return False
108
109
110class RenderingStats(object):
111  def __init__(self, renderer_process, browser_process, timeline_ranges):
112    """
113    Utility class for extracting rendering statistics from the timeline (or
114    other loggin facilities), and providing them in a common format to classes
115    that compute benchmark metrics from this data.
116
117    Stats are lists of lists of numbers. The outer list stores one list per
118    timeline range.
119
120    All *_time values are measured in milliseconds.
121    """
122    assert(len(timeline_ranges) > 0)
123    # Find the top level process with rendering stats (browser or renderer).
124    if HasRenderingStats(browser_process):
125      timestamp_process = browser_process
126    else:
127      timestamp_process = renderer_process
128
129    # A lookup from list names below to any errors or exceptions encountered
130    # in attempting to generate that list.
131    self.errors = {}
132
133    self.frame_timestamps = []
134    self.frame_times = []
135    self.paint_times = []
136    self.painted_pixel_counts = []
137    self.record_times = []
138    self.recorded_pixel_counts = []
139    self.rasterize_times = []
140    self.rasterized_pixel_counts = []
141    self.approximated_pixel_percentages = []
142    # End-to-end latency for input event - from when input event is
143    # generated to when the its resulted page is swap buffered.
144    self.input_event_latency = []
145    self.frame_queueing_durations = []
146    # Latency from when a scroll update is sent to the main thread until the
147    # resulting frame is swapped.
148    self.scroll_update_latency = []
149    # Latency for a GestureScrollUpdate input event.
150    self.gesture_scroll_update_latency = []
151
152    for timeline_range in timeline_ranges:
153      self.frame_timestamps.append([])
154      self.frame_times.append([])
155      self.paint_times.append([])
156      self.painted_pixel_counts.append([])
157      self.record_times.append([])
158      self.recorded_pixel_counts.append([])
159      self.rasterize_times.append([])
160      self.rasterized_pixel_counts.append([])
161      self.approximated_pixel_percentages.append([])
162      self.input_event_latency.append([])
163      self.scroll_update_latency.append([])
164      self.gesture_scroll_update_latency.append([])
165
166      if timeline_range.is_empty:
167        continue
168      self._InitFrameTimestampsFromTimeline(timestamp_process, timeline_range)
169      self._InitMainThreadRenderingStatsFromTimeline(
170          renderer_process, timeline_range)
171      self._InitImplThreadRenderingStatsFromTimeline(
172          renderer_process, timeline_range)
173      self._InitInputLatencyStatsFromTimeline(
174          browser_process, renderer_process, timeline_range)
175      self._InitFrameQueueingDurationsFromTimeline(
176          renderer_process, timeline_range)
177
178  def _InitInputLatencyStatsFromTimeline(
179      self, browser_process, renderer_process, timeline_range):
180    latency_events = GetInputLatencyEvents(browser_process, timeline_range)
181    # Plugin input event's latency slice is generated in renderer process.
182    latency_events.extend(GetInputLatencyEvents(renderer_process,
183                                                timeline_range))
184    input_event_latencies = ComputeInputEventLatencies(latency_events)
185    # Don't include scroll updates in the overall input latency measurement,
186    # because scroll updates can take much more time to process than other
187    # input events and would therefore add noise to overall latency numbers.
188    self.input_event_latency[-1] = [
189        latency for name, latency in input_event_latencies
190        if name != SCROLL_UPDATE_EVENT_NAME]
191    self.scroll_update_latency[-1] = [
192        latency for name, latency in input_event_latencies
193        if name == SCROLL_UPDATE_EVENT_NAME]
194    self.gesture_scroll_update_latency[-1] = [
195        latency for name, latency in input_event_latencies
196        if name == GESTURE_SCROLL_UPDATE_EVENT_NAME]
197
198  def _GatherEvents(self, event_name, process, timeline_range):
199    events = []
200    for event in process.IterAllSlicesOfName(event_name):
201      if event.start >= timeline_range.min and event.end <= timeline_range.max:
202        if 'data' not in event.args:
203          continue
204        events.append(event)
205    events.sort(key=attrgetter('start'))
206    return events
207
208  def _AddFrameTimestamp(self, event):
209    frame_count = event.args['data']['frame_count']
210    if frame_count > 1:
211      raise ValueError('trace contains multi-frame render stats')
212    if frame_count == 1:
213      self.frame_timestamps[-1].append(
214          event.start)
215      if len(self.frame_timestamps[-1]) >= 2:
216        self.frame_times[-1].append(round(self.frame_timestamps[-1][-1] -
217                                          self.frame_timestamps[-1][-2], 2))
218
219  def _InitFrameTimestampsFromTimeline(self, process, timeline_range):
220    event_name = 'BenchmarkInstrumentation::MainThreadRenderingStats'
221    for event in self._GatherEvents(event_name, process, timeline_range):
222      self._AddFrameTimestamp(event)
223
224    event_name = 'BenchmarkInstrumentation::ImplThreadRenderingStats'
225    for event in self._GatherEvents(event_name, process, timeline_range):
226      self._AddFrameTimestamp(event)
227
228  def _InitMainThreadRenderingStatsFromTimeline(self, process, timeline_range):
229    event_name = 'BenchmarkInstrumentation::MainThreadRenderingStats'
230    for event in self._GatherEvents(event_name, process, timeline_range):
231      data = event.args['data']
232      self.paint_times[-1].append(1000.0 * data['paint_time'])
233      self.painted_pixel_counts[-1].append(data['painted_pixel_count'])
234      self.record_times[-1].append(1000.0 * data['record_time'])
235      self.recorded_pixel_counts[-1].append(data['recorded_pixel_count'])
236
237  def _InitImplThreadRenderingStatsFromTimeline(self, process, timeline_range):
238    event_name = 'BenchmarkInstrumentation::ImplThreadRenderingStats'
239    for event in self._GatherEvents(event_name, process, timeline_range):
240      data = event.args['data']
241      self.rasterize_times[-1].append(1000.0 * data['rasterize_time'])
242      self.rasterized_pixel_counts[-1].append(data['rasterized_pixel_count'])
243      if data.get('visible_content_area', 0):
244        self.approximated_pixel_percentages[-1].append(
245            round(float(data['approximated_visible_content_area']) /
246                  float(data['visible_content_area']) * 100.0, 3))
247      else:
248        self.approximated_pixel_percentages[-1].append(0.0)
249
250  def _InitFrameQueueingDurationsFromTimeline(self, process, timeline_range):
251    try:
252      events = rendering_frame.GetFrameEventsInsideRange(process,
253                                                         timeline_range)
254      new_frame_queueing_durations = [e.queueing_duration for e in events]
255      self.frame_queueing_durations.append(new_frame_queueing_durations)
256    except rendering_frame.NoBeginFrameIdException:
257      self.errors['frame_queueing_durations'] = (
258          'Current chrome version does not support the queueing delay metric.')
259