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

🚧Increase fps calc perf #1078

Merged
merged 2 commits into from
Jan 21, 2025
Merged
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
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;

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Avalonia Testing App / Building AvaloniaTesting Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Avalonia Testing App / Building AvaloniaTesting Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Run Velaptor Tests / Run Unit Tests

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Run Velaptor Tests / Run Unit Tests

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Measure Text Perf Project / Building MeasureTextPerf Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Measure Text Perf Project / Building MeasureTextPerf Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Playground App Project / Building PlaygroundApp Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Playground App Project / Building PlaygroundApp Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Velaptor Build Status Check / Building Velaptor Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Velaptor Build Status Check / Building Velaptor Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Mouse Perf Project / Building MousePerf Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Mouse Perf Project / Building MousePerf Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Keyboard Perf Project / Building KeyboardPerf Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Keyboard Perf Project / Building KeyboardPerf Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Velaptor Testing App / Building VelaptorTesting Project

Check warning on line 13 in Velaptor/Services/TimerService.cs

View workflow job for this annotation

GitHub Actions / Build Velaptor Testing App / Building VelaptorTesting Project

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;
}
}
Loading