performance.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. export var inputLatency;
  6. (function (inputLatency) {
  7. const totalKeydownTime = { total: 0, min: Number.MAX_VALUE, max: 0 };
  8. const totalInputTime = Object.assign({}, totalKeydownTime);
  9. const totalRenderTime = Object.assign({}, totalKeydownTime);
  10. const totalInputLatencyTime = Object.assign({}, totalKeydownTime);
  11. let measurementsCount = 0;
  12. const state = {
  13. keydown: 0 /* EventPhase.Before */,
  14. input: 0 /* EventPhase.Before */,
  15. render: 0 /* EventPhase.Before */,
  16. };
  17. /**
  18. * Record the start of the keydown event.
  19. */
  20. function onKeyDown() {
  21. /** Direct Check C. See explanation in {@link recordIfFinished} */
  22. recordIfFinished();
  23. performance.mark('inputlatency/start');
  24. performance.mark('keydown/start');
  25. state.keydown = 1 /* EventPhase.InProgress */;
  26. queueMicrotask(markKeyDownEnd);
  27. }
  28. inputLatency.onKeyDown = onKeyDown;
  29. /**
  30. * Mark the end of the keydown event.
  31. */
  32. function markKeyDownEnd() {
  33. if (state.keydown === 1 /* EventPhase.InProgress */) {
  34. performance.mark('keydown/end');
  35. state.keydown = 2 /* EventPhase.Finished */;
  36. }
  37. }
  38. /**
  39. * Record the start of the beforeinput event.
  40. */
  41. function onBeforeInput() {
  42. performance.mark('input/start');
  43. state.input = 1 /* EventPhase.InProgress */;
  44. /** Schedule Task A. See explanation in {@link recordIfFinished} */
  45. scheduleRecordIfFinishedTask();
  46. }
  47. inputLatency.onBeforeInput = onBeforeInput;
  48. /**
  49. * Record the start of the input event.
  50. */
  51. function onInput() {
  52. if (state.input === 0 /* EventPhase.Before */) {
  53. // it looks like we didn't receive a `beforeinput`
  54. onBeforeInput();
  55. }
  56. queueMicrotask(markInputEnd);
  57. }
  58. inputLatency.onInput = onInput;
  59. function markInputEnd() {
  60. if (state.input === 1 /* EventPhase.InProgress */) {
  61. performance.mark('input/end');
  62. state.input = 2 /* EventPhase.Finished */;
  63. }
  64. }
  65. /**
  66. * Record the start of the keyup event.
  67. */
  68. function onKeyUp() {
  69. /** Direct Check D. See explanation in {@link recordIfFinished} */
  70. recordIfFinished();
  71. }
  72. inputLatency.onKeyUp = onKeyUp;
  73. /**
  74. * Record the start of the selectionchange event.
  75. */
  76. function onSelectionChange() {
  77. /** Direct Check E. See explanation in {@link recordIfFinished} */
  78. recordIfFinished();
  79. }
  80. inputLatency.onSelectionChange = onSelectionChange;
  81. /**
  82. * Record the start of the animation frame performing the rendering.
  83. */
  84. function onRenderStart() {
  85. // Render may be triggered during input, but we only measure the following animation frame
  86. if (state.keydown === 2 /* EventPhase.Finished */ && state.input === 2 /* EventPhase.Finished */ && state.render === 0 /* EventPhase.Before */) {
  87. // Only measure the first render after keyboard input
  88. performance.mark('render/start');
  89. state.render = 1 /* EventPhase.InProgress */;
  90. queueMicrotask(markRenderEnd);
  91. /** Schedule Task B. See explanation in {@link recordIfFinished} */
  92. scheduleRecordIfFinishedTask();
  93. }
  94. }
  95. inputLatency.onRenderStart = onRenderStart;
  96. /**
  97. * Mark the end of the animation frame performing the rendering.
  98. */
  99. function markRenderEnd() {
  100. if (state.render === 1 /* EventPhase.InProgress */) {
  101. performance.mark('render/end');
  102. state.render = 2 /* EventPhase.Finished */;
  103. }
  104. }
  105. function scheduleRecordIfFinishedTask() {
  106. // Here we can safely assume that the `setTimeout` will not be
  107. // artificially delayed by 4ms because we schedule it from
  108. // event handlers
  109. setTimeout(recordIfFinished);
  110. }
  111. /**
  112. * Record the input latency sample if input handling and rendering are finished.
  113. *
  114. * The challenge here is that we want to record the latency in such a way that it includes
  115. * also the layout and painting work the browser does during the animation frame task.
  116. *
  117. * Simply scheduling a new task (via `setTimeout`) from the animation frame task would
  118. * schedule the new task at the end of the task queue (after other code that uses `setTimeout`),
  119. * so we need to use multiple strategies to make sure our task runs before others:
  120. *
  121. * We schedule tasks (A and B):
  122. * - we schedule a task A (via a `setTimeout` call) when the input starts in `markInputStart`.
  123. * If the animation frame task is scheduled quickly by the browser, then task A has a very good
  124. * chance of being the very first task after the animation frame and thus will record the input latency.
  125. * - however, if the animation frame task is scheduled a bit later, then task A might execute
  126. * before the animation frame task. We therefore schedule another task B from `markRenderStart`.
  127. *
  128. * We do direct checks in browser event handlers (C, D, E):
  129. * - if the browser has multiple keydown events queued up, they will be scheduled before the `setTimeout` tasks,
  130. * so we do a direct check in the keydown event handler (C).
  131. * - depending on timing, sometimes the animation frame is scheduled even before the `keyup` event, so we
  132. * do a direct check there too (E).
  133. * - the browser oftentimes emits a `selectionchange` event after an `input`, so we do a direct check there (D).
  134. */
  135. function recordIfFinished() {
  136. if (state.keydown === 2 /* EventPhase.Finished */ && state.input === 2 /* EventPhase.Finished */ && state.render === 2 /* EventPhase.Finished */) {
  137. performance.mark('inputlatency/end');
  138. performance.measure('keydown', 'keydown/start', 'keydown/end');
  139. performance.measure('input', 'input/start', 'input/end');
  140. performance.measure('render', 'render/start', 'render/end');
  141. performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
  142. addMeasure('keydown', totalKeydownTime);
  143. addMeasure('input', totalInputTime);
  144. addMeasure('render', totalRenderTime);
  145. addMeasure('inputlatency', totalInputLatencyTime);
  146. // console.info(
  147. // `input latency=${performance.getEntriesByName('inputlatency')[0].duration.toFixed(1)} [` +
  148. // `keydown=${performance.getEntriesByName('keydown')[0].duration.toFixed(1)}, ` +
  149. // `input=${performance.getEntriesByName('input')[0].duration.toFixed(1)}, ` +
  150. // `render=${performance.getEntriesByName('render')[0].duration.toFixed(1)}` +
  151. // `]`
  152. // );
  153. measurementsCount++;
  154. reset();
  155. }
  156. }
  157. function addMeasure(entryName, cumulativeMeasurement) {
  158. const duration = performance.getEntriesByName(entryName)[0].duration;
  159. cumulativeMeasurement.total += duration;
  160. cumulativeMeasurement.min = Math.min(cumulativeMeasurement.min, duration);
  161. cumulativeMeasurement.max = Math.max(cumulativeMeasurement.max, duration);
  162. }
  163. /**
  164. * Clear the current sample.
  165. */
  166. function reset() {
  167. performance.clearMarks('keydown/start');
  168. performance.clearMarks('keydown/end');
  169. performance.clearMarks('input/start');
  170. performance.clearMarks('input/end');
  171. performance.clearMarks('render/start');
  172. performance.clearMarks('render/end');
  173. performance.clearMarks('inputlatency/start');
  174. performance.clearMarks('inputlatency/end');
  175. performance.clearMeasures('keydown');
  176. performance.clearMeasures('input');
  177. performance.clearMeasures('render');
  178. performance.clearMeasures('inputlatency');
  179. state.keydown = 0 /* EventPhase.Before */;
  180. state.input = 0 /* EventPhase.Before */;
  181. state.render = 0 /* EventPhase.Before */;
  182. }
  183. /**
  184. * Gets all input latency samples and clears the internal buffers to start recording a new set
  185. * of samples.
  186. */
  187. function getAndClearMeasurements() {
  188. if (measurementsCount === 0) {
  189. return undefined;
  190. }
  191. // Assemble the result
  192. const result = {
  193. keydown: cumulativeToFinalMeasurement(totalKeydownTime),
  194. input: cumulativeToFinalMeasurement(totalInputTime),
  195. render: cumulativeToFinalMeasurement(totalRenderTime),
  196. total: cumulativeToFinalMeasurement(totalInputLatencyTime),
  197. sampleCount: measurementsCount
  198. };
  199. // Clear the cumulative measurements
  200. clearCumulativeMeasurement(totalKeydownTime);
  201. clearCumulativeMeasurement(totalInputTime);
  202. clearCumulativeMeasurement(totalRenderTime);
  203. clearCumulativeMeasurement(totalInputLatencyTime);
  204. measurementsCount = 0;
  205. return result;
  206. }
  207. inputLatency.getAndClearMeasurements = getAndClearMeasurements;
  208. function cumulativeToFinalMeasurement(cumulative) {
  209. return {
  210. average: cumulative.total / measurementsCount,
  211. max: cumulative.max,
  212. min: cumulative.min,
  213. };
  214. }
  215. function clearCumulativeMeasurement(cumulative) {
  216. cumulative.total = 0;
  217. cumulative.min = Number.MAX_VALUE;
  218. cumulative.max = 0;
  219. }
  220. })(inputLatency || (inputLatency = {}));