Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frameRate() doesn't accurately report frame rate, it is the drawRate #6013

Closed
1 task done
quinton-ashley opened this issue Feb 10, 2023 · 27 comments · Fixed by #6269
Closed
1 task done

frameRate() doesn't accurately report frame rate, it is the drawRate #6013

quinton-ashley opened this issue Feb 10, 2023 · 27 comments · Fixed by #6269

Comments

@quinton-ashley
Copy link
Contributor

quinton-ashley commented Feb 10, 2023

Most appropriate sub-area of p5.js?

  • Core/Environment/Rendering

p5.js version

1.5.0 (but also all)

Web browser and version

Any

Operating System

Any

Steps to reproduce this

This may not be considered a bug that y'all would want to fix in p5.js but I at least wanted to point out that it's inaccurate to refer to the result of the p5.js frameRate() function as a measure of FPS, like it is on this page:

https://github.com/processing/p5.js/wiki/Optimizing-p5.js-Code-for-Performance#frames-per-second-fps

It's also not correct to say "Calling frameRate() with no arguments returns the current framerate." which is on the reference page:

https://p5js.org/reference/#/p5/frameRate

Here's the reasons why.

EDIT:

I want to try summarizing the issue again. If a sketch's fps is high enough for it to display at 60hz completely stable, verifiable in Chrome's dev tools, frameRate() will still not return 60 every time. Hence, it does not report the refresh rate of the sketch. It's actually a calculation of the difference in time between when requestAnimationFrame runs the draw function and the previous time it did so. As far as I understand that's not always the same amount of time even if the frame rate always matches the computer display's refresh rate or target frame rate set in p5.js. So even if frames are always drawn on time frameRate() changes because there's some variation in the browser's delay between when a frame is drawn and when requestAnimationFrame runs its callback function. This makes the value frameRate() returns kind of random, which can be seen in the example sketch I made. Yet, because it roughly will keep time with the real frame rate, though on average it loses time, even though its values are not technically useful for performance testing, frameRate() values are useful visually as @davepagurek pointed out.

The name of frameRate() is therefore technically a misnomer because it doesn't return values that are accurate by common definition or the gaming community definition. I think ironically drawRate is an accurate name for it since it actually measures the rate between draw function calls and does not measure the frame rate by common definition. I did some research and with JS I think there's no way to get an accurate report of the actual frame rate of a sketch, which can only be viewed in browser dev tools after a profile recording is finished. Even if frameRate is not changed this info should be included in its documentation.

Calculating accurate FPS (gaming definition) is definitely possible though, as it's just the time one frame takes to render, not including any delays. I've included a function in p5play called getFPS() to make this kind of performance testing more accessible.

ORIGINAL POST:

FPS, in the context of performance testing, is typically a measure of how many frames renders could fit in one second if the code responsible for rendering a frame was run without delays, so it shouldn't be capped by the target frame rate or monitor refresh rate.

FPS should be calculated by subtracting the time immediately after a frame is drawn from the time immediately before its drawn. Whether a program is running slower or faster than the target frame rate, that result should be reflected in the FPS calculation.

The p5.js frameRate() function is calculated by including the delay between frames, capping the measure to roughly the target frame delay. So it only returns a roughly accurate measure of FPS if the current frame rate is below the target frame rate. If p5 finishes running the pre draw functions, the draw function, and post draw functions before the delay allotted by the target frame rate, then the remaining delay between draw calls is included in the frameRate() calculation. If the sketch is running at a stable 60fps, which can be verified in the chrome dev tools, then frameRate() should just return 60 every time, but it doesn't. It returns wildly inaccurate values ranging from as low as 53 to as high as 68 in my testing. If there's some good reason for this which I can't think of, please let me know.

When a sketch is running at a stable 60fps, frameRate() returns a value that isn't a useful measure of the real frame rate or the FPS.

@welcome
Copy link

welcome bot commented Feb 10, 2023

Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, please make sure to fill out the inputs in the issue forms. Thank you!

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 10, 2023

