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:
@@ -40,6 +40,20 @@ public sealed class MxAccessValueCache
|
||||
throw new ArgumentNullException(nameof(mxEvent));
|
||||
}
|
||||
|
||||
// WRK-11: the event sink no longer clones before enqueue, so the passed
|
||||
// mxEvent is the very instance handed to the outbound queue. Deep-copy
|
||||
// the value/timestamp/statuses payload we retain here so the cache's
|
||||
// snapshot stays independent of the enqueued (and later serialized)
|
||||
// event — the two must never share mutable protobuf sub-messages.
|
||||
// Value is always set for OnDataChange; SourceTimestamp may be unset when
|
||||
// the source timestamp could not be parsed, so both are cloned only when
|
||||
// present. The null-forgiving result matches CachedValue's non-null-
|
||||
// annotated parameters, which already accepted a runtime-null value or
|
||||
// timestamp before WRK-11 (the ternary keeps the compiler's null-state
|
||||
// from poisoning to maybe-null, which a plain null check would do).
|
||||
MxValue cachedValue = mxEvent.Value is null ? null! : mxEvent.Value.Clone();
|
||||
Timestamp cachedTimestamp = mxEvent.SourceTimestamp is null ? null! : mxEvent.SourceTimestamp.Clone();
|
||||
|
||||
long key = CreateItemKey(serverHandle, itemHandle);
|
||||
lock (syncRoot)
|
||||
{
|
||||
@@ -49,10 +63,10 @@ public sealed class MxAccessValueCache
|
||||
|
||||
entries[key] = new CachedValue(
|
||||
nextVersion,
|
||||
mxEvent.Value,
|
||||
cachedValue,
|
||||
mxEvent.Quality,
|
||||
mxEvent.SourceTimestamp,
|
||||
mxEvent.Statuses);
|
||||
cachedTimestamp,
|
||||
mxEvent.Statuses.Clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user