45

How would I check the framerate in my javascript code? I'm using this to loop:

gameloopId = setInterval(gameLoop, 10);
JSCoder
  • 5
  • 4
CyanPrime
  • 5,096
  • 12
  • 58
  • 79

5 Answers5

114

The code by @Slaks gives you only the instantaneous FPS of the last frame, which may vary or be misleading with hiccups. I prefer to use an easy-to-write-and-compute low-pass filter to remove quick transients and display a reasonable pseudo-average of recent results:

// 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);

The 'halflife' of this filter—the number of frames needed to move halfway from the old value to a new, stable value—is filterStrength*Math.log(2) (roughly 70% of the strength).

For example, a strength of 20 will move halfway to an instantaneous change in 14 frames, 3/4 of the way there in 28 frames, 90% of the way there in 46 frames, and 99% of the way there in 92 frames. For a system running at about 30fps, a sudden, drastic shift in performance will be obvious in half a second, but will still 'throw away' single-frame anomalies as they will only shift the value by 5% of the difference.

Here is a visual comparison of different filter strengths for a ~30fps game that has a momentary dip to 10fps and then later speeds up to 50fps. As you can see, lower filter values more quickly reflect 'good' changes, but also are more susceptible to temporary hiccups:
enter image description here

Finally, here is an example of using the above code to actually benchmark a 'game' loop.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Very nice graphic! How did you do that? – Benny Code Feb 13 '13 at 23:12
  • 5
    @BennyNeugebauer The above was created in Excel, only because it was slightly easier for a one-off than [using HTML5 Canvas for pretty IIR graphing](http://phrogz.net/js/framerate-independent-low-pass-filter.html). – Phrogz Feb 14 '13 at 03:42
  • Is there any reason why I'm getting infinite fps when using request animation frame. I'm using a filter strength of 1 for the most accurate fps? only happens from time to time (Infinite fps) : http://jsfiddle.net/CezarisLT/JDdjp/10/ – Kivylius Dec 17 '13 at 22:23
  • @CezarisLT Infinite FPS == one callback reporting the same time (`new Date`) as the previous call. You can guard for this by bailing `if (thisLoop==lastLoop)`. However, note that if you're using a filter strength of `1`, there's no point in using the filter at all. Just use @SLaks answer. – Phrogz Dec 19 '13 at 01:49
  • @Phrogz I know i am a bit late to the party.. but even when i use your suggested guard agains the infinite fps bug. i still get infinite fps? any suggestions? – FutureCake Nov 15 '17 at 22:47
33

In gameLoop, look at the difference between new Date and new Date from the last loop (store it in a variable).
In other words:

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

thisLoop - lastLoop is the number of milliseconds that passed between the two loops.

Patrick Hund
  • 19,163
  • 11
  • 66
  • 95
SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • 5
    could your replace new Date by Date.now(), same thing, but more correct imo – caub Aug 10 '16 at 17:16
  • How to refresh the function gameLoop() depending on actual fps? Because when using setInterval() I can pass the argument of time interval only once. – Jacek Dziurdzikowski Feb 25 '18 at 11:35
  • @JacekDziurdzikowski: Call `setTimeout()` each time to schedule the next frame. – SLaks Feb 25 '18 at 15:38
  • 2
    @JacekDziurdzikowski Using `requestAnimationFrame` is better for performance than using either `setInterval()` or `setTimeout()` since it automatically adjusts the timing to the fps. – Greyson R. Jul 27 '19 at 23:49
  • If it were up to me, I'd prefer using something like performance.now instead of dates because of its advantages in accuracy and efficiency – Krokodil Nov 12 '22 at 11:08
7

My 2 cents:

Useful to me to compare optimizations. Burn a bit of resources, of course, for testing only.

Ideally, your app frame rate should always stay well under 50ms by frame, at full usage, when using events, loops, etc. This is equal to 20FPS.

The Human eye feels lags under 24 FPS, this is 1000 / 24 = 41ms

So, 41ms for a frame is the smallest time window, to maintain natural fluidity. Higher than that is to be avoided.

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>

Just a test loop to get the idea, 50ms interval, should keep up smooth!

See the progress bar above jumping? ^

Those are frames losses, the browser is trying to keep up by sacrifice, by jumping to the next frame. Those spikes are to be avoided. The next snippet burns resources (i.e FPS):

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

Recent versions of debuggers, have a FPS counter, in the performance tab, when recording. This is not perfect because it overload the testing.

enter image description here

NVRM
  • 11,480
  • 1
  • 88
  • 87
3

What about 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)
    }
 );
davidmars
  • 653
  • 6
  • 8
2

I use this to calculate 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');
    }
  }
sha1962
  • 21
  • 1