Followup to issue #642
I've followed the tutorial that suggested to call requestAnimationFrame() and render in a loop:
function render() {
requestAnimationFrame( render );
renderer.render( scene, camera );
}
render();
This bug caused my page to hog 1-2 CPU cores - 100% CPU usage on these cores - forever, even if nothing at all is happening in the scene, as long as my webpage was open, for days. It drains battery, uses electricity needlessly, and spins up fans. Thus, causing noise pollution and indirectly environmental pollution. Needless to say, that's a (page) killer. I was so frustrated, I was seriously trying to rewrite everything using another 3D library. (SceneJS doesn't seem to have the same problem.)
I think this needs to be fixed in ThreeJS. If nothing else, to save all our CPU cores on all the pages that use ThreeJS.
My workaround:
Finally (after more than 1 year), I realized that my scene only changes on user input. Therefore, I made my call to requestAnimationFrame() conditional on whether there was a user input in the last 2 seconds. If not, I would go into standby mode and not call requestAnimationFrame(). As soon as there's new action, I call render() again.
Here's my code:
function render(time) {
TWEEN.update(time);
renderer.render(scene, camera);
if (gLastMove + kStandbyAfter < Date.now()) {
gRunning = false;
} else {
gRunning = true;
requestAnimationFrame(render);
}
}
var gLastMove = Date.now();
var gRunning = true;
var kStandbyAfter = 2000; // ms
function requestRender() {
gLastMove = Date.now();
if ( !gRunning) {
requestAnimationFrame(render);
}
}
window.addEventListener("mousemove", requestRender, false);
window.addEventListener("keydown", requestRender, false);
Super-ugly. But this solved my problem. The CPU usage drops from 2 cores with 100% each to almost nothing, after 2 seconds of no activity on my page.
Your situation may wary, you may other factors other than user input that cause scene changes for you. But maybe it's a lot less than every 16ms, and more importantly, there may be phases of complete inactivity when you can turn things off entirely until some trigger. (In my case: mouse move)
I hope this helps. I still consider this to be a band-aid and ugly, and I think this needs to be fixed in ThreeJS.
I don't think this is a bug. There are many things that can change in a scene and if the engine had to keep track of everything it would affect performance. We concluded that the only person that knows when the scene needs to be rendered again is the user (you), and you've the ability to do that.
As @mrdoob said, this is very application specific. Invalidating scene state to request a re-render is an insanely hard problem and any solution will be very error prone, kind of like cache invalidation.
Btw, there are more sophisticated solutions out there, like mainloop.js, which is based on the principles of the fix your timestep series. It does not solve your problem, but gives you more control than requestAnimatonFrame does.
I don't have this problem with SceneJS. Run the SceneJS examples, run the ThreeJS examples, and compare CPU load. Esp. at idle. So, it seems it's solvable.
Can you upload the tests somewhere?
The SceneJS examples are at http://scenejs.org/examples/ . Seems like I misspoke, though: I'm getting almost the same bad CPU usage with these SceneJS examples.
My own workaround code is posted here in the 2. comment. You can see it live on http://www.labrasol.com .
@benbucksch said
Finally (after more than 1 year), I realized that my scene only changes on user input.
Why are you calling requestAnimationFrame
if you have a static scene? Just render once in response to user events.
Sorry, but this is clearly still an issue. No webpage should continue to use 100% CPU forever. But all the ThreeJS examples do that. Most developers will just copy that and waste CPU.
Instead, ThreeJS should use 1) only minimal (as little as possible) CPU power even with an animation, and 2) no CPU at all when there are no changes. This is easy to do with e.g. a dirty flag.
WestLangley:
Why are you calling requestAnimationFrame if you have a static scene?
I already answered that in comment 1: Because that's what the tutorial said. I followed the tutorial, which is what any reasonable developer will do when he starts using a library he doesn't know. There was no hint whatsoever that the render should be conditional on anything.
That's the purpose of a tutorial, to teach this.
Just render once in response to user events.
Huh? That's what I'm doing? That's what I wrote in comment 2? What's your point?
My point is that this needs to be fixed in ThreeJS, or at the very least in the tutorial and the examples need to be fixed.
@benbucksch See this fiddle for an example of how to render a static scene.
This is easy to do with e.g. a dirty flag.
PRs welcome 馃槈
I found a good way by watching only state changes if you're using React. The following sample code will stop the render animation loop if current status is not in running state. Since any state changes will trigger componentDidUpdate
when the component has been updated, it seems componentDidUpdate
is a good place to call requestAnimationFrame
to restart the loop.
class MyWorkflow extends React.Component {
state = {
workflow: WORKFLOW_STATE_IDLE
};
componentDidMount() {
// create your scene
}
componentDidUpdate(prevProps, prevState) {
requestAnimationFrame(::this.renderAnimationLoop);
}
renderAnimationLoop() {
let isAgitated = (this.state.workflow === WORKFLOW_STATE_RUNNING);
if (isAgitated) {
requestAnimationFrame(::this.renderAnimationLoop); // 60 fps refresh rate
// start animations
} else {
// stop animations
}
this.renderer.render(this.scene, this.camera);
}
}
My CPU usage is also near to 80% up to sudden spikes to 120% after few minutes with any WebGL demo. CPU fan is going like crazy in that moment.
I discovered, that WebGL is able to use GPU nicely, but browser javascript is not able to do multithreading. Overusing and overheating one processor core to maximum.
browser javascript is not able to do multithreading
That is correct.
WebWorkers were created for that, but they have no UI access.
However, that fact is not related to this bug report. See my last comment (from about 5 years ago, sadly). Using 100% CPU forever is bad, on any core. The sample code is teaching developers to do the wrong thing.
@benbucksch I do not think there is a problem with the upper code. Animation loop is running for an hour if I will not move the mouse and camera, nothing is consuming whole processor. Just about 20-40%. My problem is when I start moving around the environment with camera, or looking for demos with too dynamic animations / rendering.
I discovered WebWorkers during last night, together with Offscreen Canvas which is not supported by all browsers. I will experiment more with WebWorkers and offscreen canvas with some detection for when and how to start/stop animation loop. As my conditions are not very user based.
nothing is consuming whole processor. Just about 20-40%.
That's the bug. That's way too much for a modern CPU, when you're not even interacting with it. FYI, I said it's using 100% of 1 core (!), not the whole CPU.
detection for when and how to start/stop animation loop
Please see my first few comments here. They have a fix.
The bug here was: The lib should do this by default. Or at least the sample code should demonstrate this, as every developer needs that. It's never OK to hog the CPU like that.
I choose ThreeJS because of control is on you. Maybe some game engines do this and some caching by default. Using your solution is good for some article about optimisations. Thanks for your code as an inspiration.
In my case animations may stop only in "sugar spot" destinations. ThreeJS is simple enough to start building the stuff and tweak it slowly to what I want. Maybe because it is rendering engine, not game one.
Other JS game engines were not offering this detailed customisation.
Most helpful comment
I found a good way by watching only state changes if you're using React. The following sample code will stop the render animation loop if current status is not in running state. Since any state changes will trigger
componentDidUpdate
when the component has been updated, it seemscomponentDidUpdate
is a good place to callrequestAnimationFrame
to restart the loop.