local variable question regarding memory

I wonder if the usage of just

transition.to (…)

or

performWithDelay (…)

is adding to the memory usage more than doing this in this way:

local trans=transition.to (…)

or

local perform=performWithDelay(…)

?

My experience has been yes. I keep track of all transitions and timers in a table and clear it out periodically.

I’m having trouble believing those leak, but I’ll test the idea first before making assertions.

Also, these statements:

-- case 1 (uses less memory) transition.to (...) performWithDelay (...)

are the same as these, except, in this you have created two locals:

-- case 2 (uses more memory) local trans=transition.to (...) local perform=performWithDelay(...)

Assuming all your code isn’t in main.lua, those locals will fall out of scope eventually and both of those variables will go bye-bye.

So, in case 1, the return values are dropped on the floor and ignored.

In case 2, they are stored in temporary locals and then ignored.

UPDATE @d.mach

The second case is actually using more memory than the prior (at least until those variables fall out of scope.)

You should be right Ed, and might well be, but I’m *sure* I was getting memory leaks using lots of transitions and timers without having a handle to cancel and nil them…

I had a function collecting garbage every second but memory still crept up.

Hi again.  I investigated this and found no leaks.

Here is my test code for each (both ran 10,000 iterations):

https://github.com/roaminggamer/RG_FreeStuff/raw/master/AskEd/2018/10/leakTests.zip

The results were the same for both:

  • No increase in main memory usage.
  • No increase in texture memory usage (a small decline actually, but that may be a side-effect of measuring the first value so close to startup in the simulator.)

My guess is, if there were a leak from timer transition related work, it was from variables, objects, or functions that got created by the payload function and not cleaned up.  i.e. The leaks are most likely coming from game code, not the Corona features.

If my test sample doesn’t reflect your style of transition/timer calls, let me know.  I tried the basic calls and even used a closure for every timer which wasn’t really necessary for this test.

Transition Test & Results

io.output():setvbuf("no") display.setStatusBar(display.HiddenStatusBar) -- ===================================================== local cx = display.contentCenterX local cy = display.contentCenterY local fullw = display.actualContentWidth local fullh = display.actualContentHeight local left = cx - fullw/2 local right = cx + fullw/2 local top = cy - fullh/2 local bottom = cy + fullh/2 local moveTime = 1 -- values below a frame duration default to 'takes one frame' local moveRight local moveLeft local count = 0 local maxCount = 10000 local tmem = {} local mmem = {} local times = {} local function result() print("Texture Memory Delta: " .. (tmem[2]-tmem[1]) ) print(" Main Memory Delta: " .. (mmem[2]-mmem[1]) ) print(" Time Delta: " .. (times[2]-times[1]) ) end local function countMem( entry ) collectgarbage("collect") tmem[entry] = system.getInfo( "textureMemoryUsed" ) mmem[entry] = collectgarbage( "count" ) times[entry] = system.getTimer() -- print( entry .. " : Texture Memory: " .. tmem[entry] .. " @ " .. times[entry] ) print( entry .. " : Main Memory: " .. mmem[entry] .. " @ " .. times[entry] ) -- if( entry == 2 ) then result() end end moveRight = function( self ) count = count + 1 if( count \> maxCount ) then countMem( 2 ) return end -- self.onComplete = moveLeft transition.to( self, { x = right, time = moveTime, onComplete = self } ) end moveLeft = function( self ) count = count + 1 if( count \> maxCount ) then countMem( 2 ) return end -- self.onComplete = moveRight transition.to( self, { x = left, time = moveTime, onComplete = self } ) end local obj = display.newCircle( left, cy, 20 ) -- Make first mem + time count countMem(1) -- Start ping-pong movement moveRight( obj )

result:

1 : Texture Memory: 4 @ 18 1 : Main Memory: 244.916015625 @ 18 2 : Texture Memory: 4 @ 166715.5 2 : Main Memory: 239.3193359375 @ 166715.5 Texture Memory Delta: 0 Main Memory Delta: -5.5966796875 Time Delta: 166697.5

Timer Test & Results

