This project is read-only.

Possible memory leak in CCLabelBMFont + Monogame + WP8

Topics: Performance, Sprites and SpriteBatch, Windows Phone 8
May 2, 2014 at 9:24 AM
Hi again,
I found a weird behavior using cocos2d + monogame on Windows Phone 8.
This doesn't seem to happen on Microsoft XNA.

What I do here is adding every 2 seconds a CCNode which has inside just one CCLabelBMFont.
Before adding the new node, I remove the previous one using RemoveChild(node, true), so it should be cleaned up.

Result as follows:
Image

As you can see there are hundreds of instances of CCSprite caused by "CreateFontChars" method.
I tried debugging the code but I can't see anything wrong going on.

Any help/suggestion? Cannot release on WP8 until this problem persist.

Thank you!
May 2, 2014 at 10:41 PM
Sounds like you may be holding onto a reference to the label ??
May 3, 2014 at 1:48 AM

Nope, I clean the reference from the main layer each time I'm adding the node which contains the label.
In that case it would leak on all platforms, but it seems to happen only when I'm using Monogame.
The same code compiled on a WP7 project doesn't have any problem…

May 5, 2014 at 5:46 AM
That looks like the render targets are not being disposed properly in MG. Can you get the details of the memory heap and see what objects are staying in scope?
May 5, 2014 at 6:56 PM
I created a bug for this on GitHub:

https://github.com/Cocos2DXNA/cocos2d-xna/issues/414
May 5, 2014 at 6:59 PM
The sprite instances are just frame references into the bm font atlas..... I wonder if the temporary texture2d instance created when the setstring method is invoked is not being disposed properly ...
May 5, 2014 at 10:08 PM
I created a test case to exercise this condition - but more aggressively. I create the label every 0.5 seconds.

While I do get plenty of fun GC events, I don't see any leaking.

Here is my test case which I will push to GitHub soon:
public class LabelBMFontHDMemoryLeak : AtlasDemo
{
    public LabelBMFontHDMemoryLeak()
    {
    }

    public override void OnEnter()
    {
        base.OnEnter();
        ScheduleUpdate();
    }

    private float elapsed = 0f;
    private CCLabelBMFont label1;

    public override void Update(float dt)
    {
        elapsed += dt;
        if (elapsed > 0.5f)
        {
            elapsed = 0f;
            // CCLabelBMFont
            if (label1 != null)
            {
                RemoveChild(label1);
            }
            CCSize s = CCDirector.SharedDirector.WinSize;
            float x = s.Width * CCMacros.CCRandomBetween0And1();
            float y = s.Height * CCMacros.CCRandomBetween0And1();
            label1 = new CCLabelBMFont(string.Format("{0:N2},{1:N2} @ MEMORY LEAK", x, y), "fonts/konqa32.fnt");
            AddChild(label1);
            label1.Position = new CCPoint(x, y);
        }
    }
    public override string title()
    {
        return "Testing LabelBMFont";
    }

    public override string subtitle()
    {
        return "Looking for memory leaks";
    }
}
May 5, 2014 at 10:17 PM
I also created a test that matches your description (node + label):

public class LabelBMFontHDMemoryLeak2 : AtlasDemo
{
    public LabelBMFontHDMemoryLeak2()
    {
    }

    public override void OnEnter()
    {
        base.OnEnter();
        ScheduleUpdate();
    }

    private float elapsed = 0f;
    private CCNode label1;

    public override void Update(float dt)
    {
        elapsed += dt;
        if (elapsed > 0.5f)
        {
            elapsed = 0f;
            // CCLabelBMFont
            if (label1 != null)
            {
                RemoveChild(label1);
            }
            CCNode node = new CCNode();
            CCSize s = CCDirector.SharedDirector.WinSize;
            float x = s.Width * CCMacros.CCRandomBetween0And1();
            float y = s.Height * CCMacros.CCRandomBetween0And1();
            label1 = new CCLabelBMFont(string.Format("{0:N2},{1:N2} @ Child Mem Leak", x, y), "fonts/konqa32.fnt");
            node.AddChild(label1);
            label1.Position = new CCPoint(x, y);
            AddChild(node);
            label1 = node;
        }
    }
    public override string title()
    {
        return "Testing LabelBMFont";
    }

    public override string subtitle()
    {
        return "Looking for memory leaks with child label in node";
    }
}
}
May 5, 2014 at 10:27 PM
Then I saw that you are using a different ctor, so let's use that one too:
public class LabelBMFontHDMemoryLeak3 : AtlasDemo
{
    public LabelBMFontHDMemoryLeak3()
    {
    }

    public override void OnEnter()
    {
        base.OnEnter();
        ScheduleUpdate();
    }

    private float elapsed = 0f;
    private CCNode label1;

    public override void Update(float dt)
    {
        elapsed += dt;
        if (elapsed > 0.5f)
        {
            elapsed = 0f;
            // CCLabelBMFont
            if (label1 != null)
            {
                RemoveChild(label1);
            }
            CCNode node = new CCNode();
            CCSize s = CCDirector.SharedDirector.WinSize;
            float x = s.Width * CCMacros.CCRandomBetween0And1();
            float y = s.Height * CCMacros.CCRandomBetween0And1();
            label1 = new CCLabelBMFont(string.Format("{0:N2},{1:N2} @ Mem Leak Ctor", x,y), "fonts/konqa32.fnt", 255f, CCTextAlignment.Right, CCPoint.Zero);
            node.AddChild(label1);
            label1.Position = new CCPoint(x, y);
            AddChild(node);
            label1 = node;
        }
    }
    public override string title()
    {
        return "Testing LabelBMFont";
    }

    public override string subtitle()
    {
        return "Looking for memory leaks with full ctor";
    }
}
still no leaking.
May 6, 2014 at 3:59 AM
Hi, thanks for your help, as always. I've been able to reproduce the issue.
Please try the following code (I schedule it every 1 second, using "Schedule(<method name>, 1)" rather than overriding the Update method.
if (label1 != null)
            {
                if (Children.Contains(label1))
                {
                    RemoveChild(label1);
                }
            }
            CCNode node = new CCNode();
            CCSize s = CCDirector.SharedDirector.WinSize;
            float x = s.Width * CCMacros.CCRandomBetween0And1();
            float y = s.Height * CCMacros.CCRandomBetween0And1();
            label1 = new CCLabelBMFont(string.Format("{0:N2},{1:N2} @ Mem Leak Ctor", x, y), CDConsts.DEFAULT_BTN_FONT); //, 255f, CCTextAlignment.Right, CCPoint.Zero);
            node.Scale = 2f;
            node.AddChild(label1);
            label1.Position = new CCPoint(x, y);
            AddChild(node);

            //--> This action causes the leak
            CCScaleTo acScale = new CCScaleTo(0.1f, 1);
            CCDelayTime acShow = new CCDelayTime(0.1f);
            CCSplitRows acFadeOut = new CCSplitRows(0.1f, 20);
            CCRemoveSelf acRemove = new CCRemoveSelf(true);
            CCSequence seq = new CCSequence(acScale, acShow, acFadeOut, acRemove);
            node.RunAction(seq);

            label1 = node;
I was pretty sure the problem was not related to that action (I tried commenting it out), but now it seems that's causing the leak, I run so many tests and probably I got confused.

Perhaps I'm doing something wrong, but what I'm trying to achieve here is a label that shows for a few seconds, then automatically disappears.

In my actual code I'm not really using CCRemoveSelf, I use CCCallFunc to call a function that does the "RemoveChild". The result is the same.

What you think?
May 6, 2014 at 4:29 PM
Well, I think we need to make our Santa Shooter public source .... it would help you guys understand how to do these kinds of actions:
    /// <summary>
    /// Show the number of points in pts at the given point.
    /// </summary>
    /// <param name="pts"></param>
    /// <param name="p"></param>
    private void ShowPointsBubble(int pts, CCPoint p)
    {
        // Create a label and animate it up from the score label, fading out
        CCNode label = (CCNode)GameResources.CreateLabel("+" + pts, 24, _LabelColor);
        if (label != null)
        {
            CCPoint dest = new CCPoint(p.X, p.Y + 64f * 3f);
            label.Visible = false;
            AddChild(label, 25);
            label.RunAction(new CCSequence(
                new CCShow(),
                new CCSpawn(new CCMoveTo(1f, dest), new CCFadeOut(1f)),
                new CCCallFuncN(new Action<CCNode>(RemoveLabel))));
        }
    }

    internal static ICCLabelProtocol CreateLabel(string s, int fontSize, Color fg)
    {
        ICCLabelProtocol label = null;
        if (GameResources.BitMapLabelFont != null)
        {
            try
            {
                label = new CCLabelBMFont(s, GameResources.BitMapLabelFont);
                float fontScale = GameResources.FontScale;
                float sizeAdj = (float)fontSize / 32f * fontScale;
                // label.Scale = sizeAdj;
                ((CCLabelBMFont)label).Color = new CCColor3B(fg);
            }
            catch (Exception ex)
            {
                CCLog.Log("Failed to find the bitmap font for " + GameResources.BitMapLabelFont + " size " + fontSize);
                CCLog.Log(ex.ToString());
            }
        }
        if (label == null && GameResources.TrueTypeLabelFont != null)
        {
            try
            {
                label = new CCLabelTTF(s, GameResources.TrueTypeLabelFont, fontSize);
                ((CCLabelTTF)label).Color = new CCColor3B(fg);
            }
            catch (Exception ex)
            {
                CCLog.Log("Failed to find the spritefont for " + GameResources.TrueTypeLabelFont + " size " + fontSize);
                CCLog.Log(ex.ToString());
            }
        }
        return (label);
    }
May 6, 2014 at 4:30 PM
The remove label:
    private void RemoveLabel(CCNode obj)
    {
        obj.RemoveFromParentAndCleanup(true);
        if (obj is IDisposable)
        {
            ((IDisposable)obj).Dispose();
        }
    }
May 6, 2014 at 4:46 PM
Sorry, I cannot see the difference from my code, except you run the action on the Label while I run it on the Node.

Using your test code, and appending the following code, memory leak occurs:
CCScaleTo acScale = new CCScaleTo(0.1f, 1);
            CCDelayTime acShow = new CCDelayTime(0.1f);
            CCSplitRows acFadeOut = new CCSplitRows(0.1f, 20);
            CCCallFuncN acRemove = new CCCallFuncN(RemoveLabel);
            CCSequence seq = new CCSequence(acScale, acShow, acFadeOut, acRemove);
            node.RunAction(seq);
The RemoveLabel is same as yours (anyway, I don't see CCNode to be IDisposable, so the IF statement doesn't get hit).

Am I missing something??
May 6, 2014 at 4:56 PM
The memory leak appears to be in the CCSplitRows action. If you remove that action then the memory profile is very stable.
May 6, 2014 at 5:05 PM
May 6, 2014 at 5:17 PM
totallyevil wrote:
The memory leak appears to be in the CCSplitRows action. If you remove that action then the memory profile is very stable.
Well, thanks a lot for your help, I would have never figured that out.
Unfortunately, I will lose the nice split rows effect ending up using a simple ScaleTo.

Couple of questions, if you don't mind:
  1. How is CCSpawn different from CCParallel? They both seem to run multiple actions at the same time.
  2. If I apply "FadeOut" action to the node of your tests, nothing happens (the label simply disappears). Do I need to use CCNodeRGB or something else?
Thank you again,
Regards
May 6, 2014 at 5:20 PM
Yes, you have to use the CCNodeRGB if you are going to parent the label with a node. The node has to pass the opacity settings to the children as well.

CCSpawn and CCParallel are the same, just one has a better name than the other. I wrote CCParallel b/c I didn't like the CCSpawn name. CCSpawn is just there for backward compatibility.

The Grid based actions are all memory-hogs. There are so many temporary object instantiations in there, it's hard to believe that they work well at all on a micro memory footprint. We'll take a look at fixing up the split rows...
May 6, 2014 at 5:30 PM
totallyevil wrote:
Yes, you have to use the CCNodeRGB if you are going to parent the label with a node. The node has to pass the opacity settings to the children as well.

CCSpawn and CCParallel are the same, just one has a better name than the other. I wrote CCParallel b/c I didn't like the CCSpawn name. CCSpawn is just there for backward compatibility.

The Grid based actions are all memory-hogs. There are so many temporary object instantiations in there, it's hard to believe that they work well at all on a micro memory footprint. We'll take a look at fixing up the split rows...
Got it, thank you!
May 6, 2014 at 7:32 PM
Edited May 6, 2014 at 7:33 PM
This memory leak appears to be a problem in MonoGame WP8. I ran the same test in Windows XNA and the memory profile is stable, exactly how you would expect it to be. So my guess is that there is a bad render target dispose chain in MonoGame.
May 8, 2014 at 5:01 PM
totallyevil wrote:
This memory leak appears to be a problem in MonoGame WP8. I ran the same test in Windows XNA and the memory profile is stable, exactly how you would expect it to be. So my guess is that there is a bad render target dispose chain in MonoGame.
Yes, I confirm in windows phone 7 (Microsoft XNA) the problems doesn't occur.

One more question, if you can. I saw you're using both bitmap fonts and true type.
I have read somewhere that Bitmap fonts are much better for performances. However, in my next game I would like to use TrueType fonts (I need German, Greek and other languages special characters).

Question is, how do performances of TrueType fonts compare to bitmap fonts in cocos2d? They're worse only during scaling, updating, or... when? Is it going to kill my FPS?

Thank you!
May 8, 2014 at 5:10 PM
We don't use the CCLabel for our games yet. That's the actual true type font label.

CCLabelTTF is just a spritefont label, which is the XNA version of BMP font.

The performance of CCLabelBM and CCLabelTTF is the same. They both do the same thing.

CCLabel is far more versatile with regard to font size scaling, but it does take slightly longer to setup the label for the first time when the TT font is loaded. Plus, there are licensing issues when you embed the ttf font in your application. Make sure that you consider those licensing issues when you choose to use CCLabel. You can't just use a personal use license on your TTF font file for your game. You have to purchase an electronic download license, which is always significantly more expensive than the personal use font license.
May 8, 2014 at 5:44 PM

All right thanks again ;)