I can help do the implementation and/or documentation changes after a decision on this is reached.

@davepagurek
Copy link
Contributor

I think your use case is also a valid one, although I don't think it's necessarily more useful, or the main understanding of the term "frame rate." We have a few things that are all similar but slightly different, and all are valid things users might want to know:

  • frameRate(): how many frames p5 will draw per second, given current performance. This is useful for calculating how much to move objects each frame to maintain a constant velocity given maybe not constant draw times.
  • getTargetFrameRate(): how many frames we ideally want p5 to draw per second. This is useful if you want to animate something with constant velocity for a recording (e.g. saveGif) and don't care how long it takes to render each frame, since it will eventually be played back at an exact frame rate. This is also the way web animators used to think about frame rate in the Flash era, so there's some precedent here.
  • What you're describing, unfortunately also called frame rate by dev tools despite being different than what animation software typically calls frame rate, is useful for measuring how fast your draw call takes to execute to benchmark your code

We could maybe also add this measure in addition to the other two if we can think of a good name for it. Maybe if we measure the inverse we could call it something like drawTime? I wonder if maybe showing people the inverse could make more sense, since it's what we're measuring, and calling it something related to frame rate might give the incorrect assumption that one could ever get the browser to draw at that rate, when it's generally capped at the refresh rate of the monitor, which could be max 60Hz.

In any case, this could benefit with some more docs!

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 11, 2023

I made this sketch demonstrating the problem so that it can be seen visually:

https://editor.p5js.org/quinton-ashley/sketches/3qbl_XPxp

When the frameRate is scaled down it can be used to move objects each frame at what appears to be a constant velocity, because visually the difference is minimal. Mathematically it's still real bad but I do see this use case being totally fine visually which surprised me.

Another bug I found was that console.log causes major frame timing issues in the p5.js web editor. I think it's somehow blocking the main JS thread. Using console.log is super useful for game development so this is a big problem. I'm going to open a separate issue report in the p5js web editor repo. processing/p5.js-web-editor#2116

@quinton-ashley
Copy link
Contributor Author

I think keeping the current behavior of frameRate() is best if people are using it to try to move objects each frame to maintain a constant velocity given inconsistent draw times. Visually it actually works with enough downscaling of the frame rate value, even if technically it's wrong.

So perhaps a getFPS() function would be best for accurately getting FPS and the documentation can be changed to explain the difference.

@davepagurek
Copy link
Contributor

I'm not sure if I'm fully understanding your middle test, but here's one I've made to try to illustrate what I was describing a bit better: https://editor.p5js.org/davepagurek/sketches/K7cKw7bie The idea here is to have objects move at a constant distance per time, regardless of how much time has passed. The time we care about here is the time between frames drawn, not the time since the start of the current draw. Here I'm comparing a position calculated directly from millis(), which is the ideal value we're trying to achieve, but is not always possible to use (e.g. a physics based animation can't derive its state entirely from millis(), it needs to update based on the previous frame's state.)

In any case, I think maybe getFPS() might sound a bit too similar to frameRate(). Could we call it drawRate maybe, since it would just be measuring the rate based on how long only the draw takes?

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 12, 2023

Problem with drawRate is that it sounds like what frameRate is trying to output, the rate at which the draw function is called. getFPS seems more straightforward to me. The term "FPS" would be something people would search when seeking out the function name.

@limzykenneth
Copy link
Member

limzykenneth commented Feb 12, 2023

Sorry I don't quite understand this. FPS (or frames per second) is the measurement unit of frame rate, and for most intents and purposes are treated to be equivalent, eg. Wikipedia, Adobe.

In performance testing or benchmarking we use execution per second (or sometimes operations per second) since the concept of a frame don't make sense when you are measuring code execution, or if you are only interested in the execution time of an animation frame a millisecond measure is used which is what the web browser performance profilers tend to use.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 12, 2023

Yeah the wikipedia page is incomplete. I will add to it.

