| Index: tools/telemetry/telemetry/web_perf/metrics/smoothness.py
|
| diff --git a/tools/telemetry/telemetry/web_perf/metrics/smoothness.py b/tools/telemetry/telemetry/web_perf/metrics/smoothness.py
|
| index 301102709795b85999d30e91f87469743ab52da3..d795194d4da5671612a846d4e875eddbe5923457 100644
|
| --- a/tools/telemetry/telemetry/web_perf/metrics/smoothness.py
|
| +++ b/tools/telemetry/telemetry/web_perf/metrics/smoothness.py
|
| @@ -10,7 +10,37 @@ 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
|
| + mostly_smooth: Whether we hit 60 fps for 95% of all frames
|
| + jank: 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 None values for each of the smoothness metrics.
|
| + Similarly, older browsers without support for tracking the BeginMainFrame
|
| + events will report a None value for the queueing duration metric.
|
| + """
|
| +
|
| def __init__(self):
|
| super(SmoothnessMetric, self).__init__()
|
|
|
| @@ -20,64 +50,145 @@ class SmoothnessMetric(timeline_based_metric.TimelineBasedMetric):
|
| 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._ComputeFirstGestureScrollUpdateLatency(page, stats),
|
| + 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._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)
|
|
|
| - input_event_latency = FlattenList(stats.input_event_latency)
|
| - if input_event_latency:
|
| - mean_input_event_latency = statistics.ArithmeticMean(
|
| - input_event_latency)
|
| - input_event_latency_discrepancy = statistics.DurationsDiscrepancy(
|
| - input_event_latency)
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'mean_input_event_latency', 'ms',
|
| - round(mean_input_event_latency, 3)))
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'input_event_latency_discrepancy', 'ms',
|
| - round(input_event_latency_discrepancy, 4)))
|
| - scroll_update_latency = FlattenList(stats.scroll_update_latency)
|
| - if scroll_update_latency:
|
| - mean_scroll_update_latency = statistics.ArithmeticMean(
|
| - scroll_update_latency)
|
| - scroll_update_latency_discrepancy = statistics.DurationsDiscrepancy(
|
| - scroll_update_latency)
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'mean_scroll_update_latency', 'ms',
|
| - round(mean_scroll_update_latency, 3)))
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'scroll_update_latency_discrepancy', 'ms',
|
| - round(scroll_update_latency_discrepancy, 4)))
|
| - gesture_scroll_update_latency = FlattenList(
|
| - stats.gesture_scroll_update_latency)
|
| - if gesture_scroll_update_latency:
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'first_gesture_scroll_update_latency', 'ms',
|
| - round(gesture_scroll_update_latency[0], 4)))
|
| -
|
| - # List of queueing durations.
|
| - frame_queueing_durations = FlattenList(stats.frame_queueing_durations)
|
| - if frame_queueing_durations:
|
| - results.AddValue(list_of_scalar_values.ListOfScalarValues(
|
| - results.current_page, 'queueing_durations', 'ms',
|
| - frame_queueing_durations))
|
| -
|
| - # List of raw frame times.
|
| - frame_times = FlattenList(stats.frame_times)
|
| - results.AddValue(list_of_scalar_values.ListOfScalarValues(
|
| - results.current_page, 'frame_times', 'ms', frame_times,
|
| - description='List of raw frame times, helpful to understand the other '
|
| - 'metrics.'))
|
| -
|
| - # Arithmetic mean of frame times.
|
| - mean_frame_time = statistics.ArithmeticMean(frame_times)
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'mean_frame_time', 'ms',
|
| - round(mean_frame_time, 3),
|
| - description='Arithmetic mean of frame times.'))
|
| -
|
| - # Absolute discrepancy of frame time stamps.
|
| - frame_discrepancy = statistics.TimestampsDiscrepancy(
|
| - stats.frame_timestamps)
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'jank', 'ms', round(frame_discrepancy, 4),
|
| + 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:
|
| + mean_latency = round(statistics.ArithmeticMean(latency_list), 3)
|
| + latency_discrepancy = (
|
| + round(statistics.DurationsDiscrepancy(latency_list), 4))
|
| + else:
|
| + none_value_reason = 'No latency values recorded.'
|
| + 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:
|
| + first_gesture_scroll_update_latency = round(latency_list[0], 4)
|
| + else:
|
| + none_value_reason = 'No gesture scroll update latency values recorded.'
|
| + 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 mostly_smooth
|
| + metric which tracks whether we hit 60 fps for 95% of the frames.
|
| + """
|
| + frame_times = None
|
| + mean_frame_time = None
|
| + mostly_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 19ms as a somewhat looser threshold, instead of 1000.0/60.0.
|
| + percentile_95 = statistics.Percentile(frame_times, 95.0)
|
| + mostly_smooth = 1.0 if percentile_95 < 19.0 else 0.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, 'mostly_smooth', 'score', mostly_smooth,
|
| + description='Were 95 percent of the frames hitting 60 fps?'
|
| + 'boolean value (1/0).',
|
| + 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, 'jank', '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 '
|
| @@ -85,22 +196,23 @@ class SmoothnessMetric(timeline_based_metric.TimelineBasedMetric):
|
| '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.'))
|
| -
|
| - # Are we hitting 60 fps for 95 percent of all frames?
|
| - # We use 19ms as a somewhat looser threshold, instead of 1000.0/60.0.
|
| - percentile_95 = statistics.Percentile(frame_times, 95.0)
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'mostly_smooth', 'score',
|
| - 1.0 if percentile_95 < 19.0 else 0.0,
|
| - description='Were 95 percent of the frames hitting 60 fps?'
|
| - 'boolean value (1/0).'))
|
| -
|
| - # Mean percentage of pixels approximated (missing tiles, low resolution
|
| - # tiles, non-ideal resolution tiles).
|
| - results.AddValue(scalar.ScalarValue(
|
| - results.current_page, 'mean_pixels_approximated', 'percent',
|
| - round(statistics.ArithmeticMean(
|
| - FlattenList(stats.approximated_pixel_percentages)), 3),
|
| + '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.).'))
|
| + '(checkerboarding, low-resolution tiles, etc.).',
|
| + none_value_reason=none_value_reason)
|
|
|