| # Copyright 2014 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from telemetry.perf_tests_helper import FlattenList |
| from telemetry.util import statistics |
| from telemetry.value import list_of_scalar_values |
| from telemetry.value import scalar |
| from telemetry.web_perf.metrics import rendering_stats |
| from telemetry.web_perf.metrics import timeline_based_metric |
| |
| |
| NOT_ENOUGH_FRAMES_MESSAGE = ( |
| 'Not enough frames for smoothness metrics (at least two are required).\n' |
| 'Issues that have caused this in the past:\n' |
| '- Browser bugs that prevents the page from redrawing\n' |
| '- Bugs in the synthetic gesture code\n' |
| '- Page and benchmark out of sync (e.g. clicked element was renamed)\n' |
| '- Pages that render extremely slow\n' |
| '- Pages that can\'t be scrolled') |
| |
| |
| class SmoothnessMetric(timeline_based_metric.TimelineBasedMetric): |
| """Computes metrics that measure smoothness of animations over given ranges. |
| |
| Animations are typically considered smooth if the frame rates are close to |
| 60 frames per second (fps) and uniformly distributed over the sequence. To |
| determine if a timeline range contains a smooth animation, we update the |
| results object with several representative metrics: |
| |
| frame_times: A list of raw frame times |
| mean_frame_time: The arithmetic mean of frame times |
| percentage_smooth: Percentage of frames that were hitting 60 FPS. |
| frame_time_discrepancy: The absolute discrepancy of frame timestamps |
| mean_pixels_approximated: The mean percentage of pixels approximated |
| queueing_durations: The queueing delay between compositor & main threads |
| |
| Note that if any of the interaction records provided to AddResults have less |
| than 2 frames, we will return telemetry values with None values for each of |
| the smoothness metrics. Similarly, older browsers without support for |
| tracking the BeginMainFrame events will report a ListOfScalarValues with a |
| None value for the queueing duration metric. |
| """ |
| |
| def __init__(self): |
| super(SmoothnessMetric, self).__init__() |
| |
| def AddResults(self, model, renderer_thread, interaction_records, results): |
| self.VerifyNonOverlappedRecords(interaction_records) |
| renderer_process = renderer_thread.parent |
| stats = rendering_stats.RenderingStats( |
| renderer_process, model.browser_process, |
| [r.GetBounds() for r in interaction_records]) |
| self._PopulateResultsFromStats(results, stats) |
| |
| def _PopulateResultsFromStats(self, results, stats): |
| page = results.current_page |
| values = [ |
| self._ComputeQueueingDuration(page, stats), |
| self._ComputeFrameTimeDiscrepancy(page, stats), |
| self._ComputeMeanPixelsApproximated(page, stats) |
| ] |
| values += self._ComputeLatencyMetric(page, stats, 'input_event_latency', |
| stats.input_event_latency) |
| values += self._ComputeLatencyMetric(page, stats, 'scroll_update_latency', |
| stats.scroll_update_latency) |
| values += self._ComputeFirstGestureScrollUpdateLatency(page, stats) |
| values += self._ComputeFrameTimeMetric(page, stats) |
| for v in values: |
| results.AddValue(v) |
| |
| def _HasEnoughFrames(self, list_of_frame_timestamp_lists): |
| """Whether we have collected at least two frames in every timestamp list.""" |
| return all(len(s) >= 2 for s in list_of_frame_timestamp_lists) |
| |
| def _ComputeLatencyMetric(self, page, stats, name, list_of_latency_lists): |
| """Returns Values for the mean and discrepancy for given latency stats.""" |
| mean_latency = None |
| latency_discrepancy = None |
| none_value_reason = None |
| if self._HasEnoughFrames(stats.frame_timestamps): |
| latency_list = FlattenList(list_of_latency_lists) |
| if len(latency_list) == 0: |
| return () |
| mean_latency = round(statistics.ArithmeticMean(latency_list), 3) |
| latency_discrepancy = ( |
| round(statistics.DurationsDiscrepancy(latency_list), 4)) |
| else: |
| none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE |
| return ( |
| scalar.ScalarValue( |
| page, 'mean_%s' % name, 'ms', mean_latency, |
| description='Arithmetic mean of the raw %s values' % name, |
| none_value_reason=none_value_reason), |
| scalar.ScalarValue( |
| page, '%s_discrepancy' % name, 'ms', latency_discrepancy, |
| description='Discrepancy of the raw %s values' % name, |
| none_value_reason=none_value_reason) |
| ) |
| |
| def _ComputeFirstGestureScrollUpdateLatency(self, page, stats): |
| """Returns a Value for the first gesture scroll update latency.""" |
| first_gesture_scroll_update_latency = None |
| none_value_reason = None |
| if self._HasEnoughFrames(stats.frame_timestamps): |
| latency_list = FlattenList(stats.gesture_scroll_update_latency) |
| if len(latency_list) == 0: |
| return () |
| first_gesture_scroll_update_latency = round(latency_list[0], 4) |
| else: |
| none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE |
| return ( |
| scalar.ScalarValue( |
| page, 'first_gesture_scroll_update_latency', 'ms', |
| first_gesture_scroll_update_latency, |
| description='First gesture scroll update latency measures the time it ' |
| 'takes to process the very first gesture scroll update ' |
| 'input event. The first scroll gesture can often get ' |
| 'delayed by work related to page loading.', |
| none_value_reason=none_value_reason), |
| ) |
| |
| def _ComputeQueueingDuration(self, page, stats): |
| """Returns a Value for the frame queueing durations.""" |
| queueing_durations = None |
| none_value_reason = None |
| if 'frame_queueing_durations' in stats.errors: |
| none_value_reason = stats.errors['frame_queueing_durations'] |
| elif self._HasEnoughFrames(stats.frame_timestamps): |
| queueing_durations = FlattenList(stats.frame_queueing_durations) |
| if len(queueing_durations) == 0: |
| queueing_durations = None |
| none_value_reason = 'No frame queueing durations recorded.' |
| else: |
| none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE |
| return list_of_scalar_values.ListOfScalarValues( |
| page, 'queueing_durations', 'ms', queueing_durations, |
| description='The frame queueing duration quantifies how out of sync ' |
| 'the compositor and renderer threads are. It is the amount ' |
| 'of wall time that elapses between a ' |
| 'ScheduledActionSendBeginMainFrame event in the compositor ' |
| 'thread and the corresponding BeginMainFrame event in the ' |
| 'main thread.', |
| none_value_reason=none_value_reason) |
| |
| def _ComputeFrameTimeMetric(self, page, stats): |
| """Returns Values for the frame time metrics. |
| |
| This includes the raw and mean frame times, as well as the percentage of |
| frames that were hitting 60 fps. |
| """ |
| frame_times = None |
| mean_frame_time = None |
| percentage_smooth = None |
| none_value_reason = None |
| if self._HasEnoughFrames(stats.frame_timestamps): |
| frame_times = FlattenList(stats.frame_times) |
| mean_frame_time = round(statistics.ArithmeticMean(frame_times), 3) |
| # We use 17ms as a somewhat looser threshold, instead of 1000.0/60.0. |
| smooth_threshold = 17.0 |
| smooth_count = sum(1 for t in frame_times if t < smooth_threshold) |
| percentage_smooth = float(smooth_count) / len(frame_times) * 100.0 |
| else: |
| none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE |
| return ( |
| list_of_scalar_values.ListOfScalarValues( |
| page, 'frame_times', 'ms', frame_times, |
| description='List of raw frame times, helpful to understand the ' |
| 'other metrics.', |
| none_value_reason=none_value_reason), |
| scalar.ScalarValue( |
| page, 'mean_frame_time', 'ms', mean_frame_time, |
| description='Arithmetic mean of frame times.', |
| none_value_reason=none_value_reason), |
| scalar.ScalarValue( |
| page, 'percentage_smooth', 'score', percentage_smooth, |
| description='Percentage of frames that were hitting 60 fps.', |
| none_value_reason=none_value_reason) |
| ) |
| |
| def _ComputeFrameTimeDiscrepancy(self, page, stats): |
| """Returns a Value for the absolute discrepancy of frame time stamps.""" |
| |
| frame_discrepancy = None |
| none_value_reason = None |
| if self._HasEnoughFrames(stats.frame_timestamps): |
| frame_discrepancy = round(statistics.TimestampsDiscrepancy( |
| stats.frame_timestamps), 4) |
| else: |
| none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE |
| return scalar.ScalarValue( |
| page, 'frame_time_discrepancy', 'ms', frame_discrepancy, |
| description='Absolute discrepancy of frame time stamps, where ' |
| 'discrepancy is a measure of irregularity. It quantifies ' |
| 'the worst jank. For a single pause, discrepancy ' |
| 'corresponds to the length of this pause in milliseconds. ' |
| 'Consecutive pauses increase the discrepancy. This metric ' |
| 'is important because even if the mean and 95th ' |
| 'percentile are good, one long pause in the middle of an ' |
| 'interaction is still bad.', |
| none_value_reason=none_value_reason) |
| |
| def _ComputeMeanPixelsApproximated(self, page, stats): |
| """Add the mean percentage of pixels approximated. |
| |
| This looks at tiles which are missing or of low or non-ideal resolution. |
| """ |
| mean_pixels_approximated = None |
| none_value_reason = None |
| if self._HasEnoughFrames(stats.frame_timestamps): |
| mean_pixels_approximated = round(statistics.ArithmeticMean( |
| FlattenList(stats.approximated_pixel_percentages)), 3) |
| else: |
| none_value_reason = NOT_ENOUGH_FRAMES_MESSAGE |
| return scalar.ScalarValue( |
| page, 'mean_pixels_approximated', 'percent', mean_pixels_approximated, |
| description='Percentage of pixels that were approximated ' |
| '(checkerboarding, low-resolution tiles, etc.).', |
| none_value_reason=none_value_reason) |