Among game developers and players, refresh rate or hertz (hz) is the rate at which a monitor can refresh per second. Frame rate or fps is the rate at which the game is able to render a frame, regardless of the actual refresh rate of the computer's monitor.

Companies like Nvidia and Steam offer on screen FPS performance tools that show the FPS of a game and big Youtube channels like LinusTechTips also use the above definition of the terms frame rate and FPS.

https://www.nvidia.com/en-us/geforce/news/what-is-fps-and-how-it-helps-you-win-games/#:~:text=Simply%20put%2C%20FPS%20is%20the,display%20shows%20those%20completed%20frames.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 12, 2023

The reason that FPS (in gaming terms) can't be measured in p5.js accurately by users using millis() is that they have no way to include the p5.js pre and post draw functions while not including the delay for the next animation frame. I think I could implement FPS calculations in the pre and post registered methods of p5play but it'd be a bit less accurate than if it was implemented in p5.js _draw. It wouldn't be a big difference though so it's a good alternative.

Because frame rate among game devs and gamers has a different definition than frame rate among animators and visual artists, I think having separate frameRate and getFPS functions would be best. If it'd be better for the broader p5.js community for it to have a different name like drawRate, that's fine and then in p5play I can just make getFPS an alias to the p5js function. I do think the function would be useful for visual artists testing their programs too.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 12, 2023

Also I noticed a potential problem with the p5.js _draw function.

_this.redraw();
_this._frameRate = 1000 / (now - _this._lastFrameTime);
_this.deltaTime = now - _this._lastFrameTime;
_this._setProperty('deltaTime', _this.deltaTime);
_this._lastFrameTime = now;

Am I wrong or is it wrong for it to update the frame rate calculation after redraw has already called the user's draw function. It's using now which is actually the start time of _draw and lastFrameTime is the start time of the previous frame. So to me it seems like when frameRate is called in draw it's always returning the previous frame's frame rate.

I think that's why the bottom circle looks like it's lagging behind the top circle in @davepagurek 's sketch even though I think they should technically move at the same rate.

The solution would be to move the calculation above redraw

I would reference the code directly but I can't seem to find the _draw function from the p5.js source code in this repo though.

@davepagurek
Copy link
Contributor

The term "FPS" would be something people would search when seeking out the function name.

If we add a method for this measurement in addition to frameRate() and getTargetFrameRate(), I think we'd need to include an explanation of the different between the three in the docs for all three, since all have such similar interpretations that one could conceivably end up on the docs for any of them from a google search of these terms.

FPS (or frames per second) is the measurement unit of frame rate, and for most intents and purposes are treated to be equivalent

Because of this, I think I'd still opt for calling this measurement something more descriptive but perhaps less standardized, like drawTime (or another alternative if someone has a suggestion):

  • "frameRate" + "targetFrameRate" are different enough that I think one can talk about their difference clearly, but in conversation, frame rate and FPS could be more easily mistaken for synonyms
  • While the Hz/FPS distinction Nvidia claims may be the case in the gaming world, the film/animation world has been using "frame rate" (FPS being the unit one measures frame rate in) without that distinction for longer

Am I wrong or is it wrong for it to update the frame rate calculation after redraw has already called the user's draw function. It's using now which is actually the start time of _draw and lastFrameTime is the start time of the previous frame. So to me it seems like when frameRate is called in draw it's always returning the previous frame's frame rate.

Here's a sketch using the changes below, confirming that one can then use frameRate to exactly match millis(): https://editor.p5js.org/davepagurek/sketches/VXHoBahkN

