确切的显示时间:requestAnimationFrame的用法和时间线 [英] Exact time of display: requestAnimationFrame usage and timeline

查看:89
本文介绍了确切的显示时间:requestAnimationFrame的用法和时间线的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想要实现的是检测屏幕上出现某些更改的准确时间(主要是使用Google Chrome浏览器)。例如,我使用 $( xelement)。show(); 显示项目,或使用 $(#xelement)。text进行更改( sth new); ,然后我想查看一下Performance.now()到底是什么,当更改通过给定的屏幕重绘出现在用户的屏幕上时。因此,我完全可以接受任何解决方案-在下文中,我主要指的是requestAnimationFrame(rAF),因为该功能可以帮助您准确地实现这一目标,只是看起来并非如此。

What I want to achieve is to detect the precise time of when a certain change appeared on the screen (primarily with Google Chrome). For example I show an item using $("xelement").show(); or change it using $("#xelement").text("sth new"); and then I would want to see what the performance.now() was exactly when the change appeared on the user's screen with the given screen repaint. So I'm totally open to any solutions - below I just refer primarily to requestAnimationFrame (rAF) because that is the function that is supposed to help achieve exactly this, only it doesn't seem to; see below.

基本上,正如我所想象的,rAF应该在0-17毫秒内执行其中的所有操作(每当下一帧出现在我的标准60 Hz屏幕上)。此外,timestamp参数应该给出执行时间的值(该值基于与performance.now()相同的DOMHighResTimeStamp度量。)

Basically, as I imagine, rAF should execute everything inside it in about 0-17 ms (whenever the next frame appears on my standard 60 Hz screen). Moreover, the timestamp argument should give the value of the time of this execution (and this value is based on the same DOMHighResTimeStamp measure as performance.now()).

Now这是我为此所做的众多测试之一: https://jsfiddle.net/gasparl/k5nx7zvh / 31 /

Now here is one of the many tests I made for this: https://jsfiddle.net/gasparl/k5nx7zvh/31/

function item_display() {
    var before = performance.now();
    requestAnimationFrame(function(timest){
        var r_start = performance.now();
        var r_ts = timest;
        console.log("before:", before);
        console.log("RAF callback start:", r_start);
        console.log("RAF stamp:", r_ts);
        console.log("before vs. RAF callback start:", r_start - before);
        console.log("before vs. RAF stamp:", r_ts - before);
        console.log("")
    });
}
setInterval(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);

我在Chrome中看到的是:rAF中的函数始终在大约0到3毫秒内执行(从其之前的performance.now()计数),最奇怪的是,rAF时间戳与我在rAF内的performance.now()获得的时间戳完全不同,通常早于0-17毫秒performance.now()在rAF之前称为 (但有时在之后大约0-1毫秒)。

What I see in Chrome is: the function inside rAF is executed always within about 0-3 ms (counting from a performance.now() immediately before it), and, what's weirdest, the rAF timestamp is something totally different from what I get with the performance.now() inside the rAF, being usually about 0-17 ms earlier than the performance.now() called before the rAF (but sometimes about 0-1 ms afterwards).

以下是一个典型示例:

before: 409265.00000001397
RAF callback start: 409266.30000001758
RAF stamp: 409260.832 
before vs. RAF callback start: 1.30000000353902
before vs. RAF stamp: -4.168000013974961 

在Firefox和IE中那不一样。在Firefox中,在开始之前vs. RAF回调开始大约是1-3毫秒或大约16-17毫秒。 对RAF戳之前始终为正,通常在0-3毫秒左右,但有时在3-17毫秒之间。在IE中,两种差异几乎总是在15-18毫秒左右(正值)。这些或多或少与不同的PC相同。但是,当我在手机的Chrome上运行它时,直到那时,这似乎是正确的:之前对RAF戳随机在0-17左右,而 RAF回调开始总是在几毫秒后。

In Firefox and in IE it is different. In Firefox the "before vs. RAF callback start" is either around 1-3 ms or around 16-17 ms. The "before vs. RAF stamp" is always positive, usually around 0-3 ms, but sometimes anything between 3-17 ms. In IE both differences are almost always around 15-18 ms (positive). These are more or less the same of different PCs. However, when I run it on my phone's Chrome, then, and only then, it seems plausibly correct: "before vs. RAF stamp" randomly around 0-17, and "RAF callback start" always a few ms afterwards.

更多信息:这是一个在线响应时间实验,用户使用自己的PC(但是我通常将浏览器限制为Chrome,因此这是唯一对我真正重要的浏览器)。反复显示各种项目,将响应时间测量为从显示元素(当人们看到它的那一刻)到按下键的那一刻,并从记录的响应时间中计算出平均值项,然后检查某些项类型之间的差异。这也意味着,只要记录的时间始终沿某个方向偏斜(例如,始终在元素实际出现之前3毫秒)就没关系,只要该偏斜对于每个显示都是一致的即可,因为只有差异真的很重要1-2毫秒的精度将是理想的选择,但是任何可以缓解随机刷新率噪声(0-17毫秒)的方法都可以。

For more context: This is for an online response-time experiment where users use their own PC (but I typically restrict browser to Chrome, so that's the only browser that really matters to me). I show various items repeatedly, and measure the response time as "from the moment of the display of the element (when the person sees it) to the moment when they press a key", and count an average from the recorded response times for specific items, and then check the difference between certain item types. This also means that it doesn't matter much if the recorded time is always a bit skewed in a direction (e.g. always 3 ms before the actual appearance of the element) as long as this skew is consistent for each display, because only the difference really matters. A 1-2 ms precision would be the ideal, but anything that mitigates the random "refresh rate noise" (0-17 ms) would be nice.

尝试 jQuery.show()回调,但是没有考虑刷新率:> https://jsfiddle.net/gasparl/k5nx7zvh/67/

I also gave a try to jQuery.show() callback, but it does not take refresh rate into account: https://jsfiddle.net/gasparl/k5nx7zvh/67/

var r_start;
function shown() {
    r_start = performance.now();
}
function item_display() {
    var before = performance.now();
    $("#stim_id").show(complete = shown())
    var after = performance.now();
    var text = "before: " + before + "<br>callback RT: " + r_start + "<br>after: " + after + "<br>before vs. callback: " + (r_start - before) + "<br>before vs. after: " + (after - r_start)
    console.log("")
    console.log(text)
    $("p").html(text);
    setTimeout(function(){ $("#stim_id").hide(); }, 500);
}
setInterval(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 800);

使用HTML:

<p><br><br><br><br><br></p>
<span id="stim_id">STIMULUS</span>






解决方案(基于Kaiido的回答)以及显示示例:

function monkeyPatchRequestPostAnimationFrame() {
  const channel = new MessageChannel();
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  channel.port2.onmessage = e => {
    called = false;
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (e) {}
    });
  };
  window.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Argument 1 is not callable');
    }
    callbacks.push(callback);
    if (!called) {
      requestAnimationFrame((time) => {
        timestamp = time;
        channel.port1.postMessage('');
      });
      called = true;
    }
  };
}

