mirror of
https://github.com/neocities/neocities.git
synced 2025-04-30 03:58:00 +02:00
220 lines
9.6 KiB
JavaScript
220 lines
9.6 KiB
JavaScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 = {}));
|