@@ -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 {
0 commit comments