performanceMonitor.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import { Nullable } from "../types";
  2. import { PrecisionDate } from "./precisionDate";
  3. /**
  4. * Performance monitor tracks rolling average frame-time and frame-time variance over a user defined sliding-window
  5. */
  6. export class PerformanceMonitor {
  7. private _enabled: boolean = true;
  8. private _rollingFrameTime: RollingAverage;
  9. private _lastFrameTimeMs: Nullable<number>;
  10. /**
  11. * constructor
  12. * @param frameSampleSize The number of samples required to saturate the sliding window
  13. */
  14. constructor(frameSampleSize: number = 30) {
  15. this._rollingFrameTime = new RollingAverage(frameSampleSize);
  16. }
  17. /**
  18. * Samples current frame
  19. * @param timeMs A timestamp in milliseconds of the current frame to compare with other frames
  20. */
  21. public sampleFrame(timeMs: number = PrecisionDate.Now) {
  22. if (!this._enabled) { return; }
  23. if (this._lastFrameTimeMs != null) {
  24. let dt = timeMs - this._lastFrameTimeMs;
  25. this._rollingFrameTime.add(dt);
  26. }
  27. this._lastFrameTimeMs = timeMs;
  28. }
  29. /**
  30. * Returns the average frame time in milliseconds over the sliding window (or the subset of frames sampled so far)
  31. */
  32. public get averageFrameTime(): number {
  33. return this._rollingFrameTime.average;
  34. }
  35. /**
  36. * Returns the variance frame time in milliseconds over the sliding window (or the subset of frames sampled so far)
  37. */
  38. public get averageFrameTimeVariance(): number {
  39. return this._rollingFrameTime.variance;
  40. }
  41. /**
  42. * Returns the frame time of the most recent frame
  43. */
  44. public get instantaneousFrameTime(): number {
  45. return this._rollingFrameTime.history(0);
  46. }
  47. /**
  48. * Returns the average framerate in frames per second over the sliding window (or the subset of frames sampled so far)
  49. */
  50. public get averageFPS(): number {
  51. return 1000.0 / this._rollingFrameTime.average;
  52. }
  53. /**
  54. * Returns the average framerate in frames per second using the most recent frame time
  55. */
  56. public get instantaneousFPS(): number {
  57. let history = this._rollingFrameTime.history(0);
  58. if (history === 0) {
  59. return 0;
  60. }
  61. return 1000.0 / history;
  62. }
  63. /**
  64. * Returns true if enough samples have been taken to completely fill the sliding window
  65. */
  66. public get isSaturated(): boolean {
  67. return this._rollingFrameTime.isSaturated();
  68. }
  69. /**
  70. * Enables contributions to the sliding window sample set
  71. */
  72. public enable() {
  73. this._enabled = true;
  74. }
  75. /**
  76. * Disables contributions to the sliding window sample set
  77. * Samples will not be interpolated over the disabled period
  78. */
  79. public disable() {
  80. this._enabled = false;
  81. //clear last sample to avoid interpolating over the disabled period when next enabled
  82. this._lastFrameTimeMs = null;
  83. }
  84. /**
  85. * Returns true if sampling is enabled
  86. */
  87. public get isEnabled(): boolean {
  88. return this._enabled;
  89. }
  90. /**
  91. * Resets performance monitor
  92. */
  93. public reset() {
  94. //clear last sample to avoid interpolating over the disabled period when next enabled
  95. this._lastFrameTimeMs = null;
  96. //wipe record
  97. this._rollingFrameTime.reset();
  98. }
  99. }
  100. /**
  101. * RollingAverage
  102. *
  103. * Utility to efficiently compute the rolling average and variance over a sliding window of samples
  104. */
  105. export class RollingAverage {
  106. /**
  107. * Current average
  108. */
  109. public average: number;
  110. /**
  111. * Current variance
  112. */
  113. public variance: number;
  114. protected _samples: Array<number>;
  115. protected _sampleCount: number;
  116. protected _pos: number;
  117. protected _m2: number; //sum of squares of differences from the (current) mean
  118. /**
  119. * constructor
  120. * @param length The number of samples required to saturate the sliding window
  121. */
  122. constructor(length: number) {
  123. this._samples = new Array<number>(length);
  124. this.reset();
  125. }
  126. /**
  127. * Adds a sample to the sample set
  128. * @param v The sample value
  129. */
  130. public add(v: number) {
  131. //http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
  132. let delta: number;
  133. //we need to check if we've already wrapped round
  134. if (this.isSaturated()) {
  135. //remove bottom of stack from mean
  136. let bottomValue = this._samples[this._pos];
  137. delta = bottomValue - this.average;
  138. this.average -= delta / (this._sampleCount - 1);
  139. this._m2 -= delta * (bottomValue - this.average);
  140. } else {
  141. this._sampleCount++;
  142. }
  143. //add new value to mean
  144. delta = v - this.average;
  145. this.average += delta / (this._sampleCount);
  146. this._m2 += delta * (v - this.average);
  147. //set the new variance
  148. this.variance = this._m2 / (this._sampleCount - 1);
  149. this._samples[this._pos] = v;
  150. this._pos++;
  151. this._pos %= this._samples.length; //positive wrap around
  152. }
  153. /**
  154. * Returns previously added values or null if outside of history or outside the sliding window domain
  155. * @param i Index in history. For example, pass 0 for the most recent value and 1 for the value before that
  156. * @return Value previously recorded with add() or null if outside of range
  157. */
  158. public history(i: number): number {
  159. if ((i >= this._sampleCount) || (i >= this._samples.length)) {
  160. return 0;
  161. }
  162. let i0 = this._wrapPosition(this._pos - 1.0);
  163. return this._samples[this._wrapPosition(i0 - i)];
  164. }
  165. /**
  166. * Returns true if enough samples have been taken to completely fill the sliding window
  167. * @return true if sample-set saturated
  168. */
  169. public isSaturated(): boolean {
  170. return this._sampleCount >= this._samples.length;
  171. }
  172. /**
  173. * Resets the rolling average (equivalent to 0 samples taken so far)
  174. */
  175. public reset() {
  176. this.average = 0;
  177. this.variance = 0;
  178. this._sampleCount = 0;
  179. this._pos = 0;
  180. this._m2 = 0;
  181. }
  182. /**
  183. * Wraps a value around the sample range boundaries
  184. * @param i Position in sample range, for example if the sample length is 5, and i is -3, then 2 will be returned.
  185. * @return Wrapped position in sample range
  186. */
  187. protected _wrapPosition(i: number): number {
  188. let max = this._samples.length;
  189. return ((i % max) + max) % max;
  190. }
  191. }