Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

faster percentile updates #122

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 56 additions & 57 deletions Runtime/Fps/G_FpsMonitor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* ---------------------------------------
* Author: Martin Pane ([email protected]) (@martinTayx)
* Author: Martin Pane ([email protected]) (@martinTayx), modified by Paul Sinnett ([email protected]) (@paulsinnett)
* Contributors: https://github.com/Tayx94/graphy/graphs/contributors
* Project: Graphy - Ultimate Stats Monitor
* Date: 15-Dec-17
Expand All @@ -11,7 +11,7 @@
* Attribution is not required, but it is always welcomed!
* -------------------------------------*/

using System;
using Tayx.Graphy.Utils;
using UnityEngine;

namespace Tayx.Graphy.Fps
Expand All @@ -20,15 +20,19 @@ public class G_FpsMonitor : MonoBehaviour
{
#region Variables -> Private

private short[] m_fpsSamples;
private G_DoubleEndedQueue m_fpsSamples;
private short[] m_fpsSamplesSorted;
private short m_fpsSamplesCapacity = 1024;
private short m_onePercentSamples = 10;
private short m_zero1PercentSamples = 1;
private short m_fpsSamplesCount = 0;
private short m_indexSample = 0;

private float m_unscaledDeltaTime = 0f;
private int m_fpsAverageWindowSum = 0;
private G_Histogram m_histogram;

// This cap prevents the histogram from re-allocating memory in the
// case of an unexpectedly high frame rate. The limit is somewhat
// arbitrary. The only real cost to a higher cap is memory.
private const short m_histogramFpsCap = 999;

#endregion

Expand Down Expand Up @@ -60,63 +64,25 @@ private void Update()

uint averageAddedFps = 0;

m_indexSample++;

if( m_indexSample >= m_fpsSamplesCapacity ) m_indexSample = 0;

m_fpsSamples[ m_indexSample ] = CurrentFPS;

if( m_fpsSamplesCount < m_fpsSamplesCapacity )
{
m_fpsSamplesCount++;
}
m_fpsSamplesCount = UpdateStatistics( CurrentFPS );

for( int i = 0; i < m_fpsSamplesCount; i++ )
{
averageAddedFps += (uint) m_fpsSamples[ i ];
}
averageAddedFps = (uint) m_fpsAverageWindowSum;

AverageFPS = (short) ((float) averageAddedFps / (float) m_fpsSamplesCount);

// Update percent lows

m_fpsSamples.CopyTo( m_fpsSamplesSorted, 0 );

/*
* TODO: Find a faster way to do this.
* We can probably avoid copying the full array every time
* and insert the new item already sorted in the list.
*/
Array.Sort( m_fpsSamplesSorted,
( x, y ) => x.CompareTo( y ) ); // The lambda expression avoids garbage generation

bool zero1PercentCalculated = false;

uint totalAddedFps = 0;

short samplesToIterateThroughForOnePercent = m_fpsSamplesCount < m_onePercentSamples
? m_fpsSamplesCount
: m_onePercentSamples;

short samplesToIterateThroughForZero1Percent = m_fpsSamplesCount < m_zero1PercentSamples
? m_fpsSamplesCount
: m_zero1PercentSamples;
short samplesBelowOnePercent = (short) Mathf.Min( m_fpsSamplesCount - 1, m_onePercentSamples );

short sampleToStartIn = (short) (m_fpsSamplesCapacity - m_fpsSamplesCount);
m_histogram.WriteToSortedArray( m_fpsSamplesSorted, samplesBelowOnePercent + 1 );

for( short i = sampleToStartIn; i < sampleToStartIn + samplesToIterateThroughForOnePercent; i++ )
{
totalAddedFps += (ushort) m_fpsSamplesSorted[ i ];

if( !zero1PercentCalculated && i >= samplesToIterateThroughForZero1Percent - 1 )
{
zero1PercentCalculated = true;
// Calculate 0.1% and 1% quantiles, these values represent the fps
// values below which fall 0.1% and 1% of the samples within the
// moving window.

Zero1PercentFps = (short) ((float) totalAddedFps / (float) m_zero1PercentSamples);
}
}
Zero1PercentFps = (short) Mathf.RoundToInt( CalculateQuantile( 0.001f ) );

OnePercentFPS = (short) ((float) totalAddedFps / (float) m_onePercentSamples);
OnePercentFPS = (short) Mathf.RoundToInt( CalculateQuantile( 0.01f ) );
}

#endregion
Expand All @@ -126,7 +92,10 @@ private void Update()
public void UpdateParameters()
{
m_onePercentSamples = (short) (m_fpsSamplesCapacity / 100);
m_zero1PercentSamples = (short) (m_fpsSamplesCapacity / 1000);
if( m_onePercentSamples + 1 > m_fpsSamplesSorted.Length )
{
m_fpsSamplesSorted = new short[ m_onePercentSamples + 1 ];
}
}

#endregion
Expand All @@ -135,12 +104,42 @@ public void UpdateParameters()

private void Init()
{
m_fpsSamples = new short[m_fpsSamplesCapacity];
m_fpsSamplesSorted = new short[m_fpsSamplesCapacity];

m_fpsSamples = new G_DoubleEndedQueue( m_fpsSamplesCapacity );
m_fpsSamplesSorted = new short[ m_onePercentSamples + 1 ];
m_histogram = new G_Histogram( 0, m_histogramFpsCap );
UpdateParameters();
}

