/*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; });