AS3: Resource Management pt 3

This one took a little longer than I’d hoped – life’s too busy right now! In this third part of my series on resource management in ActionScript 3 I will be focusing on a few new tools in AS3 / Flex2 that let you track and manage memory more effectively. There are only a couple new “official” features that are specifically geared towards resource management, but they are very useful. These are supplemented by a handy unofficial feature, and of course there are lots of language features that can help you in more generic ways.

System.totalMemory
This is simple tool, but it’s important because it marks the first run-time profiling tool developers have had in Flash. It allows you to monitor how much memory is in use by the Flash player at run-time. This gives you some ability to profile your own work during development without a system monitor. More importantly, it let’s you preemptively deal with major memory leaks in your content before it causes a serious issue for your user. It’s better to throw an error, and abort your application, than bog down the user’s system or even stall it completely.

Here’s a simple example of what this could look like:

import flash.system.System;
import flash.net.navigateToURL;
import flash.net.URLRequest;
...
// check our memory every 1 second:
var checkMemoryIntervalID:uint = setInterval(checkMemoryUsage,1000);
...
var showWarning:Boolean = true;
var warningMemory:uint = 1000*1000*500;
var abortMemory:uint = 1000*1000*625;
...
function checkMemoryUsage() {
if (System.totalMemory > warningMemory && showWarning) {
// show an error to the user warning them that we're running out of memory and might quit
// try to free up memory if possible
showWarning = false; // so we don't show an error every second
} else if (System.totalMemory > abortMemory) {
// save current user data to an LSO for recovery later?
abort();
}
}
function abort() {
// send the user to a page explaining what happpened:
navigateToURL(new URLRequest("memoryError.html"));
}

This could obviously be enhanced in a number of ways, but hopefully demonstrates the basic concept well.

It is important to note that totalMemory is a shared value within a single process. A single process may be just one browser window, or all open browser windows, depending on the browser, the OS, and how the windows were opened (ex. in OSX all Safari windows share a single process and totalMemory value, whereas it is much more convoluted in Windows).

Weak References
One of the features I’m really happy to see implemented in AS3 is weak references. These are references to objects that are not counted by the Garbage Collector in determining an object’s availability for collection. That is, if the only references remaining to an object are weak, that object will be removed by the GC on its next pass. Unfortunately, weak references are only supported in two contexts. The first is event listeners, which is great because event listeners are one of the most common references that cause problems with garbage collection. I strongly recommend always using weak references with listeners by passing true as the fifth parameter of your addEventListener calls:

someObj.addEventListener("eventName",listenerFunction,useCapture,priority,weakReference);
stage.addEventListener(Event.CLICK,handleClick,false,0,true);
// the reference back to handleClick (and this object) will be weak.

To learn more, read my article on weakly referenced listeners.

The other place that weak references are supported is in the Dictionary object. Simply pass true as the first parameter when you instantiate a new Dictionary to have it use weak references as its keys:

var dict:Dictionary = new Dictionary(true);
dict[myObj] = myOtherObj;
// the reference to myObj is weak, the reference to myOtherObj is strong

To learn more about using dictionaries in ActionScript 3, read my article about the Dictionary Object in AS3. One of the cool things about having weak reference support in Dictionary is that we can hook into it to use weak references in other contexts. For example, I used Dictionary to create WeakReference and WeakProxyReference classes that can be used to create weak references anywhere.

WeakReference class
WeakReference takes advantage of Dictionary to allow you to hold a weak reference to any object within any context. It has a small amount of overhead for instantiation and access, so I would only recommend using it for potentially large objects that may not be properly freed. This should not replace writing code that cleans up properly after itself, but it can help you to ensure large data objects are freed properly for garbage collection.

I based the ActionScript 3 WeakReference class on the WeakReference class for Java. To use it, you simply instantiate a new WeakReference, passing the referent (object you wish to reference) as the first parameter. You then store a strong reference to the instance of WeakReference, not the referent, and access the referent via the get() method of WeakReference.

import com.gskinner.utils.WeakReference;
var dataModelReference:WeakReference;
function registerModel(data:BigDataObject):void {
dataModelReference = new WeakReference(data);
}
...
function doSomething():void {
// get a local, typed reference to the data:
var dataModel:BigDataObject = dataModelReference.get() as BigDataObject;
// call methods, or access properties of the data object:
dataModel.doSomethingElse();
}

