Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(9)

Side by Side Diff: tools/perf/metrics/speedindex.py

Issue 93733002: [Telemetry] Refactoring in preparation for video-based Speed Index support. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fix lint Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 self._impl = (VideoSpeedIndexImpl(tab) if tab.video_capture_supported else
38 PaintRectSpeedIndexImpl(tab))
39 self._impl.Start()
36 self._script_is_loaded = False 40 self._script_is_loaded = False
37 self._is_finished = False 41 self._is_finished = False
38 42
39 def Stop(self, _, tab): 43 def Stop(self, _, tab):
40 """Stop timeline recording.""" 44 """Stop timeline recording."""
41 assert self.IsFinished(tab) 45 assert self._impl, 'Must call Start() before Stop()'
42 tab.StopTimelineRecording() 46 assert self.IsFinished(tab), 'Must wait for IsFinished() before Stop()'
47 self._impl.Stop()
43 48
44 # Optional argument chart_name is not in base class Metric. 49 # Optional argument chart_name is not in base class Metric.
45 # pylint: disable=W0221 50 # pylint: disable=W0221
46 def AddResults(self, tab, results, chart_name=None): 51 def AddResults(self, tab, results, chart_name=None):
47 """Calculate the speed index and add it to the results.""" 52 """Calculate the speed index and add it to the results."""
48 events = tab.timeline_model.GetAllEvents() 53 index = self._impl.CalculateSpeedIndex()
49 index = _SpeedIndex(events, _GetViewportSize(tab))
50 results.Add('speed_index', 'ms', index, chart_name=chart_name) 54 results.Add('speed_index', 'ms', index, chart_name=chart_name)
51 55
52 def IsFinished(self, tab): 56 def IsFinished(self, tab):
53 """Decide whether the timeline recording should be stopped. 57 """Decide whether the timeline recording should be stopped.
54 58
55 When the timeline recording is stopped determines which paint events 59 When the timeline recording is stopped determines which paint events
56 are used in the speed index metric calculation. In general, the recording 60 are used in the speed index metric calculation. In general, the recording
57 should continue if there has just been some data received, because 61 should continue if there has just been some data received, because
58 this suggests that painting may continue. 62 this suggests that painting may continue.
59 63
(...skipping 19 matching lines...) Expand all
79 if not self._script_is_loaded: 83 if not self._script_is_loaded:
80 tab.ExecuteJavaScript(self._js) 84 tab.ExecuteJavaScript(self._js)
81 self._script_is_loaded = True 85 self._script_is_loaded = True
82 86
83 time_since_last_response_ms = tab.EvaluateJavaScript( 87 time_since_last_response_ms = tab.EvaluateJavaScript(
84 "window.timeSinceLastResponseAfterLoadMs()") 88 "window.timeSinceLastResponseAfterLoadMs()")
85 self._is_finished = time_since_last_response_ms > 2000 89 self._is_finished = time_since_last_response_ms > 2000
86 return self._is_finished 90 return self._is_finished
87 91
88 92
89 def _GetViewportSize(tab): 93 class SpeedIndexImpl(object):
90 """Returns dimensions of the viewport.""" 94
91 return tab.EvaluateJavaScript('[ window.innerWidth, window.innerHeight ]') 95 def __init__(self, tab):
92 96 """Constructor.
93 97
94 def _SpeedIndex(events, viewport): 98 Args:
95 """Calculate the speed index of a page load from a list of events. 99 tab: The telemetry.core.Tab object for which to calculate SpeedIndex.
96 100 """
97 The speed index number conceptually represents the number of milliseconds 101 self.tab = tab
98 that the page was "visually incomplete". If the page were 0% complete for 102
99 1000 ms, then the score would be 1000; if it were 0% complete for 100 ms 103 def Start(self):
100 then 90% complete (ie 10% incomplete) for 900 ms, then the score would be 104 raise NotImplementedError()
101 1.0*100 + 0.1*900 = 190. 105
102 106 def Stop(self):
103 Args: 107 raise NotImplementedError()
104 events: A list of telemetry.core.timeline.slice.Slice objects 108
105 viewport: A tuple (width, height) of the window. 109 def GetTimeCompletenessList(self):
106 110 """Returns a list of time to visual completeness tuples.
107 Returns: 111
108 A single number, milliseconds of visual incompleteness. 112 In the WPT PHP implementation, this is also called 'visual progress'.
109 """ 113 """
110 paint_events = _IncludedPaintEvents(events) 114 raise NotImplementedError()
111 time_area_dict = _TimeAreaDict(paint_events, viewport) 115
112 time_completeness_dict = _TimeCompletenessDict(time_area_dict) 116 def CalculateSpeedIndex(self):
113 # The first time interval starts from the start of the first event. 117 """Calculate the speed index.
114 prev_time = events[0].start 118
115 prev_completeness = 0.0 119 The speed index number conceptually represents the number of milliseconds
116 speed_index = 0.0 120 that the page was "visually incomplete". If the page were 0% complete for
117 for time, completeness in sorted(time_completeness_dict.items()): 121 1000 ms, then the score would be 1000; if it were 0% complete for 100 ms
118 # Add the incemental value for the interval just before this event. 122 then 90% complete (ie 10% incomplete) for 900 ms, then the score would be
119 elapsed_time = time - prev_time 123 1.0*100 + 0.1*900 = 190.
120 incompleteness = (1.0 - prev_completeness) 124
121 speed_index += elapsed_time * incompleteness 125 Returns:
122 126 A single number, milliseconds of visual incompleteness.
123 # Update variables for next iteration. 127 """
124 prev_completeness = completeness 128 time_completeness_list = self.GetTimeCompletenessList()
125 prev_time = time 129 prev_completeness = 0.0
126 130 speed_index = 0.0
127 return speed_index 131 prev_time = time_completeness_list[0][0]
128 132 for time, completeness in time_completeness_list:
129 133 # Add the incemental value for the interval just before this event.
130 def _TimeCompletenessDict(time_area_dict): 134 elapsed_time = time - prev_time
131 """Make a dictionary of time to visual completeness. 135 incompleteness = (1.0 - prev_completeness)
132 136 speed_index += elapsed_time * incompleteness
133 In the WPT PHP implementation, this is also called 'visual progress'. 137
134 """ 138 # Update variables for next iteration.
135 total_area = sum(time_area_dict.values()) 139 prev_completeness = completeness
136 assert total_area > 0.0, 'Total paint event area must be greater than 0.' 140 prev_time = time
137 completeness = 0.0 141 return speed_index
138 time_completeness_dict = {} 142
139 for time, area in sorted(time_area_dict.items()): 143
140 completeness += float(area) / total_area 144 class VideoSpeedIndexImpl(SpeedIndexImpl):
141 # Visual progress is rounded to the nearest percentage point as in WPT. 145
142 time_completeness_dict[time] = round(completeness, 2) 146 def __init__(self, tab):
143 return time_completeness_dict 147 super(VideoSpeedIndexImpl, self).__init__(tab)
144 148 assert self.tab.video_capture_supported
145 149 self._time_completeness_list = None
146 def _IncludedPaintEvents(events): 150
147 """Get all events that are counted in the calculation of the speed index. 151 def Start(self):
148 152 # TODO(tonyg): Bitrate is arbitrary here. Experiment with screen capture
149 There's one category of paint event that's filtered out: paint events 153 # overhead vs. speed index accuracy and set the bitrate appropriately.
150 that occur before the first 'ResourceReceiveResponse' and 'Layout' events. 154 self.tab.StartVideoCapture(min_bitrate_mbps=4)
151 155
152 Previously in the WPT speed index, paint events that contain children paint 156 def Stop(self):
153 events were also filtered out. 157 self._time_completeness_list = []
154 """ 158 self.tab.StopVideoCapture()
155 def FirstLayoutTime(events): 159 # TODO(tonyg/szym): Implement this.
156 """Get the start time of the first layout after a resource received.""" 160 raise NotImplementedError('SpeedIndex video calculation not implemented.')
157 has_received_response = False 161
158 for event in events: 162 def GetTimeCompletenessList(self):
159 if event.name == 'ResourceReceiveResponse': 163 assert self._time_completeness_list, 'Must call Stop() first.'
160 has_received_response = True 164 return self._time_completeness_list
161 elif has_received_response and event.name == 'Layout': 165
162 return event.start 166
163 assert False, 'There were no layout events after resource receive events.' 167 class PaintRectSpeedIndexImpl(SpeedIndexImpl):
164 168
165 first_layout_time = FirstLayoutTime(events) 169 def __init__(self, tab):
166 paint_events = [e for e in events 170 super(PaintRectSpeedIndexImpl, self).__init__(tab)
167 if e.start >= first_layout_time and e.name == 'Paint'] 171
168 return paint_events 172 def Start(self):
169 173 self.tab.StartTimelineRecording()
170 174
171 def _TimeAreaDict(paint_events, viewport): 175 def Stop(self):
172 """Make a dict from time to adjusted area value for events at that time. 176 self.tab.StopTimelineRecording()
173 177
174 The adjusted area value of each paint event is determined by how many paint 178 def GetTimeCompletenessList(self):
175 events cover the same rectangle, and whether it's a full-window paint event. 179 events = self.tab.timeline_model.GetAllEvents()
176 "Adjusted area" can also be thought of as "points" of visual completeness -- 180 viewport = self._GetViewportSize()
177 each rectangle has a certain number of points and these points are 181 paint_events = self._IncludedPaintEvents(events)
178 distributed amongst the paint events that paint that rectangle. 182 time_area_dict = self._TimeAreaDict(paint_events, viewport)
179 183 total_area = sum(time_area_dict.values())
180 Args: 184 assert total_area > 0.0, 'Total paint event area must be greater than 0.'
181 paint_events: A list of paint events 185 completeness = 0.0
182 viewport: A tuple (width, height) of the window. 186 time_completeness_list = []
183 187
184 Returns: 188 # TODO(tonyg): This sets the start time to the start of the first paint
185 A dictionary of times of each paint event (in milliseconds) to the 189 # event. That can't be correct. The start time should be navigationStart.
186 adjusted area that the paint event is worth. 190 # Since the previous screen is not cleared at navigationStart, we should
187 """ 191 # probably assume the completeness is 0 until the first paint and add the
188 width, height = viewport 192 # time of navigationStart as the start. We need to confirm what WPT does.
189 fullscreen_area = width * height 193 time_completeness_list.append(
190 194 (self.tab.timeline_model.GetAllEvents()[0].start, completeness))
191 def ClippedArea(rectangle): 195
192 """Returns rectangle area clipped to viewport size.""" 196 for time, area in sorted(time_area_dict.items()):
193 _, x0, y0, x1, y1 = rectangle 197 completeness += float(area) / total_area
194 x0 = max(0, x0) 198 # Visual progress is rounded to the nearest percentage point as in WPT.
195 y0 = max(0, y0) 199 time_completeness_list.append((time, round(completeness, 2)))
196 x1 = min(width, x1) 200 return time_completeness_list
197 y1 = min(height, y1) 201
198 return max(0, x1 - x0) * max(0, y1 - y0) 202 def _GetViewportSize(self):
199 203 """Returns dimensions of the viewport."""
200 grouped = _GroupEventByRectangle(paint_events) 204 return self.tab.EvaluateJavaScript(
201 event_area_dict = collections.defaultdict(int) 205 '[ window.innerWidth, window.innerHeight ]')
202 206
203 for rectangle, events in grouped.items(): 207 def _IncludedPaintEvents(self, events):
204 # The area points for each rectangle are divided up among the paint 208 """Get all events that are counted in the calculation of the speed index.
205 # events in that rectangle. 209
206 area = ClippedArea(rectangle) 210 There's one category of paint event that's filtered out: paint events
207 update_count = len(events) 211 that occur before the first 'ResourceReceiveResponse' and 'Layout' events.
208 adjusted_area = float(area) / update_count 212
209 213 Previously in the WPT speed index, paint events that contain children paint
210 # Paint events for the largest-area rectangle are counted as 50%. 214 events were also filtered out.
211 if area == fullscreen_area: 215 """
212 adjusted_area /= 2 216 def FirstLayoutTime(events):
213 217 """Get the start time of the first layout after a resource received."""
214 for event in events: 218 has_received_response = False
215 # The end time for an event is used for that event's time. 219 for event in events:
216 event_time = event.end 220 if event.name == 'ResourceReceiveResponse':
217 event_area_dict[event_time] += adjusted_area 221 has_received_response = True
218 222 elif has_received_response and event.name == 'Layout':
219 return event_area_dict 223 return event.start
220 224 assert False, 'There were no layout events after resource receive events.'
221 225
222 def _GetRectangle(paint_event): 226 first_layout_time = FirstLayoutTime(events)
223 """Get the specific rectangle on the screen for a paint event. 227 paint_events = [e for e in events
224 228 if e.start >= first_layout_time and e.name == 'Paint']
225 Each paint event belongs to a frame (as in html <frame> or <iframe>). 229 return paint_events
226 This, together with location and dimensions, comprises a rectangle. 230
227 In the WPT source, this 'rectangle' is also called a 'region'. 231 def _TimeAreaDict(self, paint_events, viewport):
228 """ 232 """Make a dict from time to adjusted area value for events at that time.
229 def GetBox(quad): 233
230 """Gets top-left and bottom-right coordinates from paint event. 234 The adjusted area value of each paint event is determined by how many paint
231 235 events cover the same rectangle, and whether it's a full-window paint event.
232 In the timeline data from devtools, paint rectangle dimensions are 236 "Adjusted area" can also be thought of as "points" of visual completeness --
233 represented x-y coordinates of four corners, clockwise from the top-left. 237 each rectangle has a certain number of points and these points are
234 See: function WebInspector.TimelinePresentationModel.quadFromRectData 238 distributed amongst the paint events that paint that rectangle.
235 in file src/out/Debug/obj/gen/devtools/TimelinePanel.js. 239
236 """ 240 Args:
237 x0, y0, _, _, x1, y1, _, _ = quad 241 paint_events: A list of paint events
238 return (x0, y0, x1, y1) 242 viewport: A tuple (width, height) of the window.
239 243
240 assert paint_event.name == 'Paint' 244 Returns:
241 frame = paint_event.args['frameId'] 245 A dictionary of times of each paint event (in milliseconds) to the
242 return (frame,) + GetBox(paint_event.args['data']['clip']) 246 adjusted area that the paint event is worth.
243 247 """
244 248 width, height = viewport
245 def _GroupEventByRectangle(paint_events): 249 fullscreen_area = width * height
246 """Group all paint events according to the rectangle that they update.""" 250
247 result = collections.defaultdict(list) 251 def ClippedArea(rectangle):
248 for event in paint_events: 252 """Returns rectangle area clipped to viewport size."""
249 assert event.name == 'Paint' 253 _, x0, y0, x1, y1 = rectangle
250 result[_GetRectangle(event)].append(event) 254 clipped_width = max(0, min(width, x1) - max(0, x0))
251 return result 255 clipped_height = max(0, min(height, y1) - max(0, y0))
256 return clipped_width * clipped_height
257
258 grouped = self._GroupEventByRectangle(paint_events)
259 event_area_dict = collections.defaultdict(int)
260
261 for rectangle, events in grouped.items():
262 # The area points for each rectangle are divided up among the paint
263 # events in that rectangle.
264 area = ClippedArea(rectangle)
265 update_count = len(events)
266 adjusted_area = float(area) / update_count
267
268 # Paint events for the largest-area rectangle are counted as 50%.
269 if area == fullscreen_area:
270 adjusted_area /= 2
271
272 for event in events:
273 # The end time for an event is used for that event's time.
274 event_time = event.end
275 event_area_dict[event_time] += adjusted_area
276
277 return event_area_dict
278
279 def _GetRectangle(self, paint_event):
280 """Get the specific rectangle on the screen for a paint event.
281
282 Each paint event belongs to a frame (as in html <frame> or <iframe>).
283 This, together with location and dimensions, comprises a rectangle.
284 In the WPT source, this 'rectangle' is also called a 'region'.
285 """
286 def GetBox(quad):
287 """Gets top-left and bottom-right coordinates from paint event.
288
289 In the timeline data from devtools, paint rectangle dimensions are
290 represented x-y coordinates of four corners, clockwise from the top-left.
291 See: function WebInspector.TimelinePresentationModel.quadFromRectData
292 in file src/out/Debug/obj/gen/devtools/TimelinePanel.js.
293 """
294 x0, y0, _, _, x1, y1, _, _ = quad
295 return (x0, y0, x1, y1)
296
297 assert paint_event.name == 'Paint'
298 frame = paint_event.args['frameId']
299 return (frame,) + GetBox(paint_event.args['data']['clip'])
300
301 def _GroupEventByRectangle(self, paint_events):
302 """Group all paint events according to the rectangle that they update."""
303 result = collections.defaultdict(list)
304 for event in paint_events:
305 assert event.name == 'Paint'
306 result[self._GetRectangle(event)].append(event)
307 return result
OLDNEW
« no previous file with comments | « no previous file | tools/perf/metrics/speedindex_unittest.py » ('j') | tools/telemetry/telemetry/core/tab.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698