Skip to content

Commit 3120b00

Browse files
committed
Improve measurement, ensure render doesn't overlap input
1 parent f8506ce commit 3120b00

File tree

5 files changed

+134
-45
lines changed

5 files changed

+134
-45
lines changed

src/vs/base/browser/performance.ts

Lines changed: 125 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,136 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
export function reportInputLatency() {
7-
// Measures from cursor edit to the end of the task
8-
window.queueMicrotask(() => {
9-
if ((window as any).frameStart && (window as any).frameSelection && (window as any).frameRender) {
10-
performance.mark('inputlatency/end');
11-
performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
12-
const measure = performance.getEntriesByName('inputlatency')[0];
13-
const startMark = performance.getEntriesByName('inputlatency/start')[0];
14-
15-
performance.measure('render', 'render/start', 'render/end');
16-
const measure2 = performance.getEntriesByName('render')[0];
17-
18-
console.info(`frame stats for ${(startMark as any).detail.padEnd(5, ' ')}, text render time: ${(measure2.duration).toFixed(1).padStart(4, ' ')}ms, latency: ${(measure.duration).toFixed(1).padStart(4, ' ')}ms`);
19-
20-
performance.clearMarks('inputlatency/start');
21-
performance.clearMarks('inputlatency/end');
22-
performance.clearMeasures('inputlatency');
23-
performance.clearMarks('render/start');
24-
performance.clearMarks('render/end');
25-
performance.clearMeasures('render');
26-
27-
(window as any).frameStart = false;
28-
(window as any).frameSelection = false;
29-
(window as any).frameRender = false;
30-
}
31-
});
32-
}
6+
export namespace inputLatency {
337

34-
export function recordRenderStart() {
35-
if ((window as any).frameStart && !(window as any).frameRender) {
8+
const enum Constants {
9+
bufferLength = 256
10+
}
11+
12+
const enum EventPhase {
13+
Before = 0,
14+
InProgress = 1,
15+
Finished = 2
16+
}
17+
18+
const state = {
19+
keydown: EventPhase.Before,
20+
input: EventPhase.Before,
21+
render: EventPhase.Before,
22+
selection: EventPhase.Before
23+
};
24+
25+
const measurementsKeydown = new Float32Array(Constants.bufferLength);
26+
const measurementsInput = new Float32Array(Constants.bufferLength);
27+
const measurementsRender = new Float32Array(Constants.bufferLength);
28+
const measurementsInputLatency = new Float32Array(Constants.bufferLength);
29+
let measurementsIndex = 0;
30+
31+
export function markKeydownStart() {
32+
performance.mark('inputlatency/start');
33+
performance.mark('keydown/start');
34+
state.keydown = EventPhase.InProgress;
35+
queueMicrotask(() => markKeydownEnd());
36+
}
37+
38+
function markKeydownEnd() {
3639
// Only measure the first render after keyboard input
37-
performance.mark('render/start');
40+
performance.mark('keydown/end');
41+
state.keydown = EventPhase.Finished;
42+
}
43+
44+
export function markInputStart() {
45+
performance.mark('input/start');
46+
state.input = EventPhase.InProgress;
47+
}
48+
49+
export function markInputEnd() {
50+
queueMicrotask(() => {
51+
performance.mark('input/end');
52+
state.input = EventPhase.Finished;
53+
});
54+
}
55+
56+
export function markRenderStart() {
57+
// Render may be triggered during input, but we only measure the following animation frame
58+
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.render === EventPhase.Before) {
59+
// Only measure the first render after keyboard input
60+
performance.mark('render/start');
61+
state.render = EventPhase.InProgress;
62+
queueMicrotask(() => markRenderEnd());
63+
}
3864
}
39-
}
4065

41-
export function recordRenderEnd() {
42-
if ((window as any).frameStart && !(window as any).frameRender) {
66+
function markRenderEnd() {
4367
// Only measure the first render after keyboard input
4468
performance.mark('render/end');
45-
(window as any).frameRender = true;
69+
state.render = EventPhase.Finished;
70+
record();
71+
}
72+
73+
export function markTextareaSelection() {
74+
state.selection = EventPhase.Finished;
75+
record();
4676
}
47-
reportInputLatency();
77+
78+
function record() {
79+
// Skip recording this frame if the buffer is full
80+
if (measurementsIndex >= Constants.bufferLength) {
81+
return;
82+
}
83+
// Selection and render must have finished to record
84+
if (state.selection !== EventPhase.Finished || state.render !== EventPhase.Finished) {
85+
return;
86+
}
87+
// Measures from cursor edit to the end of the task
88+
window.queueMicrotask(() => {
89+
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.selection === EventPhase.Finished && state.render === EventPhase.Finished) {
90+
performance.mark('inputlatency/end');
91+
92+
performance.measure('keydown', 'keydown/start', 'keydown/end');
93+
performance.measure('input', 'input/start', 'input/end');
94+
performance.measure('render', 'render/start', 'render/end');
95+
performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
96+
97+
measurementsKeydown[measurementsIndex] = performance.getEntriesByName('keydown')[0].duration;
98+
measurementsInput[measurementsIndex] = performance.getEntriesByName('input')[0].duration;
99+
measurementsRender[measurementsIndex] = performance.getEntriesByName('render')[0].duration;
100+
measurementsInputLatency[measurementsIndex] = performance.getEntriesByName('inputlatency')[0].duration;
101+
102+
console.info(
103+
`input latency=${measurementsInputLatency[measurementsIndex].toFixed(1)} [` +
104+
`keydown=${measurementsKeydown[measurementsIndex].toFixed(1)}, ` +
105+
`input=${measurementsInput[measurementsIndex].toFixed(1)}, ` +
106+
`render=${measurementsRender[measurementsIndex].toFixed(1)}` +
107+
`]`
108+
);
109+
110+
measurementsIndex++;
111+
112+
reset();
113+
}
114+
});
115+
}
116+
117+
function reset() {
118+
performance.clearMarks('keydown/start');
119+
performance.clearMarks('keydown/end');
120+
performance.clearMarks('input/start');
121+
performance.clearMarks('input/end');
122+
performance.clearMarks('render/start');
123+
performance.clearMarks('render/end');
124+
performance.clearMarks('inputlatency/start');
125+
performance.clearMarks('inputlatency/end');
126+
127+
performance.clearMeasures('keydown');
128+
performance.clearMeasures('input');
129+
performance.clearMeasures('render');
130+
performance.clearMeasures('inputlatency');
131+
132+
state.keydown = EventPhase.Before;
133+
state.input = EventPhase.Before;
134+
state.render = EventPhase.Before;
135+
state.selection = EventPhase.Before;
136+
}
137+
48138
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as browser from 'vs/base/browser/browser';
77
import * as dom from 'vs/base/browser/dom';
88
import { DomEmitter } from 'vs/base/browser/event';
99
import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
10-
import { reportInputLatency } from 'vs/base/browser/performance';
10+
import { inputLatency } from 'vs/base/browser/performance';
1111
import { RunOnceScheduler } from 'vs/base/common/async';
1212
import { Emitter, Event } from 'vs/base/common/event';
1313
import { KeyCode } from 'vs/base/common/keyCodes';
@@ -485,9 +485,7 @@ export class TextAreaInput extends Disposable {
485485
let previousSelectionChangeEventTime = 0;
486486
return dom.addDisposableListener(document, 'selectionchange', (e) => {
487487

488-
// Measures from cursor edit to the end of the task
489-
(window as any).frameSelection = true;
490-
reportInputLatency();
488+
inputLatency.markTextareaSelection();
491489

492490
if (!this._hasFocus) {
493491
return;
@@ -704,6 +702,9 @@ export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrap
704702
super();
705703
this._ignoreSelectionChangeTime = 0;
706704

705+
this.onBeforeInput(() => inputLatency.markInputStart());
706+
this.onInput(() => inputLatency.markInputEnd());
707+
707708
this._register(dom.addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire()));
708709
}
709710

src/vs/editor/browser/view.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { getThemeTypeSelector, IColorTheme } from 'vs/platform/theme/common/them
4949
import { EditorOption } from 'vs/editor/common/config/editorOptions';
5050
import { PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget';
5151
import { BlockDecorations } from 'vs/editor/browser/viewParts/blockDecorations/blockDecorations';
52+
import { inputLatency } from 'vs/base/browser/performance';
5253

5354

5455
export interface IContentWidgetData {
@@ -222,6 +223,7 @@ export class View extends ViewEventHandler {
222223
}
223224

224225
private _flushAccumulatedAndRenderNow(): void {
226+
inputLatency.markRenderStart();
225227
this._renderNow();
226228
}
227229

src/vs/editor/browser/viewParts/lines/viewLines.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { Viewport } from 'vs/editor/common/viewModel';
2323
import { EditorOption } from 'vs/editor/common/config/editorOptions';
2424
import { Constants } from 'vs/base/common/uint';
2525
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor';
26-
import { recordRenderEnd, recordRenderStart } from 'vs/base/browser/performance';
2726

2827
class LastRenderedData {
2928

@@ -574,7 +573,6 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>,
574573
}
575574

576575
public renderText(viewportData: ViewportData): void {
577-
recordRenderStart();
578576

579577
// (1) render lines - ensures lines are in the DOM
580578
this._visibleLines.renderLines(viewportData);
@@ -637,8 +635,6 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost<ViewLine>,
637635
const adjustedScrollTop = this._context.viewLayout.getCurrentScrollTop() - viewportData.bigNumbersDelta;
638636
this._linesContent.setTop(-adjustedScrollTop);
639637
this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft());
640-
641-
recordRenderEnd();
642638
}
643639

644640
// --- width

src/vs/workbench/services/keybinding/browser/keybindingService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { dirname } from 'vs/base/common/resources';
5050
import { getAllUnboundCommands } from 'vs/workbench/services/keybinding/browser/unboundCommands';
5151
import { UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels';
5252
import { DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
53+
import { inputLatency } from 'vs/base/browser/performance';
5354

5455
interface ContributedKeyBinding {
5556
command: string;
@@ -249,8 +250,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
249250

250251
// for standard keybindings
251252
this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
252-
performance.mark('inputlatency/start', { detail: e.key });
253-
(window as any).frameStart = true;
253+
inputLatency.markKeydownStart();
254254
this.isComposingGlobalContextKey.set(e.isComposing);
255255
const keyEvent = new StandardKeyboardEvent(e);
256256
this._log(`/ Received keydown event - ${printKeyboardEvent(e)}`);

0 commit comments

Comments
 (0)