Chromium Code Reviews| Index: tools/perf/metrics/speedindex.py |
| diff --git a/tools/perf/metrics/speedindex.py b/tools/perf/metrics/speedindex.py |
| index 9a453413408fdab328ab4b9e544465bdb9d02a7d..f54a83a0842737a181245d59764793e5b5f54551 100644 |
| --- a/tools/perf/metrics/speedindex.py |
| +++ b/tools/perf/metrics/speedindex.py |
| @@ -7,6 +7,7 @@ import os |
| from metrics import Metric |
| + |
| class SpeedIndexMetric(Metric): |
| """The speed index metric is one way of measuring page load speed. |
| @@ -15,11 +16,12 @@ class SpeedIndexMetric(Metric): |
| 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 the devtools speed index at |
| - WebPageTest.org (WPT). For more info see: http://goo.gl/e7AH5l |
| + 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 |
| self._script_is_loaded = False |
| self._is_finished = False |
| with open(os.path.join(os.path.dirname(__file__), 'speedindex.js')) as f: |
| @@ -32,21 +34,24 @@ class SpeedIndexMetric(Metric): |
| a PageMeasurement, so that all the events can be captured. If it's called |
| in DidNavigateToPage, that will be too late. |
| """ |
| - tab.StartTimelineRecording() |
| + has_video = tab.browser.platform.CanCaptureBrowserVideo() |
| + self._impl = (VideoSpeedIndexImpl(tab) if has_video else |
| + PaintRectSpeedIndexImpl(tab)) |
| + self._impl.Start() |
| self._script_is_loaded = False |
| self._is_finished = False |
| def Stop(self, _, tab): |
| """Stop timeline recording.""" |
| - assert self.IsFinished(tab) |
| - tab.StopTimelineRecording() |
| + assert self._impl, 'Must call Start() before Stop()' |
| + assert self.IsFinished(tab), 'Must wait for IsFinished() before Stop()' |
| + self._impl.Stop() |
| # Optional argument chart_name is not in base class Metric. |
| # pylint: disable=W0221 |
| def AddResults(self, tab, results, chart_name=None): |
| """Calculate the speed index and add it to the results.""" |
| - events = tab.timeline_model.GetAllEvents() |
| - index = _SpeedIndex(events, _GetViewportSize(tab)) |
| + index = self._impl.CalculateSpeedIndex() |
| results.Add('speed_index', 'ms', index, chart_name=chart_name) |
| def IsFinished(self, tab): |
| @@ -86,166 +91,213 @@ class SpeedIndexMetric(Metric): |
| return self._is_finished |
| -def _GetViewportSize(tab): |
| - """Returns dimensions of the viewport.""" |
| - return tab.EvaluateJavaScript('[ window.innerWidth, window.innerHeight ]') |
| - |
| +class SpeedIndexImpl(object): |
| -def _SpeedIndex(events, viewport): |
| - """Calculate the speed index of a page load from a list of events. |
| + def __init__(self, tab): |
| + self.tab = tab |
| - 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. |
| + def Start(self): |
| + raise NotImplementedError() |
| - Args: |
| - events: A list of telemetry.core.timeline.slice.Slice objects |
| - viewport: A tuple (width, height) of the window. |
| + def Stop(self): |
| + raise NotImplementedError() |
| - Returns: |
| - A single number, milliseconds of visual incompleteness. |
| - """ |
| - paint_events = _IncludedPaintEvents(events) |
| - time_area_dict = _TimeAreaDict(paint_events, viewport) |
| - time_completeness_dict = _TimeCompletenessDict(time_area_dict) |
| - # The first time interval starts from the start of the first event. |
| - prev_time = events[0].start |
| - prev_completeness = 0.0 |
| - speed_index = 0.0 |
| - for time, completeness in sorted(time_completeness_dict.items()): |
| - # Add the incemental 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 speed_index |
| - |
| - |
| -def _TimeCompletenessDict(time_area_dict): |
| - """Make a dictionary of time to visual completeness. |
| - |
| - In the WPT PHP implementation, this is also called 'visual progress'. |
| - """ |
| - total_area = sum(time_area_dict.values()) |
| - assert total_area > 0.0, 'Total paint event area must be greater than 0.' |
| - completeness = 0.0 |
| - time_completeness_dict = {} |
| - for time, area in sorted(time_area_dict.items()): |
| - completeness += float(area) / total_area |
| - # Visual progress is rounded to the nearest percentage point as in WPT. |
| - time_completeness_dict[time] = round(completeness, 2) |
| - return time_completeness_dict |
| + def GetTimeCompletenessDict(self): |
| + """Make a dictionary of time to visual completeness. |
| + In the WPT PHP implementation, this is also called 'visual progress'. |
| + """ |
| + raise NotImplementedError() |
| -def _IncludedPaintEvents(events): |
| - """Get all events that are counted in the calculation of the speed index. |
| + def GetStartTime(self): |
| + raise NotImplementedError() |
| - There's one category of paint event that's filtered out: paint events |
| - that occur before the first 'ResourceReceiveResponse' and 'Layout' events. |
| + def CalculateSpeedIndex(self): |
| + """Calculate the speed index. |
| - Previously in the WPT speed index, paint events that contain children paint |
| - events were also filtered out. |
| - """ |
| - def FirstLayoutTime(events): |
| - """Get the start time of the first layout after a resource received.""" |
| - has_received_response = False |
| - for event in events: |
| - if event.name == 'ResourceReceiveResponse': |
| - has_received_response = True |
| - elif has_received_response and event.name == 'Layout': |
| - return event.start |
| - assert False, 'There were no layout events after resource receive events.' |
| - |
| - first_layout_time = FirstLayoutTime(events) |
| - paint_events = [e for e in events |
| - if e.start >= first_layout_time and e.name == 'Paint'] |
| - return paint_events |
| - |
| - |
| -def _TimeAreaDict(paint_events, viewport): |
| - """Make a dict from time to adjusted area value for events at that time. |
| - |
| - The adjusted area value of each paint event is determined by how many paint |
| - events cover the same rectangle, and whether it's a full-window paint event. |
| - "Adjusted area" can also be thought of as "points" of visual completeness -- |
| - each rectangle has a certain number of points and these points are |
| - distributed amongst the paint events that paint that rectangle. |
| - |
| - Args: |
| - paint_events: A list of paint events |
| - viewport: A tuple (width, height) of the window. |
| - |
| - Returns: |
| - A dictionary of times of each paint event (in milliseconds) to the |
| - adjusted area that the paint event is worth. |
| - """ |
| - width, height = viewport |
| - fullscreen_area = width * height |
| - |
| - def ClippedArea(rectangle): |
| - """Returns rectangle area clipped to viewport size.""" |
| - _, x0, y0, x1, y1 = rectangle |
| - x0 = max(0, x0) |
| - y0 = max(0, y0) |
| - x1 = min(width, x1) |
| - y1 = min(height, y1) |
| - return max(0, x1 - x0) * max(0, y1 - y0) |
| - |
| - grouped = _GroupEventByRectangle(paint_events) |
| - event_area_dict = collections.defaultdict(int) |
| - |
| - for rectangle, events in grouped.items(): |
| - # The area points for each rectangle are divided up among the paint |
| - # events in that rectangle. |
| - area = ClippedArea(rectangle) |
| - update_count = len(events) |
| - adjusted_area = float(area) / update_count |
| - |
| - # Paint events for the largest-area rectangle are counted as 50%. |
| - if area == fullscreen_area: |
| - adjusted_area /= 2 |
| - |
| - for event in events: |
| - # The end time for an event is used for that event's time. |
| - event_time = event.end |
| - event_area_dict[event_time] += adjusted_area |
| - |
| - return event_area_dict |
| - |
| - |
| -def _GetRectangle(paint_event): |
| - """Get the specific rectangle on the screen for a paint event. |
| - |
| - Each paint event belongs to a frame (as in html <frame> or <iframe>). |
| - This, together with location and dimensions, comprises a rectangle. |
| - In the WPT source, this 'rectangle' is also called a 'region'. |
| - """ |
| - def GetBox(quad): |
| - """Gets top-left and bottom-right coordinates from paint event. |
| + 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. |
| - In the timeline data from devtools, paint rectangle dimensions are |
| - represented x-y coordinates of four corners, clockwise from the top-left. |
| - See: function WebInspector.TimelinePresentationModel.quadFromRectData |
| - in file src/out/Debug/obj/gen/devtools/TimelinePanel.js. |
| + Returns: |
| + A single number, milliseconds of visual incompleteness. |
| """ |
| - x0, y0, _, _, x1, y1, _, _ = quad |
| - return (x0, y0, x1, y1) |
| + time_completeness_dict = self.GetTimeCompletenessDict() |
| + prev_completeness = 0.0 |
| + speed_index = 0.0 |
| + prev_time = min(time_completeness_dict.items())[0] |
| + prev_time = self.GetStartTime() |
|
szym
2013/11/29 11:05:12
What is GetStartTime for? It's probably simpler to
tonyg
2013/11/29 20:42:07
Totally. Done.
|
| + for time, completeness in sorted(time_completeness_dict.items()): |
|
szym
2013/11/29 11:05:12
I'd strongly suggest that GetTimeCompleteness shou
tonyg
2013/11/29 20:42:07
You are right, dict is an odd choice here. Fixed.
|
| + # Add the incemental 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 speed_index |
| + |
| + |
| +class VideoSpeedIndexImpl(SpeedIndexImpl): |
| + |
| + def __init__(self, tab): |
| + super(VideoSpeedIndexImpl, self).__init__(tab) |
| + assert self.tab.browser.platform.CanCaptureBrowserVideo() |
| + self._bitmap_frames = None |
| + |
| + def Start(self): |
| + self.tab.browser.platform.StartBrowserVideoCapture(self.tab, 4) |
|
szym
2013/11/29 11:05:12
Why 4?
tonyg
2013/11/29 20:42:07
4 is the screenshot capture util's default. I'm pl
|
| + |
| + def Stop(self): |
| + self._bitmap_frames = self.tab.browser.platform.StopBrowserVideoCapture() |
| + |
| + def GetTimeCompletenessDict(self): |
| + assert self._bitmap_frames, 'Must call Start()/Stop() first.' |
| - assert paint_event.name == 'Paint' |
| - frame = paint_event.args['frameId'] |
| - return (frame,) + GetBox(paint_event.args['data']['clip']) |
| + # TODO(tonyg/szym): Implement this. |
| + raise NotImplementedError('Speed Index video calculation not implemented.') |
| + def GetStartTime(self): |
| + return 0 |
| -def _GroupEventByRectangle(paint_events): |
| - """Group all paint events according to the rectangle that they update.""" |
| - result = collections.defaultdict(list) |
| - for event in paint_events: |
| - assert event.name == 'Paint' |
| - result[_GetRectangle(event)].append(event) |
| - return result |
| + |
| +class PaintRectSpeedIndexImpl(SpeedIndexImpl): |
| + |
| + def __init__(self, tab): |
| + super(PaintRectSpeedIndexImpl, self).__init__(tab) |
| + |
| + def Start(self): |
| + self.tab.StartTimelineRecording() |
| + |
| + def Stop(self): |
| + self.tab.StopTimelineRecording() |
| + |
| + def GetTimeCompletenessDict(self): |
| + events = self.tab.timeline_model.GetAllEvents() |
| + viewport = self._GetViewportSize() |
| + paint_events = self._IncludedPaintEvents(events) |
| + time_area_dict = self._TimeAreaDict(paint_events, viewport) |
| + total_area = sum(time_area_dict.values()) |
| + assert total_area > 0.0, 'Total paint event area must be greater than 0.' |
| + completeness = 0.0 |
| + time_completeness_dict = {} |
| + for time, area in sorted(time_area_dict.items()): |
| + completeness += float(area) / total_area |
| + # Visual progress is rounded to the nearest percentage point as in WPT. |
| + time_completeness_dict[time] = round(completeness, 2) |
| + return time_completeness_dict |
| + |
| + def GetStartTime(self): |
| + return self.tab.timeline_model.GetAllEvents()[0].start |
| + |
| + def _GetViewportSize(self): |
| + """Returns dimensions of the viewport.""" |
| + return self.tab.EvaluateJavaScript( |
| + '[ window.innerWidth, window.innerHeight ]') |
| + |
| + def _IncludedPaintEvents(self, events): |
| + """Get all events that are counted in the calculation of the speed index. |
| + |
| + There's one category of paint event that's filtered out: paint events |
| + that occur before the first 'ResourceReceiveResponse' and 'Layout' events. |
| + |
| + Previously in the WPT speed index, paint events that contain children paint |
| + events were also filtered out. |
| + """ |
| + def FirstLayoutTime(events): |
| + """Get the start time of the first layout after a resource received.""" |
| + has_received_response = False |
| + for event in events: |
| + if event.name == 'ResourceReceiveResponse': |
| + has_received_response = True |
| + elif has_received_response and event.name == 'Layout': |
| + return event.start |
| + assert False, 'There were no layout events after resource receive events.' |
| + |
| + first_layout_time = FirstLayoutTime(events) |
| + paint_events = [e for e in events |
| + if e.start >= first_layout_time and e.name == 'Paint'] |
| + return paint_events |
| + |
| + def _TimeAreaDict(self, paint_events, viewport): |
| + """Make a dict from time to adjusted area value for events at that time. |
| + |
| + The adjusted area value of each paint event is determined by how many paint |
| + events cover the same rectangle, and whether it's a full-window paint event. |
| + "Adjusted area" can also be thought of as "points" of visual completeness -- |
| + each rectangle has a certain number of points and these points are |
| + distributed amongst the paint events that paint that rectangle. |
| + |
| + Args: |
| + paint_events: A list of paint events |
| + viewport: A tuple (width, height) of the window. |
| + |
| + Returns: |
| + A dictionary of times of each paint event (in milliseconds) to the |
| + adjusted area that the paint event is worth. |
| + """ |
| + width, height = viewport |
| + fullscreen_area = width * height |
| + |
| + def ClippedArea(rectangle): |
| + """Returns rectangle area clipped to viewport size.""" |
| + _, x0, y0, x1, y1 = rectangle |
| + x0 = max(0, x0) |
| + y0 = max(0, y0) |
| + x1 = min(width, x1) |
| + y1 = min(height, y1) |
| + return max(0, x1 - x0) * max(0, y1 - y0) |
| + |
| + grouped = self._GroupEventByRectangle(paint_events) |
| + event_area_dict = collections.defaultdict(int) |
| + |
| + for rectangle, events in grouped.items(): |
| + # The area points for each rectangle are divided up among the paint |
| + # events in that rectangle. |
| + area = ClippedArea(rectangle) |
| + update_count = len(events) |
| + adjusted_area = float(area) / update_count |
| + |
| + # Paint events for the largest-area rectangle are counted as 50%. |
| + if area == fullscreen_area: |
| + adjusted_area /= 2 |
| + |
| + for event in events: |
| + # The end time for an event is used for that event's time. |
| + event_time = event.end |
| + event_area_dict[event_time] += adjusted_area |
| + |
| + return event_area_dict |
| + |
| + def _GetRectangle(self, paint_event): |
| + """Get the specific rectangle on the screen for a paint event. |
| + |
| + Each paint event belongs to a frame (as in html <frame> or <iframe>). |
| + This, together with location and dimensions, comprises a rectangle. |
| + In the WPT source, this 'rectangle' is also called a 'region'. |
| + """ |
| + def GetBox(quad): |
| + """Gets top-left and bottom-right coordinates from paint event. |
| + |
| + In the timeline data from devtools, paint rectangle dimensions are |
| + represented x-y coordinates of four corners, clockwise from the top-left. |
| + See: function WebInspector.TimelinePresentationModel.quadFromRectData |
| + in file src/out/Debug/obj/gen/devtools/TimelinePanel.js. |
| + """ |
| + x0, y0, _, _, x1, y1, _, _ = quad |
| + return (x0, y0, x1, y1) |
| + |
| + assert paint_event.name == 'Paint' |
| + frame = paint_event.args['frameId'] |
| + return (frame,) + GetBox(paint_event.args['data']['clip']) |
| + |
| + def _GroupEventByRectangle(self, paint_events): |
| + """Group all paint events according to the rectangle that they update.""" |
| + result = collections.defaultdict(list) |
| + for event in paint_events: |
| + assert event.name == 'Paint' |
| + result[self._GetRectangle(event)].append(event) |
| + return result |