Skip to content

Commit

Permalink
🚧Increase fps calc perf (#1078)
Browse files Browse the repository at this point in the history
* Start work for issue #1077

* perf: improve perf of fps calcs and its accuracy
  • Loading branch information
CalvinWilkinson authored Jan 21, 2025
1 parent df8e25a commit 7da7bdf
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 160 deletions.
1 change: 0 additions & 1 deletion Testing/VelaptorTests/OpenGL/GLWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1558,7 +1558,6 @@ public void GLWindow_WhenRenderingFrame_InvokesDrawAndSwapsBuffer()
this.mockGLContext.Received(1).SwapBuffers();
this.mockTimerService.Received(1).Stop();
sut.Fps.Should().Be(250);
this.mockTimerService.Received(1).Reset();
}

[Fact]
Expand Down
111 changes: 31 additions & 80 deletions Testing/VelaptorTests/Services/TimerServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace VelaptorTests.Services;

using System;
using FluentAssertions;
using NSubstitute;
using Velaptor.Services;
Expand All @@ -15,129 +14,81 @@ namespace VelaptorTests.Services;
/// </summary>
public class TimerServiceTests
{
private const long Frequency = 10_000_000;
private readonly IStopWatchWrapper mockStopWatchWrapper;

/// <summary>
/// Initializes a new instance of the <see cref="TimerServiceTests"/> class.
/// </summary>
public TimerServiceTests() => this.mockStopWatchWrapper = Substitute.For<IStopWatchWrapper>();

#region Method Tests
[Fact]
public void Start_WhenInvoked_StartsTheTimer()
public TimerServiceTests()
{
// Arrange
var sut = CreateSystemUnderTest();

// Act
sut.Start();

// Assert
this.mockStopWatchWrapper.Received().Start();
this.mockStopWatchWrapper = Substitute.For<IStopWatchWrapper>();
this.mockStopWatchWrapper.Frequency.Returns(Frequency);
}

#region Method Tests
[Fact]
public void Stop_WithTimerRunning_StopsTheTimer()
public void StartAndStop_WhenSamplesAreNotFull_CalculatesMillisecondsPassed()
{
// Arrange
this.mockStopWatchWrapper.IsRunning.Returns(true);
var sut = CreateSystemUnderTest();

// Act
sut.Stop();

// Assert
this.mockStopWatchWrapper.Received().Stop();
}
var isStarted = false;
this.mockStopWatchWrapper.GetTimestamp().Returns(_ =>
{
var result = isStarted ? 320_000L : 160_000L;
isStarted = true;

[Fact]
public void Stop_WithTimerNotRunning_DoesNotStopTimerOrRecordData()
{
// Arrange
this.mockStopWatchWrapper.IsRunning.Returns(false);
return result;
});
var sut = CreateSystemUnderTest();

// Act
sut.Stop();

// Assert
this.mockStopWatchWrapper.DidNotReceive().Start();
_ = this.mockStopWatchWrapper.DidNotReceive().Elapsed;
}

[Fact]
public void Stop_WhenInvoked_AddsSampleToSamplesList()
{
// Arrange
var sut = CreateSystemUnderTest();

this.mockStopWatchWrapper.IsRunning.Returns(true);
this.mockStopWatchWrapper.Elapsed.Returns(new TimeSpan(0, 0, 0, 0, 100));
sut.Start();

// Act
sut.Stop();

// Assert
Assert.Equal(100, sut.MillisecondsPassed);
this.mockStopWatchWrapper.Received(2).GetTimestamp();
sut.MillisecondsPassed.Should().Be(16);
}

[Fact]
public void Stop_WhenStartingAndStopping_CorrectlyRecordsAndReturnsAverageTime()
public void StartAndStop_WhenSamplesAreFull_CalculatesMillisecondsPassed()
{
// Arrange
var sut = CreateSystemUnderTest();
this.mockStopWatchWrapper.IsRunning.Returns(true);

// Act
// Record sample 1
this.mockStopWatchWrapper.Elapsed.Returns(new TimeSpan(0, 0, 0, 0, 100));
sut.Stop();

// Record sample 2
this.mockStopWatchWrapper.Elapsed.Returns(new TimeSpan(0, 0, 0, 0, 1000));
sut.Stop();

// Assert
sut.MillisecondsPassed.Should().Be(550);
}
var isStarted = false;
this.mockStopWatchWrapper.GetTimestamp().Returns(_ =>
{
var result = isStarted ? 320_000L : 160_000L;
isStarted = !isStarted;

[Fact]
public void Stop_WhenRecordingMoreThanTotalSamples_StartsRecordingBackAtBeginning()
{
// Arrange
return result;
});
var sut = CreateSystemUnderTest();
this.mockStopWatchWrapper.IsRunning.Returns(true);
this.mockStopWatchWrapper.Elapsed.Returns(new TimeSpan(0, 0, 0, 0, 1000));

// Make sure that every single sample in the total 1000 sample range
// is recorded.
for (var i = 1; i <= 1000; i++)
// Act
for (var i = 0; i < 1000; i++)
{
sut.Start();
sut.Stop();
}

// This should be the first sample spot
this.mockStopWatchWrapper.Elapsed.Returns(new TimeSpan(0, 0, 0, 0, 10));

// Act
sut.Stop();

// Assert
sut.MillisecondsPassed.Should().Be(999.01f);
this.mockStopWatchWrapper.Received(2000).GetTimestamp();
sut.MillisecondsPassed.Should().Be(16);
}

[Fact]
public void Reset_WhenInvoked_ResetsTheTimer()
{
// Arrange
var sut = CreateSystemUnderTest();
sut.Start();
sut.Stop();

// Act
sut.Reset();

// Assert
this.mockStopWatchWrapper.Received().Reset();
sut.MillisecondsPassed.Should().Be(0);
}
#endregion

Expand Down
1 change: 0 additions & 1 deletion Velaptor/OpenGL/GLWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,6 @@ private void GLWindow_Render(double time)

this.timerService.Stop();
Fps = 1000f / this.timerService.MillisecondsPassed;
this.timerService.Reset();
}

