- add sources.
[platform/framework/web/crosswalk.git] / src / tools / perf / metrics / speedindex.py
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.
4
5 import collections
6 import os
7
8 from metrics import Metric
9
10 class SpeedIndexMetric(Metric):
11   """The speed index metric is one way of measuring page load speed.
12
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.
17
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
20   """
21   def __init__(self):
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:
26       self._js = f.read()
27
28   def Start(self, _, tab):
29     """Start recording events.
30
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.
34     """
35     tab.StartTimelineRecording()
36     self._script_is_loaded = False
37     self._is_finished = False
38
39   def Stop(self, _, tab):
40     """Stop timeline recording."""
41     assert self.IsFinished(tab)
42     tab.StopTimelineRecording()
43
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)
51
52   def IsFinished(self, tab):
53     """Decide whether the timeline recording should be stopped.
54
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.
59
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.:
62       def IsDone():
63         return self._speedindex.IsFinished(tab)
64       util.WaitFor(IsDone, 60)
65
66     Returns:
67       True if 2 seconds have passed since last resource received, false
68       otherwise.
69     """
70     if self._is_finished:
71       return True
72
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
82
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
87
88
89 def _GetViewportSize(tab):
90   """Returns dimensions of the viewport."""
91   return tab.EvaluateJavaScript('[ window.innerWidth, window.innerHeight ]')
92
93
94 def _SpeedIndex(events, viewport):
95   """Calculate the speed index of a page load from a list of events.
96
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.
102
103   Args:
104     events: A list of telemetry.core.timeline.slice.Slice objects
105     viewport: A tuple (width, height) of the window.
106
107   Returns:
108     A single number, milliseconds of visual incompleteness.
109   """
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
116   speed_index = 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
122
123     # Update variables for next iteration.
124     prev_completeness = completeness
125     prev_time = time
126
127   return speed_index
128
129
130 def _TimeCompletenessDict(time_area_dict):
131   """Make a dictionary of time to visual completeness.
132
133   In the WPT PHP implementation, this is also called 'visual progress'.
134   """
135   total_area = sum(time_area_dict.values())
136   assert total_area > 0.0, 'Total paint event area must be greater than 0.'
137   completeness = 0.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
144
145
146 def _IncludedPaintEvents(events):
147   """Get all events that are counted in the calculation of the speed index.
148
149   There's one category of paint event that's filtered out: paint events
150   that occur before the first 'ResourceReceiveResponse' and 'Layout' events.
151
152   Previously in the WPT speed index, paint events that contain children paint
153   events were also filtered out.
154   """
155   def FirstLayoutTime(events):
156     """Get the start time of the first layout after a resource received."""
157     has_received_response = False
158     for event in events:
159       if event.name == 'ResourceReceiveResponse':
160         has_received_response = True
161       elif has_received_response and event.name == 'Layout':
162         return event.start
163     assert False, 'There were no layout events after resource receive events.'
164
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']
168   return paint_events
169
170
171 def _TimeAreaDict(paint_events, viewport):
172   """Make a dict from time to adjusted area value for events at that time.
173
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.
179
180   Args:
181     paint_events: A list of paint events
182     viewport: A tuple (width, height) of the window.
183
184   Returns:
185     A dictionary of times of each paint event (in milliseconds) to the
186     adjusted area that the paint event is worth.
187   """
188   width, height = viewport
189   fullscreen_area = width * height
190
191   def ClippedArea(rectangle):
192     """Returns rectangle area clipped to viewport size."""
193     _, x0, y0, x1, y1 = rectangle
194     x0 = max(0, x0)
195     y0 = max(0, y0)
196     x1 = min(width, x1)
197     y1 = min(height, y1)
198     return max(0, x1 - x0) * max(0, y1 - y0)
199
200   grouped = _GroupEventByRectangle(paint_events)
201   event_area_dict = collections.defaultdict(int)
202
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
209
210     # Paint events for the largest-area rectangle are counted as 50%.
211     if area == fullscreen_area:
212       adjusted_area /= 2
213
214     for event in events:
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
218
219   return event_area_dict
220
221
222 def _GetRectangle(paint_event):
223   """Get the specific rectangle on the screen for a paint event.
224
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'.
228   """
229   def GetBox(quad):
230     """Gets top-left and bottom-right coordinates from paint event.
231
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.
236     """
237     x0, y0, _, _, x1, y1, _, _ = quad
238     return (x0, y0, x1, y1)
239
240   assert paint_event.name == 'Paint'
241   frame = paint_event.args['frameId']
242   return (frame,) + GetBox(paint_event.args['data']['clip'])
243
244
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)
251   return result