FrameRateMonitor.js 14 KB

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