Framerate Instability

Hi all,

I’ve noticed recently an annoying framerate instability causing stutters in animations. Even when doing absolutely nothing in the draw() function other than tracking framerate I’m seeing framerate drop to as low as 55 52 46 fps on a 60Hz device on occasion. That’s 14 frames dropped! :frowning:

In most circumstances the framerate will sit happily at 60 fps throughout, but I’ve noticed the drops are more common immediately after launching a project from the editor and continue for the duration of the run. If I then re-run the project a few times the stutters eventually go away.

This is a slimmed down example of the issue and it will error as soon as the framerate dips below 60 fps:

FPS = 0
FPS_TARGET = 60

-- FPS Tracker
local frames = 0
local time = 0
function onFrame()
    time = time + DeltaTime
    if time > 1.0 then
        FPS = frames
        frames = 0
        time = time - 1.0
        
        -- Error if we're not at the expected FPS
        assert(FPS >= FPS_TARGET, string.format("Unstable framerate!\
FPS = %d\
FPS_TARGET = %d", FPS, FPS_TARGET))
    end
    frames = frames + 1
end

function setup()
    viewer.preferredFPS = FPS_TARGET
    parameter.watch("FPS")
end

function draw()
    -- No drawing, just FPS tracking
    onFrame()
end

If anyone could run this quickly and let me know if they’re seeing something similar that would be most appreciated.

Cheers,
Steppers

@steppers tried and crashed almost immediately several times. Attached image on last one.

@steppers - what OS are you running and what machine ? Mine is 10.9 iPad Pro with iPadOS 15.

Since this OS added noted a some features are slower in general.

Yeah, the crash was the assert triggering because it wasn’t at 60fps. I’m on an iPad Air 4, iPadOS 15 as well.

honestly might be because you’re using parameter.watch, it’s not a free operation and will run every frame

@skar It’s not :confused: No watches here…

fwiw, most of the time this runs with no issues at 60 fps. And another project does exactly the same thing and that is doing A LOT more…

I’ve attached the project too. It tracks past frame times to determine the true framerate over the previous second.

Here’s another display of frame rate. The white line is the 60 fps base line. The red line is the current fps. Every now and then you’ll see a red dot above or below the base line which shows a change. I also display the max, current, and min fps. When the count starts to reach higher values, the current fps will start to run lower because of the display demand. But it pretty much stays the same early in the display with few deviations and kind of levels off as it slows down.

viewer.mode=FULLSCREEN

function setup()
    tab={}
    cnt=0
    curr,fmin,fmax=0,999,0
    smooth()
end

