| 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 logging |
| 6 | 7 |
| 7 from metrics import Metric | 8 from metrics import Metric |
| 8 from telemetry.image_processing import image_util | 9 from telemetry.image_processing import image_util |
| 9 from telemetry.image_processing import rgba_color | 10 from telemetry.image_processing import rgba_color |
| 10 from telemetry.value import scalar | 11 from telemetry.value import scalar |
| 11 | 12 |
| 12 | 13 |
| 13 class SpeedIndexMetric(Metric): | 14 class SpeedIndexMetric(Metric): |
| 14 """The speed index metric is one way of measuring page load speed. | 15 """The speed index metric is one way of measuring page load speed. |
| 15 | 16 |
| (...skipping 13 matching lines...) Expand all Loading... |
| 29 def CustomizeBrowserOptions(cls, options): | 30 def CustomizeBrowserOptions(cls, options): |
| 30 options.AppendExtraBrowserArgs('--disable-infobars') | 31 options.AppendExtraBrowserArgs('--disable-infobars') |
| 31 | 32 |
| 32 def Start(self, _, tab): | 33 def Start(self, _, tab): |
| 33 """Start recording events. | 34 """Start recording events. |
| 34 | 35 |
| 35 This method should be called in the WillNavigateToPage method of | 36 This method should be called in the WillNavigateToPage method of |
| 36 a PageTest, so that all the events can be captured. If it's called | 37 a PageTest, so that all the events can be captured. If it's called |
| 37 in DidNavigateToPage, that will be too late. | 38 in DidNavigateToPage, that will be too late. |
| 38 """ | 39 """ |
| 39 self._impl = (VideoSpeedIndexImpl() if tab.video_capture_supported else | 40 if not tab.video_capture_supported: |
| 40 PaintRectSpeedIndexImpl()) | 41 return |
| 42 self._impl = VideoSpeedIndexImpl() |
| 41 self._impl.Start(tab) | 43 self._impl.Start(tab) |
| 42 | 44 |
| 43 def Stop(self, _, tab): | 45 def Stop(self, _, tab): |
| 44 """Stop timeline recording.""" | 46 """Stop recording.""" |
| 47 if not tab.video_capture_supported: |
| 48 return |
| 45 assert self._impl, 'Must call Start() before Stop()' | 49 assert self._impl, 'Must call Start() before Stop()' |
| 46 assert self.IsFinished(tab), 'Must wait for IsFinished() before Stop()' | 50 assert self.IsFinished(tab), 'Must wait for IsFinished() before Stop()' |
| 47 self._impl.Stop(tab) | 51 self._impl.Stop(tab) |
| 48 | 52 |
| 49 # Optional argument chart_name is not in base class Metric. | 53 # Optional argument chart_name is not in base class Metric. |
| 50 # pylint: disable=W0221 | 54 # pylint: disable=W0221 |
| 51 def AddResults(self, tab, results, chart_name=None): | 55 def AddResults(self, tab, results, chart_name=None): |
| 52 """Calculate the speed index and add it to the results.""" | 56 """Calculate the speed index and add it to the results.""" |
| 53 index = self._impl.CalculateSpeedIndex(tab) | 57 try: |
| 54 # Release the tab so that it can be disconnected. | 58 if tab.video_capture_supported: |
| 55 self._impl = None | 59 index = self._impl.CalculateSpeedIndex(tab) |
| 60 none_value_reason = None |
| 61 else: |
| 62 index = None |
| 63 none_value_reason = 'Video capture is not supported.' |
| 64 finally: |
| 65 self._impl = None # Release the tab so that it can be disconnected. |
| 66 |
| 56 results.AddValue(scalar.ScalarValue( | 67 results.AddValue(scalar.ScalarValue( |
| 57 results.current_page, '%s_speed_index' % chart_name, 'ms', index, | 68 results.current_page, '%s_speed_index' % chart_name, 'ms', index, |
| 58 description='Speed Index. This focuses on time when visible parts of ' | 69 description='Speed Index. This focuses on time when visible parts of ' |
| 59 'page are displayed and shows the time when the ' | 70 'page are displayed and shows the time when the ' |
| 60 'first look is "almost" composed. If the contents of the ' | 71 'first look is "almost" composed. If the contents of the ' |
| 61 'testing page are composed by only static resources, load ' | 72 'testing page are composed by only static resources, load ' |
| 62 'time can measure more accurately and speed index will be ' | 73 'time can measure more accurately and speed index will be ' |
| 63 'smaller than the load time. On the other hand, If the ' | 74 'smaller than the load time. On the other hand, If the ' |
| 64 'contents are composed by many XHR requests with small ' | 75 'contents are composed by many XHR requests with small ' |
| 65 'main resource and javascript, speed index will be able to ' | 76 'main resource and javascript, speed index will be able to ' |
| 66 'get the features of performance more accurately than load ' | 77 'get the features of performance more accurately than load ' |
| 67 'time because the load time will measure the time when ' | 78 'time because the load time will measure the time when ' |
| 68 'static resources are loaded. If you want to get more ' | 79 'static resources are loaded. If you want to get more ' |
| 69 'detail, please refer to http://goo.gl/Rw3d5d. Currently ' | 80 'detail, please refer to http://goo.gl/Rw3d5d. Currently ' |
| 70 'there are two implementations: for Android and for ' | 81 'there are two implementations: for Android and for ' |
| 71 'Desktop. The Android version uses video capture; the ' | 82 'Desktop. The Android version uses video capture; the ' |
| 72 'Desktop one uses paint events and has extra overhead to ' | 83 'Desktop one uses paint events and has extra overhead to ' |
| 73 'catch paint events.')) | 84 'catch paint events.', none_value_reason=none_value_reason)) |
| 74 | 85 |
| 75 def IsFinished(self, tab): | 86 def IsFinished(self, tab): |
| 76 """Decide whether the timeline recording should be stopped. | 87 """Decide whether the recording should be stopped. |
| 77 | |
| 78 When the timeline recording is stopped determines which paint events | |
| 79 are used in the speed index metric calculation. In general, the recording | |
| 80 should continue if there has just been some data received, because | |
| 81 this suggests that painting may continue. | |
| 82 | 88 |
| 83 A page may repeatedly request resources in an infinite loop; a timeout | 89 A page may repeatedly request resources in an infinite loop; a timeout |
| 84 should be placed in any measurement that uses this metric, e.g.: | 90 should be placed in any measurement that uses this metric, e.g.: |
| 85 def IsDone(): | 91 def IsDone(): |
| 86 return self._speedindex.IsFinished(tab) | 92 return self._speedindex.IsFinished(tab) |
| 87 util.WaitFor(IsDone, 60) | 93 util.WaitFor(IsDone, 60) |
| 88 | 94 |
| 89 Returns: | 95 Returns: |
| 90 True if 2 seconds have passed since last resource received, false | 96 True if 2 seconds have passed since last resource received, false |
| 91 otherwise. | 97 otherwise. |
| (...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 175 else: | 181 else: |
| 176 return 0.0 | 182 return 0.0 |
| 177 return 1 - histogram.Distance(final_histogram) / total_distance | 183 return 1 - histogram.Distance(final_histogram) / total_distance |
| 178 | 184 |
| 179 self._time_completeness_list = [(time, FrameProgress(hist)) | 185 self._time_completeness_list = [(time, FrameProgress(hist)) |
| 180 for time, hist in histograms] | 186 for time, hist in histograms] |
| 181 | 187 |
| 182 def GetTimeCompletenessList(self, tab): | 188 def GetTimeCompletenessList(self, tab): |
| 183 assert self._time_completeness_list, 'Must call Stop() first.' | 189 assert self._time_completeness_list, 'Must call Stop() first.' |
| 184 return self._time_completeness_list | 190 return self._time_completeness_list |
| 185 | |
| 186 | |
| 187 class PaintRectSpeedIndexImpl(SpeedIndexImpl): | |
| 188 | |
| 189 def __init__(self): | |
| 190 super(PaintRectSpeedIndexImpl, self).__init__() | |
| 191 | |
| 192 def Start(self, tab): | |
| 193 tab.StartTimelineRecording() | |
| 194 | |
| 195 def Stop(self, tab): | |
| 196 tab.StopTimelineRecording() | |
| 197 | |
| 198 def GetTimeCompletenessList(self, tab): | |
| 199 events = tab.timeline_model.GetAllEvents() | |
| 200 viewport = self._GetViewportSize(tab) | |
| 201 paint_events = self._IncludedPaintEvents(events) | |
| 202 time_area_dict = self._TimeAreaDict(paint_events, viewport) | |
| 203 total_area = sum(time_area_dict.values()) | |
| 204 assert total_area > 0.0, 'Total paint event area must be greater than 0.' | |
| 205 completeness = 0.0 | |
| 206 time_completeness_list = [] | |
| 207 | |
| 208 # TODO(tonyg): This sets the start time to the start of the first paint | |
| 209 # event. That can't be correct. The start time should be navigationStart. | |
| 210 # Since the previous screen is not cleared at navigationStart, we should | |
| 211 # probably assume the completeness is 0 until the first paint and add the | |
| 212 # time of navigationStart as the start. We need to confirm what WPT does. | |
| 213 time_completeness_list.append( | |
| 214 (tab.timeline_model.GetAllEvents()[0].start, completeness)) | |
| 215 | |
| 216 for time, area in sorted(time_area_dict.items()): | |
| 217 completeness += float(area) / total_area | |
| 218 # Visual progress is rounded to the nearest percentage point as in WPT. | |
| 219 time_completeness_list.append((time, round(completeness, 2))) | |
| 220 return time_completeness_list | |
| 221 | |
| 222 def _GetViewportSize(self, tab): | |
| 223 """Returns dimensions of the viewport.""" | |
| 224 return tab.EvaluateJavaScript('[ window.innerWidth, window.innerHeight ]') | |
| 225 | |
| 226 def _IncludedPaintEvents(self, events): | |
| 227 """Get all events that are counted in the calculation of the speed index. | |
| 228 | |
| 229 There's one category of paint event that's filtered out: paint events | |
| 230 that occur before the first 'ResourceReceiveResponse' and 'Layout' events. | |
| 231 | |
| 232 Previously in the WPT speed index, paint events that contain children paint | |
| 233 events were also filtered out. | |
| 234 """ | |
| 235 def FirstLayoutTime(events): | |
| 236 """Get the start time of the first layout after a resource received.""" | |
| 237 has_received_response = False | |
| 238 for event in events: | |
| 239 if event.name == 'ResourceReceiveResponse': | |
| 240 has_received_response = True | |
| 241 elif has_received_response and event.name == 'Layout': | |
| 242 return event.start | |
| 243 assert False, 'There were no layout events after resource receive events.' | |
| 244 | |
| 245 first_layout_time = FirstLayoutTime(events) | |
| 246 paint_events = [e for e in events | |
| 247 if e.start >= first_layout_time and e.name == 'Paint'] | |
| 248 return paint_events | |
| 249 | |
| 250 def _TimeAreaDict(self, paint_events, viewport): | |
| 251 """Make a dict from time to adjusted area value for events at that time. | |
| 252 | |
| 253 The adjusted area value of each paint event is determined by how many paint | |
| 254 events cover the same rectangle, and whether it's a full-window paint event. | |
| 255 "Adjusted area" can also be thought of as "points" of visual completeness -- | |
| 256 each rectangle has a certain number of points and these points are | |
| 257 distributed amongst the paint events that paint that rectangle. | |
| 258 | |
| 259 Args: | |
| 260 paint_events: A list of paint events | |
| 261 viewport: A tuple (width, height) of the window. | |
| 262 | |
| 263 Returns: | |
| 264 A dictionary of times of each paint event (in milliseconds) to the | |
| 265 adjusted area that the paint event is worth. | |
| 266 """ | |
| 267 width, height = viewport | |
| 268 fullscreen_area = width * height | |
| 269 | |
| 270 def ClippedArea(rectangle): | |
| 271 """Returns rectangle area clipped to viewport size.""" | |
| 272 _, x0, y0, x1, y1 = rectangle | |
| 273 clipped_width = max(0, min(width, x1) - max(0, x0)) | |
| 274 clipped_height = max(0, min(height, y1) - max(0, y0)) | |
| 275 return clipped_width * clipped_height | |
| 276 | |
| 277 grouped = self._GroupEventByRectangle(paint_events) | |
| 278 event_area_dict = collections.defaultdict(int) | |
| 279 | |
| 280 for rectangle, events in grouped.items(): | |
| 281 # The area points for each rectangle are divided up among the paint | |
| 282 # events in that rectangle. | |
| 283 area = ClippedArea(rectangle) | |
| 284 update_count = len(events) | |
| 285 adjusted_area = float(area) / update_count | |
| 286 | |
| 287 # Paint events for the largest-area rectangle are counted as 50%. | |
| 288 if area == fullscreen_area: | |
| 289 adjusted_area /= 2 | |
| 290 | |
| 291 for event in events: | |
| 292 # The end time for an event is used for that event's time. | |
| 293 event_time = event.end | |
| 294 event_area_dict[event_time] += adjusted_area | |
| 295 | |
| 296 return event_area_dict | |
| 297 | |
| 298 def _GetRectangle(self, paint_event): | |
| 299 """Get the specific rectangle on the screen for a paint event. | |
| 300 | |
| 301 Each paint event belongs to a frame (as in html <frame> or <iframe>). | |
| 302 This, together with location and dimensions, comprises a rectangle. | |
| 303 In the WPT source, this 'rectangle' is also called a 'region'. | |
| 304 """ | |
| 305 def GetBox(quad): | |
| 306 """Gets top-left and bottom-right coordinates from paint event. | |
| 307 | |
| 308 In the timeline data from devtools, paint rectangle dimensions are | |
| 309 represented x-y coordinates of four corners, clockwise from the top-left. | |
| 310 See: function WebInspector.TimelinePresentationModel.quadFromRectData | |
| 311 in file src/out/Debug/obj/gen/devtools/TimelinePanel.js. | |
| 312 """ | |
| 313 x0, y0, _, _, x1, y1, _, _ = quad | |
| 314 return (x0, y0, x1, y1) | |
| 315 | |
| 316 assert paint_event.name == 'Paint' | |
| 317 frame = paint_event.args['frameId'] | |
| 318 return (frame,) + GetBox(paint_event.args['data']['clip']) | |
| 319 | |
| 320 def _GroupEventByRectangle(self, paint_events): | |
| 321 """Group all paint events according to the rectangle that they update.""" | |
| 322 result = collections.defaultdict(list) | |
| 323 for event in paint_events: | |
| 324 assert event.name == 'Paint' | |
| 325 result[self._GetRectangle(event)].append(event) | |
| 326 return result | |
| OLD | NEW |