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

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

Merged
merged 5 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
52 changes: 38 additions & 14 deletions Libraries/Opc.Ua.PubSub/IntervalRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
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();

Expand Down Expand Up @@ -154,30 +155,53 @@
/// </summary>
private async Task ProcessAsync()
{
lock (m_lock)

Check warning on line 158 in Libraries/Opc.Ua.PubSub/IntervalRunner.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.PubSub/IntervalRunner.cs#L158

Added line #L158 was not covered by tests
{
m_nextPublishTick = HiResClock.Ticks;
}

Check warning on line 161 in Libraries/Opc.Ua.PubSub/IntervalRunner.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.PubSub/IntervalRunner.cs#L160-L161

Added lines #L160 - L161 were not covered by tests
do
{
int sleepCycle = 0;
DateTime now = DateTime.UtcNow;
DateTime nextPublishTime = DateTime.MinValue;

lock (m_lock)
if (m_cancellationToken.IsCancellationRequested)
{
sleepCycle = Convert.ToInt32(m_interval);
break;
}

nextPublishTime = m_nextPublishTime;
long nowTick = HiResClock.Ticks;
double nextPublishTick = 0;

Check warning on line 170 in Libraries/Opc.Ua.PubSub/IntervalRunner.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.PubSub/IntervalRunner.cs#L169-L170

Added lines #L169 - L170 were not covered by tests

lock(m_lock)

Check warning on line 172 in Libraries/Opc.Ua.PubSub/IntervalRunner.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.PubSub/IntervalRunner.cs#L172

Added line #L172 was not covered by tests
{
nextPublishTick = m_nextPublishTick;

Check warning on line 174 in Libraries/Opc.Ua.PubSub/IntervalRunner.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.PubSub/IntervalRunner.cs#L174

Added line #L174 was not covered by tests
}

if (nextPublishTime > now)
double sleepCycle = (nextPublishTick - nowTick) / HiResClock.TicksPerMillisecond;

Check warning on line 177 in Libraries/Opc.Ua.PubSub/IntervalRunner.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.PubSub/IntervalRunner.cs#L177

Added line #L177 was not covered by tests
if (sleepCycle > 16)
{
mregen marked this conversation as resolved.
Show resolved Hide resolved
sleepCycle = (int)Math.Min((nextPublishTime - now).TotalMilliseconds, sleepCycle);
sleepCycle = (int)Math.Max(kMinInterval, sleepCycle);
// Use Task.Delay if sleep cycle is larger
await Task.Delay(TimeSpan.FromMilliseconds(sleepCycle), m_cancellationToken.Token).ConfigureAwait(false);
mregen marked this conversation as resolved.
Show resolved Hide resolved
}

// Still ticks to consume (spurious wakeup too early), improbable
nowTick = HiResClock.Ticks;

Check warning on line 184 in Libraries/Opc.Ua.PubSub/IntervalRunner.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.PubSub/IntervalRunner.cs#L184

Added line #L184 was not covered by tests
if (nowTick < nextPublishTick)
{
while (HiResClock.Ticks < nextPublishTick)
{
mregen marked this conversation as resolved.
Show resolved Hide resolved
// Busy-wait and avoid overhead of Task.Delay for verry small wait times
mregen marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
else if (sleepCycle >= 0 && sleepCycle <= 16)
{
while (HiResClock.Ticks < nextPublishTick)
{
// Busy-wait and avoid overhead of Task.Delay for verry small wait times
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

}

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;

Check warning on line 204 in Libraries/Opc.Ua.PubSub/IntervalRunner.cs

View check run for this annotation

Codecov / codecov/patch

Libraries/Opc.Ua.PubSub/IntervalRunner.cs#L203-L204

Added lines #L203 - L204 were not covered by tests

if (IntervalAction != null && CanExecuteFunc != null && CanExecuteFunc())
{
Expand Down
43 changes: 31 additions & 12 deletions Tests/Opc.Ua.PubSub.Tests/Configuration/UaPublisherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@

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]
Expand All @@ -53,12 +54,17 @@ public void ValidateUaPublisherPublishIntervalDeviation(
[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;
Expand All @@ -70,25 +76,38 @@ public void ValidateUaPublisherPublishIntervalDeviation(
//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);
}
}
}
Loading