⬜   Lock scrolling & track pointer ⃞✓   Lock scrolling & track pointer

Pointer latency

This tests the delay of pointermove events in this web browser as well as demonstrates the usefulness (or perhaps the uselessness) of pointer prediction.

Move your pointer around to sample latency and read the numbers below. Times only includes latency inside the web browser. Keep in mind that there is usually some 5-10ms delay between a pointer event being dispatched from the OS to the web browser itself, so add on that estimated value to the times you see here for a "real" or "perceptive" delay.

On a touch device, turn on the "Lock scrolling & track pointer" at the top of the screen to switch from navigating the page to tracking your pointer or finger.

Data is recorded in a sliding window of 100 samples which allows us to get an average that smooths out occasional hiccups like GC pauses.

Source code for all these experiments are contained within this HTML file — simply "view source" to get a copy and please feel free to copy and redistribute as you wish.

dispatch → requestAnimationFrame: avg/min/max - / - / - ms

This measures the time between an event being dispatched in the document and the event being read in a requestAnimationFrame callback function. This is a good measure for the average input delay a game would experience. requestAnimationFrame(() => sample = now() - ev.timeStamp)

Note that an input event is usually delayed by 5-20ms on top of the delay you see above; the time it takes an event to be queued in the OS until the web browser app has received it and processed the event.

dispatch → event handler: avg/min/max - / - / - ms

This measures the time between an event being dispatched in the document and the event handler function being called. onpointermove = () => sample = now() - ev.timeStamp

Pointer prediction

velocity +0.000 , +0.000 px
future time - ms
input latency - ms
acceleration - px/sec
frame at rest
position direct
The tracer circle rectangle turns blue when its position is predicted.
to experience tracking with only the colored tracer circle rectangle
This options highlights the fact that the human brain is very good at compensating for latency — predictive tracking will feel much worse than direct (technically lagging) tracking when there is no system cursors to match.
instead of directly in input event callback.
by artificially lower the input resolution and show traces.

When the circle rectangle following your pointer is red, it is tracking your pointer in the most efficient way possible in a web browser: using CSS transform directly from a pointermove event handler. If you move your pointer left and right (or up and down) in sweeping motions and follow it with your eyes, you'll notice that the circle rectangle is trailing behind the pointer by quite a long distance. This is the effect of a combination of three delays:
the time it takes for an event to make from the OS through the web browser +
the time it takes for an event to make it from the browser into the JS runtime event loop +
the time it takes the web browser and OS to update the display.

When you turn on predictive tracking using the checkbox below, a dead reckoning-based prediction of the future is done to position the circle rectangle tracking the pointer. Everytime the web browser is about to update the display, we estimate where the pointer will be in the future and position our tracking element, the colored circle rectangle accordingly. "Future" really means "now" though, since the most recent input event will be considerably aged when we actually render a new frame in this web browser (done with requestAnimationFrame, which can be toggled below for testing.)

What is happening and why can't I track the OS pointer perfectly?

Pointer prediction is a method useful primarily when an element on screen rendered by an application needs to track the system pointer cursor. It is usually a bad idea to use pointer prediction in cases where the user directly "controls" an element of the screen; when the system pointer cursor is hidden as is the case with most games.

Most modern operating systems (macOS and Windows included) use a method for updating the screen usually called "composition" where the OS renders the next display into a buffer, composing all the various windows' and UI widgets' visual presentations into one big image; the image that is the complete screen. This happens in a buffer and is normally one display update behind in time. A display hardware running at 60Hz means the screen is upated every ~16.5ms (1/60th of a second) and thus something drawn in your at time T will be displayed at T+16.5ms (best-case scenario.) Web browsers like Chrome uses a compositor internally as well as a queued event system and sometimes multiple "run loops"; software "data mills" that call application code whenever a change happens, like a pointer moving. This means that in a web browser, what you paint at time T is often displayed on the screen at T+33ms (±16ms assuming a 60Hz display refresh rate.)

However since pointing with a cursor is such a core experience in these OS'es, the "screen compositor" usually have special code to draw the cursor on screen as late as possible—as close in time to an actual display refresh as possible—to be able to use the most recent position data from the input device driver. This means that the OS cursor will be more responsive, have less input lag, than any app drawing though the normal OS compositor can be.

So combining this knowledge, we can finally paint full picture (ha!) of why we are unable to draw a rectangle in our app that is perfectly positioned at the cursor — we always have outdated information about the pointer position as our code runs ahead of time before the display is refreshed, meaning the input pointer position data we use for drawing is of a past position compared the position used by the system when it draws the OS cursor.

Prediction visualization

This is work in progress, exploring ways to improve on the dead reckoning-based method of movement prediction demonstrated above. Here Bézier curves are used to attempt "fitting" the past and then extending the curve into the future in hopes of reaching lower error rates in prediction.