io.output():setvbuf("no") display.setStatusBar(display.HiddenStatusBar) -- ===================================================== local cx = display.contentCenterX local cy = display.contentCenterY local fullw = display.actualContentWidth local fullh = display.actualContentHeight local left = cx - fullw/2 local right = cx + fullw/2 local top = cy - fullh/2 local bottom = cy + fullh/2 local moveTime = 1 -- values below a frame duration default to 'takes one frame' local moveRight local moveLeft local count = 0 local maxCount = 10000 local tmem = {} local mmem = {} local times = {} local function result() print("Texture Memory Delta: " .. (tmem[2]-tmem[1]) ) print(" Main Memory Delta: " .. (mmem[2]-mmem[1]) ) print(" Time Delta: " .. (times[2]-times[1]) ) end local function countMem( entry ) collectgarbage("collect") tmem[entry] = system.getInfo( "textureMemoryUsed" ) mmem[entry] = collectgarbage( "count" ) times[entry] = system.getTimer() -- print( entry .. " : Texture Memory: " .. tmem[entry] .. " @ " .. times[entry] ) print( entry .. " : Main Memory: " .. mmem[entry] .. " @ " .. times[entry] ) -- if( entry == 2 ) then result() end end moveRight = function( self ) count = count + 1 if( count \> maxCount ) then countMem( 2 ) return end -- self.x = right timer.performWithDelay( moveTime, function() moveLeft( self ) end ) end moveLeft = function( self ) count = count + 1 if( count \> maxCount ) then countMem( 2 ) return end -- self.x = left timer.performWithDelay( moveTime, function() moveRight( self ) end ) end local obj = display.newCircle( left, cy, 20 ) -- Make first mem + time count countMem(1) -- Start ping-pong movement moveRight( obj )

result:

1 : Texture Memory: 4 @ 13 1 : Main Memory: 245.03125 @ 13 2 : Texture Memory: 4 @ 166641.4 2 : Main Memory: 239.6103515625 @ 166641.4 Texture Memory Delta: 0 Main Memory Delta: -5.4208984375 Time Delta: 166628.4

Note: I responded to the above assertions because I always like to check when folks say, “I think this thing is broken.”

If I find it is broken then I ask it be fixed (via bug report). 

If it isn’t broken, then we all know a little more and those who encountered issues can go back and re-investigate to find the real cause of their troubles.

This way everyone benefits.

I don’t believe in dropping ‘possible bugs’ on the floor for future users to stumble over.

PS - If that last statement sounds like an admonition it is not. 

I realize we are all busy and sometimes getting the game/project done is the only priority.

Still, if one does find a possibly serious issue, filing a bug on it (after verification) is awesome.

Just for fun…

bug-report.jpg

I actually also wrote a test function to this when the same question was asked some time ago as it seemed to be a frequently debated issue. It’s not as fancy (or probably even as correct) as above, but here it is :smiley:

 

local startingMemory local function checkMemory(cmd) collectgarbage( "collect" ) local memUsage\_str = string.format( "MEMORY = %.3f KB", collectgarbage( "count" ) ) print( memUsage\_str ) if cmd ~= nil then return memUsage\_str end end startingMemory = checkMemory(1) timer.performWithDelay(500,checkMemory,0) local t = {} local iterations = 5000 local function junk(i) -- print(i) if i == iterations then for i = 1, iterations do t[i] = nil end print("---\nstartingMemory\n"..startingMemory.."\n---") end end for i = 1, iterations do -- method 1, bloat a table with timers t[i] = timer.performWithDelay( 55, function() junk (i) end ) -- method 2, just fire away some timers -- timer.performWithDelay( 55, function() junk (i) end ) end

 

Using either method 1 or method 2 resulted in exactly the same memory usage, which I thought was odd. So, I decided to borrow some code from https://docs.coronalabs.com/tutorial/data/outputTable/index.html and here’s that borrowed code in action:

 

local function nothing() end for i = 1, 5 do timer.performWithDelay( 1000, nothing ) end local function printTable( t ) local printTable\_cache = {} local function sub\_printTable( t, indent ) if ( printTable\_cache[tostring(t)] ) then print( indent .. "\*" .. tostring(t) ) else printTable\_cache[tostring(t)] = true if ( type( t ) == "table" ) then for pos,val in pairs( t ) do if ( type(val) == "table" ) then print( indent .. "[" .. pos .. "] =\> " .. tostring( t ).. " {" ) sub\_printTable( val, indent .. string.rep( " ", string.len(pos)+8 ) ) print( indent .. string.rep( " ", string.len(pos)+6 ) .. "}" ) elseif ( type(val) == "string" ) then print( indent .. "[" .. pos .. '] =\> "' .. val .. '"' ) else print( indent .. "[" .. pos .. "] =\> " .. tostring(val) ) end end else print( indent..tostring(t) ) end end end if ( type(t) == "table" ) then print( tostring(t) .. " {" ) sub\_printTable( t, " " ) print( "}" ) else sub\_printTable( t, " " ) end end printTable( timer )

