Skip to content
Kim edited this page Mar 21, 2018 · 4 revisions

Frame statistics.

Overview

'FrameClock' is a utility class designed to keep track of various fame-rate related statistics. At its most basic, FrameClock could be used as an 'sf::Clock' replacement for measuring the elapsed time during some specified unit of execution (typically a frame). However, the real utility of FrameClock lies in its ability to track statistics such as the average frame rate over a user specified number of frames.

Features

Over successive calls to matched FrameClock::beginFrame/endFrame pairs, FrameClock keeps track of:

  • total number of frames;
  • total frame time;
  • current frame time;
  • shortest measured frame time;
  • longest measured frame time;
  • average frame time over a user specified number of frames (called sample depth);
  • frames per second;
  • lowest measured frames per second;
  • highest measured frames per second, and;
  • average frames per second over a user specified number of frames (called sample depth).

Basic usage

The FrameClock class provides two member functions beginFrame and endFrame, to be called at the beginning and end, respectively, of each new frame (or whatever unit of execution is to be measured). The time elapsed during the last sampled frame (i.e. between a beginFrame/endFrame pair) can be obtained with the member function getLastFrameTime. The frame frequency can be obtained with the member function getFramesPerSecond.

A game loop employing FrameClock to measure frame time and calculate the frames per second might look something like this:

sfx::FrameClock clock;

while (gameIsRunning)
{
    clock.beginFrame();
    // ...
    updateGameObjects(clock.getLastFrameTime());
    drawFPS(clock.getFramesPerSecond());
    // ...
    clock.endFrame();
}

Note that the FrameClock class lives in the namespace sfx.

Measuring the average

Instantaneous frame rate changes often look erratic when printed to the screen and are, in fact, a poor measure of overall performance. It might be more instructive (and less visually jarring) to sample a number of successive frames and then calculate the average over those frames. FrameClock tracks both the average frames per second and the average frame duration over a user specified number of sample frames. The number of frame to sample (called 'sample depth') can be specified as either a constructor argument or with the member function setSampleDepth, and obtained with the member function getSampleDepth. The average frames per second and average frame duration over the latest 'sample depth' number of frames can be obtained with the member functions getAverageFramesPerSecond and getAverageFrameTime, respectively.

sfx::FrameClock clock;

// Sample the latest 1000 frames for averaging.
clock.setSampleDepth(1000);

while (gameIsRunning)
{
    clock.beginFrame();
    // ...
    updateGameObjects(clock.getLastFrameTime());
    drawFPS(clock.getFramesPerSecond());
    drawAverageFPS(clock.getAverageFramesPerSecond());
    drawDelta(clock.getLastFrameTime());
    drawAverageDelta(clock.getAverageFrameTime());
    // ...
    clock.endFrame();
}

Code

For convenience, the code is presented here as a single header file, 'FrameClock.h'. The only dependencies are 'sf::Clock' and 'sf::Time' from SFML 2.0.

FrameClock.h

////////////////////////////////////////////////////////////
//
// FrameClock - Utility class for tracking frame statistics.
// Copyright (C) 2013 Lee R (lee-r@outlook.com)
//
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the authors be held liable for any damages arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented;
//    you must not claim that you wrote the original software.
//    If you use this software in a product, an acknowledgment
//    in the product documentation would be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such,
//    and must not be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
////////////////////////////////////////////////////////////

#ifndef FRAMECLOCK_H_INCLUDED
#define FRAMECLOCK_H_INCLUDED

#include <limits>
#include <cassert>
#include <algorithm>

#include <SFML/System/Time.hpp>
#include <SFML/System/Clock.hpp>

namespace sfx
{
    namespace detail
    {
        // Utility function to disambiguate from macro 'max'.
        template<typename T>
        inline T limit()
        {
            return (std::numeric_limits<T>::max)();
        }
    }	// namespace detail

    class FrameClock
    {
    public:

        // Constructs a FrameClock object with sample depth 'depth'.
        explicit FrameClock(std::size_t depth = 100)
        {
            assert(depth >= 1);
            m_sample.data.resize(depth);

            m_freq.minimum = detail::limit<float>();
            m_time.minimum = sf::microseconds(detail::limit<sf::Int64>());
        }

        // Resets all times to zero and discards accumulated samples.
        void clear()
        {
            FrameClock(getSampleDepth()).swap(*this);
        }

        // Begin frame timing.
        // Should be called once at the start of each new frame.
        void beginFrame()
        {
            m_clock.restart();
        }

