Class to calculate and view an average frame rate and a FPS history graph

As the framerate changes too quick when I just display 1.0 / DeltaTime each frame, I wanted to show an average frame rate over a number of frames to stabilize it more. Here you find the source code to this small class including a draw function.

Simplest usage is to initialize the class as framerate=FrameRateC(25, 100). Then call framerate:calc() and framerate:draw(1, 255) in your draw code, that shows the FPS as average over 25 frames and a history of 100 FPS. Initialize as FrameRateC(25, 0) to only show the number.

You can also retrieve the current average and frame number with getAverageFrameRate() and getCurrentFrameNr(). The average is calculated over 20 frames in this sample which I found good for my use cases.

The algorithm used is called Simple Moving Average. It avoids calculating the median over the whole table each frame, so the runtime is the same independent of the number of frames you average. But showing a large history will take time.

Have fun,
Kilam Malik :slight_smile:

Source of Main function to test it:

-- Main.lua

frameRate = nil
frameNr = 1
frameAvg = 0

--displayMode(FULLSCREEN)

-- Use this function to perform your initial setup
function setup()
    frameRate = FrameRateC(20, 100)

    watch("frameNr")
    watch("frameAvg")
    iparameter("nrObjects", 1, 500, 50)
    iparameter("position", 1, 4, 2)
    iparameter("alpha", 50, 255, 127)
    iparameter("bgcol", 0, 255, 255)
end

-- This function gets called once every frame
function draw()
    frameNr = frameRate:getCurrentFrameNr()
    frameAvg = frameRate:getAverageFrameRate()

    -- This sets a dark background color 
    background(bgcol, bgcol, bgcol, 255)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    stroke(0, 0, 0, 127)
    strokeWidth(1)
    for i = 1, nrObjects do
        local r = 200 + 100*math.sin(i*0.180)
        local inner = (ElapsedTime*20+i)/20
        local cs = math.sin(inner) + 1 
        local cc = math.cos(inner*0.3429495) + 1
        fill(127*cs, 127*cc, 127*cs*cc, 50)
        ellipse(WIDTH/2 + r*math.sin(inner),HEIGHT/2 + r*math.cos(inner),30)
    end

    -- Calc and draw the frame rate:
    frameRate:calc()
    frameRate:draw(position, alpha)
end

Source of the Framerate class:

-- Framerate.lua
--
-- Framerate calculation and display
-- =================================
-- 
-- This class calculates the average framerate over a given number
-- of frames. It uses the Simple Moving Average algorithm to avoid
-- calculating the median over the whole table all the time.
--
-- The higher you choose the number of frames to average, the more
-- stable the framerate will be. But also it will react slower on
-- changes.
--
-- Additionaly, there is a framerate history graph. It has its own table,
-- as the average is e.g. fine with 20 values, but the graph makes sense with more.
--
-- A large graph will slow down your program.
--
-- Usage:
-- Add a variable e.g. in your main:
--     frameRate = nil
--
-- Initialize the class:
--     frameRate = FrameRateC(25, 100)
--
-- Call this in the draw function. Call it as last to overlay everything else:
--     frameRate:calc()
--     frameRate:draw(position, alpha) -- position = 1-4 for the corners and alpha 1-255.
--
-- Have fun,
--   Kilam Malik.

local frameNr = 1 -- Frame counter.
local frameRateAvg = -1 -- Average frame rate.
local frameRateTable = {} -- Table the recent frame rates.
local tabIdx = 1 -- Current table index.
local tabSize = 0 -- Currently used size of table. Only for startup.
local tabMaxSize = 25 -- Table size.
local tabSum = 0 -- Current sum of all values in table.

local frameRateGraphTable = {} -- Table for history graph.
local graphTableIdx = 1 -- Current position.
local graphWidth = 100 -- Width of graph.
local graphHeight = 50-- Height of graph.

FrameRateC = class()

-- avgSize: # frames to average.
-- gw: Width of graph, means # frames in graph.
-- gh: Height of graph shown.
function FrameRateC:init(avgSize, gw, gh)
    local i
    tabMaxSize = avgSize
    for i = 1,tabMaxSize do
        frameRateTable[i] = 0
    end

    graphWidth = gw
    graphHeight = gh
    for i = 1,graphWidth do
        frameRateGraphTable[i] = 0
    end
end