For well architected code, this is a good solution, because it lets you maintain type safety, and is non-ambiguous. For those who just want to hack code together quickly, I also put together the WeakProxyReference class (which was a great learning experience for the new Proxy object).

WeakProxyReference class
WeakProxyReference uses the new Proxy class to transparently wrap a weakly referenced object. It works the same as WeakReference, except that you can call methods on the weak reference object directly and have them passed to the referent. The main issues are the loss of type safety, and slightly more ambiguous code. Note that it will still throw appropriate run-time errors (ex. if you try to access a non-existent property on the object), but not compile-time errors.

import com.gskinner.utils.WeakProxyReference;
var dataModel:Object; // note that it is untyped, and not named as a reference
function registerModel(data:BigDataObject):void {
dataModel = new WeakProxyReference(data);
}
function doSomething():void {
// don't need to get() the referent, you can access members directly on the reference:
dataModel.doSomethingElse();
dataModel.length++;
delete(dataModel.someProperty);
// if you do need access to the referent, you need to use the weak_proxy_reference namespace:
var bdo:BigDataObject = dataModel.weak_proxy_reference::get() as BigDataObject;
}

You can download WeakReference and WeakProxyReference at the end of this article.

Unsupported Way to Force GC
In my previous article on Garbage Collection in AS3 I said that the GC is indeterminate – that there is no way to know when it will run. That is not entirely true, there is a trick that will let you force the Flash player to carry out a full GC pass. This trick can be really handy for exploring Garbage Collection, and testing your applications during development, but it should never be deployed in production code because it can wreak havoc with processor load. It is also officially unsupported, so you cannot rely on it to work in updated versions of the player.

To force an immediate GC mark/sweep, all you have to do is call connect() on two LocalConnections with the same name. This will throw an error, so you’ll have to wrap it in a try/catch block.

try {
new LocalConnection().connect('foo');
new LocalConnection().connect('foo');
} catch (e:*) {}
// the GC will perform a full mark/sweep on the second call.

Again, this should only be used as a development aid. It should never be used in production code! There is an example of this in use in the demo FLAs for the WeakReference class, which you can download at the end of this article.

Summary
ActionScript 3 has substantially increased the amount of work developers must do to manage resources in their applications. While we have only been provided with a few new tools to help us with this, they are better than nothing and signify that Adobe is at least aware of the issue. Pairing these new tools with effective strategies and approaches (the subject of my next article) should allow you to successfully manage resources in Flash 9 and Flex 2 projects.

Download WeakReference and WeakProxyReference.

A big thank you to Thomas Reilly for proofreading this article, and ensuring I’m not misleading the rest of the Flash world too badly. Any errors are mine, not his.

Grant Skinner

The "g" in gskinner. Also the "skinner".

@gskinner