if (typeof requestPostAnimationFrame !== 'function') {
  monkeyPatchRequestPostAnimationFrame();
}

function chromeWorkaroundLoop() {
  if (needed) {
    requestAnimationFrame(chromeWorkaroundLoop);
  }
}

// here is how I display items
// includes a 100 ms "warm-up"
function item_display() {
  window.needed = true;
  chromeWorkaroundLoop();
  setTimeout(function() {
    var before = performance.now();
    $("#stim_id").text("Random new text: " + Math.round(Math.random()*1000) + ".");
    $("#stim_id").show();
    // I ask for display above, and get display time below
    requestPostAnimationFrame(function() {
      var rPAF_now = performance.now();
      console.log("before vs. rPAF now:", rPAF_now - before);
      console.log("");
      needed = false;
    });
  }, 100);
}

// below is just running example instances of displaying stuff
function example_loop(count) {
  $("#stim_id").hide();
  setTimeout(function() {
    item_display();
    if (count > 1) {
      example_loop(--count);
    }
  }, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
}

example_loop(10);

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
<div id="stim_id">Any text</div>

推荐答案

您遇到的情况是一个Chrome错误(甚至两个)。

What you are experiencing is a Chrome bug (and even two).

基本上,当 requestAnimationFrame 回调池为空时,它们将在当前事件循环结束时直接调用它,而无需等待

Basically, when the pool of requestAnimationFrame callbacks is empty, they'll call it directly at the end of the current event loop, without waiting for the actual painting frame as the specs require.

要变通解决此错误,您可以保持一个持续不断的 requestAnimationFrame 循环,但是请当心会将您的文档标记为带有动画,并在页面上触发一系列副作用(例如在每次屏幕刷新时强制重新绘制)。因此,我不确定您在做什么,但这通常不是一个好主意,我宁愿邀请您仅在需要时运行此动画循环。

To workaround this bug, you can keep an ever-going requestAnimationFrame loop, but beware this will mark your document as "animated" and will trigger a bunch of side-effects on your page (like forcing a repaint at every screen refresh). So I'm not sure what you are doing, but it's generally not a great idea to do this, and I would rather invite you to run this animation loop only when required.

let needed = true; // set to false when you don't need the rAF loop anymore

function item_display() {
  var before = performance.now();
  requestAnimationFrame(function(timest) {
    var r_start = performance.now();
    var r_ts = timest;
    console.log("before:", before);
    console.log("RAF callback start:", r_start);
    console.log("RAF stamp:", r_ts);
    console.log("before vs. RAF callback start:", r_start - before);
    console.log("before vs. RAF stamp:", r_ts - before);
    console.log("")
    setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
  });
}
chromeWorkaroundLoop();
item_display();

function chromeWorkaroundLoop() {
  if (needed) {
    requestAnimationFrame(chromeWorkaroundLoop);
  }
};

现在, requestAnimationFrame 回调会在下一次绘制(实际上是在同一事件循环中)之前发生在之前,并且TimeStamp参数应表示执行当前帧的所有主要任务和微任务之后的时间,然后才开始更新呈现子任务(步骤9 此处)。

Now, requestAnimationFrame callbacks fire before the next paint (actually in the same event loop), and the TimeStamp argument should represent the time after all main tasks and microtasks of the current frame were executed, before it's starts its "update the rendering" sub-task (step 9 here).

因此,这并不是您所能拥有的最精确的方法,而正确的是使用 performance.now()在此回调中,应该使您更接近实际绘制时间。

So it's not the most precise you can have, and you are right that using performance.now() inside this callback should get you closer to the actual painting time.

此外,当Chrome在此处遇到其他错误(可能与第一个错误有关)时,他们确实将此rAF timeStamp设置为...我必须承认我没有知道什么...也许是先前绘画帧的时间戳。

Moreover when Chrome faces yet an other bug here, probably related to the first one, when they do set this rAF timeStamp to... I must admit I don't know what... maybe the previous painting frame's timeStamp.

(function() {
let raf_id,
  eventLoopReport = {
    id: 0,
    timeStamp: 0,
    now: 0
  },
  report = {
  	nb_of_loops_between_call_and_start: -1,
  	mouseClick_timeStamp: 0,
    calling_task: {
    	eventLoop: null,
      now: 0
    },
    rAF_task: {
    	eventLoop: null,
      now: 0,
      timeStamp: 0
    }
  };
  
startEventLoopCounter();
  
btn.onclick = triggerSingleFrame;


// increments eventLoop_id at every event loop
// (or at least every time our postMessage loop fires)
function startEventLoopCounter() {
  const channel = new MessageChannel()
  channel.port2.onmessage = e => {
    eventLoopReport.id ++;
    eventLoopReport.timeStamp = e.timeStamp;
    eventLoopReport.now = performance.now();
    channel.port1.postMessage('*');
  };
  channel.port1.postMessage('*');
}

function triggerSingleFrame(e) {
  // mouseClick Event should be generated at least the previous event loop, so its timeStamp should be in the past
	report.mouseClick_timeStamp = e.timeStamp;
	const report_calling = report.calling_task;
  report_calling.now = performance.now();
  report_calling.eventLoop = Object.assign({}, eventLoopReport);

	cancelAnimationFrame(raf_id);
  
	raf_id = requestAnimationFrame((raf_ts) => {
  	const report_rAF = report.rAF_task;
		report_rAF.now = performance.now();
    report_rAF.timeStamp = raf_ts;
    report_rAF.eventLoop = Object.assign({}, eventLoopReport);
    report.nb_of_loops_between_call_and_start = report_rAF.eventLoop.id - report_calling.eventLoop.id;
    // this should always be positive
    report_el.textContent = "rAF.timeStamp - mouse_click.timeStamp: " +
			(report.rAF_task.timeStamp - report.mouseClick_timeStamp) + '\n\n' +
      // verbose
    	JSON.stringify(report, null, 2) ;
  });
}
})();

<button id="btn">flash</button>
<div id="out"></div>
<pre id="report_el"></pre>

再一次,运行无限的rAF循环将修复此奇怪的错误。

Once again, running an infinite rAF loop will fix this weird bug.

所以您可能要检查的一件事是也许是传入的 requestPostAnimationFrame 方法

So one thing you might want to check is the maybe incoming requestPostAnimationFrame method.

chrome:flags 中启用实验性Web平台功能后,即可在Chrome中访问它。如果该方法被html标准接受,则允许我们在进行绘制操作后 立即触发回调。

You can access it in Chrome, after you enable "Experimental Web Platform features" in chrome:flags. This method if accepted by html standards will allow us to fire callbacks immediately after the paint operation occurred.

从那里开始,您应该是最靠近绘画的地方。

From there, you should be at the closest of the painting.

var needed = true;
function item_display() {
  var before = performance.now();
  requestAnimationFrame(function() {
    requestPostAnimationFrame(function() {
      var rPAF_now = performance.now();
      console.log("before vs. rPAF now:", rPAF_now - before);
      console.log("");
      setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
    });
  });
}

if (typeof requestPostAnimationFrame === 'function') {
  chromeWorkaroundLoop();
  item_display();
} else {
  console.error("Your browser doesn't support 'requestPostAnimationFrame' method, be sure you enabled 'Experimental Web Platform features' in chrome:flags");
}

function chromeWorkaroundLoop() {
  if (needed) {
    requestAnimationFrame(chromeWorkaroundLoop);
  }
};

对于尚未实施此建议的浏览器,或者如果该提案从不通过规范进行,则可以尝试使用MessageEvent对其进行猴子补丁,这应该是在下一个事件循环时触发的第一件事。但是,由于除了运行无限的rAF循环外,我们无法知道是否已经位于rAF回调内部,因此无法从rAF回调内部调用此猴子补丁。

And for browsers that do not yet implement this proposal, or if this proposal never does it through the specs, you can try to monkeyPatch it using a MessageEvent, which should be the first thing to fire at the next event loop. However, since there is no way for us to know if we are inside an rAF callback already, except by running an infinite rAF loop, this monkey-patch will not be callable from inside an rAF callback.

// monkey-patches requestPostAnimationFrame
//!\ Can not be called from inside a requestAnimationFrame callback
function monkeyPatchRequestPostAnimationFrame() {
  console.warn('using a MessageEvent workaround');
  const channel = new MessageChannel();
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  channel.port2.onmessage = e => {
    called = false;
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (e) {}
    });
  }
  window.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Argument 1 is not callable');
    }
    callbacks.push(callback);
    if (!called) {
      requestAnimationFrame((time) => {
        timestamp = time;
        channel.port1.postMessage('');
      });
      called = true;
    }
  };
}

