phaserjs / phaser

Phaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.
https://phaser.io
MIT License
37.24k stars 7.1k forks source link

Performance issue when shutting down scene on Phaser 3.60.0 #6482

Closed kpagan closed 1 year ago

kpagan commented 1 year ago

Version

Description

Hello, I was developing a space shooter game with Phaser 3.55.2 and after the 3.60.0 release I started migrating to the new version. After the code migration I tested the game and found out that when I complete a level and click the OK button to return to the main menu scene it takes almost 10 seconds to transition to the scene. I run a performance analysis with the Chrome profile and it seems that the removeListener, destroy and preDestroy methods to take too long. In 3.55.2 this would take just milliseconds. You can check the screenshot of the Chrome profiler. When the scenes are shutting down I remove all listeners. However, I tried to comment out the code that cleans up but it still takes around 10 seconds.

Example Test Code

Additional Information

image

photonstorm commented 1 year ago

I can't debug screenshots I'm afraid. While it's clear that your game is taking a while to destroy this Scene, it's not a global change that impacts every game in 3.60, so it's going to need a lot more debugging to narrow down - ideally the game itself, so a test case can be created.

kpagan commented 1 year ago

Thank you for your prompt reply! The game itself is hosted on GitHub and you can view the source code on the https://github.com/kpagan/Phaser3Airfighter/tree/phaser3.60 branch. I don't know if I am doing something extraordinary but if you like to take a look then check src\scenes\ui\LevelCompleteModal.ts where you click the OK button to go to the main menu scene. The scenes that are shut down are the src\scenes\CloudScene.ts and the src\scenes\UiScene.ts.

Thanks

kpagan commented 1 year ago

Hello! I managed to narrow down the bottleneck!

I use particle emitters on the enemy sprites. These sprites are pooled in a GameObjects group. When the sprite was hit I would deactivate it and return it to the group. But when the sprite was spawned again pulling it from the group then I was re-creating the particle emitter. With the change in the API of Phaser 3.60.0 to the particle emitters it seems that the old one was still around and when the scene was shutting down then all the emitters were destroyed during that time. Now I check if the particle emitter is already instantiated so I will not create it again and the shutdown takes a lot less time, under 1 second.

However, it still takes about twice the time compared to 3.55.2 which would take under 500ms.

Using again the Chrome profiler I saw that it takes some milliseconds (around 20) in the Particle.destroy() method and especially in the this.anims.destroy(); line. However, I don't use animation in my emitters. This time adds up and takes some considerable amount of time to cleanup. Maybe this can be improved.

samme commented 1 year ago

Looks like removeListener() is the culprit.

photonstorm commented 1 year ago

Yeah, we can skip installing the AnimationState unless the particle config has an anims property - that should help a lot.

kpagan commented 1 year ago

Thank you very much. I am glad that my info helped. Let me know if I can help further! Currently this is not a blocking issue for me anymore. However, I am looking forward for a fix!

samme commented 1 year ago

@kpagan I made a quick test and found destroying 10k particles took 500ms — but that's quite a lot of particles. So you might also check to see that you're not creating more particles than you intend to somehow.

kpagan commented 1 year ago

Thanks @samme! I don't think I use that many particles. The configuration for the emitter is the following and I have about 7 instances of them:

        const particles = this.scene.add.particles(undefined, undefined, texture, {
            ...(config.frame && { frame: config.frame }),
            quantity: 100,
            lifespan: { min: 0, max: 70 },
            accelerationX: -500,
            speedX: -1000,
            alpha: { start: 0.5, end: 0, ease: 'Sine.easeIn' },
            scale: { start: 0.7, end: 0 },
            blendMode: 'SCREEN',
            follow: f,
            followOffset: { x: -10 + (config.followOffsetX ?? 0), y: -2 + (config.followOffsetY ?? 0) },
            moveToX: {
                onEmit: () => {
                    return f.x + (config.moveToXOffset ?? 0);
                },
                onUpdate: () => {
                    return f.x + (config.moveToXOffset ?? 0);
                }
            },
            moveToY: {
                onEmit: () => {
                    return f.y + (config.moveToYOffset ?? 0) + Phaser.Math.Between(-3, 3);
                },
                onUpdate: () => {
                    return f.y + (config.moveToYOffset ?? 0) + Phaser.Math.Between(-3, 3);
                }
            },
        });
ddanushkin commented 1 year ago

Thanks @samme! I don't think I use that many particles. The configuration for the emitter is the following and I have about 7 instances of them:

        const particles = this.scene.add.particles(undefined, undefined, texture, {
            ...(config.frame && { frame: config.frame }),
            quantity: 100,
            lifespan: { min: 0, max: 70 },
            accelerationX: -500,
            speedX: -1000,
            alpha: { start: 0.5, end: 0, ease: 'Sine.easeIn' },
            scale: { start: 0.7, end: 0 },
            blendMode: 'SCREEN',
            follow: f,
            followOffset: { x: -10 + (config.followOffsetX ?? 0), y: -2 + (config.followOffsetY ?? 0) },
            moveToX: {
                onEmit: () => {
                    return f.x + (config.moveToXOffset ?? 0);
                },
                onUpdate: () => {
                    return f.x + (config.moveToXOffset ?? 0);
                }
            },
            moveToY: {
                onEmit: () => {
                    return f.y + (config.moveToYOffset ?? 0) + Phaser.Math.Between(-3, 3);
                },
                onUpdate: () => {
                    return f.y + (config.moveToYOffset ?? 0) + Phaser.Math.Between(-3, 3);
                }
            },
        });

Maybe this is the problem? image

samme commented 1 year ago

That could be 7 * 1000 = 7000 particles at least.

kpagan commented 1 year ago

Wow thanks @ddanushkin ! I had this code taken from the labs examples and never thought what this reserve function does. Removing it altogether saved me about 500ms.

@samme please note that this reserve is not in the particle emitter that would have 7 instances but still this FireSmokeComponent class has 3 particle emitters each one reserving 1000 particles and I had it created 2 times so in total 2 3 1000 = 6000 particles that were actually useless because they would not add a visual effect.

It makes me wonder how it worked in Phaser 3.55.2 though. I guess the particle emitters were never destroyed but they would remain in memory and never garbage collected thus causing a memory leak. Good thing that this is fixed now.

Thank you all guys for all the help!!!

samme commented 1 year ago

reserve() always creates new particles.

You can check these with

console.log('particles', this.fire.getParticleCount());
kpagan commented 1 year ago

Since not much have been posted on this issue and I have resolved the problem this issue can be closed. Thank you all for your precious help!