/// <summary>
Expand Down
18 changes: 4 additions & 14 deletions Velaptor/Services/IStopWatchWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,14 @@

namespace Velaptor.Services;

using System;
using System.Diagnostics;

/// <inheritdoc cref="Stopwatch"/>
internal interface IStopWatchWrapper
{
/// <inheritdoc cref="Stopwatch.Elapsed"/>
TimeSpan Elapsed { get; }
/// <inheritdoc cref="Stopwatch.Frequency"/>
long Frequency => Stopwatch.Frequency;

/// <inheritdoc cref="Stopwatch.IsRunning"/>
bool IsRunning { get; }

/// <inheritdoc cref="Stopwatch.Start"/>
void Start();

/// <inheritdoc cref="Stopwatch.Stop"/>
void Stop();

/// <inheritdoc cref="Stopwatch.Reset"/>
void Reset();
/// <inheritdoc cref="Stopwatch.GetTimestamp"/>
long GetTimestamp();
}
23 changes: 3 additions & 20 deletions Velaptor/Services/StopWatchWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Velaptor.Services;

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

Expand All @@ -19,25 +18,9 @@ namespace Velaptor.Services;
[ExcludeFromCodeCoverage(Justification = "Thin wrapper around the Stopwatch class.")]
internal class StopWatchWrapper : IStopWatchWrapper
{
private readonly Stopwatch stopWatch;

/// <summary>
/// Initializes a new instance of the <see cref="StopWatchWrapper"/> class.
/// </summary>
public StopWatchWrapper() => this.stopWatch = new Stopwatch();

/// <inheritdoc/>
public TimeSpan Elapsed => this.stopWatch.Elapsed;

/// <inheritdoc/>
public bool IsRunning => this.stopWatch.IsRunning;

/// <inheritdoc/>
public void Start() => this.stopWatch.Start();

/// <inheritdoc/>
public void Stop() => this.stopWatch.Stop();
/// <inheritdoc />
public long Frequency => Stopwatch.Frequency;

/// <inheritdoc/>
public void Reset() => this.stopWatch.Reset();
public long GetTimestamp() => Stopwatch.GetTimestamp();
}
88 changes: 44 additions & 44 deletions Velaptor/Services/TimerService.cs
Original file line number Diff line number Diff line change
@@ -1,87 +1,87 @@
// <copyright file="TimerService.cs" company="KinsonDigital">
// <copyright file="TimerService.cs" company="KinsonDigital">
// Copyright (c) KinsonDigital. All rights reserved.
// </copyright>

