Use HiresClock ticks instead of DateTime for IntervalRunner publish time calculation (#2753)

* Use HiresClock instead of DateTime for IntervalRunner publish time calculation.
This commit is contained in:
Suciu Mircea Adrian
2024-09-25 13:18:02 +03:00
committed by GitHub
parent 36dbdd3873
commit 5349d9a033
2 changed files with 64 additions and 27 deletions
+33 -15
View File
@@ -42,7 +42,8 @@ namespace Opc.Ua.PubSub
private readonly object m_lock = new object();
private double m_interval = kMinInterval;
private DateTime m_nextPublishTime = DateTime.MinValue;
private double m_nextPublishTick = 0;
// event used to cancel run
private CancellationTokenSource m_cancellationToken = new CancellationTokenSource();
@@ -154,30 +155,47 @@ namespace Opc.Ua.PubSub
/// </summary>
private async Task ProcessAsync()
{
lock (m_lock)
{
m_nextPublishTick = HiResClock.Ticks;
}
do
{
int sleepCycle = 0;
DateTime now = DateTime.UtcNow;
DateTime nextPublishTime = DateTime.MinValue;
lock (m_lock)
if (m_cancellationToken.IsCancellationRequested)
{
sleepCycle = Convert.ToInt32(m_interval);
nextPublishTime = m_nextPublishTime;
break;
}
if (nextPublishTime > now)
long nowTick = HiResClock.Ticks;
double nextPublishTick = 0;
lock(m_lock)
{
sleepCycle = (int)Math.Min((nextPublishTime - now).TotalMilliseconds, sleepCycle);
sleepCycle = (int)Math.Max(kMinInterval, sleepCycle);
nextPublishTick = m_nextPublishTick;
}
double sleepCycle = (nextPublishTick - nowTick) / HiResClock.TicksPerMillisecond;
if (sleepCycle > 16)
{
// Use Task.Delay if sleep cycle is larger
await Task.Delay(TimeSpan.FromMilliseconds(sleepCycle), m_cancellationToken.Token).ConfigureAwait(false);
}
// Still ticks to consume (spurious wakeup too early), improbable
nowTick = HiResClock.Ticks;
if (nowTick < nextPublishTick)
{
SpinWait.SpinUntil(() => HiResClock.Ticks >= nextPublishTick);
}
}
else if (sleepCycle >= 0 && sleepCycle <= 16)
{
SpinWait.SpinUntil(() => HiResClock.Ticks >= nextPublishTick);
}
lock (m_lock)
{
var nextCycle = Convert.ToInt32(m_interval);
m_nextPublishTime = DateTime.UtcNow.AddMilliseconds(nextCycle);
var nextCycle = (long)m_interval * HiResClock.TicksPerMillisecond;
m_nextPublishTick += nextCycle;
if (IntervalAction != null && CanExecuteFunc != null && CanExecuteFunc())
{
@@ -37,10 +37,11 @@ using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Opc.Ua.PubSub.Tests.Configuration
{
[TestFixture(Description = "Tests for UAPublisher class")]
[TestFixture(Description = "Tests for UAPublisher class"), SingleThreaded]
public class UaPublisherTests
{
static IList<DateTime> s_publishTimes = new List<DateTime>();
static List<long> s_publishTicks = new List<long>();
static object s_lock = new Object();
[Test(Description = "Test that PublishMessage method is called after a UAPublisher is started.")]
[Combinatorial]
@@ -53,12 +54,17 @@ namespace Opc.Ua.PubSub.Tests.Configuration
[Values(10)] int publishTimeInSeconds)
{
//Arrange
s_publishTimes.Clear();
s_publishTicks.Clear();
var mockConnection = new Mock<IUaPubSubConnection>();
mockConnection.Setup(x => x.CanPublish(It.IsAny<WriterGroupDataType>())).Returns(true);
mockConnection.Setup(x => x.CreateNetworkMessages(It.IsAny<WriterGroupDataType>(), It.IsAny<WriterGroupPublishState>()))
.Callback(() => s_publishTimes.Add(DateTime.Now));
.Callback(() => {
lock (s_lock)
{
s_publishTicks.Add(HiResClock.Ticks);
}
});
WriterGroupDataType writerGroupDataType = new WriterGroupDataType();
writerGroupDataType.PublishingInterval = publishingInterval;
@@ -70,25 +76,38 @@ namespace Opc.Ua.PubSub.Tests.Configuration
//wait so many seconds
Thread.Sleep(publishTimeInSeconds * 1000);
publisher.Stop();
int faultIndex = -1;
double faultDeviation = 0;
s_publishTimes = (from t in s_publishTimes
s_publishTicks = (from t in s_publishTicks
orderby t
select t).ToList();
//Assert
for (int i = 1; i < s_publishTimes.Count; i++)
for (int i = 1; i < s_publishTicks.Count; i++)
{
double interval = s_publishTimes[i].Subtract(s_publishTimes[i - 1]).TotalMilliseconds;
double deviation = Math.Abs(publishingInterval - interval);
if (deviation >= maxDeviation && deviation > faultDeviation)
double interval = (s_publishTicks[i] - s_publishTicks[i - 1])/HiResClock.TicksPerMillisecond;
if (interval != 0)
{
faultIndex = i;
faultDeviation = deviation;
double deviation = -1;
if (interval != publishingInterval)
{
deviation = Math.Abs(publishingInterval - interval);
}
if (deviation >= maxDeviation && deviation > faultDeviation)
{
faultIndex = i;
faultDeviation = deviation;
}
}
}
Assert.IsTrue(faultIndex < 0, "publishingInterval={0}, maxDeviation={1}, publishTimeInSecods={2}, deviation[{3}] = {4} has maximum deviation", publishingInterval, maxDeviation, publishTimeInSeconds, faultIndex, faultDeviation);
Assert.IsTrue(faultIndex < 0, "publishingInterval={0}, maxDeviation={1}, publishTimeInSecods={2}, deviation[{3}] = {4} as max deviation",
publishingInterval,
maxDeviation,
publishTimeInSeconds,
faultIndex,
faultDeviation);
}
}
}