/
FrameRateMonitor.js
335 lines (296 loc) · 14.2 KB
/
FrameRateMonitor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
define([
'../Core/defaultValue',
'../Core/defined',
'../Core/defineProperties',
'../Core/destroyObject',
'../Core/DeveloperError',
'../Core/Event',
'../Core/getTimestamp',
'../Core/TimeConstants'
], function(
defaultValue,
defined,
defineProperties,
destroyObject,
DeveloperError,
Event,
getTimestamp,
TimeConstants) {
'use strict';
/**
* Monitors the frame rate (frames per second) in a {@link Scene} and raises an event if the frame rate is
* lower than a threshold. Later, if the frame rate returns to the required level, a separate event is raised.
* To avoid creating multiple FrameRateMonitors for a single {@link Scene}, use {@link FrameRateMonitor.fromScene}
* instead of constructing an instance explicitly.
*
* @alias FrameRateMonitor
* @constructor
*
* @param {Object} [options] Object with the following properties:
* @param {Scene} options.scene The Scene instance for which to monitor performance.
* @param {Number} [options.samplingWindow=5.0] The length of the sliding window over which to compute the average frame rate, in seconds.
* @param {Number} [options.quietPeriod=2.0] The length of time to wait at startup and each time the page becomes visible (i.e. when the user
* switches back to the tab) before starting to measure performance, in seconds.
* @param {Number} [options.warmupPeriod=5.0] The length of the warmup period, in seconds. During the warmup period, a separate
* (usually lower) frame rate is required.
* @param {Number} [options.minimumFrameRateDuringWarmup=4] The minimum frames-per-second that are required for acceptable performance during
* the warmup period. If the frame rate averages less than this during any samplingWindow during the warmupPeriod, the
* lowFrameRate event will be raised and the page will redirect to the redirectOnLowFrameRateUrl, if any.
* @param {Number} [options.minimumFrameRateAfterWarmup=8] The minimum frames-per-second that are required for acceptable performance after
* the end of the warmup period. If the frame rate averages less than this during any samplingWindow after the warmupPeriod, the
* lowFrameRate event will be raised and the page will redirect to the redirectOnLowFrameRateUrl, if any.
*/
function FrameRateMonitor(options) {
//>>includeStart('debug', pragmas.debug);
if (!defined(options) || !defined(options.scene)) {
throw new DeveloperError('options.scene is required.');
}
//>>includeEnd('debug');
this._scene = options.scene;
/**
* Gets or sets the length of the sliding window over which to compute the average frame rate, in seconds.
* @type {Number}
*/
this.samplingWindow = defaultValue(options.samplingWindow, FrameRateMonitor.defaultSettings.samplingWindow);
/**
* Gets or sets the length of time to wait at startup and each time the page becomes visible (i.e. when the user
* switches back to the tab) before starting to measure performance, in seconds.
* @type {Number}
*/
this.quietPeriod = defaultValue(options.quietPeriod, FrameRateMonitor.defaultSettings.quietPeriod);
/**
* Gets or sets the length of the warmup period, in seconds. During the warmup period, a separate
* (usually lower) frame rate is required.
* @type {Number}
*/
this.warmupPeriod = defaultValue(options.warmupPeriod, FrameRateMonitor.defaultSettings.warmupPeriod);
/**
* Gets or sets the minimum frames-per-second that are required for acceptable performance during
* the warmup period. If the frame rate averages less than this during any <code>samplingWindow</code> during the <code>warmupPeriod</code>, the
* <code>lowFrameRate</code> event will be raised and the page will redirect to the <code>redirectOnLowFrameRateUrl</code>, if any.
* @type {Number}
*/
this.minimumFrameRateDuringWarmup = defaultValue(options.minimumFrameRateDuringWarmup, FrameRateMonitor.defaultSettings.minimumFrameRateDuringWarmup);
/**
* Gets or sets the minimum frames-per-second that are required for acceptable performance after
* the end of the warmup period. If the frame rate averages less than this during any <code>samplingWindow</code> after the <code>warmupPeriod</code>, the
* <code>lowFrameRate</code> event will be raised and the page will redirect to the <code>redirectOnLowFrameRateUrl</code>, if any.
* @type {Number}
*/
this.minimumFrameRateAfterWarmup = defaultValue(options.minimumFrameRateAfterWarmup, FrameRateMonitor.defaultSettings.minimumFrameRateAfterWarmup);
this._lowFrameRate = new Event();
this._nominalFrameRate = new Event();
this._frameTimes = [];
this._needsQuietPeriod = true;
this._quietPeriodEndTime = 0.0;
this._warmupPeriodEndTime = 0.0;
this._frameRateIsLow = false;
this._lastFramesPerSecond = undefined;
this._pauseCount = 0;
var that = this;
this._preUpdateRemoveListener = this._scene.preUpdate.addEventListener(function(scene, time) {
update(that, time);
});
this._hiddenPropertyName = (document.hidden !== undefined) ? 'hidden' :
(document.mozHidden !== undefined) ? 'mozHidden' :
(document.msHidden !== undefined) ? 'msHidden' :
(document.webkitHidden !== undefined) ? 'webkitHidden' : undefined;
var visibilityChangeEventName = (document.hidden !== undefined) ? 'visibilitychange' :
(document.mozHidden !== undefined) ? 'mozvisibilitychange' :
(document.msHidden !== undefined) ? 'msvisibilitychange' :
(document.webkitHidden !== undefined) ? 'webkitvisibilitychange' : undefined;
function visibilityChangeListener() {
visibilityChanged(that);
}
this._visibilityChangeRemoveListener = undefined;
if (defined(visibilityChangeEventName)) {
document.addEventListener(visibilityChangeEventName, visibilityChangeListener, false);
this._visibilityChangeRemoveListener = function() {
document.removeEventListener(visibilityChangeEventName, visibilityChangeListener, false);
};
}
}
/**
* The default frame rate monitoring settings. These settings are used when {@link FrameRateMonitor.fromScene}
* needs to create a new frame rate monitor, and for any settings that are not passed to the
* {@link FrameRateMonitor} constructor.
*
* @memberof FrameRateMonitor
* @type {Object}
*/
FrameRateMonitor.defaultSettings = {
samplingWindow : 5.0,
quietPeriod : 2.0,
warmupPeriod : 5.0,
minimumFrameRateDuringWarmup : 4,
minimumFrameRateAfterWarmup : 8
};
/**
* Gets the {@link FrameRateMonitor} for a given scene. If the scene does not yet have
* a {@link FrameRateMonitor}, one is created with the {@link FrameRateMonitor.defaultSettings}.
*
* @param {Scene} scene The scene for which to get the {@link FrameRateMonitor}.
* @returns {FrameRateMonitor} The scene's {@link FrameRateMonitor}.
*/
FrameRateMonitor.fromScene = function(scene) {
//>>includeStart('debug', pragmas.debug);
if (!defined(scene)) {
throw new DeveloperError('scene is required.');
}
//>>includeEnd('debug');
if (!defined(scene._frameRateMonitor) || scene._frameRateMonitor.isDestroyed()) {
scene._frameRateMonitor = new FrameRateMonitor({
scene : scene
});
}
return scene._frameRateMonitor;
};
defineProperties(FrameRateMonitor.prototype, {
/**
* Gets the {@link Scene} instance for which to monitor performance.
* @memberof FrameRateMonitor.prototype
* @type {Scene}
*/
scene : {
get : function() {
return this._scene;
}
},
/**
* Gets the event that is raised when a low frame rate is detected. The function will be passed
* the {@link Scene} instance as its first parameter and the average number of frames per second
* over the sampling window as its second parameter.
* @memberof FrameRateMonitor.prototype
* @type {Event}
*/
lowFrameRate : {
get : function() {
return this._lowFrameRate;
}
},
/**
* Gets the event that is raised when the frame rate returns to a normal level after having been low.
* The function will be passed the {@link Scene} instance as its first parameter and the average
* number of frames per second over the sampling window as its second parameter.
* @memberof FrameRateMonitor.prototype
* @type {Event}
*/
nominalFrameRate : {
get : function() {
return this._nominalFrameRate;
}
},
/**
* Gets the most recently computed average frames-per-second over the last <code>samplingWindow</code>.
* This property may be undefined if the frame rate has not been computed.
* @memberof FrameRateMonitor.prototype
* @type {Number}
*/
lastFramesPerSecond : {
get : function() {
return this._lastFramesPerSecond;
}
}
});
/**
* Pauses monitoring of the frame rate. To resume monitoring, {@link FrameRateMonitor#unpause}
* must be called once for each time this function is called.
* @memberof FrameRateMonitor
*/
FrameRateMonitor.prototype.pause = function() {
++this._pauseCount;
if (this._pauseCount === 1) {
this._frameTimes.length = 0;
this._lastFramesPerSecond = undefined;
}
};
/**
* Resumes monitoring of the frame rate. If {@link FrameRateMonitor#pause} was called
* multiple times, this function must be called the same number of times in order to
* actually resume monitoring.
* @memberof FrameRateMonitor
*/
FrameRateMonitor.prototype.unpause = function() {
--this._pauseCount;
if (this._pauseCount <= 0) {
this._pauseCount = 0;
this._needsQuietPeriod = true;
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @memberof FrameRateMonitor
*
* @returns {Boolean} True if this object was destroyed; otherwise, false.
*
* @see FrameRateMonitor#destroy
*/
FrameRateMonitor.prototype.isDestroyed = function() {
return false;
};
/**
* Unsubscribes this instance from all events it is listening to.
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @memberof FrameRateMonitor
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @see FrameRateMonitor#isDestroyed
*/
FrameRateMonitor.prototype.destroy = function() {
this._preUpdateRemoveListener();
if (defined(this._visibilityChangeRemoveListener)) {
this._visibilityChangeRemoveListener();
}
return destroyObject(this);
};
function update(monitor, time) {
if (monitor._pauseCount > 0) {
return;
}
var timeStamp = getTimestamp();
if (monitor._needsQuietPeriod) {
monitor._needsQuietPeriod = false;
monitor._frameTimes.length = 0;
monitor._quietPeriodEndTime = timeStamp + (monitor.quietPeriod / TimeConstants.SECONDS_PER_MILLISECOND);
monitor._warmupPeriodEndTime = monitor._quietPeriodEndTime + ((monitor.warmupPeriod + monitor.samplingWindow) / TimeConstants.SECONDS_PER_MILLISECOND);
} else if (timeStamp >= monitor._quietPeriodEndTime) {
monitor._frameTimes.push(timeStamp);
var beginningOfWindow = timeStamp - (monitor.samplingWindow / TimeConstants.SECONDS_PER_MILLISECOND);
if (monitor._frameTimes.length >= 2 && monitor._frameTimes[0] <= beginningOfWindow) {
while (monitor._frameTimes.length >= 2 && monitor._frameTimes[1] < beginningOfWindow) {
monitor._frameTimes.shift();
}
var averageTimeBetweenFrames = (timeStamp - monitor._frameTimes[0]) / (monitor._frameTimes.length - 1);
monitor._lastFramesPerSecond = 1000.0 / averageTimeBetweenFrames;
var maximumFrameTime = 1000.0 / (timeStamp > monitor._warmupPeriodEndTime ? monitor.minimumFrameRateAfterWarmup : monitor.minimumFrameRateDuringWarmup);
if (averageTimeBetweenFrames > maximumFrameTime) {
if (!monitor._frameRateIsLow) {
monitor._frameRateIsLow = true;
monitor._needsQuietPeriod = true;
monitor.lowFrameRate.raiseEvent(monitor.scene, monitor._lastFramesPerSecond);
}
} else if (monitor._frameRateIsLow) {
monitor._frameRateIsLow = false;
monitor._needsQuietPeriod = true;
monitor.nominalFrameRate.raiseEvent(monitor.scene, monitor._lastFramesPerSecond);
}
}
}
}
function visibilityChanged(monitor) {
if (document[monitor._hiddenPropertyName]) {
monitor.pause();
} else {
monitor.unpause();
}
}
return FrameRateMonitor;
});