Galveston01
Galveston01

Reputation: 356

JavaScript EventListener "pointerMove": points per second

I have an element with a "pointerMove" EventListener added to it. Now when moving my mouse around, I can measure the number of data points "pointerMove" delivers per second (pps) by counting the total number of points drawn since "pointerDown" and dividing this by the time that passed since "pointerDown". So far, so good. What is strange though is the fact that I get a higher pps rate when the developer console is opened.

Example: Pressing my mouse button and then moving the cursor around chaotically gives me about 60pps. But when opening the developer console and then doing exactly the same, my pps rises to about 235 - almost 400% increase!

This test was done in Chrome 76 on Windows 10. Similar results may be obtained using Firefox. This issue also concerns input via touch or pen and is present in Chrome OS as well (behaviour on other operating systems has not yet been examined). Interestingly though Microsoft Edge seems not to be affected.

So the question is: Why does this happen? And how may I get the higher number of pps without having to open the developer console?


Executable example here: https://jsfiddle.net/Galveston01/unxmrchw/

var pointerid = undefined;
var start, count;
var canvas;

function startup() {
  canvas = document.getElementById("canvas");
  canvas.addEventListener("pointerdown", pointerdown, false);
  canvas.addEventListener("pointermove", pointermove, false);
  canvas.addEventListener("pointerup", pointerup, false);
  canvas.addEventListener("pointercancel", pointerup, false);
  canvas.addEventListener("touchstart", touch, false);
  canvas.addEventListener("touchmove", touch, false);
  canvas.addEventListener("touchend", touch, false);
  canvas.addEventListener("touchcancel", touch, false);
}

function pointerdown(event) {
  event.preventDefault();
  rect = canvas.getBoundingClientRect();
  if ((event.pointerType == "pen" || event.pointerType == "mouse") && pointerid == undefined) {
    pointerid = event.pointerId;
    var x = event.clientX - rect.left,
      y = event.clientY - rect.top;
    start = new Date().getTime();
    count = 1;
  }
}

function pointermove(event) {
  event.preventDefault();
  if (pointerid == event.pointerId) {
    var x = event.clientX - rect.left,
      y = event.clientY - rect.top;
    count++;
  }
}

function pointerup(event) {
  event.preventDefault();
  if (pointerid == event.pointerId) {
    var x = event.clientX - rect.left,
      y = event.clientY - rect.top;
    pointerid = undefined;
    count++;
    console.log((count / (new Date().getTime() - start) * 1000) + "pps");
  }
}

function touch(event) {
  if (pointerid != undefined) {
    event.preventDefault();
  }
}
startup();
<div id="canvas" style="width:2000px; height:2000px; ">
</div>

Upvotes: 4

Views: 3882

Answers (1)

Kaiido
Kaiido

Reputation: 137131

Specs now encourage browser vendors to threshold most UI Events in order to improve performances.

For instance you can find such a notice in mousemove UI-Event specs:

Implementations are encouraged to determine the optimal frequency rate to balance responsiveness with performance.

And about the same in PointerEvents' pointermove specs drafts

These events may be coalesced or aligned to animation frame callbacks based on UA decision.

In the facts, both Firefox and Chrome do currently coalesce these events in animation frame callback i.e it's actually aligned on your monitor's refresh rate.

This also means that for pointermove we can retrieve all these events using the PointerEvent.getCoalescedEvents method.

const canvas = document.getElementById("canvas");
let
  count = 0,
  start = 0;
const opts = {passive: true};
canvas.addEventListener("pointerdown", pointerdown, opts);
canvas.addEventListener("pointermove", pointermove, opts);
canvas.addEventListener("pointerup", pointerup, opts);
canvas.addEventListener("touchstart", prevent);
canvas.addEventListener("touchmove", prevent);


function pointermove(event) {
  if(canvas.hasPointerCapture(event.pointerId)) {
    const coalesced = event.getCoalescedEvents();
    count += coalesced.length;
    points.push(...coalesced);
    draw();
  }
}
function pointerdown(event) {
  canvas.setPointerCapture(event.pointerId);
  count = 1;
  start = new Date().getTime();
  points.length = 0;
}
function pointerup(event) {
  if(canvas.hasPointerCapture(event.pointerId)) {
    canvas.releasePointerCapture(event.pointerId);
    count++;
    const PPS = (count / (new Date().getTime() - start) * 1000);
    log.textContent = PPS + "pps";
  }
}

// just to show we have real Events
const ctx = canvas.getContext('2d');
const points = [];
function draw() {
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.beginPath();
  points.forEach(evt => {
    ctx.lineTo(evt.offsetX, evt.offsetY);
  });
  ctx.stroke();
}
// prevent default touch events or pointer ones are discarded
function prevent(evt) {
  evt.preventDefault();
}
#canvas {
  border: 1px solid;
}
<pre id="log"></pre>
<canvas id="canvas" width="2000" heigth="2000"></canvas>

Note that in Chrome these Events do have their own timestamp, so you can know when they should have fired, but in Firefox this property is set to 0...

However, a pointerrawupdate event is coming in the drafts and is already available in Chrome under the Experimental Web Platform features flag.
This event will not be aligned to animation frames and instead will fire "as soon as possible".

if(!('onpointerrawupdate' in window)) {
  console.error("Your browser doesn't support 'pointerrawupdate' event. You may need to toggle some config flags");
}
else {
const canvas = document.getElementById("canvas");
let
  count = 0,
  start = 0;
const opts = {passive: true};
canvas.addEventListener("pointerdown", pointerdown, opts);
canvas.addEventListener("pointerrawupdate", pointermove, opts);
canvas.addEventListener("pointerup", pointerup, opts);


function pointermove(event) {
  if(canvas.hasPointerCapture(event.pointerId)) {
    const coalesced = event.getCoalescedEvents();
    count += coalesced.length;
  }
}
function pointerdown(event) {
  canvas.setPointerCapture(event.pointerId);
  count = 1;
  start = new Date().getTime();
}
function pointerup(event) {
  if(canvas.hasPointerCapture(event.pointerId)) {
    canvas.releasePointerCapture(event.pointerId);
    count++;
    const PPS = (count / (new Date().getTime() - start) * 1000);
    log.textContent = PPS + "pps";
  }
}

// Removed the drawing part because drawing should be made in animation frames
}
#canvas {
  border: 1px solid;
}
<pre id="log"></pre>
<canvas id="canvas" width="2000" heigth="2000"></canvas>

But in your case (a drawing app) you'd be better to stick with getCoalescedEvents since your drawings should anyway happen only in animation frames.


Ps: about why the threshold is deactivated when dev-tools are open, that's probably a browser bug.

Upvotes: 5

Related Questions