Kevin McGowan
Kevin McGowan

Reputation: 463

HTMLLoader / StageWebView Memory Leak

When creating an instance of StageWebView, if you switch between multiple sites you'll notice that the amount of memory used slowly goes up. This isn't an issue if you're viewing 2/3 sites, but when it comes to creating an AIR application with a built in browser, I've now hit a brick wall in terms of memory management.

No matter what I do

The problem persists. Each load of a web page will increase memory by 3mb, which gradually increases to over 1gb and crashes the application.

I have no event listeners on the StageWebView instance. I hold absolutely no references to it. The memory just does not fully reset after the 2nd URL loads.

This can be seen by running the below in AIR:

package kazo
{
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.geom.Rectangle;
    import flash.media.StageWebView;
    import mx.controls.Button;
    import mx.core.UIComponent;
    import flash.system.System;

    /**
     * ...
     * @author KM
     */
    public class Controller extends UIComponent
    {

        private var stageWeb:StageWebView;
        private var url:uint = 0;

        private const URL_ARRAY:Array = [
            'http://www.mmo-champion.com/',
            'http://www.bbc.co.uk/news/',
            'http://www.twitch.tv/riotgames/',
            'http://www.stackoverflow.com/'
        ]

        /**
         * 
         */
        public function Controller() 
        {
            addEventListener(Event.ADDED_TO_STAGE, init);
        }

        /**
         * 
         * @param   e
         */
        private function init(e:Event):void {
            removeEventListener(Event.ADDED_TO_STAGE, init);

            stageWeb = new StageWebView();
            stageWeb.stage = stage;
            stageWeb.viewPort = new Rectangle(0, 80, width, height - 50);

            var btn:Button = new Button();
            addChild(btn);
            btn.label = 'Load URL';
            btn.x = 0;
            btn.y = 10;;
            btn.width = 100;
            btn.height = 30;
            btn.addEventListener(MouseEvent.CLICK, load);

            btn = new Button();
            btn.label = 'Try to GC';
            btn.x = 150;
            btn.y = 10;;
            btn.width = 100;
            btn.height = 30;
            addChild(btn);
            btn.addEventListener(MouseEvent.CLICK, tryGC);

            /// 26,576k
        }

        /**
         * 
         * @param   e
         */
        private function load(e:MouseEvent):void {
            if (!stageWeb) {
                stageWeb = new StageWebView();
                stageWeb.stage = stage;
                stageWeb.viewPort = new Rectangle(0, 80, width, height - 50);
            }               

            stageWeb.loadURL(URL_ARRAY[url % 4]);

            url++;
        }

        /**
         * 
         * @param   e
         */
        private function tryGC(e:MouseEvent):void {
            stageWeb.stage = null;
            stageWeb.viewPort = null;
            stageWeb.dispose();
            stageWeb = null;
            System.gc();
        }

    }

}

Does anyone have any experience with this?

Upvotes: 3

Views: 1087

Answers (2)

VC.One
VC.One

Reputation: 15936

This is just a random passer-by's thoughts ramblings.. note: I did not get to test-run your code since I've not got access to Flash at this moment. Nor do I back my rambling 100% (I know it's a WTF? but please read on, hopefully something useful in there..)

In your function tryGC(e:MouseEvent):void you set stageWeb = null; which is fine but then in your function load(e:MouseEvent):void you say "If stageWeb is null then make a new one". Well since you previously set Flash to consider that one null it obediently creates a new one in memory (with the old memory imprint still possibly not yet garbage collected (just in the queue for it) Therefore it looks like ram usage has increased.

The whole tryGC thing is kinda flawed if you are then going to load other pages immediately after. The garbage collection (possibly) never happened since there's also a Mouse Event lurking around that says if (!stageWeb) { //etc } so basically you're switching its status to null but it cant be truly removed cos something else is using a reference to it and that something could be called up anytime in the future so it just sticks around in memory (with a null status)..

Possible solutions:

1) In your function tryGC remove the mouse listener for load URL

private function tryGC(e:MouseEvent):void {
stageWeb.stage = null;
stageWeb.viewPort = null;
stageWeb.dispose();
stageWeb = null;
btn.removeEventListener(MouseEvent.CLICK, load);
System.gc();
}

Now wait a few seconds and check is the ram usage dcreased? If it helps load at least 10+ urls before trying GC since you wont be able to load URLs via clicking. This is just for testing to see if the mouselistener was an issue for garbage collector

2) What you really need is a way to clear/overwrite the current url content/cache... not recreating new HTML page views. That does nothing helpful in this case. I think your problem stems from this also... History Forward/Back... I suspect each url load is adding to the history, and history in turn is more of a "content cache" instead of just a "string with previous URL".
Considering your plans for a BAT file... well that would open 100 urls, cache them in turn, but your viewport only show one item at a time (with 99 others pages held in memory, fully loaded & ready for instant display..). I don't know of a workaround for this. There is no stageWeb.unloadURL(); or even stageWeb.clearCache(); I didn't look hard enough but somebody out there on this planet knows a trick (and hopefully) blogged about it...

