mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-24 00:05:46 +01:00
Try to end the input latency measurement in the very first task after the browser does layout and painting
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ export class View extends ViewEventHandler {
|
||||
}
|
||||
|
||||
private _flushAccumulatedAndRenderNow(): void {
|
||||
inputLatency.markRenderStart();
|
||||
inputLatency.onRenderStart();
|
||||
this._renderNow();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user