Low performance and high memory usage of text()

I noticed recently that using text() to print several rapidly changing numbers to the screen has a significant performance cost on my iPad Air and Codea 2.3.1. It seems to depend on the font - in case of the program below I see the drop in FPS (histogram at the top) starting with the 4th string. One new string is added every 5 seconds, at first there is no penalty but later the performance goes down. Notice that the strings must be different for this effect to be visible, printing just ElapsedTime multiple times would not affect FPS.

I first noticed this strange behavior on an iPad2 which started to run out of memory quitting Codea to desktop. I set up collectgarbage() monitoring there but the LUA memory stayed down and the application still quit. Then I started researching on iPad Air which seemingly has more memory to start with, so you will see the performance drop first.

My educated guess is that there is internal Codea runtime cache (not reflected in LUA stack) that holds the images of strings being rendered, so that they would not be created from scratch every time (that’s probably why multiple copies of the same string are still drawn by text() very fast). But over time the if the internal cache grows the lookups start to be very slow and also this affects the allocated memory.

Please see this effect for yourself, the upper half of the screen is FPS histogram, the lower contains unique drawn strings:

-- Performance

-- Use this function to perform your initial setup
function setup()
    print("Hello World!")
    fps=60
    t = ElapsedTime
    backingMode(RETAINED)
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    --background(40, 40, 50)

    -- This sets the line thickness
    strokeWidth(0)

    -- Do your drawing here
    fill(44, 31, 101, 255)
    rectMode(CORNER)
    rect(0,0,WIDTH,HEIGHT/2)
    font("AppleColorEmoji")
    fontSize(30)
    fill(255, 0, 0, 255)
    fps=0.95*fps+0.05*1/(ElapsedTime-t)
    t=ElapsedTime
    pushMatrix()
    translate(100,10)
    for i = 0,(ElapsedTime//5) do
        translate(20,20)
        text(string.format("%6.3f %d",fps,i))
    end
    popMatrix()
    fill(255, 245, 0, 255)
    if (ElapsedTime//1)%5==0 then
        fill(255, 0, 0, 255)
    end
    rect(ElapsedTime//1,HEIGHT/2,1,fps)
    
    
    
end


For stuff that’s updated regularly (scorelines etc), but not every frame, I use something like this:

TextBox = class()

function TextBox:init(str,x,y,col) 
    local x=x or WIDTH * 0.5 --work out dimensions
    local y=y or HEIGHT * 0.5
    fontSize(30)
    local w,h=textSize(str)

    self.img=image(w,h)

    self:update(str) --write text to image

    local m=mesh()
    m.texture=self.img

    --shadow effects (optional)
    local pos ={} --positions of shadows
    local deg=math.rad(100) --math functions 0 top, clockwise
    for i=1,8 do
        pos[i]=vec2(math.sin(deg),math.cos(deg))*7
        deg = deg + math.rad(8)
    end

    for i=1,#pos do
        local v = pos[i]
        m:addRect(x+v.x, y+v.y, w*1.01,h*1.02 ) --w*1.01, h*1.2) --shadow rects
        m:setRectColor(i, color(0,10) )
    end

    -- the main text
    local col=col or color(0, 167, 255, 255)
    local i = m:addRect(x,y,w,h) --the main rect
    m:setRectColor(i, col)
    self.mesh= m
end

function TextBox:draw() --call this every cycle
    self.mesh:draw()
end

function TextBox:update(str) --call this to change content of text. Don't call from a 3D projection. You must be in an orthogonal viewing mode.
    setContext(self.img)
    background(0,0)
    pushStyle()
    textMode(CORNER) --draw from 0,0
    fill(255)
    text(str)
    popStyle()
    setContext()
end

Yes, that’s useful. However I cannot use this approach to rendering the timer or other constantly changing values like x, y, speed. I am afraid that as a workaround I would need to render digits to a texture and display parts of the mesh to show quickly changing numbers as individual digits. That’s a lot of work though and hard to do with variable width fonts.

Maybe there is an easy way in Codea runtime to improve performance of the text() itself?

@quiath I think you’re giving the text statement a bad rap. You say it’s slow, but yet you’re doing everything else in your code that’s slowing things down. You’re using backingMode(retained), push and pop matrix, translate, rect, etc. There are a lot of things in your code that only need to be done once, yet you’re doing them 60 times a second. Here’s some code to show the performance of text. You can use the slider to change the number of text statements shown. I also show the average FPS and memory used. On my iPad Air, the FPS is still 56 when I’m showing 300 text statements. Also, the memory usage cycles between 400 and 1100 kb.

EDIT: If I comment out the mem= line, the FPS rises to 58 showing 300 text lines.


function setup()
    parameter.integer("nbr",1,500,clear)
    parameter.watch("mem")
    fill(255)
end

function draw()
    background(40, 40, 50)
    tot=tot+DeltaTime
    count=count+1
    for z=1,nbr do
        text(z,400,HEIGHT-d*z)
    end
    text("Average fps "..((count/tot)//1),200,400)
    text("text statements +3  "..nbr+3,200,350)
    text("tap screen to recalculate avg fps",200,HEIGHT-50)
    mem=collectgarbage("count")
end

function clear()
    tot=0
    count=0
    d=HEIGHT/nbr
end

function touched(t)
    if t.state==BEGAN then
        tot=0
        count=0
    end
end

Do you really need that data to update every frame though? What about updating every 10 frames? Pre-rendering digits is certainly a possibility, you could create a sprite-sheet. I would reduce the frequency of updates first though.

Sounds like @epicurus101 had a similar issue to you on this thread: http://codea.io/talk/discussion/6604/memory-and-ios-7#latest

Is the iPad 2 running iOS7?

@yojimbo2000

Yes, the mentioned issue on iPad2 seems to be the same one. I have a problem on that iOS7 machine when the only changing string is the timer. And it happens in a program I wrote in 2013, I made no updates to the source since and only recently it started crashing to desktop after a few minutes, making my game unplayable in the long run. Thank you for the suggestion about lowering the frequency of updates, I’ll check how much time it buys on that iPad2.

@dave1707
Thank you for the insight. I provided the RETAINED mode and rect() only to show the histogram, push/pop matrix is wholly unnecessary and copied from my code like translate(). I will surely adhere to your advice regarding the performance.

However I believe that my issue still stands. Your code does show hundreds of strings each frame - but these are the same every time. Please look at the slight modification to your program and see what happens when the strings are random. If you ask why I need that functionality - for example I have a swarm of objects and want to show each object’s position, speed and angle on the screen, even if only for debug purposes.



function setup()
    parameter.integer("nbr",1,500,clear)
    parameter.watch("mem")
    fill(255)
end

function draw()
    background(40, 40, 50)
    tot=tot+DeltaTime
    count=count+1
    for z=1,nbr do
        text(z .."," .. math.random(100),400,HEIGHT-d*z)
    end
    text("Average fps "..((count/tot)//1),200,400)
    text("text statements +3  "..nbr+3,200,350)
    text("tap screen to recalculate avg fps",200,HEIGHT-50)
    mem=collectgarbage("count")
end

function clear()
    tot=0
    count=0
    d=HEIGHT/nbr
end

function touched(t)
    if t.state==BEGAN then
        tot=0
        count=0
    end
end

@quiath Interesting, that was quite a drop in FPS. I didn’t think a different value would have that much influence.

@quiath I think this example verifies your thoughts of strings being cached. I added another slider that varies the size of random numbers displayed with text. Displaying 100 text statements with a random number starts to drop in FPS once you get above 40 random numbers.

function setup()
    parameter.integer("nbr",1,500,100,clear)
    parameter.integer("size",1,200,1,clear)
    parameter.watch("mem")
    fill(255)
end

function draw()
    background(40, 40, 50)
    tot=tot+DeltaTime
    count=count+1
    for z=1,nbr do
        text(math.random(size),400,HEIGHT-d*z)
    end
    text("Average fps "..((count/tot)//1),200,400)
    text("text statements +3  "..nbr+3,200,350)
    text("tap screen to recalculate avg fps",200,HEIGHT-50)
    mem=collectgarbage("count")
end

function clear()
    tot=0
    count=0
    d=HEIGHT/nbr
end

function touched(t)
    if t.state==BEGAN then
        tot=0
        count=0
    end
end

@quiath if your key issue is with counters (as mine was) then have a look back on my previous thread to see the solution which is working well for me.

@epicurus101 Yes, I’m going to try it out. Thank you!