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 telemetry.perf_tests_helper import FlattenList
6 from telemetry.util import statistics
7 from telemetry.value import list_of_scalar_values
8 from telemetry.value import scalar
9 from telemetry.web_perf.metrics import rendering_stats
10 from telemetry.web_perf.metrics import timeline_based_metric
13 NOT_ENOUGH_FRAMES_MESSAGE = (
14 'Not enough frames for smoothness metrics (at least two are required).\n'
15 'Issues that have caused this in the past:\n'
16 '- Browser bugs that prevents the page from redrawing\n'
17 '- Bugs in the synthetic gesture code\n'
18 '- Page and benchmark out of sync (e.g. clicked element was renamed)\n'
19 '- Pages that render extremely slow\n'
20 '- Pages that can\'t be scrolled')
23 class SmoothnessMetric(timeline_based_metric.TimelineBasedMetric):
24 """Computes metrics that measure smoothness of animations over given ranges.
26 Animations are typically considered smooth if the frame rates are close to
27 60 frames per second (fps) and uniformly distributed over the sequence. To
28 determine if a timeline range contains a smooth animation, we update the
29 results object with several representative metrics:
31 frame_times: A list of raw frame times
32 mean_frame_time: The arithmetic mean of frame times
33 percentage_smooth: Percentage of frames that were hitting 60 FPS.
34 frame_time_discrepancy: The absolute discrepancy of frame timestamps
35 mean_pixels_approximated: The mean percentage of pixels approximated
36 queueing_durations: The queueing delay between compositor & main threads
38 Note that if any of the interaction records provided to AddResults have less
39 than 2 frames, we will return telemetry values with None values for each of
40 the smoothness metrics. Similarly, older browsers without support for
41 tracking the BeginMainFrame events will report a ListOfScalarValues with a
42 None value for the queueing duration metric.
46 super(SmoothnessMetric, self).__init__()
48 def AddResults(self, model, renderer_thread, interaction_records, results):
49 self.VerifyNonOverlappedRecords(interaction_records)
50 renderer_process = renderer_thread.parent
51 stats = rendering_stats.RenderingStats(
52 renderer_process, model.browser_process,
53 [r.GetBounds() for r in interaction_records])
54 self._PopulateResultsFromStats(results, stats)
56 def _PopulateResultsFromStats(self, results, stats):
57 page = results.current_page
59 self._ComputeQueueingDuration(page, stats),
60 self._ComputeFrameTimeDiscrepancy(page, stats),
61 self._ComputeMeanPixelsApproximated(page, stats)
63 values += self._ComputeLatencyMetric(page, stats, 'input_event_latency',
64 stats.input_event_latency)
65 values += self._ComputeLatencyMetric(page, stats, 'scroll_update_latency',
66 stats.scroll_update_latency)
67 values += self._ComputeFirstGestureScrollUpdateLatency(page, stats)
68 values += self._ComputeFrameTimeMetric(page, stats)
72 def _HasEnoughFrames(self, list_of_frame_timestamp_lists):
73 """Whether we have collected at least two frames in every timestamp list."""
74 return all(len(s) >= 2 for s in list_of_frame_timestamp_lists)
76 def _ComputeLatencyMetric(self, page, stats, name, list_of_latency_lists):
77 """Returns Values for the mean and discrepancy for given latency stats."""
79 latency_discrepancy = None
80 none_value_reason = None
81 if self._HasEnoughFrames(stats.frame_timestamps):
82 latency_list = FlattenList(list_of_latency_lists)
83 if len(latency_list) == 0:
85 mean_latency = round(statistics.ArithmeticMean(latency_list), 3)
86 latency_discrepancy = (
87 round(statistics.DurationsDiscrepancy(latency_list), 4))
89 none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE
92 page, 'mean_%s' % name, 'ms', mean_latency,
93 description='Arithmetic mean of the raw %s values' % name,
94 none_value_reason=none_value_reason),
96 page, '%s_discrepancy' % name, 'ms', latency_discrepancy,
97 description='Discrepancy of the raw %s values' % name,
98 none_value_reason=none_value_reason)
101 def _ComputeFirstGestureScrollUpdateLatency(self, page, stats):
102 """Returns a Value for the first gesture scroll update latency."""
103 first_gesture_scroll_update_latency = None
104 none_value_reason = None
105 if self._HasEnoughFrames(stats.frame_timestamps):
106 latency_list = FlattenList(stats.gesture_scroll_update_latency)
107 if len(latency_list) == 0:
109 first_gesture_scroll_update_latency = round(latency_list[0], 4)
111 none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE
114 page, 'first_gesture_scroll_update_latency', 'ms',
115 first_gesture_scroll_update_latency,
116 description='First gesture scroll update latency measures the time it '
117 'takes to process the very first gesture scroll update '
118 'input event. The first scroll gesture can often get '
119 'delayed by work related to page loading.',
120 none_value_reason=none_value_reason),
123 def _ComputeQueueingDuration(self, page, stats):
124 """Returns a Value for the frame queueing durations."""
125 queueing_durations = None
126 none_value_reason = None
127 if 'frame_queueing_durations' in stats.errors:
128 none_value_reason = stats.errors['frame_queueing_durations']
129 elif self._HasEnoughFrames(stats.frame_timestamps):
130 queueing_durations = FlattenList(stats.frame_queueing_durations)
131 if len(queueing_durations) == 0:
132 queueing_durations = None
133 none_value_reason = 'No frame queueing durations recorded.'
135 none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE
136 return list_of_scalar_values.ListOfScalarValues(
137 page, 'queueing_durations', 'ms', queueing_durations,
138 description='The frame queueing duration quantifies how out of sync '
139 'the compositor and renderer threads are. It is the amount '
140 'of wall time that elapses between a '
141 'ScheduledActionSendBeginMainFrame event in the compositor '
142 'thread and the corresponding BeginMainFrame event in the '
144 none_value_reason=none_value_reason)
146 def _ComputeFrameTimeMetric(self, page, stats):
147 """Returns Values for the frame time metrics.
149 This includes the raw and mean frame times, as well as the percentage of
150 frames that were hitting 60 fps.
153 mean_frame_time = None
154 percentage_smooth = None
155 none_value_reason = None
156 if self._HasEnoughFrames(stats.frame_timestamps):
157 frame_times = FlattenList(stats.frame_times)
158 mean_frame_time = round(statistics.ArithmeticMean(frame_times), 3)
159 # We use 17ms as a somewhat looser threshold, instead of 1000.0/60.0.
160 smooth_threshold = 17.0
161 smooth_count = sum(1 for t in frame_times if t < smooth_threshold)
162 percentage_smooth = float(smooth_count) / len(frame_times) * 100.0
164 none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE
166 list_of_scalar_values.ListOfScalarValues(
167 page, 'frame_times', 'ms', frame_times,
168 description='List of raw frame times, helpful to understand the '
170 none_value_reason=none_value_reason),
172 page, 'mean_frame_time', 'ms', mean_frame_time,
173 description='Arithmetic mean of frame times.',
174 none_value_reason=none_value_reason),
176 page, 'percentage_smooth', 'score', percentage_smooth,
177 description='Percentage of frames that were hitting 60 fps.',
178 none_value_reason=none_value_reason)
181 def _ComputeFrameTimeDiscrepancy(self, page, stats):
182 """Returns a Value for the absolute discrepancy of frame time stamps."""
184 frame_discrepancy = None
185 none_value_reason = None
186 if self._HasEnoughFrames(stats.frame_timestamps):
187 frame_discrepancy = round(statistics.TimestampsDiscrepancy(
188 stats.frame_timestamps), 4)
190 none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE
191 return scalar.ScalarValue(
192 page, 'frame_time_discrepancy', 'ms', frame_discrepancy,
193 description='Absolute discrepancy of frame time stamps, where '
194 'discrepancy is a measure of irregularity. It quantifies '
195 'the worst jank. For a single pause, discrepancy '
196 'corresponds to the length of this pause in milliseconds. '
197 'Consecutive pauses increase the discrepancy. This metric '
198 'is important because even if the mean and 95th '
199 'percentile are good, one long pause in the middle of an '
200 'interaction is still bad.',
201 none_value_reason=none_value_reason)
203 def _ComputeMeanPixelsApproximated(self, page, stats):
204 """Add the mean percentage of pixels approximated.
206 This looks at tiles which are missing or of low or non-ideal resolution.
208 mean_pixels_approximated = None
209 none_value_reason = None
210 if self._HasEnoughFrames(stats.frame_timestamps):
211 mean_pixels_approximated = round(statistics.ArithmeticMean(
212 FlattenList(stats.approximated_pixel_percentages)), 3)
214 none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE
215 return scalar.ScalarValue(
216 page, 'mean_pixels_approximated', 'percent', mean_pixels_approximated,
217 description='Percentage of pixels that were approximated '
218 '(checkerboarding, low-resolution tiles, etc.).',
219 none_value_reason=none_value_reason)