        // End frame timing.
        // Should be called once at the end of each frame.
        // Returns: Time elapsed since the matching FrameClock::beginFrame.
        sf::Time endFrame()
        {
            m_time.current = m_clock.getElapsedTime();

            m_sample.accumulator -= m_sample.data[m_sample.index];
            m_sample.data[m_sample.index] = m_time.current;

            m_sample.accumulator += m_time.current;
            m_time.elapsed       += m_time.current;

            if (++m_sample.index >= getSampleDepth())
            {
                m_sample.index = 0;
            }

            if (m_time.current != sf::microseconds(0))
            {
                m_freq.current = 1.0f / m_time.current.asSeconds();
            }

            if (m_sample.accumulator != sf::microseconds(0))
            {
                const float smooth = static_cast<float>(getSampleDepth());
                m_freq.average = smooth / m_sample.accumulator.asSeconds();
            }

            const sf::Int64 smooth = static_cast<sf::Int64>(getSampleDepth());
            m_time.average = sf::microseconds(m_sample.accumulator.asMicroseconds() / smooth);

            if (m_freq.current < m_freq.minimum)
                m_freq.minimum = m_freq.current;
            if (m_freq.current > m_freq.maximum)
                m_freq.maximum = m_freq.current;

            if (m_time.current < m_time.minimum)
                m_time.minimum = m_time.current;
            if (m_time.current > m_time.maximum)
                m_time.maximum = m_time.current;

            ++m_freq.elapsed;

            return m_time.current;
        }

        // Sets the number of frames to be sampled for averaging.
        // 'depth' must be greater than or equal to 1.
        void setSampleDepth(std::size_t depth)
        {
            assert(depth >= 1);
            FrameClock(depth).swap(*this);
        }

        // Returns: The number of frames to be sampled for averaging.
        std::size_t getSampleDepth() const
        {
            return m_sample.data.size();
        }

        // Returns: The total accumulated frame time.
        sf::Time getTotalFrameTime() const
        {
            return m_time.elapsed;
        }

        // Returns: The total accumulated number of frames.
        sf::Uint64 getTotalFrameCount() const
        {
            return m_freq.elapsed;
        }

        // Returns: Time elapsed during the last 'FrameClock::beginFrame/endFrame' pair.
        sf::Time getLastFrameTime() const
        {
            return m_time.current;
        }

        // Returns: The shortest measured frame time.
        sf::Time getMinFrameTime() const
        {
            return m_time.minimum;
        }

        // Returns: The longest measured frame time.
        sf::Time getMaxtFrameTime() const
        {
            return m_time.maximum;
        }

        // Returns: Average frame time over the last getSampleDepth() frames.
        sf::Time getAverageFrameTime() const
        {
            return m_time.average;
        }

        // Returns: Frames per second, considering the pervious frame only.
        float getFramesPerSecond() const
        {
            return m_freq.current;
        }

        // Returns: The lowest measured frames per second.
        float getMinFramesPerSecond() const
        {
            return m_freq.minimum;
        }

        // Returns: The highest measured frames per second.
        float getMaxFramesPerSecond() const
        {
            return m_freq.maximum;
        }

        // Returns: Average frames per second over the last getSampleDepth() frames.
        float getAverageFramesPerSecond() const
        {
            return m_freq.average;
        }

        // Swaps the value of this FrameClock instance with 'other'.
        void swap(FrameClock& other)
        {
            this->m_time.swap(other.m_time);
            this->m_freq.swap(other.m_freq);
            this->m_sample.swap(other.m_sample);
            std::swap(this->m_clock, other.m_clock);
        }

    private:

        template<typename T, typename U>
        struct Range
        {
            Range()
                : minimum()
                , maximum()
                , average()
                , current()
                , elapsed()
            {}
            void swap(Range& other)
            {
                std::swap(this->minimum, other.minimum);
                std::swap(this->maximum, other.maximum);
                std::swap(this->average, other.average);
                std::swap(this->current, other.current);
                std::swap(this->elapsed, other.elapsed);
            }
            T minimum;
            T maximum;
            T average;
            T current;
            U elapsed;
        };

        Range<sf::Time, sf::Time> m_time;
        Range<float, sf::Uint64>  m_freq;

        struct SampleData
        {
            SampleData()
                : accumulator()
                , data()
                , index()
            {}
            sf::Time accumulator;
            std::vector<sf::Time> data;
            std::vector<sf::Time>::size_type index;
            void swap(SampleData& other)
            {
                std::swap(this->accumulator, other.accumulator);
                std::swap(this->data,        other.data);
                std::swap(this->index,       other.index);
            }
        } m_sample;

        sf::Clock m_clock;
    };
}	// namespace sfx

#endif	// FRAMECLOCK_H_INCLUDED

Example

Because the FrameClock class does not concern itself with the details of how and/or when its values are rendered to the screen, a simple example of how this might be achieved is presented below. The example is split across two files, 'ClockHUD.h' and a driver file 'Example.cpp':

'ClockHUD.h' contains a single class, 'ClockHUD', which derives from 'sf::Drawable' and takes as constructor arguments an 'sf::Font' instance and a 'sfx::FrameClock' instance. When passed to sf::RenderTarget::draw, an instance of 'ClockHUD' will render colour coded frame statistics to the upper left hand corner of the target. Note however that 'ClockHUD.h' is for example only, and should not be considered production ready.

