perf(worker): message-driven completion waits — the STA pumps continuously while waiting
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
@@ -177,6 +178,77 @@ public sealed class MxAccessValueCacheTests
|
||||
Assert.Equal(1UL, value.Version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the wait is message/signal-driven rather than sleep-polled:
|
||||
/// with a poll interval far longer than the deadline, a value set from
|
||||
/// another thread still wakes the wait and is returned well inside the
|
||||
/// deadline. The sleep-polled loop this replaced would have sat blind for
|
||||
/// the whole 10 s interval — on the STA that means 10 s with no Windows
|
||||
/// messages dispatched, so the OnDataChange being waited for could not
|
||||
/// have arrived at all — and then reported a timeout.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryWaitForUpdate_WakesOnCrossThreadSet_DespiteLongPollInterval()
|
||||
{
|
||||
MxAccessValueCache cache = new();
|
||||
Timestamp sourceTimestamp = Timestamp.FromDateTime(DateTime.UtcNow);
|
||||
using ManualResetEventSlim waitEntered = new(false);
|
||||
|
||||
Task setter = Task.Run(() =>
|
||||
{
|
||||
// Handshake so the value cannot land before the wait starts —
|
||||
// otherwise the first check would satisfy it and prove nothing.
|
||||
waitEntered.Wait(TimeSpan.FromSeconds(5));
|
||||
Thread.Sleep(50);
|
||||
cache.Set(7, 21, BuildEvent(7, 21, intValue: 8080, quality: 192, sourceTimestamp));
|
||||
});
|
||||
|
||||
Stopwatch elapsed = Stopwatch.StartNew();
|
||||
bool found = cache.TryWaitForUpdate(
|
||||
serverHandle: 7,
|
||||
itemHandle: 21,
|
||||
sinceVersion: 0,
|
||||
deadlineUtc: DateTime.UtcNow.AddSeconds(3),
|
||||
pumpStep: () => waitEntered.Set(),
|
||||
out MxAccessValueCache.CachedValue value,
|
||||
pollIntervalMs: 10_000);
|
||||
elapsed.Stop();
|
||||
setter.Wait(TimeSpan.FromSeconds(5));
|
||||
|
||||
Assert.True(found);
|
||||
Assert.Equal(8080, value.Value.Int32Value);
|
||||
Assert.True(
|
||||
elapsed.Elapsed < TimeSpan.FromSeconds(2),
|
||||
$"The wait should have woken on the cached value, not slept out the poll interval; took {elapsed.ElapsedMilliseconds} ms.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the pump step keeps running throughout a wait that nothing
|
||||
/// ever signals: the caller's poll interval is capped at the 50 ms
|
||||
/// fallback tick, so a timing-out wait still pumps repeatedly instead of
|
||||
/// once. This is what keeps the STA dispatching COM events in a process
|
||||
/// whose message queue never wakes the wait, and it is the safety net
|
||||
/// behind the ReadBulk per-tag timeout.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryWaitForUpdate_KeepsPumping_WhenPollIntervalExceedsTheFallbackTick()
|
||||
{
|
||||
MxAccessValueCache cache = new();
|
||||
int pumpCalls = 0;
|
||||
|
||||
bool found = cache.TryWaitForUpdate(
|
||||
serverHandle: 7,
|
||||
itemHandle: 21,
|
||||
sinceVersion: 0,
|
||||
deadlineUtc: DateTime.UtcNow.AddMilliseconds(400),
|
||||
pumpStep: () => Interlocked.Increment(ref pumpCalls),
|
||||
out _,
|
||||
pollIntervalMs: 10_000);
|
||||
|
||||
Assert.False(found);
|
||||
Assert.True(pumpCalls >= 3, $"Expected repeated pumping across the 400 ms wait, saw {pumpCalls} calls.");
|
||||
}
|
||||
|
||||
private static MxEvent BuildEvent(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
|
||||
Reference in New Issue
Block a user