perf(worker): event hot-path allocation + flush cuts (WRK-06/11/12, IPC-15)

WRK-06: MxStatusProxyConverter caches the four resolved FieldInfo per status
type in a static ConcurrentDictionary (the GetField metadata scan ran 4x per
status per event on the STA path). GetValue+Convert.ToInt32 still run per event
(late-bound RCW). Exceptions byte-identical: missing-field message unchanged
(ResolveField, not cached on throw via GetOrAdd); null-value message unchanged.

WRK-11: MxAccessEventQueue.Enqueue takes ownership of the passed MxEvent -
stamps WorkerSequence/WorkerTimestamp on it in place and enqueues it, no
Clone(). Audited all 3 callers (base/alarm event sinks, provider-mode handler):
each builds a fresh event per Enqueue, none reuse it. MxAccessValueCache.Set now
deep-copies its retained Value/SourceTimestamp/Statuses so the cache snapshot
never aliases the queue-owned (later serialized) event. Net: alarm/other events
clone nothing (was full clone); data-change clones payload-only.

WRK-12: WorkerFrameWriter coalesces the flush across a drained batch - each
frame is written but not flushed individually; one FlushAsync after the batch,
then all written frames complete. Preserves the written+flushed completion
contract; a burst of N events costs 1 flush, not N. On write failure the whole
in-flight batch + queue fail so no caller hangs.

IPC-15 (doc): the multi-event WorkerEnvelope body remains unimplemented (wire
still carries one event per worker_event frame); gateway.md Performance section
now distinguishes the shipped flush-coalescing from that deferred proto change.

net48-safe (no init/records; readonly struct cache entry). Worker builds x86
only - verification on windev. Tests added: converter cache-reuse, queue
ownership-transfer, value-cache snapshot independence, writer batch-flush-once.

Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
This commit is contained in:
Joseph Doherty
2026-07-09 15:52:49 -04:00
parent 73ce824f6c
commit f61c816acf
9 changed files with 303 additions and 28 deletions
@@ -106,6 +106,35 @@ public sealed class MxStatusProxyConverterTests
Assert.Contains("success", exception.Message);
}
/// <summary>
/// Verifies that repeated conversions of the same status type produce
/// identical results. WRK-06 caches the resolved FieldInfo objects per
/// type after the first conversion; the cached path must yield the same
/// message the uncached first call produced.
/// </summary>
[Fact]
public void Convert_RepeatedForSameType_ProducesIdenticalResults()
{
FakeMxStatusProxy status = new()
{
success = 0,
category = 3,
detectedBy = 1,
detail = 21,
};
// First call populates the per-type FieldInfo cache; the second reuses it.
MxStatusProxy first = _converter.Convert(status);
MxStatusProxy second = _converter.Convert(status);
Assert.Equal(first, second);
Assert.Equal(MxStatusCategory.CommunicationError, second.Category);
Assert.Equal(MxStatusSource.RespondingLmx, second.DetectedBy);
Assert.Equal(0, second.Success);
Assert.Equal(21, second.Detail);
Assert.Equal("Invalid reference", second.DiagnosticText);
}
public struct FakeMxStatusProxy
{
public short success;