Tuesday, January 29, 2013

Plugging Memory Leaks in Corona


I’m finding out that Lua is not as forgiving a language as I had first thought.  These were the changes I made in level0 to get rid of the various memory leaks I had created.
Destroying Objects
This was the most obvious thing to do, and so became the first order of business.  We have been inserting objects into a self.view group – and that order of removal is a very important thing in Lua.
Using this article on removing objects properly as a guide I found a number of things.  First just removing an object from a group isn’t enough for the garbage collector – the reference the variable is pointing to must also get removed.  So there are two steps to destroying an object – first is to remove it from a group by calling the group:remove( object name or index ) or object:removeSelf() methods.  Then you have to set the object = nil to remove the reference.
But wait – there’s more!  The order of removal from a group is also important.  In fact a common pitfall is to iterate thru a group in a normal forware fashion – which may in fact skip every other item in the group.  I’m going to copy and paste the relevant section from the above link here:
Common Pitfalls
A common mistake is to improperly remove all objects from a group. This typically happens when you write code that iterates through a group attempting to remove each child from the display hierarchy. It’s natural to iterate through the group in the forward direction. However, this can cause no end of confusion.
Continuing with our solar system example, consider the following where we attempt (incorrectly) to remove all the objects from the solar system.
for i=1,solarSystem.numChildren do
        local child = solarSystem[i]
        child.parent:remove( child )
end
The problem here is that we are modifying a collection (i.e. the group’s children array) as we iterate through that same collection. The result is we remove every other child. The easiest way to illustrate this is with a parallel example involving an array of integers:
local array = {1,2,3,4,5,6,7,8,9,10}
print( table.concat( array, " " ) ) --> 1 2 3 4 5 6 7 8 9 10
 
for i=1,#array do
        table.remove( array, i )
end
 
print( table.concat( array, " " ) ) --> 2 4 6 8 10
The fix is to iterate backwards.
for i=solarSystem.numChildren,1,-1 do
        local child = solarSystem[i]
        child.parent:remove( child )
end
Of course, this only ensures that all children have been removed from the display hierarchy; you till have to set all references to these display objects to nil. So in this example, we were merely trying to illustrate the highlight the pitfalls of iterating forward through the children of a group and modifying the group at the same time. A good implementation would also set the corresponding properties in solarSystem to nil for proper cleanup. 
So as part of my exitScene function I’ve added the following:
for i=screenGroup.numChildren,1,-1 do
        local child = screenGroup[i]
        child.parent:remove( child )
end 
Then I do this:
screenGroup:removeSelf()
And then set the various objects equal to nil.
We aren’t done yet tho…

Forward Referencing

This part comes from reading this handy article on The Significance of Local Variables in Lua.  We’ve all seen this:
local myFunction = function()
        callback()  -- ERROR: function 'callback' does not exist yet
end 
local callback()
        print( "Hello World" )
end 
myFunction()    -- will produce error 
Refactored this way, the code works: 
local callback    -- forward reference (for later use)
local myFunction = function()
        callback()    -- output: "Hello World"
end
-- function below is still local, because of forward reference
callback = function()
        print( "Hello World" )
end
 
myFunction()    -- no error; happy code :-) 
So I took every function that was not part of the scene template and moved it above function scene:createScene( event ).  This took care of a few ambiguous runtime errors.  This includes the addListeners() and removeListeners functions.
At this point things were supposed to be better, but I started to get screen freezes – holy cow things are getting worse!

Runtime Listeners

I next turned to this handy guide, Corona SDK Memory Leak Prevention 101.  And found this handy scrap if information:
When you remove a display object, listeners that are attached to it are also freed from memory. However, when you add listeners to Runtime (e.g. enterFrame listeners), those never get freed until you manually remove them.
 
A common leak that occurs with Runtime/enterFrame listeners is when a developer adds a Runtime listener to the particular screen they’re in, but they forget to remove the event listener whenever the user leaves the screen. So what happens is, the user leaves the screen, and then when they come back to it, there are two identical Runtime listeners running on top of one another.
Now, we do add a Runtime enterFrame listener with the gameloop code.  I make extra sure that the removeListeners function has the Runtime:removeEventListener('enterFrame', gameloop ) and that it gets called.

Timers and Transitions

Again back to this handy guide, Corona SDK Memory Leak Prevention 101.  I’m just going to copy and paste from the guide:
Timers and Transitions are probably one of the most common causes of crashes, mostly due to the fact that countless (unaccounted for) things could happen between the time the timer/transition starts, and the time it ends. What’s worse, if you forget to cancel these and nil out their variables when no longer needed, things could get stuck in memory and cause more leaks.
One way you could manage these is to store ALL of your timers and transitions in a table, so that when you know for sure no timers or transitions should be running, you can cancel all of them at once.
If you add the following code to your main.lua file (or another module and require it in), you can easily keep track of your timers/transitions and also cancel them all at once if and when needed:
timerStash = {}
transitionStash = {} 
function cancelAllTimers()
    local k, v
    for k,v in pairs(timerStash) do
        timer.cancel( v )
        v = nil; k = nil
    end
 
    timerStash = nil
    timerStash = {}
end
--
function cancelAllTransitions()
    local k, v
    for k,v in pairs(transitionStash) do
        transition.cancel( v )
        v = nil; k = nil
    end 
    transitionStash = nil
    transitionStash = {}
end
Then, whenever you start a new timer or transition, you assign it to the respective table first, like so:
timerStash.newTimer = timer.performWithDelay( ...
transitionStash.newTransition = transition.to( myObject { ... 
Then you can use the cancelAllTimers() and cancelAllTransitions() functions to stop all timers and transitions all at once. If you have timers you don’t want to be cancelled, then don’t add them to the “stash” tables in the first place (but be sure not to forget to keep track of them). 
So I added the above cancel functions to level0, and then call them after the removeListeners call.  I found I could do this in the gameloop function and it would work:
transitionStash.newTransition1 = bg1:translate(0,2)
transitionStash.newTransition2 = bg2:translate(0,2)
 
Things are better but I still was getting some runtime errors – not as many, and no crashes but still annoying.

Proper Placement of Cleanup Code

I was experimenting with the right place to put all this garbage collection code – and in desperation decided to go back to the documentation.  One last link – storyboard.gotoScene():
Description:
Used to transition to a specific scene (determined by scene's module name). Internally, this will handle all loading/unloading of the scenes. Scenes may also be explicitly unloaded/removed by the user.
When this function is called, the "exitScene" event will be dispatched for the currently loaded scene (if one exists), followed by the "createScene" event (for the specified scene—sceneName—if no "view" property is present), and then the "enterScene" event (also for the specified scene—sceneName).
It dawned on me – keep the code in the exitScene and it would always get called in correct order. 
At this point I was about 99.5% there.

Zombie Scenes

Yes, the dead still inhabited the program.  I was seeing calls to level0 when in level2 – no crashes but still getting some runtime errors.  So I found this reference to storyboard.purgeAll().  In cutscene1, the scene:createScene( event )function I replaced the call to storyboard.purgeScene( priorScene ) with storyboard.purgeAll().  Inside the Lua command box I was seeing calls to scene:destroyScene( event ) in both level0 AND train3.  More importantly it got rid of the last runtime errors.

No comments:

Post a Comment