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

requestAnimationFrame loop causes CPU hog #7670

Closed
benbucksch opened this issue Nov 24, 2015 · 23 comments
Closed

requestAnimationFrame loop causes CPU hog #7670

benbucksch opened this issue Nov 24, 2015 · 23 comments

Comments

@benbucksch
Copy link

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.

@benbucksch
Copy link
Author

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.

@mrdoob
Copy link
Owner

mrdoob commented Nov 24, 2015

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.

@GGAlanSmithee
Copy link
Contributor

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.

@GGAlanSmithee
Copy link
Contributor

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.

@benbucksch
Copy link
Author

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.

@mrdoob
Copy link
Owner

mrdoob commented Nov 24, 2015

Can you upload the tests somewhere?

@benbucksch
Copy link
Author

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 .

@WestLangley
Copy link
Collaborator

@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.

@mrdoob mrdoob closed this as completed Nov 24, 2015
@benbucksch
Copy link
Author

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.

@WestLangley
Copy link
Collaborator

@benbucksch See this fiddle for an example of how to render a static scene.

@mrdoob
Copy link
Owner

mrdoob commented Nov 25, 2015

This is easy to do with e.g. a dirty flag.

PRs welcome 😉

@cheton
Copy link

cheton commented Dec 20, 2015

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

@Antarian
Copy link

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.

@benbucksch
Copy link
Author

benbucksch commented Jun 20, 2020

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.

@Antarian
Copy link

@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.

@benbucksch
Copy link
Author

benbucksch commented Jun 20, 2020

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.

@benbucksch
Copy link
Author

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.

@Antarian
Copy link

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.

@magland
Copy link

magland commented Nov 23, 2021

I ran into the same problem -- didn't realize it would be using a lot of CPU even when the user was not interacting with the static scene. My remedy was to use an event listener on the controls. I wish the example was more like this (rather than using requestAnimationFrame):

        // within a useEffect hook
        const render = () => {
            renderer.render( scene, camera );
        }
        controls.addEventListener( 'change', render );
        controls.update()
        
        return () => {
            controls.removeEventListener('change', render)
        }

@benbucksch
Copy link
Author

benbucksch commented Nov 23, 2021

@mrdoob : Would it be possible to update the tutorials, sample code and README, to reflect the above changes?

@mrdoob
Copy link
Owner

mrdoob commented Nov 25, 2021

My guess is that using just event listeners only works for 20% of the projects.

@shrey-fog
Copy link

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 .

The live is not exististing anymore. Could you also share your full code for the same

@benbucksch
Copy link
Author

My own workaround code is posted here in the 2. comment. You can see it live on http://www.labrasol.com .

The live is not exististing anymore. Could you also share your full code for the same

Labrasol was a commercial project and doesn't exist anymore. I shared the relevant portion of the code in comment 2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants