/*global define*/
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.
*/
var FrameRateMonitor = function(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 samplingWindow
during the warmupPeriod
, the
* lowFrameRate
event will be raised and the page will redirect to the redirectOnLowFrameRateUrl
, 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 samplingWindow
after the warmupPeriod
, the
* lowFrameRate
event will be raised and the page will redirect to the redirectOnLowFrameRateUrl
, 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._preRenderRemoveListener = this._scene.preRender.addEventListener(function(scene, time) {
update(that, time);
});
this._hiddenPropertyName = defined(document.hidden) ? 'hidden' :
defined(document.mozHidden) ? 'mozHidden' :
defined(document.msHidden) ? 'msHidden' :
defined(document.webkitHidden) ? 'webkitHidden' : undefined;
var visibilityChangeEventName = defined(document.hidden) ? 'visibilitychange' :
defined(document.mozHidden) ? 'mozvisibilitychange' :
defined(document.msHidden) ? 'msvisibilitychange' :
defined(document.webkitHidden) ? '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
*/
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 samplingWindow
.
* 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.
*
* If this object was destroyed, it should not be used; calling any function other than
* isDestroyed
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
* isDestroyed
will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (undefined
) to the object as done in the example.
*
* @memberof FrameRateMonitor
*
* @returns {undefined}
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
* @see FrameRateMonitor#isDestroyed
*/
FrameRateMonitor.prototype.destroy = function() {
this._preRenderRemoveListener();
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;
});