Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/performance.ts
3292 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
export namespace inputLatency {
7
8
// Measurements are recorded as totals, the average is calculated when the final measurements
9
// are created.
10
interface ICumulativeMeasurement {
11
total: number;
12
min: number;
13
max: number;
14
}
15
const totalKeydownTime: ICumulativeMeasurement = { total: 0, min: Number.MAX_VALUE, max: 0 };
16
const totalInputTime: ICumulativeMeasurement = { ...totalKeydownTime };
17
const totalRenderTime: ICumulativeMeasurement = { ...totalKeydownTime };
18
const totalInputLatencyTime: ICumulativeMeasurement = { ...totalKeydownTime };
19
let measurementsCount = 0;
20
21
22
23
// The state of each event, this helps ensure the integrity of the measurement and that
24
// something unexpected didn't happen that could skew the measurement.
25
const enum EventPhase {
26
Before = 0,
27
InProgress = 1,
28
Finished = 2
29
}
30
const state = {
31
keydown: EventPhase.Before,
32
input: EventPhase.Before,
33
render: EventPhase.Before,
34
};
35
36
/**
37
* Record the start of the keydown event.
38
*/
39
export function onKeyDown() {
40
/** Direct Check C. See explanation in {@link recordIfFinished} */
41
recordIfFinished();
42
performance.mark('inputlatency/start');
43
performance.mark('keydown/start');
44
state.keydown = EventPhase.InProgress;
45
queueMicrotask(markKeyDownEnd);
46
}
47
48
/**
49
* Mark the end of the keydown event.
50
*/
51
function markKeyDownEnd() {
52
if (state.keydown === EventPhase.InProgress) {
53
performance.mark('keydown/end');
54
state.keydown = EventPhase.Finished;
55
}
56
}
57
58
/**
59
* Record the start of the beforeinput event.
60
*/
61
export function onBeforeInput() {
62
performance.mark('input/start');
63
state.input = EventPhase.InProgress;
64
/** Schedule Task A. See explanation in {@link recordIfFinished} */
65
scheduleRecordIfFinishedTask();
66
}
67
68
/**
69
* Record the start of the input event.
70
*/
71
export function onInput() {
72
if (state.input === EventPhase.Before) {
73
// it looks like we didn't receive a `beforeinput`
74
onBeforeInput();
75
}
76
queueMicrotask(markInputEnd);
77
}
78
79
function markInputEnd() {
80
if (state.input === EventPhase.InProgress) {
81
performance.mark('input/end');
82
state.input = EventPhase.Finished;
83
}
84
}
85
86
/**
87
* Record the start of the keyup event.
88
*/
89
export function onKeyUp() {
90
/** Direct Check D. See explanation in {@link recordIfFinished} */
91
recordIfFinished();
92
}
93
94
/**
95
* Record the start of the selectionchange event.
96
*/
97
export function onSelectionChange() {
98
/** Direct Check E. See explanation in {@link recordIfFinished} */
99
recordIfFinished();
100
}
101
102
/**
103
* Record the start of the animation frame performing the rendering.
104
*/
105
export function onRenderStart() {
106
// Render may be triggered during input, but we only measure the following animation frame
107
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.render === EventPhase.Before) {
108
// Only measure the first render after keyboard input
109
performance.mark('render/start');
110
state.render = EventPhase.InProgress;
111
queueMicrotask(markRenderEnd);
112
/** Schedule Task B. See explanation in {@link recordIfFinished} */
113
scheduleRecordIfFinishedTask();
114
}
115
}
116
117
/**
118
* Mark the end of the animation frame performing the rendering.
119
*/
120
function markRenderEnd() {
121
if (state.render === EventPhase.InProgress) {
122
performance.mark('render/end');
123
state.render = EventPhase.Finished;
124
}
125
}
126
127
function scheduleRecordIfFinishedTask() {
128
// Here we can safely assume that the `setTimeout` will not be
129
// artificially delayed by 4ms because we schedule it from
130
// event handlers
131
setTimeout(recordIfFinished);
132
}
133
134
/**
135
* Record the input latency sample if input handling and rendering are finished.
136
*
137
* The challenge here is that we want to record the latency in such a way that it includes
138
* also the layout and painting work the browser does during the animation frame task.
139
*
140
* Simply scheduling a new task (via `setTimeout`) from the animation frame task would
141
* schedule the new task at the end of the task queue (after other code that uses `setTimeout`),
142
* so we need to use multiple strategies to make sure our task runs before others:
143
*
144
* We schedule tasks (A and B):
145
* - we schedule a task A (via a `setTimeout` call) when the input starts in `markInputStart`.
146
* If the animation frame task is scheduled quickly by the browser, then task A has a very good
147
* chance of being the very first task after the animation frame and thus will record the input latency.
148
* - however, if the animation frame task is scheduled a bit later, then task A might execute
149
* before the animation frame task. We therefore schedule another task B from `markRenderStart`.
150
*
151
* We do direct checks in browser event handlers (C, D, E):
152
* - if the browser has multiple keydown events queued up, they will be scheduled before the `setTimeout` tasks,
153
* so we do a direct check in the keydown event handler (C).
154
* - depending on timing, sometimes the animation frame is scheduled even before the `keyup` event, so we
155
* do a direct check there too (E).
156
* - the browser oftentimes emits a `selectionchange` event after an `input`, so we do a direct check there (D).
157
*/
158
function recordIfFinished() {
159
if (state.keydown === EventPhase.Finished && state.input === EventPhase.Finished && state.render === EventPhase.Finished) {
160
performance.mark('inputlatency/end');
161
162
performance.measure('keydown', 'keydown/start', 'keydown/end');
163
performance.measure('input', 'input/start', 'input/end');
164
performance.measure('render', 'render/start', 'render/end');
165
performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
166
167
addMeasure('keydown', totalKeydownTime);
168
addMeasure('input', totalInputTime);
169
addMeasure('render', totalRenderTime);
170
addMeasure('inputlatency', totalInputLatencyTime);
171
172
// console.info(
173
// `input latency=${performance.getEntriesByName('inputlatency')[0].duration.toFixed(1)} [` +
174
// `keydown=${performance.getEntriesByName('keydown')[0].duration.toFixed(1)}, ` +
175
// `input=${performance.getEntriesByName('input')[0].duration.toFixed(1)}, ` +
176
// `render=${performance.getEntriesByName('render')[0].duration.toFixed(1)}` +
177
// `]`
178
// );
179
180
measurementsCount++;
181
182
reset();
183
}
184
}
185
186
function addMeasure(entryName: string, cumulativeMeasurement: ICumulativeMeasurement): void {
187
const duration = performance.getEntriesByName(entryName)[0].duration;
188
cumulativeMeasurement.total += duration;
189
cumulativeMeasurement.min = Math.min(cumulativeMeasurement.min, duration);
190
cumulativeMeasurement.max = Math.max(cumulativeMeasurement.max, duration);
191
}
192
193
/**
194
* Clear the current sample.
195
*/
196
function reset() {
197
performance.clearMarks('keydown/start');
198
performance.clearMarks('keydown/end');
199
performance.clearMarks('input/start');
200
performance.clearMarks('input/end');
201
performance.clearMarks('render/start');
202
performance.clearMarks('render/end');
203
performance.clearMarks('inputlatency/start');
204
performance.clearMarks('inputlatency/end');
205
206
performance.clearMeasures('keydown');
207
performance.clearMeasures('input');
208
performance.clearMeasures('render');
209
performance.clearMeasures('inputlatency');
210
211
state.keydown = EventPhase.Before;
212
state.input = EventPhase.Before;
213
state.render = EventPhase.Before;
214
}
215
216
export interface IInputLatencyMeasurements {
217
keydown: IInputLatencySingleMeasurement;
218
input: IInputLatencySingleMeasurement;
219
render: IInputLatencySingleMeasurement;
220
total: IInputLatencySingleMeasurement;
221
sampleCount: number;
222
}
223
224
export interface IInputLatencySingleMeasurement {
225
average: number;
226
min: number;
227
max: number;
228
}
229
230
/**
231
* Gets all input latency samples and clears the internal buffers to start recording a new set
232
* of samples.
233
*/
234
export function getAndClearMeasurements(): IInputLatencyMeasurements | undefined {
235
if (measurementsCount === 0) {
236
return undefined;
237
}
238
239
// Assemble the result
240
const result = {
241
keydown: cumulativeToFinalMeasurement(totalKeydownTime),
242
input: cumulativeToFinalMeasurement(totalInputTime),
243
render: cumulativeToFinalMeasurement(totalRenderTime),
244
total: cumulativeToFinalMeasurement(totalInputLatencyTime),
245
sampleCount: measurementsCount
246
};
247
248
// Clear the cumulative measurements
249
clearCumulativeMeasurement(totalKeydownTime);
250
clearCumulativeMeasurement(totalInputTime);
251
clearCumulativeMeasurement(totalRenderTime);
252
clearCumulativeMeasurement(totalInputLatencyTime);
253
measurementsCount = 0;
254
255
return result;
256
}
257
258
function cumulativeToFinalMeasurement(cumulative: ICumulativeMeasurement): IInputLatencySingleMeasurement {
259
return {
260
average: cumulative.total / measurementsCount,
261
max: cumulative.max,
262
min: cumulative.min,
263
};
264
}
265
266
function clearCumulativeMeasurement(cumulative: ICumulativeMeasurement): void {
267
cumulative.total = 0;
268
cumulative.min = Number.MAX_VALUE;
269
cumulative.max = 0;
270
}
271
272
}
273
274