'Example.cpp' provides the driver code to set up a render window and draw the 'ClockHUD', along with an example of using the last measured frame time to update the position of an object.

ClockHUD.h

#include <vector>
#include <string>
#include <sstream>
#include <iomanip>

#include <SFML/Graphics.hpp>
#include "FrameClock.h"

class ClockHUD : public sf::Drawable
{
    struct Stat
    {
        sf::Color color;
        std::string str;
    };

    typedef std::vector<Stat> Stats_t;

public:

    ClockHUD(const sfx::FrameClock& clock, const sf::Font& font)
        : m_clock (&clock)
        , m_font  (&font)
    {}

private:

    void draw(sf::RenderTarget& rt, sf::RenderStates states) const
    {
        // Gather the available frame time statistics.
        const Stats_t stats = build();

        sf::Text elem;
        elem.setFont(*m_font);
        elem.setCharacterSize(16);
        elem.setPosition(5.0f, 5.0f);

        // Draw the available frame time statistics.
        for (std::size_t i = 0; i < stats.size(); ++i)
        {
            elem.setString(stats[i].str);
            elem.setColor(stats[i].color);

            rt.draw(elem, states);

            // Next line.
            elem.move(0.0f, 16.0f);
        }
    }

private:

    template<typename T>
    static std::string format(std::string name, std::string resolution, T value)
    {
        std::ostringstream os;
        os.precision(4);
        os << std::left << std::setw(5);
        os << name << " : ";
        os << std::setw(5);
        os << value << " " << resolution;
        return os.str();
    }

    Stats_t build() const
    {
        const int count = 10;
        const Stat stats[count] = {
            { sf::Color::Yellow, format("Time",  "(sec)", m_clock->getTotalFrameTime().asSeconds())        },
            { sf::Color::White,  format("Frame", "",      m_clock->getTotalFrameCount())                   },
            { sf::Color::Green,  format("FPS",   "",      m_clock->getFramesPerSecond())                   },
            { sf::Color::Green,  format("min.",  "",      m_clock->getMinFramesPerSecond())                },
            { sf::Color::Green,  format("avg.",  "",      m_clock->getAverageFramesPerSecond())            },
            { sf::Color::Green,  format("max.",  "",      m_clock->getMaxFramesPerSecond())                },
            { sf::Color::Cyan,   format("Delta", "(ms)",  m_clock->getLastFrameTime().asMilliseconds())    },
            { sf::Color::Cyan,   format("min.",  "(ms)",  m_clock->getMinFrameTime().asMilliseconds())     },
            { sf::Color::Cyan,   format("avg.",  "(ms)",  m_clock->getAverageFrameTime().asMilliseconds()) },
            { sf::Color::Cyan,   format("max.",  "(ms)",  m_clock->getMaxtFrameTime().asMilliseconds())    }
        };
        return Stats_t(&stats[0], &stats[0] + count);
    }

private:

    const sf::Font* m_font;
    const sfx::FrameClock* m_clock;
};

Example.cpp

#include <cstdlib>

#include <SFML/Graphics.hpp>
#include "FrameClock.h"
#include "ClockHUD.h"

int main()
{
    // Create the game window.
    sf::RenderWindow window(sf::VideoMode(800, 600), "FrameClock", sf::Style::Close);
    window.setFramerateLimit(60); // Something reasonable to measure.

    // Exit on error.
    if (!window.isOpen())
    {
        sf::err() << "Failed to create window";
        return EXIT_FAILURE;
    }

    sf::Font font;
    // Load the font; exit on error.
    if (!font.loadFromFile("Data/VeraMono.ttf"))
    {
        sf::err() << "Failed to load VeraMono.ttf";
        return EXIT_FAILURE;
    }

    // We rotate this circle based on measured frame time.
    sf::CircleShape circle;
    circle.setRadius(50.0f);
    circle.setOrigin(100.0f, 0.0f);
    circle.setPosition(400.0f, 300.0f);
    circle.setFillColor(sf::Color::Magenta);

    sfx::FrameClock clock;
    ClockHUD hud(clock, font);

    clock.setSampleDepth(100); // Sample 100 frames for averaging.


    while (window.isOpen())
    {
        // Start a new frame.
        clock.beginFrame();

        sf::Event event;
        while (window.pollEvent(event))
        {
            // "close requested" event: we close the window
            if (event.type == sf::Event::Closed)
            {
                window.close();
            }
        }

        // Update clicle's rotation using the last frame time.
        const float delta = clock.getLastFrameTime().asSeconds();
        const float speed = 500.0f;
        circle.rotate(speed * delta);

        // Clear the screen to black.
        window.clear();

        // Draw circle at the new position.
        window.draw(circle);

        // Draw the frame statistics.
        window.draw(hud);

        // Update the screen.
        window.display();

        // End frame.
        clock.endFrame();
    }

    return EXIT_SUCCESS;
}
Clone this wiki locally