为什么我在 chrome 开发工具性能面板中得到 30 fps,但 JS 和 React 都说它是 60?

Why I get 30 fps in chrome dev tool performance panel, but JS and React both say it's 60?

我正在尝试用react开发经典游戏教程how-to-make-a-simple-html5-canvas-game

一切顺利,直到我发现我的动作有点问题,online test link and code

而原来的game用JS写的更流畅:

所以我稍微研究了一下,发现实际的 fps 是不同的:

反应:

纯 JS:

奇怪的是,在我向 calc fps 添加一些代码后,我在 react hook 和 useEffect 中都得到“60 fps”:


// log interval in useEffect
  useEffect(() => {
    console.log('interval', Date.now() - renderTime.current);
    renderTime.current = Date.now();
  });

// calc fps in hook directly
fps: rangeShrink(Math.round(1000 / (Date.now() - time.current)), 0, 60),

// render
         <Text 
          x={width - 120} 
          y={borderWidth} 
          text={`FPS: ${fps}`}
          fill="white"
          fontSize={24}
          align="right"
          fontFamily="Helvetica"
        />

定位问题

我添加了一个对比 canvas,它会在每次更新 heroPos 时呈现。它让我在 chrome 开发工具中达到 60FPS。现在问题肯定是由我正在使用的 canvas 库引起的:react-konva.

  const canvasRef = useRef(null);

  useEffect(() => {
    const ctx = canvasRef.current.getContext('2d');
    if (backgroundStatus === 'loaded') {
      ctx.drawImage(backgroundImage, 0, 0);
    }
    if (heroStatus === 'loaded') {
      ctx.drawImage(heroImage, heroPos.x, heroPos.y);
    }
  }, [backgroundStatus, heroStatus, heroPos]);

找到问题

我找到了问题,它是由使用的 batchDraw react-konva 引起的:

更改此行后,我现在可以获得 60fps 的运动。

-  drawingNode && drawingNode.batchDraw();
+  drawingNode && drawingNode.draw();

根据他们的 doc,batchDraw 会在 the next animationFrame 中绘制。但是 react 本身也使用 RAF 来触发下一个道具更新,所以这里的 batchDraw 发生在我 setHeroPos().

之后 2 frames

解决方案:

我要向他们的项目提交 pull-request

开发工具会给设备增加很多额外的负载。当您记录性能日志时更是如此。

React 是我最不想用于实时应用程序的东西,因为它为甚至最简单的任务添加了幕后 JS 的分配。

通过测量帧之间的时间来计算性能并不能准确指示性能。

性能

要衡量函数的性能,请使用 performance API. The simplest way is via performance.now 使用它来获取函数完成所需的时间。

比如获取游戏中主循环函数的时间

function mainLoop(frameTime) {
    const now = performance.now(); // MUST BE FIRST LINE OF CODE TO TEST!!!!

    requestAnimationFrame(mainLoop);
    const executeTime = performance.now() - now; // MUST BE LAST LINE OF CODE TO TEST!!!
}

这将为您提供以毫秒为单位的执行时间。因为JS是阻塞的,所以只测量两行内的代码。

  • 注意 没有测到附加开销,比如GC,Compositing,同步加载等等...

  • 注意 毫秒(1/1,000,000)

  • 注意 这个值的精度 performance.now has deliberately been reduced 是为了保护用户,在 100 毫秒到 200 毫秒之间的任何地方,具体取决于浏览器(1 毫秒可以是在标志和系统配置后面访问))

有意义的表现

JS 执行是不确定的,这使得单个时间测量完全不可靠。 (为什么最好使用 performance.now than peformance.mark

为了克服 JS 执行的不确定性和计时器的不准确性,请使用 运行ning 方法为您的代码计时。下面的示例显示了如何执行此操作。

使用与应用程序需求相关的指标而不是显示时间。例如,一帧有多少时间用于执行代码。 (见示例)

例子

此示例使用 requestAnimationFrame.

对某些 canvas 内容进行动画处理

滑块可让您 select 函数应该花在渲染上的大约时间量。

顶部的信息文本将计时结果显示为 运行宁平均值。

您会注意到在帧速率下降之前,理想化帧负载 (IFL) 远低于 100%。

实验

  1. 开发工具和性能监控如何影响性能。

当帧速率低于 60 时,将滑块移动到正下方。

打开开发工具以查看它是否以及如何影响明显的性能。记下任何变化。有没有影响,有多少?

记录性能日志并查看 FPS 和/或 IFL 是否受到记录的影响

  1. 在帧速率受到影响之前,您的设备可以分配给渲染的最长时间是多少。

向右缓慢移动滑块。

当帧速率降至 60 以下时,将幻灯片向后移动一步,直到再次读取 60FPS。

IFL 将给出完美帧(第 60 秒)执行代码的百分比。 时间 以毫秒为单位的绝对执行时间

Math.rand = (min, max) => Math.random() * (max - min) + min;
Math.randItem = arr => arr[Math.random() * arr.length | 0];
CPULoad.addEventListener("input",() => loadTimeMS = Number(CPULoad.value));
var loadTimeMS = Number(CPULoad.value);
const ctx = canvas.getContext("2d");
requestAnimationFrame(mainLoop);

function mainLoop(frameTime) {

    /* Timed section starts on next line */
    const now = performance.now();
    
    CPU_Load(loadTimeMS);
    ctx.globalAlpha = 0.3;
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    requestAnimationFrame(mainLoop);
    
    const exeTime = performance.now() - now;
    /* Timed section ends at above line*/

    measure(info, frameTime, exeTime);
    
}

const measure = (() => {
    const MEAN = (t, f) => t += f;
    const fTimes = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], bTimes = [...fTimes];
    var pos = 0, prevTime, busyFraction;
    return (el, time, busy) => {
        if (prevTime) {
            bTimes[pos % bTimes.length] = busy;  
            fTimes[(pos ++) % fTimes.length] = time - prevTime;  
            const meanBusy = bTimes.reduce(MEAN, 0) / bTimes.length;
            const meanFPS = fTimes.reduce(MEAN, 0) / fTimes.length;
            el.textContent = "Load: " + loadTimeMS.toFixed(1) + "ms " +
                " FPS: " + Math.round(1000 / meanFPS) + 
                " IFL: " + (meanBusy / (1000 / 60) * 100).toFixed(1) + "%" +
                " Time: " + meanBusy.toFixed(3) + "ms";
            busyFraction = meanBusy / (1000/60);
        }
        prevTime = time;
    };
})();

const colors = "#F00,#FF0,#0F0,#0FF,#00F,#F0F,#000,#FFF".split(",");
// This function shares the load between CPU and GPU reducing CPU
// heating and preventing clock speed throttling on slower systems.
function CPU_Load(ms) { // ms = microsecond and is a min value only
   const now = performance.now();
   ctx.globalAlpha = 0.1;
   do {
      ctx.fillStyle = Math.randItem(colors);
      ctx.fillRect(Math.rand(-50,250), Math.rand(-50, 100), Math.rand(1, 200), Math.rand(1,100))
   } while(performance.now()-now <= ms);
   ctx.globalAlpha = 1;
}
body {
  font-family: arial;
}
#info {
    position: absolute;
    top: 10px;
    left: 10px;
    background: white;
    font-size:small;
    width:345px;
    padding-left: 3px;
}
#canvas {
    background: #8AF;
    border: 1px solid black;
}
#CPULoad {
    font-family: arial;
    position: absolute;
    top: 130px;
    left: 10px;
    color: black;
    width: 340px !important;
}
<code id="info"></code>
<input id="CPULoad" min="0" max="36" step="0.5" value="2"  type="range" list="marks"/>
<canvas id="canvas" width="350"></canvas>
<datalist id="marks">
  <option value="0"></option>
  <option value="4"></option>
  <option value="8"></option>
  <option value="12"></option>
  <option value="16"></option>
  <option value="20"></option>
  <option value="24"></option>
  <option value="28"></option>
  <option value="32"></option>
  <option value="36"></option>
</datalist>

注意 时间的显示会影响结果。事实上,此代码 运行 宁在沙盒代码段中会影响结果。要获得最准确的结果 运行,请在独立页面上编写代码。将结果记录到JS数据结构并在测试后显示结果运行.

  • 加载:请求CPU/GPU执行加载在第1/1000秒。

  • FPS:运行 平均每秒帧数。

  • IFL:理想化帧负载,第 60 秒执行代码的百分比。

  • Time:平均测量执行时间,以 1/1000 秒为单位。