Try to end the input latency measurement in the very first task after the browser does layout and painting

This commit is contained in:
Alex Dima
2022-11-14 20:12:21 +01:00
parent 98a618afd4
commit fb4e8de4d2
4 changed files with 97 additions and 72 deletions
+91 -65
View File
@@ -31,119 +31,146 @@ export namespace inputLatency {
keydown: EventPhase.Before,
input: EventPhase.Before,
render: EventPhase.Before,
selection: EventPhase.Before
};
/**
* Mark the start of the keydown event.
* Record the start of the keydown event.
*/
export function markKeydownStart() {
export function onKeyDown() {
/** Direct Check C. See explanation in {@link recordIfFinished} */
recordIfFinished();
performance.mark('inputlatency/start');
performance.mark('keydown/start');
state.keydown = EventPhase.InProgress;
queueMicrotask(() => markKeydownEnd());
queueMicrotask(markKeyDownEnd);
}
/**
* Mark the end of the keydown event.
*/
function markKeydownEnd() {
// Only measure the first render after keyboard input
function markKeyDownEnd() {
performance.mark('keydown/end');
state.keydown = EventPhase.Finished;
}
/**
* Mark the start of the input event.
* Record the start of the beforeinput event.
*/
export function markInputStart() {
export function onBeforeInput() {
performance.mark('input/start');
state.input = EventPhase.InProgress;
/** Schedule Task A. See explanation in {@link recordIfFinished} */
scheduleRecordIfFinishedTask();
}
/**
* Mark the end of the input event.
* Record the start of the input event.
*/
export function markInputEnd() {
queueMicrotask(() => {
performance.mark('input/end');
state.input = EventPhase.Finished;
});
export function onInput() {
queueMicrotask(markInputEnd);
}
function markInputEnd() {
performance.mark('input/end');
state.input = EventPhase.Finished;
}
/**
* Mark the start of the animation frame performing the rendering.
* Record the start of the keyup event.
*/
export function markRenderStart() {
export function onKeyUp() {
/** Direct Check D. See explanation in {@link recordIfFinished} */
recordIfFinished();
}
/**
* Record the start of the selectionchange event.
*/
export function onSelectionChange() {
/** Direct Check E. See explanation in {@link recordIfFinished} */
recordIfFinished();
}
/**
* Record the start of the animation frame performing the rendering.
*/
export function onRenderStart() {
// Render may be triggered during input, but we only measure the following animation frame
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.render === EventPhase.Before) {
// Only measure the first render after keyboard input
performance.mark('render/start');
state.render = EventPhase.InProgress;
queueMicrotask(() => markRenderEnd());
queueMicrotask(markRenderEnd);
/** Schedule Task B. See explanation in {@link recordIfFinished} */
scheduleRecordIfFinishedTask();
}
}
/**
* Mark the end of the animation frame performing the rendering.
*
* An input latency sample is complete when both the textarea selection change event and the
* animation frame performing the rendering has triggered.
*/
function markRenderEnd() {
performance.mark('render/end');
state.render = EventPhase.Finished;
record();
}
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);
}
/**
* Mark when the editor textarea selection change event occurs.
* Record the input latency sample if input handling and rendering are finished.
*
* An input latency sample is complete when both the textarea selection change event and the
* animation frame performing the rendering has triggered.
* 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).
*/
export function markTextareaSelection() {
state.selection = EventPhase.Finished;
record();
}
function recordIfFinished() {
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.render === EventPhase.Finished) {
performance.mark('inputlatency/end');
/**
* Record the input latency sample if it's ready.
*/
function record() {
// Selection and render must have finished to record
if (state.selection !== EventPhase.Finished || state.render !== EventPhase.Finished) {
return;
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();
}
// Finish the recording, use a timer to ensure that layout/paint is captured. setImmediate
// is used if available (Electron) to get slightly more accurate results
('setImmediate' in window ? (window as any).setImmediate : setTimeout)(() => {
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.selection === EventPhase.Finished && state.render === 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=${measurementsInputLatency[measurementsCount].toFixed(1)} [` +
// `keydown=${measurementsKeydown[measurementsCount].toFixed(1)}, ` +
// `input=${measurementsInput[measurementsCount].toFixed(1)}, ` +
// `render=${measurementsRender[measurementsCount].toFixed(1)}` +
// `]`
// );
measurementsCount++;
reset();
}
}, 0);
}
function addMeasure(entryName: string, cumulativeMeasurement: ICumulativeMeasurement): void {
@@ -174,7 +201,6 @@ export namespace inputLatency {
state.keydown = EventPhase.Before;
state.input = EventPhase.Before;
state.render = EventPhase.Before;
state.selection = EventPhase.Before;
}
export interface IInputLatencyMeasurements {
@@ -35,7 +35,6 @@ import { TokenizationRegistry } from 'vs/editor/common/languages';
import { ColorId, ITokenPresentation } from 'vs/editor/common/encodedTokenAttributes';
import { Color } from 'vs/base/common/color';
import { TimeoutTimer } from 'vs/base/common/async';
import { inputLatency } from 'vs/base/browser/performance';
export interface IVisibleRangeProvider {
visibleRangeForPosition(position: Position): HorizontalPosition | null;
@@ -305,7 +304,6 @@ export class TextAreaHandler extends ViewPart {
this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, textAreaWrapper, platform.OS, browser));
this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => {
inputLatency.markKeydownStart();
this._viewController.emitKeyDown(e);
}));
@@ -489,8 +489,7 @@ export class TextAreaInput extends Disposable {
// so throttle multiple `selectionchange` events that burst in a short period of time.
let previousSelectionChangeEventTime = 0;
return dom.addDisposableListener(document, 'selectionchange', (e) => {
inputLatency.markTextareaSelection();
inputLatency.onSelectionChange();
if (!this._hasFocus) {
return;
@@ -707,8 +706,10 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap
super();
this._ignoreSelectionChangeTime = 0;
this._register(this.onBeforeInput(() => inputLatency.markInputStart()));
this._register(this.onInput(() => inputLatency.markInputEnd()));
this._register(this.onKeyDown(() => inputLatency.onKeyDown()));
this._register(this.onBeforeInput(() => inputLatency.onBeforeInput()));
this._register(this.onInput(() => inputLatency.onInput()));
this._register(this.onKeyUp(() => inputLatency.onKeyUp()));
this._register(dom.addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire()));
}
+1 -1
View File
@@ -223,7 +223,7 @@ export class View extends ViewEventHandler {
}
private _flushAccumulatedAndRenderNow(): void {
inputLatency.markRenderStart();
inputLatency.onRenderStart();
this._renderNow();
}