I’ve been too busy to update the devlog recently but I have in fact posted new content to Bowmaster Winter Storm every Friday for the past few weeks. Go check it out if you haven’t taken a look lately.
www.lostvectors.com/winterstormbeta
I’ve had some recent major triumphs, some mini defeats and mini triumphs lately. In the not so distant past I had some issues with a memory leak in Winter Storm. After much investigation and some thrashing I decided it was time to do some much needed refactoring. At this time I still had no real idea where the cause of the leak was.
My first step was to create a more formal process for handling event listeners. In AS3 I take issue with the lack of management functions that exist for working with events. The event model is much improved over AS2, but a few things caused some issues for me.
Function “removeEventListener” does not provide feedback on whether the operation was successful.
Ideally you would always call this function with the correct parameters to remove an event listener. However if you mistakenly give just a single incorrect parameter then the removeEventListener function call fails silently. The event listener that you though you removed remains in memory along with all of its attached references thus causing a memory leak.
for example:
1 | myHugeObject.addEventListener("destroy", handleDestroy); |
… later in the process, somewhere else in the code…
1 | myHugeObject.removeListener("destroyed", handleDestroy); |
Note: The event string parameter is the incorrect string. Syntactically there is no error. The program will compile. Not only that, but there is also no runtime error as well. Even though no such listener exists with the signature of event string “destroyed” and “handleDestroy” the function will not throw an error.
Even if you now do this:
1 | myHugeObject = null; |
The object remains in memory, now with no way to reference it, hence memory leak.
I was certain that I used removeEventListener responsibly and everywhere I added a listener I made sure to include a removeEventListener in a destroy function that was called when the object is no longer used. The problem was that I had made the mistake I described above. I found this out with the help of a custom event management class I created called EventManager.
Essentially I created a class to handle all event listener adds and removes so that all such operations could be audited for validity.
My syntax changed from:
1 2 | myDispatcher.addEventListener("event", listenerFunc); myDispatcher.removeEventListener("event", listenerFunc); |
to:
1 2 | EventManager.addListener(myDispatcher, "event", listenerFunc); EventManager.removeListener(myDispatcher, "event", listenerFunc); |
By funneling all event listener add and remove calls through this static class I was able to provide extra debugging feedback. For example, whereas before with removeEventListener if you attempted to remove an event listener that does not exist Flash would not care to inform you. With EventManager, if you call removeListener and it cannot find the listener you’re looking for a runtime error will be thrown.
EventManager essentially maintains a global registry of all event listeners that is now visible to the developer. I had to convert hundreds of calls to add/removeEventListner to use EventManager but it was worth it. With the added visibility and error checking of my EventManager class I was able to see when and if the registry was continually growing instead of staying within a certain size. And upon initially testing the fully converted code I immediately discovered a few listeners that were lingering and occasionally had runtime errors indicating that I was attempting to remove events listeners that did not exists.
So this was a major triumph in my battle against those evil pesky memory leaks. After fixing all the issues with event listeners I thought I was sure to see no more memory problems. While I did notice fewer memory issues, I still noticed some instances where memory usage would continue to creep up. It was then that I decided to revisit the method of disposing of application objects.
For all of my objects I have some form of “destroy” function that is called when the object is no longer needed and this function primarily just removed event listeners. I didn’t attempt to nullify references to other objects stored as member data within this object because I figured as long as my application didn’t refer to this object (i.e. store a reference to it) then garbage collection would eventually take care of everything.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class ObjectA { public var m_objB:ObjectB; public var m_objC:ObjectC; public function destroy():void { // remove listeners } } class MainApp { public var m_objA:ObjectA; ... public function startNewLevel():void { m_objA = new ObjectA(); } public function showUpgradesMenu():void { // show upgrades } public function endLevel():void { m_objA.destroy(); } } |
With this design, the ObjectA’s destroy function would only remove listeners. Also, in MainApp, the reference to m_objA is only overridden when startNewLevel() is called. Therefore even though endLevel() was called, if the next step is to call showUpgradesMenu() the user may be looking at new upgrades to purchase for a while before startNewLevel() is called. In the meantime the reference to m_objA still exists even though it’s destroy function was called, and within it exist references to instances of ObjectB and ObjectC.
I decided to take a more aggressive and proactive approach to freeing memory. I made the design decision to nullify all member data object references whenever an object is destroyed and then also nullify the current reference to the destroyed object.
The new way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class ObjectA { public var m_objB:ObjectB; public var m_objC:ObjectC; public function destroy():void { // remove listeners nullifyReferences(); } public function nullifyReferences():void { m_objB = null; m_objC = null; } } class MainApp { public var m_objA:ObjectA; ... public function startNewLevel():void { m_objA = new ObjectA(); } public function showUpgradesMenu():void { // show upgrades } public function endLevel():void { m_objA.destroy(); m_objA = null; } } |
You may think that just setting m_objA to null after the destroy() call should be enough, and for what you can see in this example it definitely would be, but in the context of a larger application there may exist other references to m_objA so simply nullifying it in MainApp will not guarantee that it is a candidate for garbage collection.
So I applied this design strategy to my game application. It was a bit tedious but worth it because a useful side effect of nullifying member references after destroying an object is that any other objects that may have been incorrectly using an object after destruction can no longer access public member data in the destroyed object. What happened in my game after I implemented this design was that the game application started to show runtime null reference errors everywhere in application was using a destroyed object when it wasn’t supposed to.
As part of a “design by contract” approach the game application is not supposed to use destroyed objects. This means that instead of checking “isDestroyed?” every time I want to use an object’s data, I make sure that the code doesn’t try to use destroyed objects in the first place. For instance, game unit AI is not supposed to target units that are destroyed, so when the application returns a list of all the closest enemies it makes sure to not return any “destroyed” enemies. The AI can assume the list of enemies it received consists of only “alive” enemies (a precondition for using the list). For the most part, I did a good job of ensuring that destroyed objects were not used. However, just like the event listener issues mentioned above, there were a few instances where I made mistakes.
So with a little bit of debugging and redesign I was able to not only improve memory usage efficiency but I was able to locate erroneous areas of my code. Since this design overhaul the game is running much more smoothly and previous failed memory tests are now passing and I now have a good infrastructure in place to prevent further memory issues. Now I can focus my efforts on more of the fun stuff like creating new content and game balancing.
4 Responses to AS3 Memory Fail