-- Call this every frame in your draw function to calculate the rate.
function FrameRateC:calc()
    -- Calc current frame rate
    local thisFrameRate = 1.0 / DeltaTime

    -- Calc average frame rate using simple moving average
    tabSum = tabSum - frameRateTable[tabIdx] -- Subtract remove value from sum
    tabSum = tabSum + thisFrameRate -- Add new value to sum
    frameRateTable[tabIdx] = thisFrameRate -- Set value in table
    if tabIdx < tabMaxSize then
        tabIdx = tabIdx + 1
    else
        tabIdx = 1
    end

    -- To have a correct framerate when the table is filled the first time,
    -- the tabSize variable is incremented until it is tabMaxSize
    if tabSize < tabMaxSize then
        tabSize = tabSize + 1
    end

    -- Calc Framerate over table sum
    frameRateAvg = tabSum / tabSize

    -- Increase frame counter
    frameNr = frameNr + 1

    -- Fill graph
    frameRateGraphTable[graphTableIdx] = thisFrameRate -- Set value in table
    if graphTableIdx < graphWidth then
        graphTableIdx = graphTableIdx + 1
    else
        graphTableIdx = 1
    end
end

-- Retrieve average frame rate.
function FrameRateC:getAverageFrameRate()
    return math.floor(frameRateAvg)
end

-- Retrieve current frame number.
function FrameRateC:getCurrentFrameNr()
    return frameNr
end

function FrameRateC:draw(pos, alpha)
    pushStyle()
        local h = 50
        local center = vec2()
        local hMul = h/60
        local cMul = 255/60
        rectMode(CENTER)
        noSmooth()
        if pos == 1 then
            center=vec2(WIDTH - graphWidth - 5, HEIGHT - h - 5)
        elseif pos == 2 then
            center=vec2(WIDTH - graphWidth - 5, 5)
        elseif pos == 3 then
            center=vec2(5, 5)
        elseif pos == 4 then
            center=vec2(5, HEIGHT - h - 5)
        end

        local i
        local basePosX = center.x
        local basePosY = center.y
        strokeWidth(1)
        for i = 1,graphWidth do
            local p = basePosX + i
            local idx = (graphTableIdx + (i-2))%graphWidth+1
            local f = frameRateGraphTable[idx]
            stroke(255-f*cMul,f*cMul,0, alpha)
            line(p, basePosY, p, basePosY + f * hMul)
        end

        fontSize(20)
        font("Inconsolata")
        local s = string.format("%d", frameRateAvg)
        local wf, hf = textSize("00")

        if pos <= 2 then
            basePosX = basePosX - wf - 5
        else
            basePosX = basePosX + graphWidth + 5
        end
        textMode(CORNER)
        fill(255, 255, 255, alpha)
        text(s,basePosX, basePosY)
        fill(0, 0, 0, alpha)
        text(s,basePosX + 1, basePosY + 1)
    popStyle()
end

Your code demonstrates more than just stable FPS calculation, but if the main issue to be dealt with is stability, a lighter solution would be to reduce the frequency at which the FPS is recalculated. For example:

Use:


function setup()
    myFPSReporter = FPSReporter(4)
    iparameter("nrObjects", 1, 1500, 1)
    iparameter("position", 1, 4, 1)
    dim = math.min(WIDTH, HEIGHT)
end

function draw()
    background(0)
    myFPSReporter:draw(position)
    for i = 1, nrObjects do
        local d = i/nrObjects * 8 * math.pi
        local r = i/nrObjects * dim/2
        ellipse(WIDTH/2 + r*math.sin(ElapsedTime+d),
            HEIGHT/2 + r*math.cos(ElapsedTime+d),10)
    end
end

Class:


FPSReporter = class()

function FPSReporter:init(rate)
    self.spacing = 1/rate
    self.last = ElapsedTime
    self.count = 0
    self.rate = 60
end

function FPSReporter:draw(pos)
    self.count = self.count + 1
    local now = ElapsedTime
    local delta = now - self.last
    if delta >= self.spacing then
        self.rate = self.count/delta
        self.count = 0
        self.last = now
    end
    pushStyle()
    local s = string.format("%d", self.rate)
    local w, h = textSize(s)
    textMode(CENTER)
    fontSize(18)
    font("Inconsolata")
    fill(255)
    pos = pos - 1
    local x = math.floor(pos/2) % 2 * (WIDTH - w) + w/2
    local y = pos % 2 * (HEIGHT - h) + h/2
    text(s, x, y)  
    popStyle()
end

The calculation of x and y from pos above is inspired by Andrew Stacey’s AddStar() method from the Roller Coaster example project.

Right, thats a simpler calculation by showing the framerate only every 1/4 or 1/2 second. I like that too, think it depends on what you need it for. Thanks.

After seeing @mpilgrem’s post about his framerate test with visualizing the history, I also added some history to my FrameRateC class. Source changed above in comment 1 and 2.

A Picture, bottom right corner shows the FPS and history graph:
https://dl.dropbox.com/u/80475380/CodeaForumPictures/framerate_history.JPG

The framerate is a bit jerky when you show the display in standard mode, I have seen that in my other apps too. But with this graph I’m now sure it is something with the display mode and not with my code. When you open the display in full screen, then the framerate is stable.