Skip to content

Commit fb4e8de

Browse files
committed
Try to end the input latency measurement in the very first task after the browser does layout and painting
1 parent 98a618a commit fb4e8de

File tree

4 files changed

+93
-68
lines changed

4 files changed

+93
-68
lines changed

src/vs/base/browser/performance.ts

Lines changed: 87 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -31,119 +31,146 @@ export namespace inputLatency {
3131
keydown: EventPhase.Before,
3232
input: EventPhase.Before,
3333
render: EventPhase.Before,
34-
selection: EventPhase.Before
3534
};
3635

3736
/**
38-
* Mark the start of the keydown event.
37+
* Record the start of the keydown event.
3938
*/
40-
export function markKeydownStart() {
39+
export function onKeyDown() {
40+
/** Direct Check C. See explanation in {@link recordIfFinished} */
41+
recordIfFinished();
4142
performance.mark('inputlatency/start');
4243
performance.mark('keydown/start');
4344
state.keydown = EventPhase.InProgress;
44-
queueMicrotask(() => markKeydownEnd());
45+
queueMicrotask(markKeyDownEnd);
4546
}
4647

4748
/**
4849
* Mark the end of the keydown event.
4950
*/
50-
function markKeydownEnd() {
51-
// Only measure the first render after keyboard input
51+
function markKeyDownEnd() {
5252
performance.mark('keydown/end');
5353
state.keydown = EventPhase.Finished;
5454
}
5555

5656
/**
57-
* Mark the start of the input event.
57+
* Record the start of the beforeinput event.
5858
*/
59-
export function markInputStart() {
59+
export function onBeforeInput() {
6060
performance.mark('input/start');
6161
state.input = EventPhase.InProgress;
62+
/** Schedule Task A. See explanation in {@link recordIfFinished} */
63+
scheduleRecordIfFinishedTask();
6264
}
6365

6466
/**
65-
* Mark the end of the input event.
67+
* Record the start of the input event.
6668
*/
67-
export function markInputEnd() {
68-
queueMicrotask(() => {
69-
performance.mark('input/end');
70-
state.input = EventPhase.Finished;
71-
});
69+
export function onInput() {
70+
queueMicrotask(markInputEnd);
71+
}
72+
73+
function markInputEnd() {
74+
performance.mark('input/end');
75+
state.input = EventPhase.Finished;
76+
}
77+
78+
/**
79+
* Record the start of the keyup event.
80+
*/
81+
export function onKeyUp() {
82+
/** Direct Check D. See explanation in {@link recordIfFinished} */
83+
recordIfFinished();
84+
}
85+
86+
/**
87+
* Record the start of the selectionchange event.
88+
*/
89+
export function onSelectionChange() {
90+
/** Direct Check E. See explanation in {@link recordIfFinished} */
91+
recordIfFinished();
7292
}
7393

7494
/**
75-
* Mark the start of the animation frame performing the rendering.
95+
* Record the start of the animation frame performing the rendering.
7696
*/
77-
export function markRenderStart() {
97+
export function onRenderStart() {
7898
// Render may be triggered during input, but we only measure the following animation frame
7999
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.render === EventPhase.Before) {
80100
// Only measure the first render after keyboard input
81101
performance.mark('render/start');
82102
state.render = EventPhase.InProgress;
83-
queueMicrotask(() => markRenderEnd());
103+
queueMicrotask(markRenderEnd);
104+
/** Schedule Task B. See explanation in {@link recordIfFinished} */
105+
scheduleRecordIfFinishedTask();
84106
}
85107
}
86108

87109
/**
88110
* Mark the end of the animation frame performing the rendering.
89-
*
90-
* An input latency sample is complete when both the textarea selection change event and the
91-
* animation frame performing the rendering has triggered.
92111
*/
93112
function markRenderEnd() {
94113
performance.mark('render/end');
95114
state.render = EventPhase.Finished;
96-
record();
97115
}
98116

99-
/**
100-
* Mark when the editor textarea selection change event occurs.
101-
*
102-
* An input latency sample is complete when both the textarea selection change event and the
103-
* animation frame performing the rendering has triggered.
104-
*/
105-
export function markTextareaSelection() {
106-
state.selection = EventPhase.Finished;
107-
record();
117+
function scheduleRecordIfFinishedTask() {
118+
// Here we can safely assume that the `setTimeout` will not be
119+
// artificially delayed by 4ms because we schedule it from
120+
// event handlers
121+
setTimeout(recordIfFinished);
108122
}
109123

110124
/**
111-
* Record the input latency sample if it's ready.
125+
* Record the input latency sample if input handling and rendering are finished.
126+
*
127+
* The challenge here is that we want to record the latency in such a way that it includes
128+
* also the layout and painting work the browser does during the animation frame task.
129+
*
130+
* Simply scheduling a new task (via `setTimeout`) from the animation frame task would
131+
* schedule the new task at the end of the task queue (after other code that uses `setTimeout`),
132+
* so we need to use multiple strategies to make sure our task runs before others:
133+
*
134+
* We schedule tasks (A and B):
135+
* - we schedule a task A (via a `setTimeout` call) when the input starts in `markInputStart`.
136+
* If the animation frame task is scheduled quickly by the browser, then task A has a very good
137+
* chance of being the very first task after the animation frame and thus will record the input latency.
138+
* - however, if the animation frame task is scheduled a bit later, then task A might execute
139+
* before the animation frame task. We therefore schedule another task B from `markRenderStart`.
140+
*
141+
* We do direct checks in browser event handlers (C, D, E):
142+
* - if the browser has multiple keydown events queued up, they will be scheduled before the `setTimeout` tasks,
143+
* so we do a direct check in the keydown event handler (C).
144+
* - depending on timing, sometimes the animation frame is scheduled even before the `keyup` event, so we
145+
* do a direct check there too (E).
146+
* - the browser oftentimes emits a `selectionchange` event after an `input`, so we do a direct check there (D).
112147
*/
113-
function record() {
114-
// Selection and render must have finished to record
115-
if (state.selection !== EventPhase.Finished || state.render !== EventPhase.Finished) {
116-
return;
117-
}
118-
// Finish the recording, use a timer to ensure that layout/paint is captured. setImmediate
119-
// is used if available (Electron) to get slightly more accurate results
120-
('setImmediate' in window ? (window as any).setImmediate : setTimeout)(() => {
121-
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.selection === EventPhase.Finished && state.render === EventPhase.Finished) {
122-
performance.mark('inputlatency/end');
148+
function recordIfFinished() {
149+
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.render === EventPhase.Finished) {
150+
performance.mark('inputlatency/end');
123151

124-
performance.measure('keydown', 'keydown/start', 'keydown/end');
125-
performance.measure('input', 'input/start', 'input/end');
126-
performance.measure('render', 'render/start', 'render/end');
127-
performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
152+
performance.measure('keydown', 'keydown/start', 'keydown/end');
153+
performance.measure('input', 'input/start', 'input/end');
154+
performance.measure('render', 'render/start', 'render/end');
155+
performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
128156

129-
addMeasure('keydown', totalKeydownTime);
130-
addMeasure('input', totalInputTime);
131-
addMeasure('render', totalRenderTime);
132-
addMeasure('inputlatency', totalInputLatencyTime);
157+
addMeasure('keydown', totalKeydownTime);
158+
addMeasure('input', totalInputTime);
159+
addMeasure('render', totalRenderTime);
160+
addMeasure('inputlatency', totalInputLatencyTime);
133161

134-
// console.info(
135-
// `input latency=${measurementsInputLatency[measurementsCount].toFixed(1)} [` +
136-
// `keydown=${measurementsKeydown[measurementsCount].toFixed(1)}, ` +
137-
// `input=${measurementsInput[measurementsCount].toFixed(1)}, ` +
138-
// `render=${measurementsRender[measurementsCount].toFixed(1)}` +
139-
// `]`
140-
// );
162+
// console.info(
163+
// `input latency=${performance.getEntriesByName('inputlatency')[0].duration.toFixed(1)} [` +
164+
// `keydown=${performance.getEntriesByName('keydown')[0].duration.toFixed(1)}, ` +
165+
// `input=${performance.getEntriesByName('input')[0].duration.toFixed(1)}, ` +
166+
// `render=${performance.getEntriesByName('render')[0].duration.toFixed(1)}` +
167+
// `]`
168+
// );
141169

142-
measurementsCount++;
170+
measurementsCount++;
143171

144-
reset();
145-
}
146-
}, 0);
172+
reset();
173+
}
147174
}
148175

149176
function addMeasure(entryName: string, cumulativeMeasurement: ICumulativeMeasurement): void {
@@ -174,7 +201,6 @@ export namespace inputLatency {
174201
state.keydown = EventPhase.Before;
175202
state.input = EventPhase.Before;
176203
state.render = EventPhase.Before;
177-
state.selection = EventPhase.Before;
178204
}
179205

180206
export interface IInputLatencyMeasurements {

src/vs/editor/browser/controller/textAreaHandler.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import { TokenizationRegistry } from 'vs/editor/common/languages';
3535
import { ColorId, ITokenPresentation } from 'vs/editor/common/encodedTokenAttributes';
3636
import { Color } from 'vs/base/common/color';
3737
import { TimeoutTimer } from 'vs/base/common/async';
38-
import { inputLatency } from 'vs/base/browser/performance';
3938

4039
export interface IVisibleRangeProvider {
4140
visibleRangeForPosition(position: Position): HorizontalPosition | null;
@@ -305,7 +304,6 @@ export class TextAreaHandler extends ViewPart {
305304
this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, textAreaWrapper, platform.OS, browser));
306305

307306
this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => {
308-
inputLatency.markKeydownStart();
309307
this._viewController.emitKeyDown(e);
310308
}));
311309

src/vs/editor/browser/controller/textAreaInput.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,8 +489,7 @@ export class TextAreaInput extends Disposable {
489489
// so throttle multiple `selectionchange` events that burst in a short period of time.
490490
let previousSelectionChangeEventTime = 0;
491491
return dom.addDisposableListener(document, 'selectionchange', (e) => {
492-
493-
inputLatency.markTextareaSelection();
492+
inputLatency.onSelectionChange();
494493

495494
if (!this._hasFocus) {
496495
return;
@@ -707,8 +706,10 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap
707706
super();
708707
this._ignoreSelectionChangeTime = 0;
709708

710-
this._register(this.onBeforeInput(() => inputLatency.markInputStart()));
711-
this._register(this.onInput(() => inputLatency.markInputEnd()));
709+
this._register(this.onKeyDown(() => inputLatency.onKeyDown()));
710+
this._register(this.onBeforeInput(() => inputLatency.onBeforeInput()));
711+
this._register(this.onInput(() => inputLatency.onInput()));
712+
this._register(this.onKeyUp(() => inputLatency.onKeyUp()));
712713

713714
this._register(dom.addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire()));
714715
}

src/vs/editor/browser/view.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export class View extends ViewEventHandler {
223223
}
224224

225225
private _flushAccumulatedAndRenderNow(): void {
226-
inputLatency.markRenderStart();
226+
inputLatency.onRenderStart();
227227
this._renderNow();
228228
}
229229

0 commit comments

Comments
 (0)