FrameRateMonitor.js 14 KB

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