perf(worker): message-driven completion waits — the STA pumps continuously while waiting
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Google.Protobuf.Collections;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Worker.Sta;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
|
||||
@@ -25,6 +26,18 @@ public sealed class MxAccessWriteCompletionCache
|
||||
private readonly Dictionary<long, CompletionEntry> entries = new();
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
// Set by Record so a waiter blocked in TryWaitForCompletion wakes the moment
|
||||
// a completion lands instead of after the next blind poll tick. In the live
|
||||
// worker the completion is recorded from inside the waiter's own pumpStep
|
||||
// (COM dispatches OnWriteComplete on the STA), so the message wake is what
|
||||
// does the work there; this handle covers the cross-thread producers — the
|
||||
// tests, and any future off-STA recorder. Not IDisposable on purpose: the
|
||||
// cache is co-owned by MxAccessBaseEventSink and MxAccessSession with no
|
||||
// single teardown owner, and a late COM callback must never Set a disposed
|
||||
// handle. One kernel event per session (one session per worker process),
|
||||
// released by the handle's finalizer.
|
||||
private readonly AutoResetEvent completionSignal = new(false);
|
||||
|
||||
/// <summary>Records the status rows of a fresh OnWriteComplete callback for the given handle pair.</summary>
|
||||
/// <param name="serverHandle">MXAccess server handle.</param>
|
||||
/// <param name="itemHandle">MXAccess item handle.</param>
|
||||
@@ -47,6 +60,10 @@ public sealed class MxAccessWriteCompletionCache
|
||||
: 1UL;
|
||||
entries[key] = new CompletionEntry(version, statuses.Clone());
|
||||
}
|
||||
|
||||
// Signaled outside the lock: the waiter re-takes syncRoot the instant it
|
||||
// wakes, so signaling while holding it would hand it guaranteed contention.
|
||||
completionSignal.Set();
|
||||
}
|
||||
|
||||
/// <summary>Returns the current completion version for a handle pair, or 0 if none was recorded.</summary>
|
||||
@@ -66,19 +83,29 @@ public sealed class MxAccessWriteCompletionCache
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls for a completion newer than <paramref name="sinceVersion"/> until it
|
||||
/// Waits for a completion newer than <paramref name="sinceVersion"/> until it
|
||||
/// arrives or the deadline elapses, calling <paramref name="pumpStep"/> on every
|
||||
/// poll iteration so the worker's STA can dispatch the inbound MXAccess
|
||||
/// iteration so the worker's STA can dispatch the inbound MXAccess
|
||||
/// OnWriteComplete message. Same loop shape as
|
||||
/// <see cref="MxAccessValueCache.TryWaitForUpdate"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The idle part of each iteration blocks in
|
||||
/// <see cref="StaWaitHelper.WaitForSignalOrMessages"/> rather than
|
||||
/// <c>Thread.Sleep</c>: a sleeping STA pumps no Windows messages, so the
|
||||
/// inbound OnWriteComplete could only ever be dispatched at poll-tick
|
||||
/// granularity. The Win32 wait returns the instant a message needs
|
||||
/// pumping (or the completion is recorded from another thread), so the
|
||||
/// apartment keeps dispatching COM events continuously for the whole
|
||||
/// wait. Timeouts, deadline math, and both return shapes are unchanged.
|
||||
/// </remarks>
|
||||
/// <param name="serverHandle">MXAccess server handle.</param>
|
||||
/// <param name="itemHandle">MXAccess item handle.</param>
|
||||
/// <param name="sinceVersion">Version snapshot captured before the write COM call.</param>
|
||||
/// <param name="deadlineUtc">Absolute UTC deadline.</param>
|
||||
/// <param name="pumpStep">Action that pumps any pending Windows messages.</param>
|
||||
/// <param name="statuses">The recorded status rows if a completion arrived before the deadline; empty otherwise.</param>
|
||||
/// <param name="pollIntervalMs">How long to sleep between pump cycles. Default 5 ms.</param>
|
||||
/// <param name="pollIntervalMs">Upper bound on one idle wait slice — the cadence at which <paramref name="pumpStep"/> runs when nothing wakes the wait. Default 5 ms, itself capped at <see cref="StaWaitHelper.MaxFallbackTickMilliseconds"/>.</param>
|
||||
/// <returns><see langword="true"/> if a completion newer than <paramref name="sinceVersion"/> arrived before the deadline; otherwise <see langword="false"/>.</returns>
|
||||
public bool TryWaitForCompletion(
|
||||
int serverHandle,
|
||||
@@ -96,6 +123,12 @@ public sealed class MxAccessWriteCompletionCache
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Pumped unconditionally at the top of every iteration — including
|
||||
// the one that immediately follows a signaled wait, and the very
|
||||
// first before any waiting. Dispatching the message is what fills
|
||||
// the cache in the live worker, and unit tests stand in for the STA
|
||||
// by recording the completion from inside their fake pumpStep, so
|
||||
// this call must never be conditional on a real message queue.
|
||||
pumpStep();
|
||||
|
||||
lock (syncRoot)
|
||||
@@ -108,13 +141,18 @@ public sealed class MxAccessWriteCompletionCache
|
||||
}
|
||||
}
|
||||
|
||||
if (DateTime.UtcNow >= deadlineUtc)
|
||||
// Same expiry test as the sleep-polled version: remaining <= zero is
|
||||
// exactly DateTime.UtcNow >= deadlineUtc.
|
||||
TimeSpan remaining = deadlineUtc - DateTime.UtcNow;
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
statuses = new RepeatedField<MxStatusProxy>();
|
||||
return false;
|
||||
}
|
||||
|
||||
Thread.Sleep(pollIntervalMs);
|
||||
StaWaitHelper.WaitForSignalOrMessages(
|
||||
completionSignal,
|
||||
StaWaitHelper.ClampWaitMilliseconds(remaining, pollIntervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user