Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import collections | 5 import collections |
| 6 import os | 6 import os |
| 7 | 7 |
| 8 from metrics import Metric | 8 from metrics import Metric |
| 9 | 9 |
| 10 | |
| 10 class SpeedIndexMetric(Metric): | 11 class SpeedIndexMetric(Metric): |
| 11 """The speed index metric is one way of measuring page load speed. | 12 """The speed index metric is one way of measuring page load speed. |
| 12 | 13 |
| 13 It is meant to approximate user perception of page load speed, and it | 14 It is meant to approximate user perception of page load speed, and it |
| 14 is based on the amount of time that it takes to paint to the visual | 15 is based on the amount of time that it takes to paint to the visual |
| 15 portion of the screen. It includes paint events that occur after the | 16 portion of the screen. It includes paint events that occur after the |
| 16 onload event, and it doesn't include time loading things off-screen. | 17 onload event, and it doesn't include time loading things off-screen. |
| 17 | 18 |
| 18 This speed index metric is based on the devtools speed index at | 19 This speed index metric is based on WebPageTest.org (WPT). |
| 19 WebPageTest.org (WPT). For more info see: http://goo.gl/e7AH5l | 20 For more info see: http://goo.gl/e7AH5l |
| 20 """ | 21 """ |
| 21 def __init__(self): | 22 def __init__(self): |
| 22 super(SpeedIndexMetric, self).__init__() | 23 super(SpeedIndexMetric, self).__init__() |
| 24 self._impl = None | |
| 23 self._script_is_loaded = False | 25 self._script_is_loaded = False |
| 24 self._is_finished = False | 26 self._is_finished = False |
| 25 with open(os.path.join(os.path.dirname(__file__), 'speedindex.js')) as f: | 27 with open(os.path.join(os.path.dirname(__file__), 'speedindex.js')) as f: |
| 26 self._js = f.read() | 28 self._js = f.read() |
| 27 | 29 |
| 28 def Start(self, _, tab): | 30 def Start(self, _, tab): |
| 29 """Start recording events. | 31 """Start recording events. |
| 30 | 32 |
| 31 This method should be called in the WillNavigateToPage method of | 33 This method should be called in the WillNavigateToPage method of |
| 32 a PageMeasurement, so that all the events can be captured. If it's called | 34 a PageMeasurement, so that all the events can be captured. If it's called |
| 33 in DidNavigateToPage, that will be too late. | 35 in DidNavigateToPage, that will be too late. |
| 34 """ | 36 """ |
| 35 tab.StartTimelineRecording() | 37 has_video = tab.browser.platform.CanCaptureBrowserVideo() |
| 38 self._impl = (VideoSpeedIndexImpl(tab) if has_video else | |
| 39 PaintRectSpeedIndexImpl(tab)) | |
| 40 self._impl.Start() | |
| 36 self._script_is_loaded = False | 41 self._script_is_loaded = False |
| 37 self._is_finished = False | 42 self._is_finished = False |
| 38 | 43 |
| 39 def Stop(self, _, tab): | 44 def Stop(self, _, tab): |
| 40 """Stop timeline recording.""" | 45 """Stop timeline recording.""" |
| 41 assert self.IsFinished(tab) | 46 assert self._impl, 'Must call Start() before Stop()' |
| 42 tab.StopTimelineRecording() | 47 assert self.IsFinished(tab), 'Must wait for IsFinished() before Stop()' |
| 48 self._impl.Stop() | |
| 43 | 49 |
| 44 # Optional argument chart_name is not in base class Metric. | 50 # Optional argument chart_name is not in base class Metric. |
| 45 # pylint: disable=W0221 | 51 # pylint: disable=W0221 |
| 46 def AddResults(self, tab, results, chart_name=None): | 52 def AddResults(self, tab, results, chart_name=None): |
| 47 """Calculate the speed index and add it to the results.""" | 53 """Calculate the speed index and add it to the results.""" |
| 48 events = tab.timeline_model.GetAllEvents() | 54 index = self._impl.CalculateSpeedIndex() |
| 49 index = _SpeedIndex(events, _GetViewportSize(tab)) | |
| 50 results.Add('speed_index', 'ms', index, chart_name=chart_name) | 55 results.Add('speed_index', 'ms', index, chart_name=chart_name) |
| 51 | 56 |
| 52 def IsFinished(self, tab): | 57 def IsFinished(self, tab): |
| 53 """Decide whether the timeline recording should be stopped. | 58 """Decide whether the timeline recording should be stopped. |
| 54 | 59 |
| 55 When the timeline recording is stopped determines which paint events | 60 When the timeline recording is stopped determines which paint events |
| 56 are used in the speed index metric calculation. In general, the recording | 61 are used in the speed index metric calculation. In general, the recording |
| 57 should continue if there has just been some data received, because | 62 should continue if there has just been some data received, because |
| 58 this suggests that painting may continue. | 63 this suggests that painting may continue. |
| 59 | 64 |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 79 if not self._script_is_loaded: | 84 if not self._script_is_loaded: |
| 80 tab.ExecuteJavaScript(self._js) | 85 tab.ExecuteJavaScript(self._js) |
| 81 self._script_is_loaded = True | 86 self._script_is_loaded = True |
| 82 | 87 |
| 83 time_since_last_response_ms = tab.EvaluateJavaScript( | 88 time_since_last_response_ms = tab.EvaluateJavaScript( |
| 84 "window.timeSinceLastResponseAfterLoadMs()") | 89 "window.timeSinceLastResponseAfterLoadMs()") |
| 85 self._is_finished = time_since_last_response_ms > 2000 | 90 self._is_finished = time_since_last_response_ms > 2000 |
| 86 return self._is_finished | 91 return self._is_finished |
| 87 | 92 |
| 88 | 93 |
| 89 def _GetViewportSize(tab): | 94 class SpeedIndexImpl(object): |
| 90 """Returns dimensions of the viewport.""" | 95 |
| 91 return tab.EvaluateJavaScript('[ window.innerWidth, window.innerHeight ]') | 96 def __init__(self, tab): |
| 92 | 97 self.tab = tab |
| 93 | 98 |
| 94 def _SpeedIndex(events, viewport): | 99 def Start(self): |
| 95 """Calculate the speed index of a page load from a list of events. | 100 raise NotImplementedError() |
| 96 | 101 |
| 97 The speed index number conceptually represents the number of milliseconds | 102 def Stop(self): |
| 98 that the page was "visually incomplete". If the page were 0% complete for | 103 raise NotImplementedError() |
| 99 1000 ms, then the score would be 1000; if it were 0% complete for 100 ms | 104 |
| 100 then 90% complete (ie 10% incomplete) for 900 ms, then the score would be | 105 def GetTimeCompletenessDict(self): |
| 101 1.0*100 + 0.1*900 = 190. | 106 """Make a dictionary of time to visual completeness. |
| 102 | 107 |
| 103 Args: | 108 In the WPT PHP implementation, this is also called 'visual progress'. |
| 104 events: A list of telemetry.core.timeline.slice.Slice objects | 109 """ |
| 105 viewport: A tuple (width, height) of the window. | 110 raise NotImplementedError() |
| 106 | 111 |
| 107 Returns: | 112 def GetStartTime(self): |
| 108 A single number, milliseconds of visual incompleteness. | 113 raise NotImplementedError() |
| 109 """ | 114 |
| 110 paint_events = _IncludedPaintEvents(events) | 115 def CalculateSpeedIndex(self): |
| 111 time_area_dict = _TimeAreaDict(paint_events, viewport) | 116 """Calculate the speed index. |
| 112 time_completeness_dict = _TimeCompletenessDict(time_area_dict) | 117 |
| 113 # The first time interval starts from the start of the first event. | 118 The speed index number conceptually represents the number of milliseconds |
| 114 prev_time = events[0].start | 119 that the page was "visually incomplete". If the page were 0% complete for |
| 115 prev_completeness = 0.0 | 120 1000 ms, then the score would be 1000; if it were 0% complete for 100 ms |
| 116 speed_index = 0.0 | 121 then 90% complete (ie 10% incomplete) for 900 ms, then the score would be |
| 117 for time, completeness in sorted(time_completeness_dict.items()): | 122 1.0*100 + 0.1*900 = 190. |
| 118 # Add the incemental value for the interval just before this event. | 123 |
| 119 elapsed_time = time - prev_time | 124 Returns: |
| 120 incompleteness = (1.0 - prev_completeness) | 125 A single number, milliseconds of visual incompleteness. |
| 121 speed_index += elapsed_time * incompleteness | 126 """ |
| 122 | 127 time_completeness_dict = self.GetTimeCompletenessDict() |
| 123 # Update variables for next iteration. | 128 prev_completeness = 0.0 |
| 124 prev_completeness = completeness | 129 speed_index = 0.0 |
| 125 prev_time = time | 130 prev_time = min(time_completeness_dict.items())[0] |
| 126 | 131 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.
| |
| 127 return speed_index | 132 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.
| |
| 128 | 133 # Add the incemental value for the interval just before this event. |
| 129 | 134 elapsed_time = time - prev_time |
| 130 def _TimeCompletenessDict(time_area_dict): | 135 incompleteness = (1.0 - prev_completeness) |
| 131 """Make a dictionary of time to visual completeness. | 136 speed_index += elapsed_time * incompleteness |
| 132 | 137 |
| 133 In the WPT PHP implementation, this is also called 'visual progress'. | 138 # Update variables for next iteration. |
| 134 """ | 139 prev_completeness = completeness |
| 135 total_area = sum(time_area_dict.values()) | 140 prev_time = time |
| 136 assert total_area > 0.0, 'Total paint event area must be greater than 0.' | 141 return speed_index |
| 137 completeness = 0.0 | 142 |
| 138 time_completeness_dict = {} | 143 |
| 139 for time, area in sorted(time_area_dict.items()): | 144 class VideoSpeedIndexImpl(SpeedIndexImpl): |
| 140 completeness += float(area) / total_area | 145 |
| 141 # Visual progress is rounded to the nearest percentage point as in WPT. | 146 def __init__(self, tab): |
| 142 time_completeness_dict[time] = round(completeness, 2) | 147 super(VideoSpeedIndexImpl, self).__init__(tab) |
| 143 return time_completeness_dict | 148 assert self.tab.browser.platform.CanCaptureBrowserVideo() |
| 144 | 149 self._bitmap_frames = None |
| 145 | 150 |
| 146 def _IncludedPaintEvents(events): | 151 def Start(self): |
| 147 """Get all events that are counted in the calculation of the speed index. | 152 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
| |
| 148 | 153 |
| 149 There's one category of paint event that's filtered out: paint events | 154 def Stop(self): |
| 150 that occur before the first 'ResourceReceiveResponse' and 'Layout' events. | 155 self._bitmap_frames = self.tab.browser.platform.StopBrowserVideoCapture() |
| 151 | 156 |
| 152 Previously in the WPT speed index, paint events that contain children paint | 157 def GetTimeCompletenessDict(self): |
| 153 events were also filtered out. | 158 assert self._bitmap_frames, 'Must call Start()/Stop() first.' |
| 154 """ | 159 |
| 155 def FirstLayoutTime(events): | 160 # TODO(tonyg/szym): Implement this. |
| 156 """Get the start time of the first layout after a resource received.""" | 161 raise NotImplementedError('Speed Index video calculation not implemented.') |
| 157 has_received_response = False | 162 |
| 158 for event in events: | 163 def GetStartTime(self): |
| 159 if event.name == 'ResourceReceiveResponse': | 164 return 0 |
| 160 has_received_response = True | 165 |
| 161 elif has_received_response and event.name == 'Layout': | 166 |
| 162 return event.start | 167 class PaintRectSpeedIndexImpl(SpeedIndexImpl): |
| 163 assert False, 'There were no layout events after resource receive events.' | 168 |
| 164 | 169 def __init__(self, tab): |
| 165 first_layout_time = FirstLayoutTime(events) | 170 super(PaintRectSpeedIndexImpl, self).__init__(tab) |
| 166 paint_events = [e for e in events | 171 |
| 167 if e.start >= first_layout_time and e.name == 'Paint'] | 172 def Start(self): |
| 168 return paint_events | 173 self.tab.StartTimelineRecording() |
| 169 | 174 |
| 170 | 175 def Stop(self): |
| 171 def _TimeAreaDict(paint_events, viewport): | 176 self.tab.StopTimelineRecording() |
| 172 """Make a dict from time to adjusted area value for events at that time. | 177 |
| 173 | 178 def GetTimeCompletenessDict(self): |
| 174 The adjusted area value of each paint event is determined by how many paint | 179 events = self.tab.timeline_model.GetAllEvents() |
| 175 events cover the same rectangle, and whether it's a full-window paint event. | 180 viewport = self._GetViewportSize() |
| 176 "Adjusted area" can also be thought of as "points" of visual completeness -- | 181 paint_events = self._IncludedPaintEvents(events) |
| 177 each rectangle has a certain number of points and these points are | 182 time_area_dict = self._TimeAreaDict(paint_events, viewport) |
| 178 distributed amongst the paint events that paint that rectangle. | 183 total_area = sum(time_area_dict.values()) |
| 179 | 184 assert total_area > 0.0, 'Total paint event area must be greater than 0.' |
| 180 Args: | 185 completeness = 0.0 |
| 181 paint_events: A list of paint events | 186 time_completeness_dict = {} |
| 182 viewport: A tuple (width, height) of the window. | 187 for time, area in sorted(time_area_dict.items()): |
| 183 | 188 completeness += float(area) / total_area |
| 184 Returns: | 189 # Visual progress is rounded to the nearest percentage point as in WPT. |
| 185 A dictionary of times of each paint event (in milliseconds) to the | 190 time_completeness_dict[time] = round(completeness, 2) |
| 186 adjusted area that the paint event is worth. | 191 return time_completeness_dict |
| 187 """ | 192 |
| 188 width, height = viewport | 193 def GetStartTime(self): |
| 189 fullscreen_area = width * height | 194 return self.tab.timeline_model.GetAllEvents()[0].start |
| 190 | 195 |
| 191 def ClippedArea(rectangle): | 196 def _GetViewportSize(self): |
| 192 """Returns rectangle area clipped to viewport size.""" | 197 """Returns dimensions of the viewport.""" |
| 193 _, x0, y0, x1, y1 = rectangle | 198 return self.tab.EvaluateJavaScript( |
| 194 x0 = max(0, x0) | 199 '[ window.innerWidth, window.innerHeight ]') |
| 195 y0 = max(0, y0) | 200 |
| 196 x1 = min(width, x1) | 201 def _IncludedPaintEvents(self, events): |
| 197 y1 = min(height, y1) | 202 """Get all events that are counted in the calculation of the speed index. |
| 198 return max(0, x1 - x0) * max(0, y1 - y0) | 203 |
| 199 | 204 There's one category of paint event that's filtered out: paint events |
| 200 grouped = _GroupEventByRectangle(paint_events) | 205 that occur before the first 'ResourceReceiveResponse' and 'Layout' events. |
| 201 event_area_dict = collections.defaultdict(int) | 206 |
| 202 | 207 Previously in the WPT speed index, paint events that contain children paint |
| 203 for rectangle, events in grouped.items(): | 208 events were also filtered out. |
| 204 # The area points for each rectangle are divided up among the paint | 209 """ |
| 205 # events in that rectangle. | 210 def FirstLayoutTime(events): |
| 206 area = ClippedArea(rectangle) | 211 """Get the start time of the first layout after a resource received.""" |
| 207 update_count = len(events) | 212 has_received_response = False |
| 208 adjusted_area = float(area) / update_count | 213 for event in events: |
| 209 | 214 if event.name == 'ResourceReceiveResponse': |
| 210 # Paint events for the largest-area rectangle are counted as 50%. | 215 has_received_response = True |
| 211 if area == fullscreen_area: | 216 elif has_received_response and event.name == 'Layout': |
| 212 adjusted_area /= 2 | 217 return event.start |
| 213 | 218 assert False, 'There were no layout events after resource receive events.' |
| 214 for event in events: | 219 |
| 215 # The end time for an event is used for that event's time. | 220 first_layout_time = FirstLayoutTime(events) |
| 216 event_time = event.end | 221 paint_events = [e for e in events |
| 217 event_area_dict[event_time] += adjusted_area | 222 if e.start >= first_layout_time and e.name == 'Paint'] |
| 218 | 223 return paint_events |
| 219 return event_area_dict | 224 |
| 220 | 225 def _TimeAreaDict(self, paint_events, viewport): |
| 221 | 226 """Make a dict from time to adjusted area value for events at that time. |
| 222 def _GetRectangle(paint_event): | 227 |
| 223 """Get the specific rectangle on the screen for a paint event. | 228 The adjusted area value of each paint event is determined by how many paint |
| 224 | 229 events cover the same rectangle, and whether it's a full-window paint event. |
| 225 Each paint event belongs to a frame (as in html <frame> or <iframe>). | 230 "Adjusted area" can also be thought of as "points" of visual completeness -- |
| 226 This, together with location and dimensions, comprises a rectangle. | 231 each rectangle has a certain number of points and these points are |
| 227 In the WPT source, this 'rectangle' is also called a 'region'. | 232 distributed amongst the paint events that paint that rectangle. |
| 228 """ | 233 |
| 229 def GetBox(quad): | 234 Args: |
| 230 """Gets top-left and bottom-right coordinates from paint event. | 235 paint_events: A list of paint events |
| 231 | 236 viewport: A tuple (width, height) of the window. |
| 232 In the timeline data from devtools, paint rectangle dimensions are | 237 |
| 233 represented x-y coordinates of four corners, clockwise from the top-left. | 238 Returns: |
| 234 See: function WebInspector.TimelinePresentationModel.quadFromRectData | 239 A dictionary of times of each paint event (in milliseconds) to the |
| 235 in file src/out/Debug/obj/gen/devtools/TimelinePanel.js. | 240 adjusted area that the paint event is worth. |
| 236 """ | 241 """ |
| 237 x0, y0, _, _, x1, y1, _, _ = quad | 242 width, height = viewport |
| 238 return (x0, y0, x1, y1) | 243 fullscreen_area = width * height |
| 239 | 244 |
| 240 assert paint_event.name == 'Paint' | 245 def ClippedArea(rectangle): |
| 241 frame = paint_event.args['frameId'] | 246 """Returns rectangle area clipped to viewport size.""" |
| 242 return (frame,) + GetBox(paint_event.args['data']['clip']) | 247 _, x0, y0, x1, y1 = rectangle |
| 243 | 248 x0 = max(0, x0) |
| 244 | 249 y0 = max(0, y0) |
| 245 def _GroupEventByRectangle(paint_events): | 250 x1 = min(width, x1) |
| 246 """Group all paint events according to the rectangle that they update.""" | 251 y1 = min(height, y1) |
| 247 result = collections.defaultdict(list) | 252 return max(0, x1 - x0) * max(0, y1 - y0) |
| 248 for event in paint_events: | 253 |
| 249 assert event.name == 'Paint' | 254 grouped = self._GroupEventByRectangle(paint_events) |
| 250 result[_GetRectangle(event)].append(event) | 255 event_area_dict = collections.defaultdict(int) |
| 251 return result | 256 |
| 257 for rectangle, events in grouped.items(): | |
| 258 # The area points for each rectangle are divided up among the paint | |
| 259 # events in that rectangle. | |
| 260 area = ClippedArea(rectangle) | |
| 261 update_count = len(events) | |
| 262 adjusted_area = float(area) / update_count | |
| 263 | |
| 264 # Paint events for the largest-area rectangle are counted as 50%. | |
| 265 if area == fullscreen_area: | |
| 266 adjusted_area /= 2 | |
| 267 | |
| 268 for event in events: | |
| 269 # The end time for an event is used for that event's time. | |
| 270 event_time = event.end | |
| 271 event_area_dict[event_time] += adjusted_area | |
| 272 | |
| 273 return event_area_dict | |
| 274 | |
| 275 def _GetRectangle(self, paint_event): | |
| 276 """Get the specific rectangle on the screen for a paint event. | |
| 277 | |
| 278 Each paint event belongs to a frame (as in html <frame> or <iframe>). | |
| 279 This, together with location and dimensions, comprises a rectangle. | |
| 280 In the WPT source, this 'rectangle' is also called a 'region'. | |
| 281 """ | |
| 282 def GetBox(quad): | |
| 283 """Gets top-left and bottom-right coordinates from paint event. | |
| 284 | |
| 285 In the timeline data from devtools, paint rectangle dimensions are | |
| 286 represented x-y coordinates of four corners, clockwise from the top-left. | |
| 287 See: function WebInspector.TimelinePresentationModel.quadFromRectData | |
| 288 in file src/out/Debug/obj/gen/devtools/TimelinePanel.js. | |
| 289 """ | |
| 290 x0, y0, _, _, x1, y1, _, _ = quad | |
| 291 return (x0, y0, x1, y1) | |
| 292 | |
| 293 assert paint_event.name == 'Paint' | |
| 294 frame = paint_event.args['frameId'] | |
| 295 return (frame,) + GetBox(paint_event.args['data']['clip']) | |
| 296 | |
| 297 def _GroupEventByRectangle(self, paint_events): | |
| 298 """Group all paint events according to the rectangle that they update.""" | |
| 299 result = collections.defaultdict(list) | |
| 300 for event in paint_events: | |
| 301 assert event.name == 'Paint' | |
| 302 result[self._GetRectangle(event)].append(event) | |
| 303 return result | |
| OLD | NEW |