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

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: 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 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
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
OLDNEW
« no previous file with comments | « no previous file | tools/perf/metrics/speedindex_unittest.py » ('j') | tools/telemetry/telemetry/core/platform/__init__.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698