OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 // Inject this script on any page to measure framerate as the page is scrolled | |
6 // from top to bottom. | |
7 // | |
8 // Usage: | |
9 // 1. Define a callback that takes a RenderingStats object as a parameter. | |
10 // 2. To start the test, call new __ScrollTest(callback). | |
11 // 3a. When the test is complete, the callback will be called. | |
12 // 3b. If no callback is specified, the results is sent to the console. | |
13 | |
14 (function() { | |
15 var getTimeMs = (function() { | |
16 if (window.performance) | |
17 return (performance.now || | |
18 performance.mozNow || | |
19 performance.msNow || | |
20 performance.oNow || | |
21 performance.webkitNow).bind(window.performance); | |
22 else | |
23 return function() { return new Date().getTime(); }; | |
24 })(); | |
25 | |
26 var requestAnimationFrame = (function() { | |
27 return window.requestAnimationFrame || | |
28 window.webkitRequestAnimationFrame || | |
29 window.mozRequestAnimationFrame || | |
30 window.oRequestAnimationFrame || | |
31 window.msRequestAnimationFrame || | |
32 function(callback) { | |
33 window.setTimeout(callback, 1000 / 60); | |
34 }; | |
35 })().bind(window); | |
36 | |
37 /** | |
38 * Scrolls a given element down a certain amount to emulate user scrolling. | |
39 * Uses smooth scrolling capabilities provided by the platform, if available. | |
40 * @constructor | |
41 */ | |
42 function SmoothScrollDownGesture(opt_element) { | |
43 this.element_ = opt_element || document.body; | |
44 }; | |
45 | |
46 function min(a, b) { | |
47 if (a > b) { | |
48 return b; | |
49 } | |
50 return a; | |
51 }; | |
52 | |
53 function getBoundingVisibleRect(el) { | |
54 r = el.getBoundingClientRect(); | |
55 cur = el; | |
56 while (cur && cur.parentElement) { | |
57 r.top += cur.parentElement.offsetTop; | |
58 r.left += cur.parentElement.offsetLeft; | |
59 r.height = min(r.height, cur.parentElement.offsetHeight); | |
60 r.width = min(r.width, cur.parentElement.offsetWidth); | |
61 cur = cur.parentElement; | |
62 } | |
63 return r; | |
64 }; | |
65 | |
66 function rectsDoIntersect(r1, r2) { | |
67 return (r1.right >= r2.left && | |
68 r1.left <= r2.right && | |
69 r1.bottom >= r2.top && | |
70 r1.top <= r2.bottom); | |
71 }; | |
72 | |
73 SmoothScrollDownGesture.prototype.start = function(callback) { | |
74 this.callback_ = callback; | |
75 if (chrome && | |
76 chrome.gpuBenchmarking && | |
77 chrome.gpuBenchmarking.beginSmoothScrollDown) { | |
78 rect = getBoundingVisibleRect(this.element_); | |
79 if (chrome.gpuBenchmarking.beginSmoothScrollSupportsPositioning || | |
80 rectsDoIntersect(rect, {left: 0, | |
81 top: 0, | |
82 right: 1, | |
83 bottom: 1, | |
84 height: 1, | |
85 width: 1})) { | |
86 chrome.gpuBenchmarking.beginSmoothScrollDown(true, function() { | |
87 callback(); | |
88 }, rect.left + rect.width / 2, rect.top + rect.height / 2); | |
89 return; | |
90 } | |
91 } | |
92 | |
93 var SCROLL_DELTA = 100; | |
94 this.element_.scrollTop += SCROLL_DELTA; | |
95 requestAnimationFrame(callback); | |
96 }; | |
97 | |
98 /** | |
99 * Tracks rendering performance using the gpuBenchmarking.renderingStats API. | |
100 * @constructor | |
101 */ | |
102 function GpuBenchmarkingRenderingStats() { | |
103 } | |
104 | |
105 GpuBenchmarkingRenderingStats.prototype.start = function() { | |
106 this.initialStats_ = this.getRenderingStats_(); | |
107 } | |
108 GpuBenchmarkingRenderingStats.prototype.stop = function() { | |
109 this.finalStats_ = this.getRenderingStats_(); | |
110 } | |
111 | |
112 GpuBenchmarkingRenderingStats.prototype.getDeltas = function() { | |
113 if (!this.initialStats_) | |
114 throw new Error('Start not called.'); | |
115 | |
116 if (!this.finalStats_) | |
117 throw new Error('Stop was not called.'); | |
118 | |
119 var stats = this.finalStats_; | |
120 for (var key in stats) | |
121 stats[key] -= this.initialStats_[key]; | |
122 return stats; | |
123 }; | |
124 | |
125 GpuBenchmarkingRenderingStats.prototype.getRenderingStats_ = function() { | |
126 var stats = chrome.gpuBenchmarking.renderingStats(); | |
127 stats.totalTimeInSeconds = getTimeMs() / 1000; | |
128 return stats; | |
129 }; | |
130 | |
131 /** | |
132 * Tracks rendering performance using requestAnimationFrame. | |
133 * @constructor | |
134 */ | |
135 function RafRenderingStats() { | |
136 this.recording_ = false; | |
137 this.frameTimes_ = []; | |
138 } | |
139 | |
140 RafRenderingStats.prototype.start = function() { | |
141 if (this.recording_) | |
142 throw new Error('Already started.'); | |
143 this.recording_ = true; | |
144 requestAnimationFrame(this.recordFrameTime_.bind(this)); | |
145 } | |
146 | |
147 RafRenderingStats.prototype.stop = function() { | |
148 this.recording_ = false; | |
149 } | |
150 | |
151 RafRenderingStats.prototype.getDeltas = function() { | |
152 var results = {}; | |
153 results.numAnimationFrames = this.frameTimes_.length - 1; | |
154 results.numFramesSentToScreen = results.numAnimationFrames; | |
155 results.droppedFrameCount = this.getDroppedFrameCount_(this.frameTimes_); | |
156 results.totalTimeInSeconds = ( | |
157 this.frameTimes_[this.frameTimes_.length - 1] - | |
158 this.frameTimes_[0]) / 1000; | |
159 return results; | |
160 }; | |
161 | |
162 RafRenderingStats.prototype.recordFrameTime_ = function(timestamp) { | |
163 if (!this.recording_) | |
164 return; | |
165 | |
166 this.frameTimes_.push(timestamp); | |
167 requestAnimationFrame(this.recordFrameTime_.bind(this)); | |
168 }; | |
169 | |
170 RafRenderingStats.prototype.getDroppedFrameCount_ = function(frameTimes) { | |
171 var droppedFrameCount = 0; | |
172 for (var i = 1; i < frameTimes.length; i++) { | |
173 var frameTime = frameTimes[i] - frameTimes[i-1]; | |
174 if (frameTime > 1000 / 55) | |
175 droppedFrameCount++; | |
176 } | |
177 return droppedFrameCount; | |
178 }; | |
179 | |
180 // This class scrolls a page from the top to the bottom once. | |
181 // | |
182 // The page is scrolled down by a set of scroll gestures. These gestures | |
183 // correspond to a reading gesture on that platform. | |
184 // | |
185 // start -> startPass_ -> ...scrolling... -> onGestureComplete_ -> | |
186 // -> startPass_ -> .. scrolling... -> onGestureComplete_ -> callback_ | |
187 function ScrollTest(opt_callback) { | |
188 var self = this; | |
189 | |
190 this.callback_ = opt_callback; | |
191 } | |
192 | |
193 ScrollTest.prototype.start = function(opt_element) { | |
194 // Assign this.element_ here instead of constructor, because the constructor | |
195 // ensures this method will be called after the document is loaded. | |
196 this.element_ = opt_element || document.body; | |
197 // Some pages load more content when you scroll to the bottom. Record | |
198 // the original element height here and only scroll to that point. | |
199 this.scrollHeight_ = this.element_.scrollHeight | |
200 requestAnimationFrame(this.startPass_.bind(this)); | |
201 }; | |
202 | |
203 ScrollTest.prototype.startPass_ = function() { | |
204 this.element_.scrollTop = 0; | |
205 if (window.chrome && chrome.gpuBenchmarking && | |
206 chrome.gpuBenchmarking.renderingStats) | |
207 this.renderingStats_ = new GpuBenchmarkingRenderingStats(); | |
208 else | |
209 this.renderingStats_ = new RafRenderingStats(); | |
210 this.renderingStats_.start(); | |
211 | |
212 this.gesture_ = new SmoothScrollDownGesture(this.element_); | |
213 this.gesture_.start(this.onGestureComplete_.bind(this)); | |
214 }; | |
215 | |
216 ScrollTest.prototype.onGestureComplete_ = function(timestamp) { | |
217 // clientHeight is "special" for the body element. | |
218 var clientHeight; | |
219 if (this.element_ == document.body) | |
220 clientHeight = window.innerHeight; | |
221 else | |
222 clientHeight = this.element_.clientHeight; | |
223 | |
224 // If the scrollHeight went down, only scroll to the new scrollHeight. | |
225 this.scrollHeight_ = Math.min(this.scrollHeight_, | |
226 this.element_.scrollHeight); | |
227 | |
228 // -1 to allow for rounding errors on scaled viewports (like mobile). | |
229 var isPassComplete = | |
230 this.element_.scrollTop + clientHeight >= this.scrollHeight_ - 1; | |
231 | |
232 if (!isPassComplete) { | |
233 this.gesture_.start(this.onGestureComplete_.bind(this)); | |
234 return; | |
235 } | |
236 | |
237 this.endPass_(); | |
238 | |
239 // We're done. | |
240 if (this.callback_) | |
241 this.callback_(this.renderingStats_.getDeltas()); | |
242 else | |
243 console.log(this.renderingStats_.getDeltas()); | |
244 }; | |
245 | |
246 ScrollTest.prototype.endPass_ = function() { | |
247 this.renderingStats_.stop(); | |
248 }; | |
249 | |
250 | |
251 window.__ScrollTest = ScrollTest; | |
252 })(); | |
OLD | NEW |