perf(worker): message-driven completion waits — the STA pumps continuously while waiting
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Sta;
|
||||
|
||||
/// <summary>
|
||||
/// Message-aware blocking wait shared by STA code that must sit idle without
|
||||
/// starving the Windows message queue. <see cref="StaMessagePump"/> owns the
|
||||
/// runtime's outer idle wait; this helper serves the inner waits performed
|
||||
/// by commands that already hold the STA — the write-completion wait and the
|
||||
/// ReadBulk first-value wait — which need the same "wake on a signal or on
|
||||
/// an inbound message" behavior without owning a pump instance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately separate from <see cref="StaMessagePump"/> rather than a new
|
||||
/// method on it: the two caches that use this are plain per-session objects
|
||||
/// with no pump dependency (they take the pump step as an injected delegate
|
||||
/// so tests can drive them off the STA). Keeping the helper in this folder
|
||||
/// keeps every <c>user32</c> wait signature in the worker reviewable in one
|
||||
/// place; the declaration below mirrors the pump's exactly.
|
||||
/// </remarks>
|
||||
internal static class StaWaitHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Ceiling on the blind fallback tick. A caller's poll interval is
|
||||
/// clamped to this so no wait can go longer than 50 ms without calling
|
||||
/// its pump step, even when the process has no message queue to wake it
|
||||
/// (unit-test threads) and the wait handle is never signaled.
|
||||
/// </summary>
|
||||
internal const int MaxFallbackTickMilliseconds = 50;
|
||||
|
||||
private const uint MsgWaitFailed = 0xFFFFFFFF;
|
||||
private const uint MwmoInputAvailable = 0x0004;
|
||||
private const uint QsAllInput = 0x04FF;
|
||||
|
||||
/// <summary>
|
||||
/// Blocks the calling thread until <paramref name="signal"/> is set, a
|
||||
/// Windows message is available to pump, or the timeout elapses —
|
||||
/// whichever comes first. Unlike <see cref="Thread.Sleep(int)"/> this
|
||||
/// never blinds the thread to inbound COM event messages: the wait
|
||||
/// returns the instant one arrives, so the caller can dispatch it.
|
||||
/// </summary>
|
||||
/// <param name="signal">Wait handle set by whoever produces the awaited result.</param>
|
||||
/// <param name="timeoutMilliseconds">Maximum wait; values at or below zero return immediately.</param>
|
||||
internal static void WaitForSignalOrMessages(
|
||||
WaitHandle signal,
|
||||
int timeoutMilliseconds)
|
||||
{
|
||||
if (signal is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(signal));
|
||||
}
|
||||
|
||||
if (timeoutMilliseconds <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SafeWaitHandle safeHandle = signal.SafeWaitHandle;
|
||||
IntPtr[] handles = [safeHandle.DangerousGetHandle()];
|
||||
uint result = MsgWaitForMultipleObjectsEx(
|
||||
(uint)handles.Length,
|
||||
handles,
|
||||
(uint)timeoutMilliseconds,
|
||||
QsAllInput,
|
||||
MwmoInputAvailable);
|
||||
|
||||
// The handle is a field of a live caller object, so it cannot be
|
||||
// collected mid-call; KeepAlive states that dependency explicitly
|
||||
// rather than relying on it.
|
||||
GC.KeepAlive(signal);
|
||||
|
||||
if (result == MsgWaitFailed)
|
||||
{
|
||||
// Degrade to the pre-existing blind sleep instead of throwing or
|
||||
// hot-spinning: the caller is a bounded, deadline-driven poll loop,
|
||||
// and a failed wait must not turn a timed-out write into a fault.
|
||||
Thread.Sleep(timeoutMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the wait slice for one iteration of a deadline-driven poll
|
||||
/// loop: never past the deadline, never longer than the caller's poll
|
||||
/// interval, and never longer than <see cref="MaxFallbackTickMilliseconds"/>.
|
||||
/// </summary>
|
||||
/// <param name="remaining">Time left before the caller's absolute deadline.</param>
|
||||
/// <param name="pollIntervalMilliseconds">The caller's requested pump cadence.</param>
|
||||
/// <returns>Milliseconds to wait; zero or less when the deadline has passed.</returns>
|
||||
internal static int ClampWaitMilliseconds(
|
||||
TimeSpan remaining,
|
||||
int pollIntervalMilliseconds)
|
||||
{
|
||||
int tick = pollIntervalMilliseconds < 1
|
||||
? 1
|
||||
: Math.Min(pollIntervalMilliseconds, MaxFallbackTickMilliseconds);
|
||||
|
||||
double remainingMilliseconds = remaining.TotalMilliseconds;
|
||||
if (remainingMilliseconds <= 0d)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return remainingMilliseconds < tick
|
||||
? (int)Math.Ceiling(remainingMilliseconds)
|
||||
: tick;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern uint MsgWaitForMultipleObjectsEx(
|
||||
uint count,
|
||||
IntPtr[] handles,
|
||||
uint milliseconds,
|
||||
uint wakeMask,
|
||||
uint flags);
|
||||
}
|
||||
Reference in New Issue
Block a user