printTable outputs the following:

 

table: 0C412880 { [\_runlist] =\> table: 0C412880 { [1] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } [2] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } [3] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } [4] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } [5] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } } [resume] =\> function: 098E2220 [\_insert] =\> function: 098E2600 [performWithDelay] =\> function: 098E3D40 [\_remove] =\> function: 098E26E0 [\_nextTime] =\> 1014.9 [enterFrame] =\> function: 098E27C0 [cancel] =\> function: 098E2500 [pause] =\> function: 098E32A0 [\_updateNextTime] =\> function: 098E2480 }

So, it seems that Corona adds all timers into a table to begin with, regardless of whether or not you do. You can also access all timers that you’ve created via timer._runlist, e.g. you find the time set for the first timer by using “print(timer._runlist[1]._time)”. This means that you could also cancel timer #1 via “timer.cancel( timer._runlist[1] )”.

Hopefully this info helps someone or is at least interesting :smiley:

Oh, and just a quick tip to anyone interested in trying to control your timers by using the timer._runlist entries. Don’t. :smiley:

If you were to, for instance, create 100 identical timers in a loop, they will almost definitely not be in the correct order, i.e.

 

local function getID(i) print(i) end local iterations = 100 for i = 1, iterations do timer.performWithDelay( 1255, function() junk (i) end ) end

You might expect that the getID function outputs numbers as 1, 2, 3, …, n, but in fact all of the numbers are scrambled. This is why you want to add some identifiers to the timers so that you can properly control them.

consult source here:  https://github.com/coronalabs/framework-timer/blob/master/timer.lua

what you’ll find is that the _insert method does a “non-stable” insertion sort by time. (by “non-stable” i mean:  there is no “tie-breaker” if times are equal, so overall order among timers with same times would be unpredictable if the entire table were resorted using that method)

if you wanted to, you could generate and assign an incremental “id” in each call to performWithDelay, then use that as a tie-breaker, then you’d get more predictable behavior.  (though keep in mind that your loop takes some non-zero time to execute, so highly unlikely that all 100 have exact same time to begin with, and could vary based on fractional resolution of getTimer())

My experience has been yes. I keep track of all transitions and timers in a table and clear it out periodically.

I’m having trouble believing those leak, but I’ll test the idea first before making assertions.

Also, these statements:

-- case 1 (uses less memory) transition.to (...) performWithDelay (...)

are the same as these, except, in this you have created two locals:

-- case 2 (uses more memory) local trans=transition.to (...) local perform=performWithDelay(...)

Assuming all your code isn’t in main.lua, those locals will fall out of scope eventually and both of those variables will go bye-bye.

So, in case 1, the return values are dropped on the floor and ignored.

In case 2, they are stored in temporary locals and then ignored.

UPDATE @d.mach

The second case is actually using more memory than the prior (at least until those variables fall out of scope.)

You should be right Ed, and might well be, but I’m *sure* I was getting memory leaks using lots of transitions and timers without having a handle to cancel and nil them…

I had a function collecting garbage every second but memory still crept up.

Hi again.  I investigated this and found no leaks.

Here is my test code for each (both ran 10,000 iterations):

https://github.com/roaminggamer/RG_FreeStuff/raw/master/AskEd/2018/10/leakTests.zip

The results were the same for both:

  • No increase in main memory usage.
  • No increase in texture memory usage (a small decline actually, but that may be a side-effect of measuring the first value so close to startup in the simulator.)

My guess is, if there were a leak from timer transition related work, it was from variables, objects, or functions that got created by the payload function and not cleaned up.  i.e. The leaks are most likely coming from game code, not the Corona features.

If my test sample doesn’t reflect your style of transition/timer calls, let me know.  I tried the basic calls and even used a closure for every timer which wasn’t really necessary for this test.

Transition Test & Results