private short UpdateStatistics( short fps )
{
if( m_fpsSamples.Full )
{
short remove = m_fpsSamples.PopFront();
m_fpsAverageWindowSum -= remove;
m_histogram.RemoveSample( remove );
}
m_fpsSamples.PushBack( fps );
m_fpsAverageWindowSum += fps;
m_histogram.AddSample( fps );
return m_fpsSamples.Count;
}

private float CalculateQuantile( float quantile )
{
// If there aren't enough samples to calculate the quantile yet,
// this function will instead return the lowest value in the
// histogram.

short samples = m_fpsSamples.Count;
float position = ( samples + 1 ) * quantile - 1;
short indexLow = (short) ( position > 0 ? Mathf.FloorToInt( position ) : 0 );
short indexHigh = (short) ( indexLow + 1 < samples? indexLow + 1 : indexLow );
float valueLow = m_fpsSamplesSorted[ indexLow ];
float valueHigh = m_fpsSamplesSorted[ indexHigh ];
float lerp = Mathf.Max( position - indexLow, 0 );
return Mathf.Lerp( valueLow, valueHigh, lerp );
}

#endregion
}
}
198 changes: 198 additions & 0 deletions Runtime/Util/G_DoubleEndedQueue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/* ---------------------------------------
* Author: Paul Sinnett ([email protected]) (@paulsinnett)
* Contributors: https://github.com/Tayx94/graphy/graphs/contributors
* Project: Graphy - Ultimate Stats Monitor
* Date: 06-Sep-24
* Studio: Powered Up Games
*
* Git repo: https://github.com/Tayx94/graphy
*
* This project is released under the MIT license.
* Attribution is not required, but it is always welcomed!
* -------------------------------------*/

using UnityEngine.Assertions;

namespace Tayx.Graphy.Utils
{
public class G_DoubleEndedQueue
{
#region Variables -> Private

/// <summary>
/// Fixed size array for holding the values.
/// </summary>
private short[] m_values;

/// <summary>
/// Index of the head element.
/// </summary>
private short m_head;

/// <summary>
/// Index of the entry after the tail element.
/// </summary>
private short m_tail;

/// <summary>
/// Number of items in the queue.
/// </summary>
private short m_count;

/// <summary>
/// Programming error messages for assert failures.
/// </summary>
private const string m_errorEmpty = "queue is empty";
private const string m_errorFull = "queue is full";

#endregion

#region Properties -> Public

/// <summary>
/// The current number of items in the queue.
/// </summary>
public short Count => m_count;

/// <summary>
/// True if the queue is currently at full capacity.
/// </summary>
public bool Full => m_count == m_values.Length;

#endregion

#region Methods -> Public

/// <summary>
/// Construct a queue.
/// </summary>
/// <param name="capacity">
/// Maximum number of values in the queue.
/// </param>
public G_DoubleEndedQueue( short capacity )
{
m_values = new short[ capacity ];
m_head = 0;
m_tail = 0;
m_count = 0;
}

/// <summary>
/// Clear the content of the queue, O(1).
/// </summary>
public void Clear()
{
m_head = 0;
m_tail = 0;
m_count = 0;
}

/// <summary>
/// Add a value to the front of the queue, O(1).
/// Asserts that the queue is not already full.
/// </summary>
/// <param name="value">
/// The value of the entry.
/// </param>
public void PushFront( short value )
{
AssertNotFull();
m_head = Previous( m_head );
m_values[ m_head ] = value;
m_count++;
}

/// <summary>
/// Add a value to the back of the queue, O(1).
/// Asserts that the queue is not already full.
/// </summary>
/// <param name="value">
/// The value of the entry.
/// </param>
public void PushBack( short value )
{
AssertNotFull();
m_values[ m_tail ] = value;
m_tail = Next( m_tail );
m_count++;
}

/// <summary>
/// Removes the value at the front of the queue, O(1).
/// Asserts that the queue is not empty.
/// </summary>
/// <returns>the removed value</returns>
public short PopFront()
{
AssertNotEmpty();
short value = m_values[ m_head ];
m_head = Next( m_head );
m_count--;
return value;
}

/// <summary>
/// Removes the value at the back of the queue, O(1).
/// Asserts that the queue is not empty.
/// </summary>
/// <returns>the removed value</returns>
public short PopBack()
{
AssertNotEmpty();
m_tail = Previous( m_tail );
short value = m_values[ m_tail ];
m_count--;
return value;
}

/// <summary>
/// Returns the value at the front of the queue, O(1).
/// Asserts that the queue is not empty.
/// </summary>
/// <returns>the value at the front of the queue</returns>
public short PeekFront()
{
AssertNotEmpty();
return m_values[ m_head ];
}

/// <summary>
/// Returns the value at the back of the queue, O(1).
/// Asserts that the queue is not empty.
/// </summary>
/// <returns>the value at the back of the queue</returns>
public short PeekBack()
{
AssertNotEmpty();
return m_values[ Previous( m_tail ) ];
}

#endregion

#region Methods -> Private

void AssertNotEmpty()
{
Assert.IsTrue( m_count > 0, m_errorEmpty );
}

void AssertNotFull()
{
Assert.IsTrue( m_count < m_values.Length, m_errorFull );
}

short LastIndex => (short) ( m_values.Length - 1 );

short Next( short index )
{
return (short) ( index < LastIndex? index + 1 : 0 );
}

short Previous( short index )
{
return (short) ( index > 0? index - 1 : LastIndex );
}

#endregion
}
}
11 changes: 11 additions & 0 deletions Runtime/Util/G_DoubleEndedQueue.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading