¿Cómo verifico la velocidad de fotogramas en Javascript? [closed]

6 minutos de lectura

Avatar de usuario de CyanPrime
CyanPrime

¿Cómo verificaría la velocidad de fotogramas en mi código javascript? Estoy usando esto para hacer un bucle:

gameloopId = setInterval(gameLoop, 10);

Avatar de usuario de Phrogz
Phrogz

El código de @Slaks te brinda solo el FPS instantáneo del último cuadro, que puede variar o ser engañoso con contratiempos. Prefiero usar un filtro de paso bajo fácil de escribir y calcular para eliminar los transitorios rápidos y mostrar un pseudopromedio razonable de los resultados recientes:

// The higher this value, the less the fps will reflect temporary variations
// A value of 1 will only keep the last value
var filterStrength = 20;
var frameTime = 0, lastLoop = new Date, thisLoop;

function gameLoop(){
  // ...
  var thisFrameTime = (thisLoop=new Date) - lastLoop;
  frameTime+= (thisFrameTime - frameTime) / filterStrength;
  lastLoop = thisLoop;
}

// Report the fps only every second, to only lightly affect measurements
var fpsOut = document.getElementById('fps');
setInterval(function(){
  fpsOut.innerHTML = (1000/frameTime).toFixed(1) + " fps";
},1000);

La ‘vida media’ de este filtro (la cantidad de fotogramas necesarios para pasar a la mitad del valor anterior a un valor nuevo y estable) es filterStrength*Math.log(2) (aproximadamente el 70% de la fuerza).

Por ejemplo, una fuerza de 20 se moverá a la mitad de un cambio instantáneo en 14 fotogramas, 3/4 del camino en 28 fotogramas, el 90 % del camino en 46 fotogramas y el 99 % del camino en 92 fotogramas. Para un sistema que se ejecuta a aproximadamente 30 fps, un cambio repentino y drástico en el rendimiento será obvio en medio segundo, pero aún así “desechará” las anomalías de un solo cuadro, ya que solo cambiarán el valor en un 5% de la diferencia.

Aquí hay una comparación visual de diferentes intensidades de filtro para un juego de ~30 fps que tiene una caída momentánea a 10 fps y luego acelera hasta 50 fps. Como puede ver, los valores de filtro más bajos reflejan más rápidamente los cambios “buenos”, pero también son más susceptibles a contratiempos temporales:
ingrese la descripción de la imagen aquí

Finalmente, aquí está un ejemplo de usar el código anterior para comparar realmente un bucle de ‘juego’.

  • ¡Muy buen gráfico! ¿Cómo hiciste eso?

    – Código Benny

    13 de febrero de 2013 a las 23:12

  • @BennyNeugebauer Lo anterior se creó en Excel, solo porque era un poco más fácil para una sola vez que usando HTML5 Canvas para gráficos IIR bonitos.

    – Phrogz

    14 de febrero de 2013 a las 3:42

  • ¿Hay alguna razón por la que obtengo fps infinitos cuando uso el cuadro de animación de solicitud? ¿Estoy usando una intensidad de filtro de 1 para obtener los fps más precisos? solo sucede de vez en cuando (Infinite fps): jsfiddle.net/CezarisLT/JDdjp/10

    – Kivilius

    17 de diciembre de 2013 a las 22:23

  • @CezarisLT Infinite FPS == una devolución de llamada informando al mismo tiempo (new Date) como la convocatoria anterior. Puedes protegerte de esto rescatando if (thisLoop==lastLoop). Sin embargo, tenga en cuenta que si está utilizando una intensidad de filtro de 1, no tiene sentido usar el filtro en absoluto. Solo usa la respuesta de @SLaks.

    – Phrogz

    19 de diciembre de 2013 a las 1:49

  • @Phrogz Sé que llego un poco tarde a la fiesta… pero incluso cuando uso la protección sugerida contra el error de fps infinitos. sigo teniendo fps infinitos? ¿alguna sugerencia?

    – FutureCake

    15 de noviembre de 2017 a las 22:47


Avatar de usuario de SLaks
SLaks

En gameLoopmira la diferencia entre new Date y new Date del último ciclo (almacenarlo en una variable).
En otras palabras:

var lastLoop = new Date();
function gameLoop() { 
    var thisLoop = new Date();
    var fps = 1000 / (thisLoop - lastLoop);
    lastLoop = thisLoop;
    ...
}

