/*--------------------------------------------------------------------------------------------- * 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 = { ...totalKeydownTime }; const totalRenderTime = { ...totalKeydownTime }; const totalInputLatencyTime = { ...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 = {}));