flixel-addons icon indicating copy to clipboard operation
flixel-addons copied to clipboard

FlxSpine leaks memory

Open Tiago-Ling opened this issue 10 years ago • 9 comments
trafficstars

After instantiation any FlxSpine object will keep its allocated memory even after being removed from the FlxState, destroyed and having its reference nullified.

Steps to reproduce:

  1. Download the last version of the FlxSpine demo from github: FlxSpine Demo
  2. Add the following code snippet to MenuState.update method:
if (FlxG.keys.justPressed.D)
{
    if (spineSprite != null) {
        remove(spineSprite);
        spineSprite.destroy();
        spineSprite = null;
        openfl.system.System.gc();
    }
}
  1. Compile and run the demo. I'm almost sure this happens with any OpenFL version (next and legacy), but here i'm using Lime 2.6.8 and OpenFL 3.3.8 running this from command line: lime test windows -Dnext
  2. Using Windows Task Manager, monitor the amount of memory the application is using. Press D to destroy the spineSprite and free it's memory - you'll notice there will be no change in the amount of memory the application is using.

If you really wanna be thorough you can modify MenuState even further to see how much memory gets allocated when spineSprite comes into play. To do that simply comment lines 17 through 19 of the class and add another snippet to the update method:

if (FlxG.keys.justPressed.C) {
    spineSprite = new SpineBoyTest(FlxSpine.readSkeletonData("spineboy", "spineboy", "assets", 0.6), 300, 440);
    spineSprite.antialiasing = true;
    add(spineSprite);
}

This way you can see how much memory the application consumes before instantiation, after instantiation (by pressing C) and after destruction (by pressing D).

I figure this might be a problem with the spinehaxe library, but i wanted to create the issue here since Flixel's community is much more active.

Using FlxSpine like this will allocate more memory very quickly. In my case i'm using ~10 FlxSpine instances per level of the game (each level a FlxState). After switching states at the end of the level there'll be around 20MB of memory which never gets collected. Playing for some time will cause slowdowns, graphics not being rendered (they become invisible, which makes me think the leak is in video memory (?)) and then a crash.

I really need all the help i can get with this (and as soon as possible), so i'll gladly pay a bounty (the amount can be discussed privately) to anybody who can solve this problem, including making pull requests to spinehaxe if they are necessary.

I'm also pinging people who might be interested in this issue: @Beeblerox @bendmorris

Tiago-Ling avatar Nov 08 '15 23:11 Tiago-Ling

It looks like there may be a leak somewhere in SkeletonJson.readSkeletonData. I can reproduce this using spinehaxe + HaxePunk on a Mac and I'm still looking for the specific issue. For now depending on your use case, a workaround might be to cache the results of readSkeletonData in a static variable and only call readSkeletonData once for each kind of sprite. (Probably a good idea to just do this anyway, since the result doesn't change.) When doing this, I no longer see the leak.

bendmorris avatar Nov 09 '15 02:11 bendmorris

If that's indeed the case, it should be fairly easy to cache the results of readSkeletonData in FlxSpine.readSkeletonData, and keep a map of loaded skeleton data. That would be a good change at the engine level, but we should still investigate the cause of the leak.

gamedevsam avatar Nov 09 '15 19:11 gamedevsam

Unfortunately simply caching the skeletonData won't be enough for me. I need a proper way to completely free the memory spine is using.

I have different game states with different spine animations which play depending on what the player does. If all of these animations are cached together the game will simply crash on mobile (some of the animations are full screen "cutscenes" which are shown during game over, etc).

Btw i'm already caching what i can (i.e. spine animations which will be played on all levels) but i still need a way to clear everything after an intro or game over scene.

Tiago-Ling avatar Nov 09 '15 19:11 Tiago-Ling

Makes sense. For cases with a lot of different animations caching would be a bad idea, so I wouldn't make that change at the engine level.

I'm going to keep digging into this tonight. My best guess right now is that this is a hxcpp GC issue, so you might want to see if Hugh has any insight. I haven't seen anything in the code that would be holding onto references.

bendmorris avatar Nov 09 '15 20:11 bendmorris

One thing that seems very suspicious to me and could / should be changed to be something better is that RegionAttachment implements Dynamic<Dynamic>.

This is actually used in HaxeFlixel by creating a dynamic field named wrapperStrip to store the reference to a FlxStrip inside the RegionAttachment. This can be seen in FlxSpine.renderWithTriangles, lines 230-240:

if (region.wrapperStrip != null)
{
    wrapper = cast region.wrapperStrip;
}
else
{
    var atlasRegion:AtlasRegion = cast region.rendererObject;
    var bitmapData:BitmapData = cast(atlasRegion.page.rendererObject, BitmapData);
    wrapper = new FlxStrip(0, 0, bitmapData);
    region.wrapperStrip = wrapper;
}

When trying to test hxcpp collection using the HXCPP_GC_MOVING flag the application crashes on this line:

bool tmp13 = (region->__Field(HX_HCSTRING("wrapperStrip","\x85","\xc0","\xdb","\x32"), hx::paccDynamic ) != null());        HX_STACK_VAR(tmp13,"tmp13");

Which translates to FlxSpine.renderWithTriangles, line 276. I think that instead of using this dynamic field we could implement better HaxeFlixel support inside spinehaxe using the same structure it already has for OpenFL (see spinehaxe OpenFL support).

Tiago-Ling avatar Nov 10 '15 19:11 Tiago-Ling

ok, will look into it tomorrow evening

Beeblerox avatar Nov 10 '15 19:11 Beeblerox

What is your concern with using implements Dynamic<Dynamic>? Is this a bug in hxcpp? Just to be clear, removing it does not seem to affect the leak.

bendmorris avatar Nov 15 '15 18:11 bendmorris

I'm not sure to be honest - i think simply implementing Dynamic<Dynamic> is not a problem, but i wonder if using it to set dynamic fields like the wrapperStrip in FlxSpine and never destroying and setting it to null could make it never be collected by the gc.

I'm not entirely sure if the leak is caused by spinehaxe or FlxSpine. There seems to be some problems with hxcpp garbage collector as well. I created some dispose functions in my fork of spinehaxe to try and free any possible references that could possibly be forgotten. This plus using a different version of hxcpp (with the HXCPP_GC_MOVING flag) reduced the leak to a much more manageable level.

Tiago-Ling avatar Nov 16 '15 15:11 Tiago-Ling

I'll leave some notes here to help anyone else trying to troubleshoot this:

  • I can reproduce this issue using spinehaxe with my own engine. This suggests that the issue is not specific to FlxSpine. The class I'm using is mostly based on this: https://github.com/bendmorris/SpinePunk/blob/master/src/spinepunk/SpinePunk.hx
  • I'm not using any of the dynamic RegionAttachment fields, so that's probably not the issue (or not the only one.) I also removed implements Dynamic<Dynamic> completely for one test and it didn't make a difference.
  • I updated hxcpp due to reports of a memory leak (https://github.com/HaxeFoundation/hxcpp/issues/315) and it seems like the leak may be less severe but is still present.

bendmorris avatar Nov 16 '15 20:11 bendmorris