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
3 # found in the LICENSE file.
8 from metrics import Metric
10 class SpeedIndexMetric(Metric):
11 """The speed index metric is one way of measuring page load speed.
13 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 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.
18 This speed index metric is based on the devtools speed index at
19 WebPageTest.org (WPT). For more info see: http://goo.gl/e7AH5l
22 super(SpeedIndexMetric, self).__init__()
23 self._script_is_loaded = False
24 self._is_finished = False
25 with open(os.path.join(os.path.dirname(__file__), 'speedindex.js')) as f:
28 def Start(self, _, tab):
29 """Start recording events.
31 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
33 in DidNavigateToPage, that will be too late.
35 tab.StartTimelineRecording()
36 self._script_is_loaded = False
37 self._is_finished = False
39 def Stop(self, _, tab):
40 """Stop timeline recording."""
41 assert self.IsFinished(tab)
42 tab.StopTimelineRecording()
44 # Optional argument chart_name is not in base class Metric.
45 # pylint: disable=W0221
46 def AddResults(self, tab, results, chart_name=None):
47 """Calculate the speed index and add it to the results."""
48 events = tab.timeline_model.GetAllEvents()
49 index = _SpeedIndex(events, _GetViewportSize(tab))
50 results.Add('speed_index', 'ms', index, chart_name=chart_name)
52 def IsFinished(self, tab):
53 """Decide whether the timeline recording should be stopped.
55 When the timeline recording is stopped determines which paint events
56 are used in the speed index metric calculation. In general, the recording
57 should continue if there has just been some data received, because
58 this suggests that painting may continue.
60 A page may repeatedly request resources in an infinite loop; a timeout
61 should be placed in any measurement that uses this metric, e.g.:
63 return self._speedindex.IsFinished(tab)
64 util.WaitFor(IsDone, 60)
67 True if 2 seconds have passed since last resource received, false
73 # The script that provides the function window.timeSinceLastResponseMs()
74 # needs to be loaded for this function, but it must be loaded AFTER
75 # the Start method is called, because if the Start method is called in
76 # the PageMeasurement's WillNavigateToPage function, then it will
77 # not be available here. The script should only be re-loaded once per page
78 # so that variables in the script get reset only for a new page.
79 if not self._script_is_loaded:
80 tab.ExecuteJavaScript(self._js)
81 self._script_is_loaded = True
83 time_since_last_response_ms = tab.EvaluateJavaScript(
84 "window.timeSinceLastResponseAfterLoadMs()")
85 self._is_finished = time_since_last_response_ms > 2000
86 return self._is_finished
89 def _GetViewportSize(tab):
90 """Returns dimensions of the viewport."""
91 return tab.EvaluateJavaScript('[ window.innerWidth, window.innerHeight ]')
94 def _SpeedIndex(events, viewport):
95 """Calculate the speed index of a page load from a list of events.
97 The speed index number conceptually represents the number of milliseconds
98 that the page was "visually incomplete". If the page were 0% complete for
99 1000 ms, then the score would be 1000; if it were 0% complete for 100 ms
100 then 90% complete (ie 10% incomplete) for 900 ms, then the score would be
101 1.0*100 + 0.1*900 = 190.
104 events: A list of telemetry.core.timeline.slice.Slice objects
105 viewport: A tuple (width, height) of the window.
108 A single number, milliseconds of visual incompleteness.
110 paint_events = _IncludedPaintEvents(events)
111 time_area_dict = _TimeAreaDict(paint_events, viewport)
112 time_completeness_dict = _TimeCompletenessDict(time_area_dict)
113 # The first time interval starts from the start of the first event.
114 prev_time = events[0].start
115 prev_completeness = 0.0
117 for time, completeness in sorted(time_completeness_dict.items()):
118 # Add the incemental value for the interval just before this event.
119 elapsed_time = time - prev_time
120 incompleteness = (1.0 - prev_completeness)
121 speed_index += elapsed_time * incompleteness
123 # Update variables for next iteration.
124 prev_completeness = completeness
130 def _TimeCompletenessDict(time_area_dict):
131 """Make a dictionary of time to visual completeness.
133 In the WPT PHP implementation, this is also called 'visual progress'.
135 total_area = sum(time_area_dict.values())
136 assert total_area > 0.0, 'Total paint event area must be greater than 0.'
138 time_completeness_dict = {}
139 for time, area in sorted(time_area_dict.items()):
140 completeness += float(area) / total_area
141 # Visual progress is rounded to the nearest percentage point as in WPT.
142 time_completeness_dict[time] = round(completeness, 2)
143 return time_completeness_dict
146 def _IncludedPaintEvents(events):
147 """Get all events that are counted in the calculation of the speed index.
149 There's one category of paint event that's filtered out: paint events
150 that occur before the first 'ResourceReceiveResponse' and 'Layout' events.
152 Previously in the WPT speed index, paint events that contain children paint
153 events were also filtered out.
155 def FirstLayoutTime(events):
156 """Get the start time of the first layout after a resource received."""
157 has_received_response = False
159 if event.name == 'ResourceReceiveResponse':
160 has_received_response = True
161 elif has_received_response and event.name == 'Layout':
163 assert False, 'There were no layout events after resource receive events.'
165 first_layout_time = FirstLayoutTime(events)
166 paint_events = [e for e in events
167 if e.start >= first_layout_time and e.name == 'Paint']
171 def _TimeAreaDict(paint_events, viewport):
172 """Make a dict from time to adjusted area value for events at that time.
174 The adjusted area value of each paint event is determined by how many paint
175 events cover the same rectangle, and whether it's a full-window paint event.
176 "Adjusted area" can also be thought of as "points" of visual completeness --
177 each rectangle has a certain number of points and these points are
178 distributed amongst the paint events that paint that rectangle.
181 paint_events: A list of paint events
182 viewport: A tuple (width, height) of the window.
185 A dictionary of times of each paint event (in milliseconds) to the
186 adjusted area that the paint event is worth.
188 width, height = viewport
189 fullscreen_area = width * height
191 def ClippedArea(rectangle):
192 """Returns rectangle area clipped to viewport size."""
193 _, x0, y0, x1, y1 = rectangle
198 return max(0, x1 - x0) * max(0, y1 - y0)
200 grouped = _GroupEventByRectangle(paint_events)
201 event_area_dict = collections.defaultdict(int)
203 for rectangle, events in grouped.items():
204 # The area points for each rectangle are divided up among the paint
205 # events in that rectangle.
206 area = ClippedArea(rectangle)
207 update_count = len(events)
208 adjusted_area = float(area) / update_count
210 # Paint events for the largest-area rectangle are counted as 50%.
211 if area == fullscreen_area:
215 # The end time for an event is used for that event's time.
216 event_time = event.end
217 event_area_dict[event_time] += adjusted_area
219 return event_area_dict
222 def _GetRectangle(paint_event):
223 """Get the specific rectangle on the screen for a paint event.
225 Each paint event belongs to a frame (as in html <frame> or <iframe>).
226 This, together with location and dimensions, comprises a rectangle.
227 In the WPT source, this 'rectangle' is also called a 'region'.
230 """Gets top-left and bottom-right coordinates from paint event.
232 In the timeline data from devtools, paint rectangle dimensions are
233 represented x-y coordinates of four corners, clockwise from the top-left.
234 See: function WebInspector.TimelinePresentationModel.quadFromRectData
235 in file src/out/Debug/obj/gen/devtools/TimelinePanel.js.
237 x0, y0, _, _, x1, y1, _, _ = quad
238 return (x0, y0, x1, y1)
240 assert paint_event.name == 'Paint'
241 frame = paint_event.args['frameId']
242 return (frame,) + GetBox(paint_event.args['data']['clip'])
245 def _GroupEventByRectangle(paint_events):
246 """Group all paint events according to the rectangle that they update."""
247 result = collections.defaultdict(list)
248 for event in paint_events:
249 assert event.name == 'Paint'
250 result[_GetRectangle(event)].append(event)