Performance and Optimization

As you develop your application, you should always consider how your design choices affect performance. Despite ongoing core improvements, mobile devices still face fundamental constraints in processing power, memory usage, and battery life. Therefore, performance and optimization is crucial to achieving faster response time, minimizing memory usage, and maximizing battery life.

Using Memory Efficiently

Memory is a critical resource on mobile devices. Some devices may even terminate your application if you consume too much memory.

Global variables are not recommended in general, but if you must use them for convenience, ensure that you remove them from memory when they’re no longer needed (set them to nil).

Reducing Power Consumption

Battery life is inherently limited on mobile devices because of their small form factor. You can improve battery life by adhering to the following practices.

Transitions and Animations

If you need to set or transition/tween a specific property of several display objects to the same value — for example, fade an entire overlay menu to alpha=0 it’s better to add the objects to a display group and modify the property of the entire group. It’s easier to code and it optimizes memory and speed. See the Group Programming guide for more information.

If you’re using sprite animations, a common oversight is allowing offscreen or invisible sprites to continue animating. While these sprites may not be visible to the user, they’ll continue to use processor power while animating. We suggest that you pause all animations that move off the screen or otherwise become inactive.

Conserving Texture Memory

Texture memory is often ignored until it reaches “critical mass,” at which point it’s time-consuming to make the required changes to art assets.

As general practice, remember these tips in respect to managing texture memory:

  1. Always unload textures (remove them from the display hierarchy) when they’re no longer needed.

  2. If you’re using image sheets, consider using a tool like TexturePacker to pack your images into the smallest configuration possible.

Important

There is a limit on the maximum texture size that a device will support. If you exceed this limit, the texture will automatically downscale to fit within the maximum. You can use the system.getInfo( "maxTextureSize" ) command to determine the maximum texture size for a particular device. See system.getInfo() for more information.

Draw Calls / Batching

On devices, OpenGL performs best when you are able to minimize state changes. This is because multiple objects can be batched into a single draw call if there are no state changes required between consecutive display objects.

Solar2D’s rendering engine attempts to identify situations where multiple display objects can be submitted in a single draw call. Whenever possible, you should try to arrange the display object hierarchy such that consecutive display objects — meaning, the order in which they are rendered — can be batched into a single draw call.

There are certain situations where this can occur. The general rule is that consecutive display objects which use the same texture can be batched. This includes display objects that use different frames from the same image sheet, since the underlying texture is the same. In these situations, you can vary the position, tint, and alpha of each object without breaking the batch, but remember that some actions may prevent batching, for example adding a shader effect to an object.

Lua Optimizations

At the code level, you should adhere to as many Lua optimizations as possible. Most of the performance tricks below pertain primarily to time-critical routines — that is, points in your app where there is a lot happening or where the user experience could be adversely affected by sluggish performance. However, every bit helps and we suggest that you follow these as a habit.

Localize, Localize

In contrast to global variables, which should be avoided whenever possible, access to local variables and functions is simply faster, especially in time-critical routines.

-- Local (recommended)
local CCX = display.contentCenterX  -- Local variable

for i = 1,100 do
    local image = display.newImage( "myImage.png" )
    image.x = CCX
end
-- Non-local (discouraged)
CCX = display.contentCenterX  -- Global variable

for i = 1,100 do
    local image = display.newImage( "myImage.png" )
    image.x = CCX
end

This also applies to core Lua libraries like the math library. In time-critical routines, you should always localize library functions.

-- Local (recommended)
local sin = math.sin  -- Local reference to "math.sin"

local function foo(x)
    for i = 1,100 do
        x = x + sin(i)
    end
    return x
end
-- Non-local (discouraged)
local function foo( x )
    for i = 1,100 do
        x = x + math.sin(i)
    end
    return x
end

Finally, remember that functions should be localized whenever possible. Of course, this will require proper scoping!

-- Local (recommended)
local function func2( y )  -- "func2()" properly scoped above "func1()"
    print( y )
end

local function func1()
    func2( "myValue" )
end

