@@ -31,119 +31,146 @@ export namespace inputLatency {
31
31
keydown : EventPhase . Before ,
32
32
input : EventPhase . Before ,
33
33
render : EventPhase . Before ,
34
- selection : EventPhase . Before
35
34
} ;
36
35
37
36
/**
38
- * Mark the start of the keydown event.
37
+ * Record the start of the keydown event.
39
38
*/
40
- export function markKeydownStart ( ) {
39
+ export function onKeyDown ( ) {
40
+ /** Direct Check C. See explanation in {@link recordIfFinished} */
41
+ recordIfFinished ( ) ;
41
42
performance . mark ( 'inputlatency/start' ) ;
42
43
performance . mark ( 'keydown/start' ) ;
43
44
state . keydown = EventPhase . InProgress ;
44
- queueMicrotask ( ( ) => markKeydownEnd ( ) ) ;
45
+ queueMicrotask ( markKeyDownEnd ) ;
45
46
}
46
47
47
48
/**
48
49
* Mark the end of the keydown event.
49
50
*/
50
- function markKeydownEnd ( ) {
51
- // Only measure the first render after keyboard input
51
+ function markKeyDownEnd ( ) {
52
52
performance . mark ( 'keydown/end' ) ;
53
53
state . keydown = EventPhase . Finished ;
54
54
}
55
55
56
56
/**
57
- * Mark the start of the input event.
57
+ * Record the start of the beforeinput event.
58
58
*/
59
- export function markInputStart ( ) {
59
+ export function onBeforeInput ( ) {
60
60
performance . mark ( 'input/start' ) ;
61
61
state . input = EventPhase . InProgress ;
62
+ /** Schedule Task A. See explanation in {@link recordIfFinished} */
63
+ scheduleRecordIfFinishedTask ( ) ;
62
64
}
63
65
64
66
/**
65
- * Mark the end of the input event.
67
+ * Record the start of the input event.
66
68
*/
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 ( ) ;
72
92
}
73
93
74
94
/**
75
- * Mark the start of the animation frame performing the rendering.
95
+ * Record the start of the animation frame performing the rendering.
76
96
*/
77
- export function markRenderStart ( ) {
97
+ export function onRenderStart ( ) {
78
98
// Render may be triggered during input, but we only measure the following animation frame
79
99
if ( state . keydown === EventPhase . Finished && state . input === EventPhase . Finished && state . render === EventPhase . Before ) {
80
100
// Only measure the first render after keyboard input
81
101
performance . mark ( 'render/start' ) ;
82
102
state . render = EventPhase . InProgress ;
83
- queueMicrotask ( ( ) => markRenderEnd ( ) ) ;
103
+ queueMicrotask ( markRenderEnd ) ;
104
+ /** Schedule Task B. See explanation in {@link recordIfFinished} */
105
+ scheduleRecordIfFinishedTask ( ) ;
84
106
}
85
107
}
86
108
87
109
/**
88
110
* 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.
92
111
*/
93
112
function markRenderEnd ( ) {
94
113
performance . mark ( 'render/end' ) ;
95
114
state . render = EventPhase . Finished ;
96
- record ( ) ;
97
115
}
98
116
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 ) ;
108
122
}
109
123
110
124
/**
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).
112
147
*/
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' ) ;
123
151
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' ) ;
128
156
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 ) ;
133
161
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
+ // );
141
169
142
- measurementsCount ++ ;
170
+ measurementsCount ++ ;
143
171
144
- reset ( ) ;
145
- }
146
- } , 0 ) ;
172
+ reset ( ) ;
173
+ }
147
174
}
148
175
149
176
function addMeasure ( entryName : string , cumulativeMeasurement : ICumulativeMeasurement ) : void {
@@ -174,7 +201,6 @@ export namespace inputLatency {
174
201
state . keydown = EventPhase . Before ;
175
202
state . input = EventPhase . Before ;
176
203
state . render = EventPhase . Before ;
177
- state . selection = EventPhase . Before ;
178
204
}
179
205
180
206
export interface IInputLatencyMeasurements {
0 commit comments