FrameRateMonitor.js 13 KB

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