func1()
-- Non-local (discouraged)
function func1()
    func2( "myValue" )
end

function func2( y )
    print( y )
end

func1()

Avoid “table.insert()”

Let’s compare four methods that all achieve the same thing: the common act of inserting values into a table. Of the four, the Lua table.insert() function is a mediocre performer and should be avoided.

-- Loop index method (recommended)
local a = {}

for i = 1,100 do
    a[i] = i
end
-- Counter method (recommended)
local a = {}
local index = 1

for i = 1,100 do
    a[index] = i
    index = index+1
end
-- Table size method (acceptable)
local a = {}

for i = 1,100 do
    a[#a+1] = i
end
-- "table.insert()" (discouraged)
local a = {}

for i = 1,100 do
    table.insert( a, i )
end

Avoid “unpack()”

The Lua unpack() function is not a great performer. Fortunately, a simple loop can be written to accomplish the same thing:

-- Loop method (recommended)
local a = { 100, 200, 300, 400 }

for i = 1,100 do
    print( a[1],a[2],a[3],a[4] )
end
-- "unpack()" (discouraged)
local a = { 100, 200, 300, 400 }

for i = 1,100 do
    print( unpack(a) )
end

The caveat is that you must know the length of the table to retrieve all of its values in the loop method. Thus, unpack() still has its uses — in a table of unknown length, for example — but it should be avoided in time-critical routines.

Avoid “ipairs()”

When iterating through a table, the overhead of the Lua ipairs() function does not justify its use, especially when you can accomplish the same thing using a Lua construct.

-- Lua construct (recommended)
local t1 = {}
local t2 = {}
local t3 = {}
local t4 = {}
local a = { t1, t2, t3, t4 }

for i = 1,#a do
    print( a[i] )
end
-- "ipairs()" (discouraged)
local t1 = {}
local t2 = {}
local t3 = {}
local t4 = {}
local a = { t1, t2, t3, t4 }

for i,v in ipairs( a ) do
    print( i,v )
end

Math Performance

Certain mathematical functions and processes are faster than others. For example, multiplication is faster than division and you should usually multiply by a decimal instead of dividing.

-- Multiplication by decimal (recommended)
x * 0.5
y * 0.125
-- Division (acceptable)
x / 2
y / 8

Multiplication is also faster than exponentiation:

-- Multiplication (recommended)
x * x * x
-- Exponentiation (acceptable)
x^3

Finally, avoid math.fmod() for positive numbers and use the modulus operator instead:

-- Modulus operator (recommended)
for i = 1,100 do
    if ( ( i%30 ) < 1 ) then
        local x = 1
    end
end
-- "math.fmod()" (discouraged)
local fmod = math.fmod
for i = 1,100 do
    if ( fmod( i,30 ) < 1 ) then
        local x = 1
    end
end

Managing Audio

When using audio, you should compress/sample sounds to the smallest acceptable quality in most cases. Also, using simple, cross-platform formats like .wav do not tax the CPU heavily.

Preloading Audio

Sound effects for an app should almost always be preloaded in non-time-critical code, for example, before a scene or level begins.

If desired, sound effects can be organized in a table as follows, for easy reference and eventual disposal.

local soundTable = {
   mySound1 = audio.loadSound( "a.wav" ),
   mySound2 = audio.loadSound( "b.wav" ),
   mySound3 = audio.loadSound( "c.wav" ),
   mySound4 = audio.loadSound( "d.wav" ),
   mySound5 = audio.loadSound( "e.wav" ),
   mySound6 = audio.loadSound( "f.wav" ),
   mySound7 = audio.loadSound( "g.wav" ),
   mySound8 = audio.loadSound( "h.wav" ),
}

With this structure, playback is as simple as:

local mySound = audio.play( soundTable["mySound1"] )

Disposing Audio

Remember, you must dispose of audio files when they’re no longer needed and clear any references to them. Assuming the table structure above is used to organize and preload sounds, the following loop will dispose of the audio handles:

for s = #soundTable,1,-1 do
    audio.dispose( soundTable[s] ) ; soundTable[s] = nil
end