43 Comments

  1. Very helpful series of articles, Grant! Keep it up!

    Naturally, I’m curious to hear more about your experiences with the “Force Garbage Collection” method – how does it “wreak havoc” exactly? I could imagine that if you called it repeatedly, it might cause some problems, but wouldn’t it be safe for rare use?

  2. I have a serious memory leak that I just can’t pin-point and I’d be greatfull if you could help out

    I have a webcam running on a server which saves a jpeg file of what it currently sees a few times a second (it continuosly replaces the old pic) and I’m trying to write some as3 code that will read that… So i create a loader object and add an event listener (weakly referenced) and as soon as the picture loads i draw it to a bitmap and call loader.unload.. i then load the picture again by calling loader = new Loader(…)

    by the totalMemory it looks like nothing is ever removed from the memory, so i commented out the bitmap drawing part and it still eats the memory

    your help will be greatly appriciated,

    Oz

  3. Oz,

    It’s hard to say for sure without seeing code, but you could try reusing the loader (instead of constantly creating new loaders). You could also use, and dispose of the loaded bitmap directly. Off the top of my head (this might be wrong):

    var bmp:Bitmap = loader.content as Bitmap;

    var bmpData:BitmapData = bmp.bitmapData;

    This would also let you dispose of the bitmap when you’re done (which explicitly removes the bitmap data from memory):

    bmpData.dispose();

    Hope it helps.

    Grant.

  4. Hey Grant, thank you for the quick reply

    It’s probably best if I post some code, so here, i stripped it down to bear essentials (and there’s still a memory leak)

    ///———————————

    var loader:Loader = new Loader()

    var req:URLRequest = new URLRequest(“http://www.fisixengine.com/time.jpg”)

    loader.contentLoaderInfo.addEventListener(Event.COMPLETE,onLoadComplete,false,0,false)

    btnLoad.addEventListener(MouseEvent.CLICK,loadClick)

    btnUnload.addEventListener(MouseEvent.CLICK,unloadClick)

    addEventListener(Event.ENTER_FRAME,onEnterFrame)

    function onLoadComplete(e:Event){

    txt2.text=”image loaded”

    }

    function loadClick(e:MouseEvent){

    loader.load(req)

    txt2.text=”loading image”

    }

    function unloadClick(e:MouseEvent){

    loader.contentLoaderInfo.content.bitmapData.dispose()

    loader.unload()

    try {

    new LocalConnection().connect(‘foo’);

    new LocalConnection().connect(‘foo’);

    } catch (e:*) {}

    }

    function onEnterFrame(e:Event){

    txt.text = System.totalMemory

    }

    //————-

    when i run this and click the button to load the totalMemory increases, but when I click the unload button only a very small portion of it is recovered. can you see any obvious reasons?

    Thank you,

    Oz

  5. Hi Again,

    I have been reading through your articles on resource managment and it’s wonderful thing to be kept up to par.

    I posted to the pre-cursor to this article a few days ago, unfortunately I didn’t get any replies as of yet. Can any one give me some insite?

    I have a few questions and don’t want to clutter this blog . If anyone, feels like they can help me to clear up my understanding in regards to this artice, please find my blog relating to this topic at the following URL: everything4me.com/ComSvr/blogs/sample_weblog/archive/2006/08/02/24.aspx

  6. MetalKiller,

    You might want to check your comment system… I posted a fairly in-depth reply to your post in the comments, but it obviously hasn’t shown up yet.

    Cheers.

    Grant.

  7. Thanks grant! I’ll look into what’s going on with my comment system.

  8. Hi Grant,

    I figured out what was up with my blogger and if it’s at all possilbe, could you re post your explanation about resource managment?

    everything4me.com/ComSvr/blogs/sample_weblog/archive/2006/08/02/24.aspx

  9. The System.totalMemory property seems to be very helpful! However, I’m having trouble putting it to use.

    I made a very small program that can run in the timeline, as follows:

    ___

    var lastMemory:int = 0;

    addEventListener(Event.ENTER_FRAME, testFunc);

    function testFunc(e:Event){

    if(System.totalMemory != lastMemory){

    trace((getTimer()/60000)+” “+System.totalMemory);

    }

    lastMemory = System.totalMemory;

    }

    ___

    I’ve let this program run for thirty minutes, to get enough data. Over thirty minutes, this simple program’s totalMemory increased pretty steadily at a rate of about 300 bytes per second, usually in 4-kilobyte increments (and infrequently but regularly at 8-kilobyte increments).

    The same holds true for my larger AS3 project. Do you have any idea why Flash needs 4 more kilobytes every 14 or so seconds? I can’t make heads or tails of it.

  10. I’d like to nitpick about the comment “the first run-time profiling tool developers have had in Flash”. As a Flash Lite developer we had access to fscommand2(“GetFreePlayerMemory”) and fscommand2(“GetTotalPlayerMemory”) which are run-time profiling tools that developers have access to which predate AS3

  11. Oz,

    I believe your event listeners need to be weak references.

  12. FWIW, weak references are not the end-all solution to our memory management woes. i was doing some very simple tests with just one parent and one child DisplayObject, and i noticed that even when using a weakly-referenced event listener, the object still wasn’t getting garbage collected after being removed from the display list.

    i tried adding the LocalConnection hack to for garbage collection, and voila, the child was removed from memory. i don’t know why the GC wasn’t running, but i’m guessing it’s cause this particular program is so simple, and the overhead so low, that the GC was not even paying attention. if this is the case, this isn’t even really an issue; hopefully we can expect a more lively GC in any normal-sized project.

    however, the main point that this test revealed is that weakly-referenced listeners on their own are not enough; every object that adds itself as a listener should also have a kill() method that manually removes itself from all event sources.

  13. Seriously though, why don’t they allow us to call and force the initialization of the GC?

    Would it not be simple enough to say

    if (condition == true)

    {

    GC.forceRun();

    }

    Whats so bad about that??

    Leave it up to the developer to force it, as well as the typical garbage collection runs?

  14. Hope this helps. An excpert from an Adobe Tech Note: In ActionScript 3.0, unload() removes a child of the particular Loader object that was loaded by using the load() method. However, that the child is not necessarily destroyed because other objects might have references to it. It is no longer a child of the Loader object, but it can still be in memory, and will remain in memory until the garbage collector picks it up and destroys it.

  15. Hey Grant — word, you’re a superstar! Congrats! Thanks also on the sharing of your expertise and experience.

    My scenario is this: Flex loads and establishes a LocalConnection to a AS2 proxy which is designed to load various foreign AS2 streaming video sources. No matter how carefully I null / delete / unload / and remove all vars, listeners, movieclips, and classes, the previous content continues to stream.

    I’m at wit’s end. Any suggestions will save me from seppuku.

  16. I just wanted to add on to what ericsoco said about weak references.

    I made a small test that has a button which creates a timer object which runs trace(“timer”) constantly… I then played around with garbage collection to see when the button would be freed and what would prevent it.

    #1 – Timer objects must be declared with weak references, or they disable GC on the object until the timer ends (which could be never on some timers)

    #2 – If within a class you add an eventlistener to *ANY* object, even with abstract/inline functions and weak references, or if the object is “this” -> GC *is* prevented *UNTIL* the event is removed – Don’t ask me why, there should be no reference to the object *other* than the scope/stack trace of the event handler (in my test code the function code was empty) and weak references shouldn’t matter.

    #3 – if an event listener is attached to the object itself (from *outside* the class code) it doesn’t matter if you use weak/strong it will automatically be freed at GC -> inside the class code, refer to #2

    #4 – if *any* event listener prevents an object from disposing, ALL events which are not explicitly removed from that object will continue running (even those that normally would automatically remove themselves)

    So, basically to be safe you should always remove all events, because the only time you’re safe is very few and far between.

    However, for most events, this will not prevent adverse results (other than memory leak), because even if a mouseClick is being listened to, it should never be fired if off the displaylist, HOWEVER it will produce a slight cpu/memory strain, which when multiplied by MANY objects with leaks, could cause a large strain on the memory/cpu. -> But, for some events like stage.mouseUp, if you thought the event was gone and it keeps firing, you’re gonna have some issues – and even if GC does remove it, GC may not run for 5 min, meaning you’re still gonna have wacky results *until* GC runs.

    A final note, GC when ran by the flash can run very randomly… I started pasting like crazy into a textbox trying to produce GC and sometimes it would run after only a few memory increases (task manager/firefox) but othertimes my app would get up to 120MB (which is a lot considering my app had *only* a textbox and two buttons) – However, on a big app, GC should run fairly often.

    Disclaimer: These statements assume that the LocalConnection exception *always* produces GC, because otherwise my test results could have been skewed…

  17. Hi Grant, firstly thanks for the articles, they’ve been a great help to me!

    I have a query that hopefully you can help me with. I have set up a small class to test the GC, which I pulled from Colin Moocks EAS3.

    package com.main

    {

    import flash.display.*;

    import flash.text.*;

    import flash.utils.*;

    import flash.events.*;

    import flash.system.*;

    public class test extends MovieClip

    {

    public function test()

    {

    var s:Sprite = new Sprite();

    s.addEventListener(Event.ENTER_FRAME, enterFrameListener);

    var s:Box = new Box();

    var timer:Timer = new Timer(1, 0);

    timer.addEventListener(TimerEvent.TIMER, timerListener);

    timer.start();

    }

    private function timerListener(e:TimerEvent):void{

    new TextField();

    }

    private function enterFrameListener(e:Event):void{

    trace(“sys mem:” + System.totalMemory);

    }

    }

    }

    The one thing I’ve added is to attach a Box sprite to the stage, which has its own enterFrame and trace in it. As far as I can tell it should be ellagable for garbage collection?! Once the GC has run the class enterFrame event is stoped, but the SoundBox timeline continues to be called. Can you shed some light on where I’m going wrong.

    Cheers,

    -T.

  18. Chris Harglerode October 28, 2007 at 9:53pm

    Hey Grant,

    Great read, doing my best to implement practices like this in all future projects. Speaking of which, it seemed like I was having a memory leak of my own. I went through every line of code and realized that no change I made every changed the increase of total memory. Finally, I decided to test out a little something and I found something odd. Perhaps you could shed some light on this.

    Take the following code and run it.

    package

    {

    import flash.display.Sprite;

    import flash.system.System;

    import flash.events.Event;

    public class Main extends Sprite

    {

    private var tf:TextField;

    public function Main()

    {

    addEventListener(Event.ENTER_FRAME, Repeat);

    }

    private function Repeat(e:Event):void

    {

    trace(“System Memory = ” + System.totalMemory);

    }

    }

    }

    Notice that the memory increase by exactly 4096 every so many seconds based on the fps. No objects, no anything. Any knowledge of why this would be happening. Works the same with Timer as well. Also tested it outside of the Flash IDE and also with Flex. Same results all around.

    Any ideas?

    Thanks

  19. @Chris: This could just be the trace window filling up. Have you tried doing the memory test without the trace, or outside of the IDE?

  20. Chris Harglerode October 31, 2007 at 6:49pm

    @Grant

    I tested it outside of the IDE outputting the results to a text field. I also had a buddy try it with Flex. Same results all around.

    Basically, I have a constant code driven animation using ENTER_FRAME and after about 20 seconds the framerate drops dramatically. However, the exact same setup in Flash 8 AS2 I can leave running well over 10 minutes with no ill effects.

  21. Grant – I appreciate all of the information that you’ve been posting about the GC. Your presentations at Max2007 in Chicago were some of the best there.

    I think I have a very similar problem when trying to get the loader.unload() function to work.

    Even using the Adobe LiveDocs code under the Loader Class (last example) – replacing out the “Image.gif” file and placing in a “no AS” swf file. The progress listener still continues to fire. Even going through and removing all traces – listeners, and setting everything to null – inside the IDE – under the simulate download – the stream is still open. Outside of the IDE – the progress listener will still fire – even after the file was supposed to be unloaded. I’m not sure why when you unload something it doesn’t auto fire GC…

    Are there known issues with the GC and the IDE?

    I’ve seen the technote about the netstreams – but even if there’s no AS code in the movie – why would it still have problems unloading it?

    Thanks in advance…

  22. Hi,

    Great articles!

    I’m a beginner of AS3, can I assume this memory leak problem still exists till now (2008) after about one and half year of the first article and Flash player 9 updates?

    Thank you.

  23. System.totalMemory seems quite handy. However, is there any problem using totalMemory from a Flash file that has a separate document class ? After reading this page’s post, I have enjoyed using .totalMemory; however, get the following error only in a file that has a separate document class:

    Error #1065: Variable System::totalMemory is not defined.

    here’s the only code in the main Flash 9 timeline

    import System.*;

    startTheGame();

    addEventListener(Event.ENTER_FRAME,checkMem);

    var __x:int = 0;

    function checkMem(event:Event){

    __x++;

    if(__x> 100)

    trace(System.totalMemory);

    }

  24. @e9 , you dont have to import system if you want to use System.totalMemory in the flash timeline

    if you are using a document class then

    import flash.system.System;

    @Grant can we still expect a solution to this problem ? its been way over a year since you posted this article , and i’ve been waiting for the next article “Strategies and Solutions”, are you still working on it ?

  25. Memory leaks seem to be a non-issue when played in a broswer or standalone player – only in the IDE is it a leakage problem….we just comign to the end of a 3D game build in AS3 and ‘discovered’ a 11Mb per game play leak during testing… after many unsuccessful attampts and good lot of hours, using all the methods mentioned above, nothing would prevent the image loader from keeping hold of the memory… while testing in the IDE that was. Creating a debug text window and running in a browser showed that the leak didnt exist outised the IDE. So there you have it. Don’t just take my word for it – try it and also see the thread here http://www.kirupa.com/forum/showthread.php?t=282500 for further evidence of this.

  26. Hi Grant. I just noticed that the debugger version of flash player 115 as well as Air debug launcher have included a gc(); method in the System class

    System.gc();

    Not sure if this is old new but I thought I’d post just in case:)

    Donovan

  27. Hi. Is there any resource in AS3 to save memory when i´m working with .flv? I´m creating an application that needs to load and keep in memory up to 70 flv´s files with 30 KB. The problem is each file I load take up 5 MB in memory. When application is running in browser, windows´task manager points to an average memory usage of 200 MB. I had used all the resources you indicate to save memory but don´t work. I´m using NetConnection, NetStream and Video classes. If anybody has an idea about this I will appreciate.

  28. Sean McCracken April 8, 2008 at 9:33am

    Grant,

    That weakReference saved my life!

    I mean it. Really. Thank you.

    Sean

  29. AIR app memory leak!

    I started a parent movie A @ x MB in memory, child movieB (an image) is loaded by a loader in parent movieA and increased memory to x+100.

    Removed this child movie using *all* above best practices. Expected behavior memory goes back to x, right?

    Doesn’t happen memory is now x+20 (I can never get rid of the 20). Here’s the crazy part, I run this on IE browser, memory goes back to x no problem.

    What’s the solution now?

  30. Hi Grant don’t normally post comments but I am moving from as2 t oas3 and have read your articles on memory management and was wondering when the last Strategies and Solutions article will be available. great articles and very informative

    Regards mike

  31. Hi 2 all. I’ve recently created some monitoring classes (Cache/ResourceMonitor), which, i’ve also posted at pixelbreaker , any way i think u find it useful.

    Check it out @ my labs http://chargedweb.com/labs/

  32. hi grant,

    thanks for your insights – they’ve been a great help.

    maybe you have already thought of this, but for me it was new:

    i used ExternalInterface.addCallback in an instantiated class. even after setting the callback function to zero, the object was never garbage collected (as there is no removeCallback function i think the link stayed persistent)

    my workaround was to add the callback to the topmost class, handling it there and never to remove it.

    so maybe it’s worth to mention to avoid ExternalInterface callbacks for memory management ?

    cheers rossi

  33. Thank you very much for given this useful post!

  34. Hi, I am faceing similar problem as Ramon a year ago. I am working with NetStream, and every time I attach new NetStream to Video – it takes extra memory – so after a while and few videos played – memory usage is very high.

    Could anybody help me and tell how to properly clear memory when working with NetStream ??

    Thanks !!!!

  35. As always this is super useful, thatnks for taking the time to write this all out.

  36. Hi Grant

    Are you still going to do the next article “Strategies and Solutions”?

  37. Great series, this helped me a lot. But I wanted to ask a simple question, since I’m developing an application and I have some memory leaks.

    Imagine I have a class named BaseClass, and some classes that extend this one, such as Class1, Class2, etc.

    I declare a variable: myBaseClass:BaseClass = new Class1();

    After a while, I use it to instantiate a new object: myBaseClass = new Class2(); some time after, myBaseClass = new Class3(); and keep going.

    Is there any memory problem here? Are the unused instances correctly deleted by the garbage collector?

    Thanks!

  38. Please Grant, we need to know more why it’s so bad to force the GC, and if the method System.gc() works.

    (I have tested it and it seems to work if I run the .SWF directly from the Flash Player, not in the browser).

  39. Hi there,

    Just tried out your WeakReference class.

    It doesn’t work.

    I’m using Adobe Flex Builder 3 with Flex 3.3. I have a very simple sample project I can reproduce this with. But here’s the crazy part – if I step through the code, it works. If I don’t, it doesn’t. Still holds onto the object!!

    Mark

  40. jenry –

    the System.gc() only applies to debuggers, you can not depend on this for anything other than debugging an application. It will not work in a release version.

    Thanks for the post Grant, Great read and super helpful.

    Forgive my ignorance, I am still new-ish to caring about memory (all my apps have been small enough iIcould care less about size) In a normal application when does the GC actually run? Is it just spuradic / random whenever Flash deems it necessary? Thanks

    -Andrew

  41. Great work, I’ve only just begun to do some work in AS3 (currently looking at Away3D) and coming from Java I’ve allways wandered how to properly manage memory in Java. Thnx

    ps, what happened to the strategies and solutions 🙂

  42. Wonderful Article on this complex issue.

  43. “FileReference.load” consumes memory, but after the “Event.COMPLETE” it does not free the memory, I’ve tried everything, could you help me?

    See my question details in SO: http://stackoverflow.com/questions/22738221/how-can-i-free-up-memory-filereference

    🙂

Comments are closed.