| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- export var inputLatency;
- (function (inputLatency) {
- const totalKeydownTime = { total: 0, min: Number.MAX_VALUE, max: 0 };
- const totalInputTime = Object.assign({}, totalKeydownTime);
- const totalRenderTime = Object.assign({}, totalKeydownTime);
- const totalInputLatencyTime = Object.assign({}, totalKeydownTime);
- let measurementsCount = 0;
- const state = {
- keydown: 0 /* EventPhase.Before */,
- input: 0 /* EventPhase.Before */,
- render: 0 /* EventPhase.Before */,
- };
- /**
- * Record the start of the keydown event.
- */
- function onKeyDown() {
- /** Direct Check C. See explanation in {@link recordIfFinished} */
- recordIfFinished();
- performance.mark('inputlatency/start');
- performance.mark('keydown/start');
- state.keydown = 1 /* EventPhase.InProgress */;
- queueMicrotask(markKeyDownEnd);
- }
- inputLatency.onKeyDown = onKeyDown;
- /**
- * Mark the end of the keydown event.
- */
- function markKeyDownEnd() {
- if (state.keydown === 1 /* EventPhase.InProgress */) {
- performance.mark('keydown/end');
- state.keydown = 2 /* EventPhase.Finished */;
- }
- }
- /**
- * Record the start of the beforeinput event.
- */
- function onBeforeInput() {
- performance.mark('input/start');
- state.input = 1 /* EventPhase.InProgress */;
- /** Schedule Task A. See explanation in {@link recordIfFinished} */
- scheduleRecordIfFinishedTask();
- }
- inputLatency.onBeforeInput = onBeforeInput;
- /**
- * Record the start of the input event.
- */
- function onInput() {
- if (state.input === 0 /* EventPhase.Before */) {
- // it looks like we didn't receive a `beforeinput`
- onBeforeInput();
- }
- queueMicrotask(markInputEnd);
- }
- inputLatency.onInput = onInput;
- function markInputEnd() {
- if (state.input === 1 /* EventPhase.InProgress */) {
- performance.mark('input/end');
- state.input = 2 /* EventPhase.Finished */;
- }
- }
- /**
- * Record the start of the keyup event.
- */
- function onKeyUp() {
- /** Direct Check D. See explanation in {@link recordIfFinished} */
- recordIfFinished();
- }
- inputLatency.onKeyUp = onKeyUp;
- /**
- * Record the start of the selectionchange event.
- */
- function onSelectionChange() {
- /** Direct Check E. See explanation in {@link recordIfFinished} */
- recordIfFinished();
- }
- inputLatency.onSelectionChange = onSelectionChange;
- /**
- * Record the start of the animation frame performing the rendering.
- */
- function onRenderStart() {
- // Render may be triggered during input, but we only measure the following animation frame
- if (state.keydown === 2 /* EventPhase.Finished */ && state.input === 2 /* EventPhase.Finished */ && state.render === 0 /* EventPhase.Before */) {
- // Only measure the first render after keyboard input
- performance.mark('render/start');
- state.render = 1 /* EventPhase.InProgress */;
- queueMicrotask(markRenderEnd);
- /** Schedule Task B. See explanation in {@link recordIfFinished} */
- scheduleRecordIfFinishedTask();
- }
- }
- inputLatency.onRenderStart = onRenderStart;
- /**
- * Mark the end of the animation frame performing the rendering.
- */
- function markRenderEnd() {
- if (state.render === 1 /* EventPhase.InProgress */) {
- performance.mark('render/end');
- state.render = 2 /* EventPhase.Finished */;
- }
- }
- function scheduleRecordIfFinishedTask() {
- // Here we can safely assume that the `setTimeout` will not be
- // artificially delayed by 4ms because we schedule it from
- // event handlers
- setTimeout(recordIfFinished);
- }
- /**
- * Record the input latency sample if input handling and rendering are finished.
- *
- * The challenge here is that we want to record the latency in such a way that it includes
- * also the layout and painting work the browser does during the animation frame task.
- *
- * Simply scheduling a new task (via `setTimeout`) from the animation frame task would
- * schedule the new task at the end of the task queue (after other code that uses `setTimeout`),
- * so we need to use multiple strategies to make sure our task runs before others:
- *
- * We schedule tasks (A and B):
- * - we schedule a task A (via a `setTimeout` call) when the input starts in `markInputStart`.
- * If the animation frame task is scheduled quickly by the browser, then task A has a very good
- * chance of being the very first task after the animation frame and thus will record the input latency.
- * - however, if the animation frame task is scheduled a bit later, then task A might execute
- * before the animation frame task. We therefore schedule another task B from `markRenderStart`.
- *
- * We do direct checks in browser event handlers (C, D, E):
- * - if the browser has multiple keydown events queued up, they will be scheduled before the `setTimeout` tasks,
- * so we do a direct check in the keydown event handler (C).
- * - depending on timing, sometimes the animation frame is scheduled even before the `keyup` event, so we
- * do a direct check there too (E).
- * - the browser oftentimes emits a `selectionchange` event after an `input`, so we do a direct check there (D).
- */
- function recordIfFinished() {
- if (state.keydown === 2 /* EventPhase.Finished */ && state.input === 2 /* EventPhase.Finished */ && state.render === 2 /* EventPhase.Finished */) {
- performance.mark('inputlatency/end');
- performance.measure('keydown', 'keydown/start', 'keydown/end');
- performance.measure('input', 'input/start', 'input/end');
- performance.measure('render', 'render/start', 'render/end');
- performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
- addMeasure('keydown', totalKeydownTime);
- addMeasure('input', totalInputTime);
- addMeasure('render', totalRenderTime);
- addMeasure('inputlatency', totalInputLatencyTime);
- // console.info(
- // `input latency=${performance.getEntriesByName('inputlatency')[0].duration.toFixed(1)} [` +
- // `keydown=${performance.getEntriesByName('keydown')[0].duration.toFixed(1)}, ` +
- // `input=${performance.getEntriesByName('input')[0].duration.toFixed(1)}, ` +
- // `render=${performance.getEntriesByName('render')[0].duration.toFixed(1)}` +
- // `]`
- // );
- measurementsCount++;
- reset();
- }
- }
- function addMeasure(entryName, cumulativeMeasurement) {
- const duration = performance.getEntriesByName(entryName)[0].duration;
- cumulativeMeasurement.total += duration;
- cumulativeMeasurement.min = Math.min(cumulativeMeasurement.min, duration);
- cumulativeMeasurement.max = Math.max(cumulativeMeasurement.max, duration);
- }
- /**
- * Clear the current sample.
- */
- function reset() {
- performance.clearMarks('keydown/start');
- performance.clearMarks('keydown/end');
- performance.clearMarks('input/start');
- performance.clearMarks('input/end');
- performance.clearMarks('render/start');
- performance.clearMarks('render/end');
- performance.clearMarks('inputlatency/start');
- performance.clearMarks('inputlatency/end');
- performance.clearMeasures('keydown');
- performance.clearMeasures('input');
- performance.clearMeasures('render');
- performance.clearMeasures('inputlatency');
- state.keydown = 0 /* EventPhase.Before */;
- state.input = 0 /* EventPhase.Before */;
- state.render = 0 /* EventPhase.Before */;
- }
- /**
- * Gets all input latency samples and clears the internal buffers to start recording a new set
- * of samples.
- */
- function getAndClearMeasurements() {
- if (measurementsCount === 0) {
- return undefined;
- }
- // Assemble the result
- const result = {
- keydown: cumulativeToFinalMeasurement(totalKeydownTime),
- input: cumulativeToFinalMeasurement(totalInputTime),
- render: cumulativeToFinalMeasurement(totalRenderTime),
- total: cumulativeToFinalMeasurement(totalInputLatencyTime),
- sampleCount: measurementsCount
- };
- // Clear the cumulative measurements
- clearCumulativeMeasurement(totalKeydownTime);
- clearCumulativeMeasurement(totalInputTime);
- clearCumulativeMeasurement(totalRenderTime);
- clearCumulativeMeasurement(totalInputLatencyTime);
- measurementsCount = 0;
- return result;
- }
- inputLatency.getAndClearMeasurements = getAndClearMeasurements;
- function cumulativeToFinalMeasurement(cumulative) {
- return {
- average: cumulative.total / measurementsCount,
- max: cumulative.max,
- min: cumulative.min,
- };
- }
- function clearCumulativeMeasurement(cumulative) {
- cumulative.total = 0;
- cumulative.min = Number.MAX_VALUE;
- cumulative.max = 0;
- }
- })(inputLatency || (inputLatency = {}));
|