function draw()
    background(0)
    cnt=cnt+1
    stroke(255)
    strokeWidth(2)
    line(0,HEIGHT-100,WIDTH,HEIGHT-100)

    fps=1/DeltaTime
    stroke(255,0,0)
    fill(255,0,0)
    table.insert(tab,fps)
    for a,b in pairs(tab) do
        ellipse(a//20,HEIGHT-100-(60-b)*10,3)
    end
    fill(255)
    if fps<fmin then
        fmin=fps
    end
    if fps>fmax then 
        fmax=fps
    end
    text("tap screen to reset",WIDTH/2,HEIGHT-150)
    text("count  "..cnt,WIDTH/2,HEIGHT-200)
    text("max  FPS  "..fmax,WIDTH/2,120)
    text("curr FPS  "..fps,WIDTH/2,100)
    text("min  FPS  "..fmin,WIDTH/2,80)

end

function touched(t)
    if t.state==BEGAN then
        setup()
    end
end

@dave1707 The issue I have with monitors like that is that they only end up showing an fps of 60 or 30fps, not a ‘true’ framerate of ‘this is how many frames completed in the last second’.

It’s unfortunately due to the Codea backend missing the display’s vsync interval in which we can do the backbuffer swap. When we miss a vsync interval the next one is a whole frame away so we end up waiting for that and DeltaTime doubles from ~0.01666 to ~0.03333.

@Simeon I feel like this must be due to the preferredFPS changes a while ago.

I have a feeling you may be waiting for the intended frametime to pass fully in a single wait call which isn’t particularly reliable? If that is the case could we do it in a loop waiting for a factor of the remaining time each loop?

@Steppers That’s correct. I noticed long ago that as the fps gets bogged down, it decreases in steps. Codea is probably set at 60 fps (120 in newer devices) so when it misses a frame, it waits until the next time it should display a frame and not displaying when it could display a frame. Unless there’s a big demand on the system, it probably doesn’t really matter if it runs at 60 or 50 or maybe a lower fps because depending on what you’re doing, you might not notice it.

@dave1707 Unfortunately this seems pretty bad, not like it’s just occasionally missing a frame. Running the bare fps overlay I posted earlier I saw as low as 12 fps for an extended period of time :neutral:

Otherwise you are right, it shouldn’t make much of a difference if we run at 55fps but the question is why we are at 55 fps in the first place? It just seems very unstable and unpredictable.

@Steppers I put a print statement in your if statement to print the FPS and commented the assert statement. It ran pretty much at 60 FPS. I rotated the iPad so it had something to do and it dropped to 59 or 58 for one print then back to 60. According to the documentation, DeltaTime is the time between draw cycles. So you’re using the time between draw cycles to check draw cycles. You’re getting an assert error on the first check which probably isn’t reliable because it’s just starting the calcs.

If you want an independent time, use the socket gettime command. You have to do a require to get the socket code. That should give you a good time that’s shouldn’t be affected by Codea.

s=require(“socket”)
t=s:gettime()

@dave1707 Oh sorry, I meant FPSOverlay.zip with the graph display using a far more accurate technique. It’s perfectly capable of running at 60 fps with no dropped frames but other times the frame rate is all over the place, down to as low as 12 fps yesterday doing nothing but drawing a graph.

@Steppers Here’s another one I put together. It uses the socket gettime. Every time the draw function is called, I get the socket time and that gets subtract from the previous time. So what I have is the time between draw calls. I divide that into 1 to get the FPS for that call. Tap the screen to cause changes in the times.

viewer.mode=FULLSCREEN
viewer.retainedBacking=true

function setup()
    fill(255)
    s=require("socket")
    h=s:gettime()
    x=0
    stroke(255)
    strokeWidth(1)
    fill(255)
    dh=0
end

function draw()
    if x>WIDTH then
        background(0)    
        x=0
    end
    stroke(255,0,0)
    if x==0 then
        for r=-300,300,100 do
            line(0,HEIGHT/2+r,WIDTH,HEIGHT/2+r)
            text(60+r/20,WIDTH/2,HEIGHT/2+r)
        end
    end
    
    t=s:gettime()
    diff=1/(t-h)

    stroke(255)
    if x>10 then
        line(x,HEIGHT/2+(diff-60)*20,x-2,HEIGHT/2+(dh-60)*20)
    end
    
    x=x+2
    dh=diff
    h=t  
end

FPSOverlay 1.0.2 (Available on WebRepo) fixes a rendering issue with modified view or projection matrices.

FPSOverlay 1.0.2 (Available on WebRepo)

i don’t see it

@skar Ah, my bad. It failed that time, the approval process can be a bit flaky sometimes.

Should be available now though.

@Steppers for the new ipads shouldn’t the target be 120 not 60 fps?

@piinthesky Yes, it’s possible and can be configured like that but as this is primarily a development tool it defaults to 60 for the sake of battery saving.

If you want 120 fps add this to setup():
FPSOverlay.setup(120)

i noticed that recently the fps will drop to 80 if i set it to 120, after some time the fps might go back up to 120 @Simeon i’ve noticed this general dip since the ios 14 fix

also it reads 119 and 59 why not remove the “-1” so is 120 and 60? frame_count = i