io.output():setvbuf("no") display.setStatusBar(display.HiddenStatusBar) -- ===================================================== local cx = display.contentCenterX local cy = display.contentCenterY local fullw = display.actualContentWidth local fullh = display.actualContentHeight local left = cx - fullw/2 local right = cx + fullw/2 local top = cy - fullh/2 local bottom = cy + fullh/2 local moveTime = 1 -- values below a frame duration default to 'takes one frame' local moveRight local moveLeft local count = 0 local maxCount = 10000 local tmem = {} local mmem = {} local times = {} local function result() print("Texture Memory Delta: " .. (tmem[2]-tmem[1]) ) print(" Main Memory Delta: " .. (mmem[2]-mmem[1]) ) print(" Time Delta: " .. (times[2]-times[1]) ) end local function countMem( entry ) collectgarbage("collect") tmem[entry] = system.getInfo( "textureMemoryUsed" ) mmem[entry] = collectgarbage( "count" ) times[entry] = system.getTimer() -- print( entry .. " : Texture Memory: " .. tmem[entry] .. " @ " .. times[entry] ) print( entry .. " : Main Memory: " .. mmem[entry] .. " @ " .. times[entry] ) -- if( entry == 2 ) then result() end end moveRight = function( self ) count = count + 1 if( count \> maxCount ) then countMem( 2 ) return end -- self.onComplete = moveLeft transition.to( self, { x = right, time = moveTime, onComplete = self } ) end moveLeft = function( self ) count = count + 1 if( count \> maxCount ) then countMem( 2 ) return end -- self.onComplete = moveRight transition.to( self, { x = left, time = moveTime, onComplete = self } ) end local obj = display.newCircle( left, cy, 20 ) -- Make first mem + time count countMem(1) -- Start ping-pong movement moveRight( obj )

result:

1 : Texture Memory: 4 @ 18 1 : Main Memory: 244.916015625 @ 18 2 : Texture Memory: 4 @ 166715.5 2 : Main Memory: 239.3193359375 @ 166715.5 Texture Memory Delta: 0 Main Memory Delta: -5.5966796875 Time Delta: 166697.5

Timer Test & Results

io.output():setvbuf("no") display.setStatusBar(display.HiddenStatusBar) -- ===================================================== local cx = display.contentCenterX local cy = display.contentCenterY local fullw = display.actualContentWidth local fullh = display.actualContentHeight local left = cx - fullw/2 local right = cx + fullw/2 local top = cy - fullh/2 local bottom = cy + fullh/2 local moveTime = 1 -- values below a frame duration default to 'takes one frame' local moveRight local moveLeft local count = 0 local maxCount = 10000 local tmem = {} local mmem = {} local times = {} local function result() print("Texture Memory Delta: " .. (tmem[2]-tmem[1]) ) print(" Main Memory Delta: " .. (mmem[2]-mmem[1]) ) print(" Time Delta: " .. (times[2]-times[1]) ) end local function countMem( entry ) collectgarbage("collect") tmem[entry] = system.getInfo( "textureMemoryUsed" ) mmem[entry] = collectgarbage( "count" ) times[entry] = system.getTimer() -- print( entry .. " : Texture Memory: " .. tmem[entry] .. " @ " .. times[entry] ) print( entry .. " : Main Memory: " .. mmem[entry] .. " @ " .. times[entry] ) -- if( entry == 2 ) then result() end end moveRight = function( self ) count = count + 1 if( count \> maxCount ) then countMem( 2 ) return end -- self.x = right timer.performWithDelay( moveTime, function() moveLeft( self ) end ) end moveLeft = function( self ) count = count + 1 if( count \> maxCount ) then countMem( 2 ) return end -- self.x = left timer.performWithDelay( moveTime, function() moveRight( self ) end ) end local obj = display.newCircle( left, cy, 20 ) -- Make first mem + time count countMem(1) -- Start ping-pong movement moveRight( obj )

result:

1 : Texture Memory: 4 @ 13 1 : Main Memory: 245.03125 @ 13 2 : Texture Memory: 4 @ 166641.4 2 : Main Memory: 239.6103515625 @ 166641.4 Texture Memory Delta: 0 Main Memory Delta: -5.4208984375 Time Delta: 166628.4

Note: I responded to the above assertions because I always like to check when folks say, “I think this thing is broken.”

If I find it is broken then I ask it be fixed (via bug report). 

If it isn’t broken, then we all know a little more and those who encountered issues can go back and re-investigate to find the real cause of their troubles.

This way everyone benefits.

I don’t believe in dropping ‘possible bugs’ on the floor for future users to stumble over.

PS - If that last statement sounds like an admonition it is not. 

I realize we are all busy and sometimes getting the game/project done is the only priority.

Still, if one does find a possibly serious issue, filing a bug on it (after verification) is awesome.

Just for fun…

bug-report.jpg

I actually also wrote a test function to this when the same question was asked some time ago as it seemed to be a frequently debated issue. It’s not as fancy (or probably even as correct) as above, but here it is :smiley:

 