namespace Velaptor.Services;

using System;
using System.Runtime.CompilerServices;

/// <inheritdoc/>
internal sealed class TimerService : ITimerService
{
private readonly IStopWatchWrapper timer;
private readonly double[] timeSamples = new double[1000];
private const int SAMPLE_SIZE = 1000;
private readonly IStopWatchWrapper stopWatch;
private readonly double[] timeSamples = new double[SAMPLE_SIZE];
private readonly double tickFreqMs;
private double runningSum;
private long startTicks;
private long stopTicks;
private int index;
private bool isArrayFull;
private int divisor;

/// <summary>
/// Initializes a new instance of the <see cref="TimerService"/> class.
/// </summary>
/// <param name="stopWatch">Tracks and measures time passed.</param>
public TimerService(IStopWatchWrapper stopWatch) => this.timer = stopWatch;
public TimerService(IStopWatchWrapper stopWatch)
{
this.stopWatch = stopWatch;
this.tickFreqMs = 1000.0 / this.stopWatch.Frequency;
}

/// <inheritdoc/>
/// <remarks>This is averaged over 1000 samples.</remarks>
public float MillisecondsPassed { get; private set; }

/// <inheritdoc/>
public void Start() => this.timer.Start();
public void Start() => this.startTicks = this.stopWatch.GetTimestamp();

/// <inheritdoc/>
public void Stop()
{
if (!this.timer.IsRunning)
{
return;
}
this.stopTicks = this.stopWatch.GetTimestamp();
var sample = (this.stopTicks - this.startTicks) * this.tickFreqMs;
AddSample(sample);

this.timer.Stop();
// Set the divisor to avoid division by zero and to divide by the correct number of samples
// which is important for calculating the average
this.divisor = this.index == 1 ? 1 : this.index;

AddSample(this.timer.Elapsed.TotalMilliseconds);

// Get the average of the samples and save it
MillisecondsPassed = Average();
// Calculate average using running sum
MillisecondsPassed = (float)(this.runningSum / (this.isArrayFull ? SAMPLE_SIZE : this.divisor));
}

/// <inheritdoc/>
public void Reset() => this.timer.Reset();
public void Reset()
{
this.index = 0;
this.runningSum = 0;
this.isArrayFull = false;
Array.Clear(this.timeSamples, 0, SAMPLE_SIZE);
MillisecondsPassed = 0;
}

/// <summary>
/// Adds the given <paramref name="sample"/> to the list of samples.
/// </summary>
/// <param name="sample">The sample to add.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AddSample(double sample)
{
// Subtract the old value from running sum before replacing it
this.runningSum -= this.timeSamples[this.index];
this.runningSum += sample;
this.timeSamples[this.index] = sample;

if (this.index >= this.timeSamples.Length - 1)
{
this.index = 0;
}
else
if (this.index == SAMPLE_SIZE - 1)
{
this.index += 1;
}
}

/// <summary>
/// Calculates the average of all the samples.
/// </summary>
/// <returns>The sample average.</returns>
/// <remarks>If any of the samples are 0, it is not counted towards the average.</remarks>
private float Average()
{
var sum = 0.0;
var count = 0;

foreach (var t in this.timeSamples)
{
if (t == 0)
{
continue;
}

sum += t;
count += 1;
this.isArrayFull = true;
}

return count == 0 ? 0 : (float)sum / count;
this.index = this.index >= this.timeSamples.Length - 1
? 0
: this.index + 1;
}
}

0 comments on commit 7da7bdf

Please sign in to comment.