Upvotes: 0

jauboux
jauboux

Reputation: 888

I did some minor changes to your code to make it compilable with AIR 14 SDK (ASC 2.0, no flex), I build & tested the air application with

mxmlc -optimize=true +configname=air Controller.as && adl Controller.xml

and watched memory usage with Adobe Scout

Memory after GC was stable, stabilizing at 14481 kB

You may add -advanced-telemetry=true option to mxmlc to track allocations/deallocations precisely, just think to push the "hide cleaned objects" switch and select a range beetween two GC. You will see some object are not released immediatly, but you'll also see that if you select several of those ranges at the same time, those temporary leaks do not add up, which explains why memory does not increase constantly, in a GC environment, memory leaks are not really memory leaks

How do you measure memory usage ?

I checked memory usage of the adl process, it was higher ( > 100MB ) and less stable, but still stabilizing somewhere between 110 and 120MB after GC. In any case, it was definitely not looking like it was going to increase up to 1 GB

Just remember that ActionScript GC is lazy and keeps pool of objects for future reuse, any application doing more than hello world will have sometimes strange memory behavior, which is part of the way AVM2 is working.

package 
{
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.MouseEvent;
    import flash.geom.Rectangle;
    import flash.media.StageWebView;
    import flash.system.System;
    import flash.text.TextField;

    /**
     * ...
     * @author KM
     */
    public class Controller extends Sprite
    {

        private var stageWeb:StageWebView;
        private var url:uint = 0;

        private const URL_ARRAY:Array = [
            'http://www.mmo-champion.com/',
            'http://www.bbc.co.uk/news/',
            'http://www.twitch.tv/riotgames/',
            'http://www.stackoverflow.com/'
        ]

        /**
         * 
         */
        public function Controller() 
        {
            addEventListener(Event.ADDED_TO_STAGE, init);
        }

        /**
         * 
         * @param   e
         */
        private function init(e:Event):void {
            removeEventListener(Event.ADDED_TO_STAGE, init);

            stageWeb = new StageWebView();
            stageWeb.stage = stage;
            stageWeb.viewPort = new Rectangle(0, 80, stage.stageWidth, stage.stageHeight - 50);

            var btn:TextField = new TextField();
            btn.selectable = false;
            addChild(btn);
            btn.text = 'Load URL';
            btn.x = 0;
            btn.y = 10;;
            btn.width = 100;
            btn.height = 30;
            btn.addEventListener(MouseEvent.CLICK, load);

            btn = new TextField();
            btn.selectable = false;
            btn.text = 'Try to GC';
            btn.x = 150;
            btn.y = 10;;
            btn.width = 100;
            btn.height = 30;
            addChild(btn);
            btn.addEventListener(MouseEvent.CLICK, tryGC);

            /// 26,576k
        }

        /**
         * 
         * @param   e
         */
        private function load(e:MouseEvent):void {
            if (!stageWeb) {
                stageWeb = new StageWebView();
                stageWeb.stage = stage;
                stageWeb.viewPort = new Rectangle(0, 80, stage.stageWidth, stage.stageHeight - 50);
            }               

            stageWeb.loadURL(URL_ARRAY[url % 4]);

            url++;
        }

        /**
         * 
         * @param   e
         */
        private function tryGC(e:MouseEvent):void {
            stageWeb.stage = null;
            stageWeb.viewPort = null;
            stageWeb.dispose();
            stageWeb = null;
            System.gc();
        }

    }

}

Upvotes: 2

Related Questions