local startingMemory local function checkMemory(cmd) collectgarbage( "collect" ) local memUsage\_str = string.format( "MEMORY = %.3f KB", collectgarbage( "count" ) ) print( memUsage\_str ) if cmd ~= nil then return memUsage\_str end end startingMemory = checkMemory(1) timer.performWithDelay(500,checkMemory,0) local t = {} local iterations = 5000 local function junk(i) -- print(i) if i == iterations then for i = 1, iterations do t[i] = nil end print("---\nstartingMemory\n"..startingMemory.."\n---") end end for i = 1, iterations do -- method 1, bloat a table with timers t[i] = timer.performWithDelay( 55, function() junk (i) end ) -- method 2, just fire away some timers -- timer.performWithDelay( 55, function() junk (i) end ) end

 

Using either method 1 or method 2 resulted in exactly the same memory usage, which I thought was odd. So, I decided to borrow some code from https://docs.coronalabs.com/tutorial/data/outputTable/index.html and here’s that borrowed code in action:

 

local function nothing() end for i = 1, 5 do timer.performWithDelay( 1000, nothing ) end local function printTable( t ) local printTable\_cache = {} local function sub\_printTable( t, indent ) if ( printTable\_cache[tostring(t)] ) then print( indent .. "\*" .. tostring(t) ) else printTable\_cache[tostring(t)] = true if ( type( t ) == "table" ) then for pos,val in pairs( t ) do if ( type(val) == "table" ) then print( indent .. "[" .. pos .. "] =\> " .. tostring( t ).. " {" ) sub\_printTable( val, indent .. string.rep( " ", string.len(pos)+8 ) ) print( indent .. string.rep( " ", string.len(pos)+6 ) .. "}" ) elseif ( type(val) == "string" ) then print( indent .. "[" .. pos .. '] =\> "' .. val .. '"' ) else print( indent .. "[" .. pos .. "] =\> " .. tostring(val) ) end end else print( indent..tostring(t) ) end end end if ( type(t) == "table" ) then print( tostring(t) .. " {" ) sub\_printTable( t, " " ) print( "}" ) else sub\_printTable( t, " " ) end end printTable( timer )

printTable outputs the following:

 

table: 0C412880 { [\_runlist] =\> table: 0C412880 { [1] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } [2] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } [3] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } [4] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } [5] =\> table: 0C40FC20 { [\_time] =\> 1014.9 [\_count] =\> 1 [\_listener] =\> function: 098E9FA0 } } [resume] =\> function: 098E2220 [\_insert] =\> function: 098E2600 [performWithDelay] =\> function: 098E3D40 [\_remove] =\> function: 098E26E0 [\_nextTime] =\> 1014.9 [enterFrame] =\> function: 098E27C0 [cancel] =\> function: 098E2500 [pause] =\> function: 098E32A0 [\_updateNextTime] =\> function: 098E2480 }

So, it seems that Corona adds all timers into a table to begin with, regardless of whether or not you do. You can also access all timers that you’ve created via timer._runlist, e.g. you find the time set for the first timer by using “print(timer._runlist[1]._time)”. This means that you could also cancel timer #1 via “timer.cancel( timer._runlist[1] )”.

Hopefully this info helps someone or is at least interesting :smiley:

Oh, and just a quick tip to anyone interested in trying to control your timers by using the timer._runlist entries. Don’t. :smiley:

If you were to, for instance, create 100 identical timers in a loop, they will almost definitely not be in the correct order, i.e.

 

local function getID(i) print(i) end local iterations = 100 for i = 1, iterations do timer.performWithDelay( 1255, function() junk (i) end ) end

You might expect that the getID function outputs numbers as 1, 2, 3, …, n, but in fact all of the numbers are scrambled. This is why you want to add some identifiers to the timers so that you can properly control them.

consult source here:  https://github.com/coronalabs/framework-timer/blob/master/timer.lua

what you’ll find is that the _insert method does a “non-stable” insertion sort by time. (by “non-stable” i mean:  there is no “tie-breaker” if times are equal, so overall order among timers with same times would be unpredictable if the entire table were resorted using that method)

if you wanted to, you could generate and assign an incremental “id” in each call to performWithDelay, then use that as a tie-breaker, then you’d get more predictable behavior.  (though keep in mind that your loop takes some non-zero time to execute, so highly unlikely that all 100 have exact same time to begin with, and could vary based on fractional resolution of getTimer())