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.
5 from operator import attrgetter
7 from telemetry.page import page_test
8 from telemetry.web_perf.metrics import rendering_frame
10 # These are LatencyInfo component names indicating the various components
11 # that the input event has travelled through.
12 # This is when the input event first reaches chrome.
13 UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT'
14 # This is when the input event was originally created by OS.
15 ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT'
16 # This is when the input event was sent from browser to renderer.
17 BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT'
18 # This is when an input event is turned into a scroll update.
19 BEGIN_SCROLL_UPDATE_COMP_NAME = (
20 'INPUT_EVENT_LATENCY_BEGIN_SCROLL_UPDATE_MAIN_COMPONENT')
21 # This is when a scroll update is forwarded to the main thread.
22 FORWARD_SCROLL_UPDATE_COMP_NAME = (
23 'INPUT_EVENT_LATENCY_FORWARD_SCROLL_UPDATE_TO_MAIN_COMPONENT')
24 # This is when the input event has reached swap buffer.
25 END_COMP_NAME = 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT'
27 # Name for a main thread scroll update latency event.
28 SCROLL_UPDATE_EVENT_NAME = 'InputLatency:ScrollUpdate'
29 # Name for a gesture scroll update latency event.
30 GESTURE_SCROLL_UPDATE_EVENT_NAME = 'InputLatency:GestureScrollUpdate'
33 class NotEnoughFramesError(page_test.MeasurementFailure):
34 def __init__(self, frame_count):
35 super(NotEnoughFramesError, self).__init__(
36 'Only %i frame timestamps were collected ' % frame_count +
37 '(at least two are required).\n'
38 'Issues that have caused this in the past:\n' +
39 '- Browser bugs that prevents the page from redrawing\n' +
40 '- Bugs in the synthetic gesture code\n' +
41 '- Page and benchmark out of sync (e.g. clicked element was renamed)\n' +
42 '- Pages that render extremely slow\n' +
43 '- Pages that can\'t be scrolled')
46 def GetInputLatencyEvents(process, timeline_range):
47 """Get input events' LatencyInfo from the process's trace buffer that are
48 within the timeline_range.
50 Input events dump their LatencyInfo into trace buffer as async trace event
51 with name "InputLatency". The trace event has a memeber 'data' containing
58 for event in process.IterAllAsyncSlicesOfName('InputLatency'):
59 if event.start >= timeline_range.min and event.end <= timeline_range.max:
60 for ss in event.sub_slices:
62 input_events.append(ss)
66 def ComputeInputEventLatencies(input_events):
67 """ Compute input event latencies.
69 Input event latency is the time from when the input event is created to
70 when its resulted page is swap buffered.
71 Input event on differnt platforms uses different LatencyInfo component to
72 record its creation timestamp. We go through the following component list
73 to find the creation timestamp:
74 1. INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT -- when event is created in OS
75 2. INPUT_EVENT_LATENCY_UI_COMPONENT -- when event reaches Chrome
76 3. INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT -- when event reaches RenderWidget
78 If the latency starts with a
79 INPUT_EVENT_LATENCY_BEGIN_SCROLL_UPDATE_MAIN_COMPONENT component, then it is
80 classified as a scroll update instead of a normal input latency measure.
83 A list sorted by increasing start time of latencies which are tuples of
84 (input_event_name, latency_in_ms).
86 input_event_latencies = []
87 for event in input_events:
88 data = event.args['data']
89 if END_COMP_NAME in data:
90 end_time = data[END_COMP_NAME]['time']
91 if ORIGINAL_COMP_NAME in data:
92 start_time = data[ORIGINAL_COMP_NAME]['time']
93 elif UI_COMP_NAME in data:
94 start_time = data[UI_COMP_NAME]['time']
95 elif BEGIN_COMP_NAME in data:
96 start_time = data[BEGIN_COMP_NAME]['time']
97 elif BEGIN_SCROLL_UPDATE_COMP_NAME in data:
98 start_time = data[BEGIN_SCROLL_UPDATE_COMP_NAME]['time']
100 raise ValueError, 'LatencyInfo has no begin component'
101 latency = (end_time - start_time) / 1000.0
102 input_event_latencies.append((start_time, event.name, latency))
104 input_event_latencies.sort()
105 return [(name, latency) for _, name, latency in input_event_latencies]
108 def HasRenderingStats(process):
109 """ Returns True if the process contains at least one
110 BenchmarkInstrumentation::*RenderingStats event with a frame.
114 for event in process.IterAllSlicesOfName(
115 'BenchmarkInstrumentation::MainThreadRenderingStats'):
116 if 'data' in event.args and event.args['data']['frame_count'] == 1:
118 for event in process.IterAllSlicesOfName(
119 'BenchmarkInstrumentation::ImplThreadRenderingStats'):
120 if 'data' in event.args and event.args['data']['frame_count'] == 1:
125 class RenderingStats(object):
126 def __init__(self, renderer_process, browser_process, timeline_ranges):
128 Utility class for extracting rendering statistics from the timeline (or
129 other loggin facilities), and providing them in a common format to classes
130 that compute benchmark metrics from this data.
132 Stats are lists of lists of numbers. The outer list stores one list per
135 All *_time values are measured in milliseconds.
137 assert(len(timeline_ranges) > 0)
138 # Find the top level process with rendering stats (browser or renderer).
139 if HasRenderingStats(browser_process):
140 timestamp_process = browser_process
142 timestamp_process = renderer_process
144 self.frame_timestamps = []
145 self.frame_times = []
146 self.paint_times = []
147 self.painted_pixel_counts = []
148 self.record_times = []
149 self.recorded_pixel_counts = []
150 self.rasterize_times = []
151 self.rasterized_pixel_counts = []
152 self.approximated_pixel_percentages = []
153 # End-to-end latency for input event - from when input event is
154 # generated to when the its resulted page is swap buffered.
155 self.input_event_latency = []
156 self.frame_queueing_durations = []
157 # Latency from when a scroll update is sent to the main thread until the
158 # resulting frame is swapped.
159 self.scroll_update_latency = []
160 # Latency for a GestureScrollUpdate input event.
161 self.gesture_scroll_update_latency = []
163 for timeline_range in timeline_ranges:
164 self.frame_timestamps.append([])
165 self.frame_times.append([])
166 self.paint_times.append([])
167 self.painted_pixel_counts.append([])
168 self.record_times.append([])
169 self.recorded_pixel_counts.append([])
170 self.rasterize_times.append([])
171 self.rasterized_pixel_counts.append([])
172 self.approximated_pixel_percentages.append([])
173 self.input_event_latency.append([])
174 self.scroll_update_latency.append([])
175 self.gesture_scroll_update_latency.append([])
177 if timeline_range.is_empty:
179 self._InitFrameTimestampsFromTimeline(timestamp_process, timeline_range)
180 self._InitMainThreadRenderingStatsFromTimeline(
181 renderer_process, timeline_range)
182 self._InitImplThreadRenderingStatsFromTimeline(
183 renderer_process, timeline_range)
184 self._InitInputLatencyStatsFromTimeline(
185 browser_process, renderer_process, timeline_range)
186 self._InitFrameQueueingDurationsFromTimeline(
187 renderer_process, timeline_range)
189 # Check if we have collected at least 2 frames in every range. Otherwise we
190 # can't compute any meaningful metrics.
191 for segment in self.frame_timestamps:
193 raise NotEnoughFramesError(len(segment))
195 def _InitInputLatencyStatsFromTimeline(
196 self, browser_process, renderer_process, timeline_range):
197 latency_events = GetInputLatencyEvents(browser_process, timeline_range)
198 # Plugin input event's latency slice is generated in renderer process.
199 latency_events.extend(GetInputLatencyEvents(renderer_process,
201 input_event_latencies = ComputeInputEventLatencies(latency_events)
202 self.input_event_latency[-1] = [
203 latency for name, latency in input_event_latencies]
204 self.scroll_update_latency[-1] = [
205 latency for name, latency in input_event_latencies
206 if name == SCROLL_UPDATE_EVENT_NAME]
207 self.gesture_scroll_update_latency[-1] = [
208 latency for name, latency in input_event_latencies
209 if name == GESTURE_SCROLL_UPDATE_EVENT_NAME]
211 def _GatherEvents(self, event_name, process, timeline_range):
213 for event in process.IterAllSlicesOfName(event_name):
214 if event.start >= timeline_range.min and event.end <= timeline_range.max:
215 if 'data' not in event.args:
218 events.sort(key=attrgetter('start'))
221 def _AddFrameTimestamp(self, event):
222 frame_count = event.args['data']['frame_count']
224 raise ValueError('trace contains multi-frame render stats')
226 self.frame_timestamps[-1].append(
228 if len(self.frame_timestamps[-1]) >= 2:
229 self.frame_times[-1].append(round(self.frame_timestamps[-1][-1] -
230 self.frame_timestamps[-1][-2], 2))
232 def _InitFrameTimestampsFromTimeline(self, process, timeline_range):
233 event_name = 'BenchmarkInstrumentation::MainThreadRenderingStats'
234 for event in self._GatherEvents(event_name, process, timeline_range):
235 self._AddFrameTimestamp(event)
237 event_name = 'BenchmarkInstrumentation::ImplThreadRenderingStats'
238 for event in self._GatherEvents(event_name, process, timeline_range):
239 self._AddFrameTimestamp(event)
241 def _InitMainThreadRenderingStatsFromTimeline(self, process, timeline_range):
242 event_name = 'BenchmarkInstrumentation::MainThreadRenderingStats'
243 for event in self._GatherEvents(event_name, process, timeline_range):
244 data = event.args['data']
245 self.paint_times[-1].append(1000.0 * data['paint_time'])
246 self.painted_pixel_counts[-1].append(data['painted_pixel_count'])
247 self.record_times[-1].append(1000.0 * data['record_time'])
248 self.recorded_pixel_counts[-1].append(data['recorded_pixel_count'])
250 def _InitImplThreadRenderingStatsFromTimeline(self, process, timeline_range):
251 event_name = 'BenchmarkInstrumentation::ImplThreadRenderingStats'
252 for event in self._GatherEvents(event_name, process, timeline_range):
253 data = event.args['data']
254 self.rasterize_times[-1].append(1000.0 * data['rasterize_time'])
255 self.rasterized_pixel_counts[-1].append(data['rasterized_pixel_count'])
256 if data.get('visible_content_area', 0):
257 self.approximated_pixel_percentages[-1].append(
258 round(float(data['approximated_visible_content_area']) /
259 float(data['visible_content_area']) * 100.0, 3))
261 self.approximated_pixel_percentages[-1].append(0.0)
263 def _InitFrameQueueingDurationsFromTimeline(self, process, timeline_range):
265 events = rendering_frame.GetFrameEventsInsideRange(process,
267 new_frame_queueing_durations = [e.queueing_duration for e in events]
268 self.frame_queueing_durations.append(new_frame_queueing_durations)
269 except rendering_frame.NoBeginFrameIdException:
270 logging.warning('Current chrome version does not support the queueing '