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

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: Change to generators 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))
slamm 2013/12/02 22:51:15 Given that the two speed indexes have such differe
tonyg 2013/12/02 23:18:36 I see your point. But I think the Measurements the
slamm 2013/12/03 01:17:32 Thanks for the context.
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 self.tab = tab
93 97
94 def _SpeedIndex(events, viewport): 98 def Start(self):
95 """Calculate the speed index of a page load from a list of events. 99 raise NotImplementedError()
96 100
97 The speed index number conceptually represents the number of milliseconds 101 def Stop(self):
98 that the page was "visually incomplete". If the page were 0% complete for 102 raise NotImplementedError()
99 1000 ms, then the score would be 1000; if it were 0% complete for 100 ms 103
100 then 90% complete (ie 10% incomplete) for 900 ms, then the score would be 104 def GetTimeCompletenessList(self):
101 1.0*100 + 0.1*900 = 190. 105 """Returns a list of time to visual completeness tuples.
102 106
103 Args: 107 In the WPT PHP implementation, this is also called 'visual progress'.
104 events: A list of telemetry.core.timeline.slice.Slice objects 108 """
105 viewport: A tuple (width, height) of the window. 109 raise NotImplementedError()
106 110
107 Returns: 111 def CalculateSpeedIndex(self):
108 A single number, milliseconds of visual incompleteness. 112 """Calculate the speed index.
109 """ 113
110 paint_events = _IncludedPaintEvents(events) 114 The speed index number conceptually represents the number of milliseconds
111 time_area_dict = _TimeAreaDict(paint_events, viewport) 115 that the page was "visually incomplete". If the page were 0% complete for
112 time_completeness_dict = _TimeCompletenessDict(time_area_dict) 116 1000 ms, then the score would be 1000; if it were 0% complete for 100 ms
113 # The first time interval starts from the start of the first event. 117 then 90% complete (ie 10% incomplete) for 900 ms, then the score would be
114 prev_time = events[0].start 118 1.0*100 + 0.1*900 = 190.
115 prev_completeness = 0.0 119
116 speed_index = 0.0 120 Returns:
117 for time, completeness in sorted(time_completeness_dict.items()): 121 A single number, milliseconds of visual incompleteness.
118 # Add the incemental value for the interval just before this event. 122 """
119 elapsed_time = time - prev_time 123 time_completeness_list = self.GetTimeCompletenessList()
120 incompleteness = (1.0 - prev_completeness) 124 prev_completeness = 0.0
121 speed_index += elapsed_time * incompleteness 125 speed_index = 0.0
122 126 prev_time = time_completeness_list[0][0]
123 # Update variables for next iteration. 127 for time, completeness in time_completeness_list:
124 prev_completeness = completeness 128 # Add the incemental value for the interval just before this event.
125 prev_time = time 129 elapsed_time = time - prev_time
126 130 incompleteness = (1.0 - prev_completeness)
127 return speed_index 131 speed_index += elapsed_time * incompleteness
128 132
129 133 # Update variables for next iteration.
130 def _TimeCompletenessDict(time_area_dict): 134 prev_completeness = completeness
131 """Make a dictionary of time to visual completeness. 135 prev_time = time
132 136 return speed_index
133 In the WPT PHP implementation, this is also called 'visual progress'. 137
134 """ 138
135 total_area = sum(time_area_dict.values()) 139 class VideoSpeedIndexImpl(SpeedIndexImpl):
136 assert total_area > 0.0, 'Total paint event area must be greater than 0.' 140
137 completeness = 0.0 141 def __init__(self, tab):
slamm 2013/12/02 22:51:15 Document "tab"?
tonyg 2013/12/02 23:18:36 Done (in the base class).
138 time_completeness_dict = {} 142 super(VideoSpeedIndexImpl, self).__init__(tab)
139 for time, area in sorted(time_area_dict.items()): 143 assert self.tab.video_capture_supported
140 completeness += float(area) / total_area 144 self._time_completeness_list = None
141 # Visual progress is rounded to the nearest percentage point as in WPT. 145
142 time_completeness_dict[time] = round(completeness, 2) 146 def Start(self):
143 return time_completeness_dict 147 # TODO(tonyg): Bitrate is arbitrary here. Experiment with screen capture
144 148 # overhead vs. speed index accuracy and set the bitrate appropriately.
145 149 self.tab.StartVideoCapture(min_bitrate_mbps=4)
146 def _IncludedPaintEvents(events): 150
147 """Get all events that are counted in the calculation of the speed index. 151 def Stop(self):
148 152 self._time_completeness_list = []
149 There's one category of paint event that's filtered out: paint events 153 for time_ms, bitmap in self.tab.StopVideoCapture():
150 that occur before the first 'ResourceReceiveResponse' and 'Layout' events. 154 # TODO(tonyg/szym): Implement this.
151 155 raise NotImplementedError('SpeedIndex video calculation not implemented.')
152 Previously in the WPT speed index, paint events that contain children paint 156
153 events were also filtered out. 157 def GetTimeCompletenessList(self):
154 """ 158 assert self._time_completeness_list, 'Must call Stop() first.'
155 def FirstLayoutTime(events): 159 return self._time_completeness_list
156 """Get the start time of the first layout after a resource received.""" 160
157 has_received_response = False 161
158 for event in events: 162 class PaintRectSpeedIndexImpl(SpeedIndexImpl):
159 if event.name == 'ResourceReceiveResponse': 163
160 has_received_response = True 164 def __init__(self, tab):
161 elif has_received_response and event.name == 'Layout': 165 super(PaintRectSpeedIndexImpl, self).__init__(tab)
162 return event.start 166
163 assert False, 'There were no layout events after resource receive events.' 167 def Start(self):
164 168 self.tab.StartTimelineRecording()
165 first_layout_time = FirstLayoutTime(events) 169
166 paint_events = [e for e in events 170 def Stop(self):
167 if e.start >= first_layout_time and e.name == 'Paint'] 171 self.tab.StopTimelineRecording()
168 return paint_events 172
169 173 def GetTimeCompletenessList(self):
170 174 events = self.tab.timeline_model.GetAllEvents()
171 def _TimeAreaDict(paint_events, viewport): 175 viewport = self._GetViewportSize()
172 """Make a dict from time to adjusted area value for events at that time. 176 paint_events = self._IncludedPaintEvents(events)
173 177 time_area_dict = self._TimeAreaDict(paint_events, viewport)
174 The adjusted area value of each paint event is determined by how many paint 178 total_area = sum(time_area_dict.values())
175 events cover the same rectangle, and whether it's a full-window paint event. 179 assert total_area > 0.0, 'Total paint event area must be greater than 0.'
176 "Adjusted area" can also be thought of as "points" of visual completeness -- 180 completeness = 0.0
177 each rectangle has a certain number of points and these points are 181 time_completeness_list = []
178 distributed amongst the paint events that paint that rectangle. 182
179 183 # TODO(tonyg): This sets the start time to the start of the first paint
180 Args: 184 # event. That can't be correct. The start time should be navigationStart.
181 paint_events: A list of paint events 185 # Since the previous screen is not cleared at navigationStart, we should
182 viewport: A tuple (width, height) of the window. 186 # probably assume the completeness is 0 until the first paint and add the
183 187 # time of navigationStart as the start. We need to confirm what WPT does.
184 Returns: 188 time_completeness_list.append(
185 A dictionary of times of each paint event (in milliseconds) to the 189 (self.tab.timeline_model.GetAllEvents()[0].start, completeness))
186 adjusted area that the paint event is worth. 190
187 """ 191 for time, area in sorted(time_area_dict.items()):
188 width, height = viewport 192 completeness += float(area) / total_area
189 fullscreen_area = width * height 193 # Visual progress is rounded to the nearest percentage point as in WPT.
190 194 time_completeness_list.append((time, round(completeness, 2)))
191 def ClippedArea(rectangle): 195 return time_completeness_list
192 """Returns rectangle area clipped to viewport size.""" 196
193 _, x0, y0, x1, y1 = rectangle 197 def _GetViewportSize(self):
194 x0 = max(0, x0) 198 """Returns dimensions of the viewport."""
195 y0 = max(0, y0) 199 return self.tab.EvaluateJavaScript(
196 x1 = min(width, x1) 200 '[ window.innerWidth, window.innerHeight ]')
197 y1 = min(height, y1) 201
198 return max(0, x1 - x0) * max(0, y1 - y0) 202 def _IncludedPaintEvents(self, events):
199 203 """Get all events that are counted in the calculation of the speed index.
200 grouped = _GroupEventByRectangle(paint_events) 204
201 event_area_dict = collections.defaultdict(int) 205 There's one category of paint event that's filtered out: paint events
202 206 that occur before the first 'ResourceReceiveResponse' and 'Layout' events.
203 for rectangle, events in grouped.items(): 207
204 # The area points for each rectangle are divided up among the paint 208 Previously in the WPT speed index, paint events that contain children paint
205 # events in that rectangle. 209 events were also filtered out.
206 area = ClippedArea(rectangle) 210 """
207 update_count = len(events) 211 def FirstLayoutTime(events):
208 adjusted_area = float(area) / update_count 212 """Get the start time of the first layout after a resource received."""
209 213 has_received_response = False
210 # Paint events for the largest-area rectangle are counted as 50%. 214 for event in events:
211 if area == fullscreen_area: 215 if event.name == 'ResourceReceiveResponse':
212 adjusted_area /= 2 216 has_received_response = True
213 217 elif has_received_response and event.name == 'Layout':
214 for event in events: 218 return event.start
215 # The end time for an event is used for that event's time. 219 assert False, 'There were no layout events after resource receive events.'
216 event_time = event.end 220
217 event_area_dict[event_time] += adjusted_area 221 first_layout_time = FirstLayoutTime(events)
218 222 paint_events = [e for e in events
219 return event_area_dict 223 if e.start >= first_layout_time and e.name == 'Paint']
220 224 return paint_events
221 225
222 def _GetRectangle(paint_event): 226 def _TimeAreaDict(self, paint_events, viewport):
223 """Get the specific rectangle on the screen for a paint event. 227 """Make a dict from time to adjusted area value for events at that time.
224 228
225 Each paint event belongs to a frame (as in html <frame> or <iframe>). 229 The adjusted area value of each paint event is determined by how many paint
226 This, together with location and dimensions, comprises a rectangle. 230 events cover the same rectangle, and whether it's a full-window paint event.
227 In the WPT source, this 'rectangle' is also called a 'region'. 231 "Adjusted area" can also be thought of as "points" of visual completeness --
228 """ 232 each rectangle has a certain number of points and these points are
229 def GetBox(quad): 233 distributed amongst the paint events that paint that rectangle.
230 """Gets top-left and bottom-right coordinates from paint event. 234
231 235 Args:
232 In the timeline data from devtools, paint rectangle dimensions are 236 paint_events: A list of paint events
233 represented x-y coordinates of four corners, clockwise from the top-left. 237 viewport: A tuple (width, height) of the window.
234 See: function WebInspector.TimelinePresentationModel.quadFromRectData 238
235 in file src/out/Debug/obj/gen/devtools/TimelinePanel.js. 239 Returns:
236 """ 240 A dictionary of times of each paint event (in milliseconds) to the
237 x0, y0, _, _, x1, y1, _, _ = quad 241 adjusted area that the paint event is worth.
238 return (x0, y0, x1, y1) 242 """
239 243 width, height = viewport
240 assert paint_event.name == 'Paint' 244 fullscreen_area = width * height
241 frame = paint_event.args['frameId'] 245
242 return (frame,) + GetBox(paint_event.args['data']['clip']) 246 def ClippedArea(rectangle):
243 247 """Returns rectangle area clipped to viewport size."""
244 248 _, x0, y0, x1, y1 = rectangle
245 def _GroupEventByRectangle(paint_events): 249 x0 = max(0, x0)
246 """Group all paint events according to the rectangle that they update.""" 250 y0 = max(0, y0)
247 result = collections.defaultdict(list) 251 x1 = min(width, x1)
248 for event in paint_events: 252 y1 = min(height, y1)
slamm 2013/12/02 22:51:15 Better or worse? clipped_width = max(0, min(width
tonyg 2013/12/02 23:18:36 I like your implementation. Changed here.
249 assert event.name == 'Paint' 253 return max(0, x1 - x0) * max(0, y1 - y0)
250 result[_GetRectangle(event)].append(event) 254
251 return result 255 grouped = self._GroupEventByRectangle(paint_events)
256 event_area_dict = collections.defaultdict(int)
257
258 for rectangle, events in grouped.items():
259 # The area points for each rectangle are divided up among the paint
260 # events in that rectangle.
261 area = ClippedArea(rectangle)
262 update_count = len(events)
263 adjusted_area = float(area) / update_count
264
265 # Paint events for the largest-area rectangle are counted as 50%.
266 if area == fullscreen_area:
267 adjusted_area /= 2
268
269 for event in events:
270 # The end time for an event is used for that event's time.
271 event_time = event.end
272 event_area_dict[event_time] += adjusted_area
273
274 return event_area_dict
275
276 def _GetRectangle(self, paint_event):
277 """Get the specific rectangle on the screen for a paint event.
278
279 Each paint event belongs to a frame (as in html <frame> or <iframe>).
280 This, together with location and dimensions, comprises a rectangle.
281 In the WPT source, this 'rectangle' is also called a 'region'.
282 """
283 def GetBox(quad):
284 """Gets top-left and bottom-right coordinates from paint event.
285
286 In the timeline data from devtools, paint rectangle dimensions are
287 represented x-y coordinates of four corners, clockwise from the top-left.
288 See: function WebInspector.TimelinePresentationModel.quadFromRectData
289 in file src/out/Debug/obj/gen/devtools/TimelinePanel.js.
290 """
291 x0, y0, _, _, x1, y1, _, _ = quad
292 return (x0, y0, x1, y1)
293
294 assert paint_event.name == 'Paint'
295 frame = paint_event.args['frameId']
296 return (frame,) + GetBox(paint_event.args['data']['clip'])
slamm 2013/12/02 22:51:15 Perhaps one of the following: (frame, GetBox(...
tonyg 2013/12/02 23:18:36 GetBox returns a tuple, so I don't think those imp
slamm 2013/12/03 01:17:32 Ah, of course. It's right there.
297
298 def _GroupEventByRectangle(self, paint_events):
299 """Group all paint events according to the rectangle that they update."""
300 result = collections.defaultdict(list)
301 for event in paint_events:
302 assert event.name == 'Paint'
303 result[self._GetRectangle(event)].append(event)
304 return result
OLDNEW
« no previous file with comments | « no previous file | tools/perf/metrics/speedindex_unittest.py » ('j') | tools/telemetry/telemetry/core/platform/platform_backend.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698