if (typeof requestPostAnimationFrame !== 'function') {
  monkeyPatchRequestPostAnimationFrame();
}

var needed = true;

function item_display() {
  var before = performance.now();
  requestPostAnimationFrame(function() {
    var rPAF_now = performance.now();
    console.log("before vs. rPAF now:", rPAF_now - before);
    console.log("");
    setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
  });
}


chromeWorkaroundLoop();
item_display();

function chromeWorkaroundLoop() {
  if (needed) {
    requestAnimationFrame(chromeWorkaroundLoop);
  }
};

但是如果您不关心电池排空并需要在rAF内调用它:

But if you don't care about battery drainage and need to have it callable from inside rAF here it is:

// monkey-patches requestPostAnimationFrame
// runs an infinite rAF loop for it to be callable inside rAF
function monkeyPatchRequestPostAnimationFrame() {
  console.warn('using a MessageEvent workaround');
  const channel = new MessageChannel();
  const callbacks = [];
  let timestamp = 0;
  let called = false;
  channel.port2.onmessage = e => {
    const toCall = callbacks.slice();
    callbacks.length = 0;
    toCall.forEach(fn => {
      try {
        fn(timestamp);
      } catch (e) {}
    });
    // loop in here so that we fire after other rAF loops
    requestAnimationFrame(loop);
  };
  function loop(time) {
    timestamp = time;
    channel.port1.postMessage('');
  }
  window.requestPostAnimationFrame = function(callback) {
    if (typeof callback !== 'function') {
      throw new TypeError('Argument 1 is not callable');
    }
    callbacks.push(callback);
  };
  loop();  
}

if (typeof requestPostAnimationFrame !== 'function') {
  monkeyPatchRequestPostAnimationFrame();
}

var needed = true;

function item_display() {
  var before = performance.now();
  requestPostAnimationFrame(function() {
    var rPAF_now = performance.now();
    console.log("before vs. rPAF now:", rPAF_now - before);
    console.log("");
    setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
  });
}

item_display();

这篇关于确切的显示时间:requestAnimationFrame的用法和时间线的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