thisLoop - lastLoop es el número de milisegundos que pasaron entre los dos bucles.

  • ¿podría reemplazar la nueva Fecha por Fecha. ahora (), lo mismo, pero más correcto en mi opinión

    – caub

    10 de agosto de 2016 a las 17:16

  • ¿Cómo actualizar la función gameLoop() según los fps reales? Porque cuando uso setInterval() puedo pasar el argumento del intervalo de tiempo solo una vez.

    – Jacek Dziurdzikowski

    25 de febrero de 2018 a las 11:35


  • @JacekDziurdzikowski: Llamar setTimeout() cada vez para programar el siguiente cuadro.

    – SLaks

    25 de febrero de 2018 a las 15:38


  • @JacekDziurdzikowski usando requestAnimationFrame es mejor para el rendimiento que usar cualquiera setInterval() o setTimeout() ya que automáticamente ajusta el tiempo a los fps.

    –Greyson R.

    27 de julio de 2019 a las 23:49

  • Si fuera por mí, preferiría usar algo como performance.now en lugar de fechas debido a sus ventajas en precisión y eficiencia.

    – AlphaHowl

    12 de noviembre de 2022 a las 11:08

Avatar de usuario de NVRM
NVRM

Mis 2 centavos:

Útil para mí para comparar optimizaciones. Queme un poco de recursos, por supuesto, solo para pruebas.

Idealmente, la velocidad de fotogramas de su aplicación siempre debe permanecer por debajo de los 50 ms por fotograma, en pleno uso, cuando se usan eventos, bucles, etc. Esto es igual a 20 FPS.

El ojo humano siente retrasos por debajo de 24 FPS, esto es 1000 / 24 = 41ms

Entonces, 41ms para un marco es la ventana de tiempo más pequeña, para mantener la fluidez natural. Más alto que eso debe evitarse.

let be = Date.now(),fps=0,info='';
requestAnimationFrame(
    function loop(){
        let now = Date.now()
        fps = Math.round(1000 / (now - be))
        be = now
        requestAnimationFrame(loop)
        if (fps < 35){
          kFps.style.color = "red"
          kFps.textContent = fps 
        } if (fps >= 35 && fps <= 41) {
            kFps.style.color = "deepskyblue"
            kFps.textContent = fps + " FPS"
          } else {
            kFps.style.color = "black"
            kFps.textContent = fps + " FPS"
        }
        kpFps.value = fps;
        info+=(''+new Date()+' '+fps+'\n');
    }
 )
<span id="kFps"></span>
<progress id="kpFps" value="0" min="0" max="100" style="vertical-align:middle"></progress>
<button onclick="try{console.clear();console.info(info)}catch{}">Statistics</button>

¡Solo un ciclo de prueba para tener una idea, intervalo de 50 ms, debería mantenerse sin problemas!

¿Ves la barra de progreso arriba de saltar? ^

Esas son pérdidas de cuadros, el navegador está tratando de mantenerse al día mediante el sacrificio, saltando al siguiente cuadro. Esos picos hay que evitarlos. El siguiente fragmento quema recursos (es decir, FPS):

let t
for (let i=0;i<99999;i++){
  t = setTimeout(function(){
   console.log("I am burning your CPU! " + i)
   clearTimeout
  },50)
  
}

Las versiones recientes de los depuradores tienen un contador de FPS, en el performance pestaña, al grabar. Esto no es perfecto porque sobrecarga las pruebas.

ingrese la descripción de la imagen aquí

Qué pasa requestAnimationFrame?

var before,now,fps;
before=Date.now();
fps=0;
requestAnimationFrame(
    function loop(){
        now=Date.now();
        fps=Math.round(1000/(now-before));
        before=now;
        requestAnimationFrame(loop);
        console.log("fps",fps)
    }
 );

Yo uso esto para calcular fps

  var GameCanvas = document.getElementById("gameCanvas");
  var GameContext = doContext(GameCanvas,"GameCanvas");
  var FPS = 0;
  var TimeNow;
  var TimeTaken;
  var ASecond = 1000;
  var FPSLimit = 25;
  var StartTime = Date.now();
  var TimeBefore = StartTime;
  var FrameTime = ASecond/FPSLimit;
  var State = { Title:0, Started:1, Paused:2, Over:3 };
  var GameState = State.Title;

  function gameLoop() {
    requestAnimationFrame(gameLoop);
    TimeNow = Date.now();
    TimeTaken = TimeNow - TimeBefore;

    if (TimeTaken >= FrameTime) {
      FPS++
      if((TimeNow - StartTime) >= ASecond){
        StartTime += ASecond;
        doFPS();
        FPS = 0;
      }

      switch(GameState){
        case State.Title :
          break;
        case State.Started :
          break;
        case State.Paused :
          break;
        case State.Over :
          break;
      }
      TimeBefore = TimeNow - (TimeTaken % FrameTime);
    }
  }

  Sprites.onload = function(){
    requestAnimationFrame(gameLoop);
  }

  function drawText(Context,_Color, _X, _Y, _Text, _Size){
    Context.font =  "italic "+ _Size +" bold";
    Context.fillStyle = _Color;
    Context.fillText(_Text, _X, _Y);
  }

  function doFPS()(
    drawText(GameContext,"black",10,24,"FPS : " + FPS,"24px");
  }

  function doContext(Canvas,Name){
    if (Canvas.getContext) {
      var Context = Canvas.getContext('2d');
      return Context;
    }else{
      alert( Name + ' not supported your Browser needs updating');
    }
  }

¿Ha sido útil esta solución?