diff --git a/src/vs/base/browser/performance.ts b/src/vs/base/browser/performance.ts index b32ec2cef08..38a9cf3c8db 100644 --- a/src/vs/base/browser/performance.ts +++ b/src/vs/base/browser/performance.ts @@ -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 { diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 2d5a9853030..9f85d16df68 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -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); })); diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 70fccba9d5a..0a45967147f 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -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())); } diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 1e0f30b486f..85b95fbfe12 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -223,7 +223,7 @@ export class View extends ViewEventHandler { } private _flushAccumulatedAndRenderNow(): void { - inputLatency.markRenderStart(); + inputLatency.onRenderStart(); this._renderNow(); }