For personal education I am trying to build a Pong implementation in JavaScript / HTML.
I have been reading up on rendering performance, but when I apply the theory in practice, it seems to fall short.
From my understanding, requestAnimationFrame should provide me with fluid stable 60 fps animation, given my logic and subsequent rendering can be completed within a 16 ms timeframe. In this case, each iteration takes between 1 - 2 ms and yet, I see my framerate jumps between 45 fps and 75 fps, occasionally producing "long frames".
I am running Chrome on OS X 10.12 on a Mac Mini (Late 2012).
I have tried using a canvas, I have tried a CSS animate approach and I have tried a top/left approach. I have tried Safari and I have tried incognito. However, the jank remains.
What am I missing?
For reference, here is my script (in CoffeeScript):
class window.PongGame
#launch: ->
window.pong = new PongGame()
pong.start()
constructor: ->
#ball_size = 32
#width = 640
#height = 480
#y = #height / 2
#x = 0
#yVel = 3
#xVel = 3
#ball = document.getElementById("pong_ball")
start: ->
requestAnimationFrame => #new_frame()
new_frame: ->
#handle_collisions()
#move_ball()
#animate()
requestAnimationFrame => #new_frame()
handle_collisions: ->
if (0 <= #x <= #width - #ball_size) is false
#xVel = -#xVel
if (0 < #y < #height - #ball_size) is false
#yVel = -#yVel
move_ball: ->
#x += #xVel
#y += #yVel
animate: ->
#ball.setAttribute("style", "transform: translate(#{#x}px, #{#y}px); width: #{#ball_size}px; height: #{#ball_size}px")
Jsfiddle: https://jsfiddle.net/nielsbuus/ptLk3ma0/2/
Update
I have tested my code on an older MacBook Pro (2011) and the framerate is almost stable - a lot closer to 60 fps fixed than my Mac Mini.
There are several fun facts to keep in mind, with regard to requestAnimationFrame performance in browsers...
Using the profiler affects the results. I'm working on a three.js based WebGL project where the integrated FPS counter reports a relatively steady 60fps. But, if I activate Chrome's timeline tool and measure my frame rendering times, the timeline shows the app dropping 4-6 frames per second, even when there is no code in the render loop at all.
The browser manages lots of other activities, and they can affect animation performance. Running my app in a long lived Firefox instance with lots of memory in use always gets worse performance than running it in a fresh instance. Streaming video in another tab in Chrome cuts the framerate for my app in another down to 30 FPS (probably because they are both contending for GPU time). Garbage collection can eat tons of time out of a frame budget, minimizing object allocations and GC is an important concern for complex apps with lots of animation.
In particular, when using Chrome's timeline tool as you are, be sure to enable JS Profiling, and expand the GPU activity band. Your code is probably well within the 16.6ms frame budget, but on my system a dropped frame is usually associated with heavy GPU activity.
My advice? Don't take the profiler's results as absolute or representative of real world performance, and make peace with browser animation performance being a bit of a roller coaster :).
You might also consider using the FPS counter developed for three.js (but usable anywhere I believe), which I presume has less of a distorting impact than the heavyweight profiling tools in modern browsers. Keeping it running in development can alert you to serious performance problems as you run into them.
Related
Background
While working on a complex full-screen 2D animation utilizing multiple <canvas> layers I found out a problem with Safari. The rendering performance drops sharply after the canvas gets bigger than exactly 3840x3840. I ran multiple tests (benchmark I wrote: https://codepen.io/kiler129/full/Exbgrqp) and obtained this peculiar graph:
(click for a readable full-size copy)
Data analysis
Steady 60 FPS/realtime is kept until 3840x3840, then it drops to unusable 2-25 FPS
There's a correlation between larger fill % (100% vs 20%) of the canvas and FPS, as well as larger number of draw calls per frame (d/f) and FPS -> nothing unusual here, it's pretty linear
Buffered canvases (drawing to off-screen & then copying) are way less performant for simple operations
Brave, and any other Chromium-based browsers, keep steady 60 FPS basically forever (even with screen-scaled 10000x10000 plane)
2018 iPad running Safari behaves identically to 2021 MBP with M1 Max, just a little bit slower overall
The 3840px is exactly 4K. While I have a 4K monitor disconnecting it and rebooting doesn't change the steep drop point (MBP has a 3456x2234 screen). Even assuming that the 4K resolution is cached somewhere this doesn't explain the same drop on the iPad which has nothing to do with 4K screens.
I also checked what actually takes time in Safari and it seems that the rendering to the screen and not the actual drawing on canvas is the culprit. That will explain why using buffering gives such bad results. The times for drawing are actually slower on Chromium-based browsers, yet the times are hardly correlated to anything:
Why such a large canvas?!
To avoid X-Y problem situation, I'm answering this ahead of time. I stumbled upon this issue in a real-world tests and then prepared the benchmark trying to eliminate all variables from the real-world application. 3840px is perfectly reasonable when used on HiDPI/"retina" displays; with x2 scaling it gives 1920px screen real estate which is quite normal. While the benchmark clips the viewport the real code doesn't (it doesn't seem to make a measurable difference).
Bug?
Is there anything I can do here, or this is a some sort of a limitation/bug of the WebKit/Safari? While testing the Safari doesn't crash but window becomes very unresponsive until the rendering is stopped.
The only solution I see is detecting FPS drop and disabling retina scaling on Safari.
I am developing a web application (JS/CSS/AngularJS) which is going to run on a touchscreen client.
As the application grows I run into a performance problem: I mentioned that the applicaion is not running with 60fps but 30 or something. Therefore I used the chrome built-in timeline tool to profile what's going on under the hood.
Okay - there are many long frames which limit the fps to ~30. The reason are the large GPU blocks (green) you can see in the screenshots).
Zoomed in:
It seems that the GPU is taking much time to... render? calculate? ...?
Unfortunally I don't have any further information what the GPU does in this time. There are some CSS effects and transforms but nothing really spectacular.
It's running on a Intel HD Graphics 530 (onboard) but I think is should even perform with 60fps on this one.
I'd like to know if there is any way to get further information what is slowing down the GPU that much and/or if there are any common CSS tricks to speed up my page. Or is there anything I can tweak at the GPU itself?
Thanks!
For educational purposes I need to compare the performance of WebGL with OpenGL. I have two equivalent programs written in WebGL and OpenGL, now I need to take the frame rate of them and compare them.
In Javascript I use requestAnimationFrame to animate, and I noticed that it causes the frame rate to be always at 60 FPS, and it goes down only if I switch tab or window. On the other hand if I always call the render function recursively, the window freezes for obvious reasons.
This is how I am taking the FPS:
var stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '450px';
stats.domElement.style.top = '750px';
document.body.appendChild( stats.domElement );
setInterval( function () {
stats.begin();
stats.end();
}, 1000 / 60 );
var render= function() {
requestAnimationFrame(render);
renderer.render(scene,camera);
}
render();
Now the problem if having always the scene at 60 FPS is that I cannot actually compare it with the frame rate of OpenGL, since OpenGL redraws the scene only when it is somehow modified (for example if I rotate the object) and glutPostRedisplay() gets called.
So I guess if there is a way in WebGL to redraw the scene only when it is necessary, for example when the object is rotated or if some attributes in the shaders are changed.
You can't compare framerates directly across GPUs in WebGL by pushing frames. Rather you need to figure out how much work you can get done within a single frame.
So, basically pick some target framerate and then keep doing more and more work until you go over your target. When you've hit your target that's how much work you can do. You can compare that to some other machine or GPU using the same technique.
Some people will suggest using glFinish to check timing. Unfortunately that doesn't actually work because it stalls the graphics pipeline and that stalling itself is not something that normally happens in a real app. It would be like timing how fast a car can go from point A to point B but instead of starting long before A and ending long after B you slam on the brakes before you get to B and measure the time when you get to B. That time includes all the time it took to slow down which is different on every GPU and different between WebGL and OpenGL and even different for each browser. You have no way of knowing how much of the time spent is time spent slowing down and how much of it was spent doing the thing you actually wanted to measure.
So instead, you need to go full speed the entire time. Just like a car you'd accelerate to top speed before you got to point A and keep going top speed until after you pass B. The same way they time cars on qualifying laps.
You don't normally stall a GPU by slamming on the breaks (glFinish) so adding the stopping time to your timing measurements is irrelevant and doesn't give you useful info. Using glFinish you'd be timing drawing + stopping. If one GPU draws in 1 second and stops in 2 and another GPU draws in 2 seconds and stops in 1, your timing will say 3 seconds for both GPUs. But if you ran them without stopping one GPU would draw 3 things a second, the other GPU would only draw 1.5 things a second. One GPU is clearly faster but using glFinish you'd never know that.
Instead you run full speed by drawing as much as possible and then measure how much you were able to get done and maintain full speed.
Here's one example:
http://webglsamples.org/lots-o-objects/lots-o-objects-draw-elements.html
It basically draws each frame. If the frame rate was 60fps it draws 10 more objects the next frame. If the frame rate was less than 60fps it draws less.
Because browser timing is not perfect you might be to choose a slightly lower target like 57fps to find how fast it can go.
On top of that, WebGL and OpenGL really just talk to the GPU and the GPU does the real work. The work done by the GPU will take the exact same amount of time regardless of if WebGL asks the GPU to do it or OpenGL. The only difference is in the overhead of setting up the GPU. That means you really don't want to draw anything heavy. Ideally you'd draw almost nothing. Make your canvas 1x1 pixel, draw a single triangle, and check the timing (as in how many single triangles can you draw one triangle at a time in WebGL vs OpenGL at 60fps).
It gets even worse though. A real app will switch shaders, switch buffers, switch textures, update attributes and uniforms often. So, what are you timing? How many times you can call gl.drawBuffers at 60fps? How many times you can call gl.enable or gl.vertexAttribPointer or gl.uniform4fv at 60fps? Some combination? What's a reasonable combination? 10% calls to gl.verterAttribPointer + 5% calls to gl.bindBuffer + 10% calls to gl.uniform. The timing of those calls are the only things different between WebGL and OpenGL since ultimately they're talking to the same GPU and that GPU will run the same speed regardless.
You actually do not want to use framerate to compare these things because as you just mentioned you are artificially capped to 60 FPS due to VSYNC.
The number of frames presented will be capped by the swap buffer operation when VSYNC is employed and you want to factor that mess out of your performance measurement. What you should do is start a timer at the beginning of your frame, then at the end of the frame (just prior to your buffer swap) issue glFinish (...) and end the timer. Compare the number of milliseconds to draw (or whatever resolution your timer measures) instead of the number of frames drawn.
The correct solution is to use the ANGLE_timer_query extension when available.
Quoting from the specification:
OpenGL implementations have historically provided little to no useful
timing information. Applications can get some idea of timing by
reading timers on the CPU, but these timers are not synchronized with
the graphics rendering pipeline. Reading a CPU timer does not
guarantee the completion of a potentially large amount of graphics
work accumulated before the timer is read, and will thus produce
wildly inaccurate results. glFinish() can be used to determine when
previous rendering commands have been completed, but will idle the
graphics pipeline and adversely affect application performance.
This extension provides a query mechanism that can be used to
determine the amount of time it takes to fully complete a set of GL
commands, and without stalling the rendering pipeline. It uses the
query object mechanisms first introduced in the occlusion query
extension, which allow time intervals to be polled asynchronously by
the application.
(emphasis mine)
I've been having trouble using the canvas element and can't seem to locate the problem.
The slowdown happens every 5-15 seconds when the page is initially loaded. But, after tabbing over to facebook for a while and chatting with friends, it miraculousy stopped exhibiting slow downs.
My friends also see the periodic slowdowns and they use chrome/firefox
I have no idea what's happening! I'm reading up on memory leaks/heap profiling, but I'm having a hard time locating the problem.
I initially only had one ball bouncing around, but I decided to put in 1000 balls to try and test performance, sorry :P
EDIT1: Ok, I tried running the same code on Firefox(i've been using chrome) and the balls are moving significantly slower on firefox minus the horrible stutter I've been seeing on chrome... What's causing the browsers to use different speeds for the balls?
WARNING WARNING EPILEPSY WARNING Link to live demo
Here's the code
Because you are using request animation frame it will try to hit 60 fps, and if it can't then it will settle for 30 fps, until it can deliver 60 fps. So the slowdown is your canvas running at 30 fps, and the jump to 60 fps makes things seem to speed up.
The reason why you're only hitting 30 fps in the beginning can be varied. In my case my computer is plenty powerful but I've got multiple monitors and I just opened the example in a new window on a new monitor, I suspect my OS limits only 1 monitor at a time to 60 fps unless I'm full screened in a game that takes up all 3 monitors. But this is not your problem, I just use it as an example as to why you need to be profiling and running performance tests yourself on a variety of setups, because there are any number of things that can be causing performance problems.
One of the reasons you get a perceived "slow down" and "speed up" is because you're updating positions on a frame by frame basis, but the time in between frames varies from 33ms to 16ms depending on what the current frame rate is. It would be advisable to make all of your update functions use the delta time (or the elapsed time since the previous frame) as a measure of how much to update the positions.
What you have:
Ball.prototype.update = function() {
this.x += this.velocity.x;
this.y += this.velocity.y;
}
We want to include a delta time in here
Ball.prototype.update = function(deltaTime) {
this.x += this.velocity.x * deltaTime;
this.y += this.velocity.y * deltaTime;
}
The only difference here will be that you're going to have to express your velocity in relation to time. For example, your velocity will be expressed as 100 pixels per second, and your delta Time will be expressed in seconds as well (with a value of around .016 or .033)
This won't fix the 30 fps but it will make your balls move at a consistent speed. When you first load the page, the browser needs to load all the other scripts you're including, parse and evaluate them, and in some cases will fire off some Garbage Collection (as is the case with the included jQuery). If you don't need jQuery or other such additional files you can avoid including them.
More importantly, you don't have to start rendering full speed ahead as soon as the page loads. In the case of games, they'll have their own loading screens and title screens preceding actual game rendering that players won't be able to tell the difference between 30 fps and 60 fps on anyway.
currently i work on a little js/canvas game called "tunnel 2" (i'm pretty sure there's a well known year old version of this, but i know of none). you can try the game here. also, i'd recommend chrome.
so, i developed in google chrome, and it works fine, even on my crappy old machine. i get around ~30 fps. on my coworkers notebook it yields >100fps. so far, so good. safari seems to work well too.
next i tried it on firefox 4 beta 10 ... and i only get ~10 fps. but surely ff4 isn't that slow, right?
i started to investigate. here's my main loop:
// starts the game loop
this.run = function () {
this.draw();
var
t = this,
timeLastTurn,
timeThisTurn = (new Date()).getTime()-1;
var loop = function () {
timeLastTurn = timeThisTurn;
timeThisTurn = (new Date()).getTime();
// dt is the time difference between this turn and the last turn
var dt = timeThisTurn - timeLastTurn;
// player movement etc
t.turn(dt);
// draw game state
var res = t.draw();
// if there's no collision, game over
if (!res.collision)
t.setState(2);
// actually, there's a browser dependent minimum timeout that
// may vary. but even if it's more than 10ms - we don't care.
// game should run at the same speed (though not as smooth)
if (gameState == 1)
timer = window.setTimeout(loop, 5);
// just debug output
debug = dt;
}
// start the main loop
loop();
}
what i observed:
unsurprisingly, this.draw(); is by far the most expensive function, but it takes only some milliseconds (around 5, actually), on chrome ... and also on firefox. nowhere near the >100ms it would take for meager 10fps! the whole loop() call takes not much more either, on firefox it takes less than 10ms!
the difference can be seen if you investigate dt. it should be around time-loop()-takes+5ms timeout (or whatever the browser minimum timeout value is).
but on ff4 the value is closer to 180ms, aka the next timeout event fires in 170ms instead of 5ms! if you play a little longer, it goes up to ~800ms for a single frame (gc, for sure), then it's back to 180ms.
does anybody have an idea what the culprit could be?
is the GC to blame? on the one hand i don't think i create too many short lived variables, and hey, 150ms every time!? but of course it could be. is there an easy way to check this? the chrome profiler logs gc times (around 0.10%), but the firebug profiler doesn't.
also interesting: the game runs faster (~5fps) with firebug enabled.
add. info: using setInterval instead of setTimeout shouldn't and doesn't change anything.
I can confirm it's not working on FF 3.6.13 OS X.
While I was developing Snake JS I found a difference in setInterval behavior. You should really have a look at this:
var timer = setInterval(fn, 500)
// ...
timer = setInterval(fn, 500)
// Since I didn't use clearInterval() I now have two timers running in FF.
I see you're not using setInterval, but maybe it's something similar with setTimeout?
I have had major issues with some higher end canvas applications I've built and used when having to deal with firefox 6 and under. There are many issues that involve performance and just poorly or completely unsupported canvas capabilities in firefox. They did make a huge update in firefox 7 that improves many of the issues that you are probably noticing. It's kind of annoying, i need to support down to ff4 on a huge facebook canvas-based app that doesnt quite work that great in 6 and lower. I've done so many things to try and hack around the performance related issues and it's really difficult to find solutions for most things.