| # Copyright 2013 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.util import image_util |
| from telemetry.util import rgba_color |
| from telemetry.value import scalar |
| |
| from metrics import Metric |
| |
| |
| class SpeedIndexMetric(Metric): |
| """The speed index metric is one way of measuring page load speed. |
| |
| It is meant to approximate user perception of page load speed, and it |
| is based on the amount of time that it takes to paint to the visual |
| portion of the screen. It includes paint events that occur after the |
| onload event, and it doesn't include time loading things off-screen. |
| |
| This speed index metric is based on WebPageTest.org (WPT). |
| For more info see: http://goo.gl/e7AH5l |
| """ |
| |
| def __init__(self): |
| super(SpeedIndexMetric, self).__init__() |
| self._impl = None |
| |
| @classmethod |
| def CustomizeBrowserOptions(cls, options): |
| options.AppendExtraBrowserArgs('--disable-infobars') |
| |
| def Start(self, _, tab): |
| """Start recording events. |
| |
| This method should be called in the WillNavigateToPage method of |
| a PageTest, so that all the events can be captured. If it's called |
| in DidNavigateToPage, that will be too late. |
| """ |
| if not tab.video_capture_supported: |
| return |
| self._impl = VideoSpeedIndexImpl() |
| self._impl.Start(tab) |
| |
| def Stop(self, _, tab): |
| """Stop recording.""" |
| if not tab.video_capture_supported: |
| return |
| assert self._impl, 'Must call Start() before Stop()' |
| assert self.IsFinished(tab), 'Must wait for IsFinished() before Stop()' |
| self._impl.Stop(tab) |
| |
| # Optional argument chart_name is not in base class Metric. |
| # pylint: disable=arguments-differ |
| def AddResults(self, tab, results, chart_name=None): |
| """Calculate the speed index and add it to the results.""" |
| try: |
| if tab.video_capture_supported: |
| index = self._impl.CalculateSpeedIndex(tab) |
| none_value_reason = None |
| else: |
| index = None |
| none_value_reason = 'Video capture is not supported.' |
| finally: |
| self._impl = None # Release the tab so that it can be disconnected. |
| |
| results.AddValue(scalar.ScalarValue( |
| results.current_page, '%s_speed_index' % chart_name, 'ms', index, |
| description='Speed Index. This focuses on time when visible parts of ' |
| 'page are displayed and shows the time when the ' |
| 'first look is "almost" composed. If the contents of the ' |
| 'testing page are composed by only static resources, load ' |
| 'time can measure more accurately and speed index will be ' |
| 'smaller than the load time. On the other hand, If the ' |
| 'contents are composed by many XHR requests with small ' |
| 'main resource and javascript, speed index will be able to ' |
| 'get the features of performance more accurately than load ' |
| 'time because the load time will measure the time when ' |
| 'static resources are loaded. If you want to get more ' |
| 'detail, please refer to http://goo.gl/Rw3d5d. Currently ' |
| 'there are two implementations: for Android and for ' |
| 'Desktop. The Android version uses video capture; the ' |
| 'Desktop one uses paint events and has extra overhead to ' |
| 'catch paint events.', none_value_reason=none_value_reason)) |
| |
| def IsFinished(self, tab): |
| """Decide whether the recording should be stopped. |
| |
| A page may repeatedly request resources in an infinite loop; a timeout |
| should be placed in any measurement that uses this metric, e.g.: |
| def IsDone(): |
| return self._speedindex.IsFinished(tab) |
| util.WaitFor(IsDone, 60) |
| |
| Returns: |
| True if 2 seconds have passed since last resource received, false |
| otherwise. |
| """ |
| return tab.HasReachedQuiescence() |
| |
| |
| class SpeedIndexImpl(object): |
| |
| def Start(self, tab): |
| raise NotImplementedError() |
| |
| def Stop(self, tab): |
| raise NotImplementedError() |
| |
| def GetTimeCompletenessList(self, tab): |
| """Returns a list of time to visual completeness tuples. |
| |
| In the WPT PHP implementation, this is also called 'visual progress'. |
| """ |
| raise NotImplementedError() |
| |
| def CalculateSpeedIndex(self, tab): |
| """Calculate the speed index. |
| |
| The speed index number conceptually represents the number of milliseconds |
| that the page was "visually incomplete". If the page were 0% complete for |
| 1000 ms, then the score would be 1000; if it were 0% complete for 100 ms |
| then 90% complete (ie 10% incomplete) for 900 ms, then the score would be |
| 1.0*100 + 0.1*900 = 190. |
| |
| Returns: |
| A single number, milliseconds of visual incompleteness. |
| """ |
| time_completeness_list = self.GetTimeCompletenessList(tab) |
| prev_completeness = 0.0 |
| speed_index = 0.0 |
| prev_time = time_completeness_list[0][0] |
| for time, completeness in time_completeness_list: |
| # Add the incremental value for the interval just before this event. |
| elapsed_time = time - prev_time |
| incompleteness = (1.0 - prev_completeness) |
| speed_index += elapsed_time * incompleteness |
| |
| # Update variables for next iteration. |
| prev_completeness = completeness |
| prev_time = time |
| return int(speed_index) |
| |
| |
| class VideoSpeedIndexImpl(SpeedIndexImpl): |
| |
| def __init__(self, image_util_module=image_util): |
| # Allow image_util to be passed in so we can fake it out for testing. |
| super(VideoSpeedIndexImpl, self).__init__() |
| self._time_completeness_list = None |
| self._image_util_module = image_util_module |
| |
| def Start(self, tab): |
| assert tab.video_capture_supported |
| # Blank out the current page so it doesn't count towards the new page's |
| # completeness. |
| tab.Highlight(rgba_color.WHITE) |
| # TODO(tonyg): Bitrate is arbitrary here. Experiment with screen capture |
| # overhead vs. speed index accuracy and set the bitrate appropriately. |
| tab.StartVideoCapture(min_bitrate_mbps=4) |
| |
| def Stop(self, tab): |
| # Ignore white because Chrome may blank out the page during load and we want |
| # that to count as 0% complete. Relying on this fact, we also blank out the |
| # previous page to white. The tolerance of 8 experimentally does well with |
| # video capture at 4mbps. We should keep this as low as possible with |
| # supported video compression settings. |
| video_capture = tab.StopVideoCapture() |
| histograms = [ |
| (time, self._image_util_module.GetColorHistogram( |
| image, ignore_color=rgba_color.WHITE, tolerance=8)) |
| for time, image in video_capture.GetVideoFrameIter() |
| ] |
| |
| start_histogram = histograms[0][1] |
| final_histogram = histograms[-1][1] |
| total_distance = start_histogram.Distance(final_histogram) |
| |
| def FrameProgress(histogram): |
| if total_distance == 0: |
| if histogram.Distance(final_histogram) == 0: |
| return 1.0 |
| else: |
| return 0.0 |
| return 1 - histogram.Distance(final_histogram) / total_distance |
| |
| self._time_completeness_list = [(time, FrameProgress(hist)) |
| for time, hist in histograms] |
| |
| def GetTimeCompletenessList(self, tab): |
| assert self._time_completeness_list, 'Must call Stop() first.' |
| return self._time_completeness_list |