diff --git a/src/core/main.js b/src/core/main.js
index cb7a438b..41e4b215 100644
--- a/src/core/main.js
+++ b/src/core/main.js
@@ -385,13 +385,13 @@ class p5 {
         time_since_last >= target_time_between_frames - epsilon
       ) {
         //mandatory update values(matrixes and stack)
-        this.redraw();
         this._frameRate = 1000.0 / (now - this._lastRealFrameTime);
         this.deltaTime = now - this._lastRealFrameTime;
         this._setProperty('deltaTime', this.deltaTime);
         this._lastTargetFrameTime = Math.max(this._lastTargetFrameTime
           + target_time_between_frames, now);
         this._lastRealFrameTime = now;
+        this.redraw();

         // If the user is actually using mouse module, then update
         // coordinates, otherwise skip. We can test this by simply

@limzykenneth can you think of anything that this change would break?

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 12, 2023

Nice! @davepagurek

We could keep redraw above the lastTargetFrameTime and lastRealFrameTime calculations.

Also if the deltaTime calculation is done first it can be used in the _frameRate calculation.

I can make these edits and submit the request now that I know the file it's in.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 13, 2023

I made it clear in the pr that it doesn't fix the initial issue here though.

I want to try summarizing the issue again. If a sketch's fps is high enough for it to display at 60hz completely stable, verifiable in Chrome's dev tools, frameRate() will still not return 60 every time. Hence, it does not report the refresh rate of the sketch. It's actually a calculation of the difference in time between when requestAnimationFrame runs the draw function and the previous time it did so. As far as I understand that's not always the same amount of time even if the frame rate always matches the computer display's refresh rate or target frame rate set in p5.js. So even if frames are always drawn on time frameRate() changes because there's some variation in the browser's delay between when a frame is drawn and when requestAnimationFrame runs its callback function. This makes the value frameRate() returns kind of random, which can be seen in the example sketch I made. Yet, because it roughly will keep time with the real frame rate, though on average it loses time, even though its values are not technically useful for performance testing, frameRate() values are useful visually as @davepagurek pointed out.

The name of frameRate() is therefore technically a misnomer because it doesn't return values that are accurate by common definition or the gaming community definition. I think ironically drawRate is an accurate name for it since it actually measures the rate between draw function calls and does not measure the frame rate by common definition. I did some research and with JS I think there's no way to get an accurate report of the actual frame rate of a sketch, which can only be viewed in browser dev tools after a profile recording is finished. Even if frameRate is not changed this info should be included in its documentation.

Calculating accurate FPS (gaming definition) is definitely possible though, as it's just the time one frame takes to render, not including any delays. I think drawRate would be a confusing name because when I hear that it sounds like it'd measure the rate at which the draw function is called which would include the frame delay. I understand why getFPS would be undesirable since by common definition frame rate and fps are equivalent. I will think more about a name.

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 13, 2023

I just noticed @davepagurek you said drawTime not drawRate, whoops. Hmm I have to think about it more.

@quinton-ashley
Copy link
Contributor Author

How about this @davepagurek I like the name drawTime for a variable but instead of it storing the fps value (in gaming terms) it could store the time it took the last pre draw, draw, and post draw functions to run. That'd be a more accessible metric, easily explainable to general p5.js users.

p5play could then use drawTime in a getFPS function.

@davepagurek
Copy link
Contributor

That sounds good to me!

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 14, 2023

I'll try implementing drawTime today.

@limzykenneth
Copy link
Member

Sorry I don't have much time to review everything at the moment but from what I gather, would something like stats.js be better suited for your use case if you are only interested in the execution peformance of the code within the draw function?

It can be used like

function draw(){
  stats.begin();
  // Your code
  stats.end();
}

@davepagurek
Copy link
Contributor

@limzykenneth I think the issue is that the p5.play library registers pre-draw and post-draw hooks that @quinton-ashley would like to include in the measurement (non trivial stuff like physics I assume?)

So the begin/end would have to surround this whole block:

p5.js/src/core/structure.js

Lines 486 to 493 in 2f484fd

context._registeredMethods.pre.forEach(callMethod);
this._inUserDraw = true;
try {
context.draw();
} finally {
this._inUserDraw = false;
}
context._registeredMethods.post.forEach(callMethod);

@limzykenneth
Copy link
Member

limzykenneth commented Feb 14, 2023

Also to note that for web canvas, there is no difference between the frame rate and the monitor refresh rate because according to the W3C recommendation, the frame rate at which requestAnimationFrame should be run by the browser is tied to the monitor refresh rate. This means that for any requestAnimationFrame based animation, if you view it on a 60Hz monitor it will have a different frame rate to a 24Hz projector for example. As per MDN:

[requestAnimationFrame] will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation.

This may be different from some game engines where they may separate the update loop (which is usually CPU bound) and the render loop (which is GPU bound) in that in the browser the two loops are combined. requestAnimationFrame is also on a best effort basis, so on a 144Hz monitor if your code cannot reach 144 execution per second, it will not run at 144Hz.

@quinton-ashley
Copy link
Contributor Author

yes exactly @davepagurek the idea is to included the registered methods:
https://github.com/processing/p5.js/blob/main/contributor_docs/creating_libraries.md

p5play by default does all the physics calculations in its post draw function which typically takes longer than drawing on the canvas.

@quinton-ashley
Copy link
Contributor Author

@limzykenneth that's interesting info. If that's the case then could it be possible for frameRate work as intended in p5.js? How can the value vary so much even when rendering 60fps stable? 🤔

@davepagurek
Copy link
Contributor

Also to note that for web canvas, there is no difference between the frame rate and the monitor refresh rate because according to the W3C recommendation, the frame rate at which requestAnimationFrame should be run by the browser is tied to the monitor refresh rate. This means that for any requestAnimationFrame based animation, if you view it on a 60Hz monitor it will have a different frame rate to a 24Hz projector for example.

I think this is a good reason for making this a measurement of time instead of a measurement of rate, as it would simply be a measurement of how long your code takes to draw without implying that you could achieve 1000/drawTime frames per second rendering in your sketch. If we add this measurement, I think it'd be important we add this info to the docs for it.

@limzykenneth
Copy link
Member

limzykenneth commented Feb 14, 2023

The main thing is that requestAnimationFrame is exactly as the name implies, it is a request. The main reason is that JS is single threaded only and so everything will be put on the event queue (think a long queue of commands to execute for each cycle) so the code that's requested to be executed will be timed to be put onto the event queue at 60 fps instead of being run immediately as one may aspect. Depending on what the browser has going on at the moment, including (ergh..) garbage collection, there can be a bit of a time delay until when the animation frame is actually executed but it will not execute before that because of things before it in the event queue.

Frame rate in p5.js is in actual fact an illusion. If you try to set frame rate above 60 on a 60Hz monitor, you will notice that your sketch still runs at 60 fps. For frame rate lower than 60 fps we basically skip frames to get an average of the request frame rate over time for the illusion of a lower frame rate. The example shared in #5354 is a particularly nice example of this in action: https://editor.p5js.org/delphi1024/sketches/lF1shtB_L

@quinton-ashley
Copy link
Contributor Author

quinton-ashley commented Feb 14, 2023

@limzykenneth Yes that's what I thought.

Even though the specifications say requestAnimationFrame is tied to the monitor refresh rate, when it processes the request is not, even if the time it starts processing the request allows for plenty of time afterwards for p5.js to render a frame and maintain a stable 60fps.

Here is a test I made of requestAnimationFrame on its own without p5.js and there's still time slippage between expected and actual time values.
https://editor.p5js.org/quinton-ashley/sketches/dK1LOlNv9

I think this issue can be closed once a simplified summary of this info is added to the frameRate reference page.

@limzykenneth
Copy link
Member

Even though the specifications say requestAnimationFrame is tied to the monitor refresh rate, when it processes the request is not, even if the time it starts processing the request allows for plenty of time afterwards for p5.js to render a frame and maintain a stable 60fps.

Yes, that's right. A detailed description is here: https://javascript.info/event-loop and that requestAnimationFrame runs just before the the "render" step after the macrotasks and microtasks, so if the macrotasks or microtasks takes too long, requestAnimationFrame will just wait.

@quinton-ashley quinton-ashley changed the title frameRate() doesn't accurately report fps frameRate() doesn't accurately report frame rate, it is the drawRate Jul 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants