1

I was analyzing an unexpected memory leak in our game project and found some strange results. I am profiling using Adobe Scout and eliminated all other factors like starling, texture or our loading library. I reduced the code to simply load a png and immediately allocate an empty inline function on its complete event.

Loading a png allocates image on default and if you do nothing after loading gc clears that image. But creating an inline function seems to prevent that image to be garbage collected somehow. My test code is;

public class Main extends Sprite 
{
    private var _callbacks:Array = new Array();

    public function Main() 
    {
        load("map.png", onPngLoaded);
    }

    private function onPngLoaded(bitmap:Bitmap):void 
    {
        _callbacks.push(function():void { });
    }

    public function load(url:String, onLoaded:Function):void 
    {
        var loader:Loader = new Loader;

        var completeHandler:Function = function(e:Event):void {
            loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, completeHandler);
            onLoaded(loader.content);
        }

        loader.contentLoaderInfo.addEventListener(Event.COMPLETE, completeHandler);

        loader.load(new URLRequest(url));   
    }
}

If you remove the code which creates an inline function;

    private function onPngLoaded(bitmap:Bitmap):void 
    {
        // removed the code here!
    }

gc works and clears the image from memory.

Since having no logical explanation for this, I suspect of a flash / as3 bug. I will be glad to hear any comments who tests my code and gets the same results.

Note: To test, replace the main class of an empty as3 project with my code and import packages. You can load any png. I am using flashdevelop, flex-sdk 4.6.0 and flash player 14.

Wary Warcry
  • 93
  • 10
  • 1
    You are holding a reference in the _callback array and anonymous functions are scoped globally, thus the GC would not be able to sweep/release that memory. – SushiHangover Oct 19 '15 at 11:08
  • 1
    But it is not related with the loaded image. Anonymous function is referenced by a global array. Neither the function nor the loaded content references each other. – Wary Warcry Oct 19 '15 at 12:48
  • 1
    I would suspect, the issue is that when you create a inline function, it keeps all the vars of the context it's defined in available to that function. So your bitmap paramter in the `onPngLoaded` function will hang around as long as that inline function hangs around. – BadFeelingAboutThis Oct 19 '15 at 16:15
  • That was the reason, thanks. – Wary Warcry Mar 09 '16 at 15:05

2 Answers2

2

When you create an inline function, all local variables get stored with it in the global scope. So in this case, that would include the bitmap parameter.

For more information, see this: http://help.adobe.com/en_US/ActionScript/3.0_ProgrammingAS3/WS5b3ccc516d4fbf351e63e3d118a9b90204-7f54.html

Here is the relevant part:

Any time a function begins execution, a number of objects and properties are created. First, a special object called an activation object is created that stores the parameters and any local variables or functions declared in the function body....Second, a scope chain is created that contains an ordered list of objects that Flash Player or Adobe AIR checks for identifier declarations. Every function that executes has a scope chain that is stored in an internal property. For a nested function, the scope chain starts with its own activation object, followed by its parent function’s activation object. The chain continues in this manner until it reaches the global object.

This is another reason why inline/anonymous functions are best avoided in most situations.

BadFeelingAboutThis
  • 14,445
  • 2
  • 33
  • 40
  • You are right. When i remove the bitmap parameter from the onPngLoaded function, memory is released. Then all function scope will be stored for even an empty inline function. That was unexpected for me. Thanks. – Wary Warcry Oct 19 '15 at 16:26
  • Great reference page, I was using PlayScript to compile an existing AS3 project to Mono.Net assemblies and the original customers code used the fact that global references were actually available via anon. functions and thus used `this` references to pull objects from (i.e. try this in any anon function "trace(describeType(this));"). Of course Mono/.Net does not have that type of scope chain to globals and had to rewrite soooooo much AS3 code as it was easier than trying to hack this scoping style into the mono/mcs compiler. – SushiHangover Oct 19 '15 at 16:42
  • I wonder if this is the default behaviour in other languages. – Wary Warcry Mar 09 '16 at 15:04
1

So using asc2, Flash/Air 19 : Yes I get the same results that you are seeing, but due to the anonymous function holding global references I expected that (like my original comment stated).

I rewrote it in my style based upon Adobe's GC technical articles and bulletins and no leaks are seen as all the global references are removed.

A cut/paste AIR example:

package {

    import flash.events.MouseEvent;
    import flash.text.TextField;
    import flash.display.Sprite;
    import flash.display.Bitmap;
    import flash.display.Loader;
    import flash.events.Event;
    import flash.net.URLRequest;
    import flash.system.System;
    import flash.utils.Timer;
    import flash.events.TimerEvent;

    public class Main extends Sprite {
        var timer:Timer;
        var button:CustomSimpleButton;
        var currentMemory:TextField;
        var highMemory:TextField;
        var hi:Number;

        var _callbacks:Array = new Array();

        public function Main() {
            button = new CustomSimpleButton();
            button.addEventListener(MouseEvent.CLICK, onClickButton);
            addChild(button);
            currentMemory = new TextField();
            hi = System.privateMemory;
            currentMemory.text = "c: " + hi.toString();
            currentMemory.x = 100;
            addChild(currentMemory);
            highMemory = new TextField();
            highMemory.text = "h: " + hi.toString();
            highMemory.x = 200;
            addChild(highMemory);
            timer = new Timer(100, 1);
            timer.addEventListener(TimerEvent.TIMER_COMPLETE, timerHandler);
            timer.start();
        }

        function timerHandler(e:TimerEvent):void{
            System.pauseForGCIfCollectionImminent(.25);
            currentMemory.text = "c: " + System.privateMemory.toString();
            hi = System.privateMemory > hi ? System.privateMemory : hi;
            highMemory.text = "h: " + hi.toString();
            timer.start();
        }

        function onClickButton(event:MouseEvent):void {
            for (var i:uint = 0; i<100; i++) {
                //load("foobar.png", onPngLoaded);
                load2("foobar.png");
            }
        }

        private function onPngLoaded2(bitmap:Bitmap):void {
            var foobarBitMap:Bitmap = bitmap; // assuming you are doing something
            foobarBitMap.smoothing = false;   // with the bitmap...
            callBacks(); // not sure what you are actually doing with this
        }
        private function callBacks():void {
            _callbacks.push(function ():void {
            });
        }

        public function completeHandler2(e:Event):void {
            var target:Loader = e.currentTarget.loader as Loader;
            // create a new bitmap based what is in the loader so the loader has not refs after method exits
            var localBitmap:Bitmap = new Bitmap((target.content as Bitmap).bitmapData);
            onPngLoaded2(localBitmap);
        }

        public function load2(url:String):void {
            var loader2:Loader = new Loader;
            loader2.contentLoaderInfo.addEventListener(Event.COMPLETE, completeHandler2, false, 0, true);
            loader2.load(new URLRequest(url));
        }
    }
}

import flash.display.Shape;
import flash.display.SimpleButton;

class CustomSimpleButton extends SimpleButton {
    private var upColor:uint   = 0xFFCC00;
    private var overColor:uint = 0xCCFF00;
    private var downColor:uint = 0x00CCFF;
    private var size:uint      = 80;

    public function CustomSimpleButton() {
        downState      = new ButtonDisplayState(downColor, size);
        overState      = new ButtonDisplayState(overColor, size);
        upState        = new ButtonDisplayState(upColor, size);
        hitTestState   = new ButtonDisplayState(upColor, size * 2);
        hitTestState.x = -(size / 4);
        hitTestState.y = hitTestState.x;
        useHandCursor  = true;
    }
}

class ButtonDisplayState extends Shape {
    private var bgColor:uint;
    private var size:uint;

    public function ButtonDisplayState(bgColor:uint, size:uint) {
        this.bgColor = bgColor;
        this.size    = size;
        draw();
    }

    private function draw():void {
        graphics.beginFill(bgColor);
        graphics.drawRect(0, 0, size, size);
        graphics.endFill();
    }
}
SushiHangover
  • 73,120
  • 10
  • 106
  • 165