perf(gateway): trim event/command hot-path allocations (GWC-06/07/15, IPC-05)
Behavior-preserving allocation cuts on the per-event/per-command path: - GWC-06: StreamEvents send-timing uses Stopwatch.GetTimestamp() + GetElapsedTime() instead of a per-event Stopwatch allocation (same measured span). - GWC-07/IPC-05 (event): MapEvent transfers ownership of the inner MxEvent instead of .Clone()-ing it. Safe: WorkerEvent is parsed fresh per pipe frame with the distributor pump as its single consumer (GWC-01), MapEvent runs once before fan-out, and every downstream consumer (subscribers, replay ring) only READS the event (WorkerSequence is stamped upstream; verified no post-mapping mutation). Comment documents the invariant + restore-clone caveat if a second consumer is added. - IPC-05 (command): CreateCommandEnvelope no longer re-clones; MapCommand already isolated the graph from the caller-owned gRPC message. - GWC-15: grpc_stream_queue.depth converts from a per-event push counter to an ObservableGauge summing registered channel sources at scrape time only (name/semantics unchanged); removes all per-event .Count/lock work. Kept every load-bearing isolation clone (MapCommand, Invoke, bulk filters, MapCommandReply). Server build clean (0 warnings); EventStream/Metrics/ Distributor/Mapper tests 62/62 (incl. formerly-flaky queue-depth tests, now green under the lazy gauge, + 2 new MapEvent ownership tests). Docs: Metrics.md, Grpc.md. Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
This commit is contained in:
@@ -76,4 +76,35 @@ public sealed class MxAccessGrpcMapperTests
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.ProtocolViolation, publicReply.ProtocolStatus.Code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies MapEvent transfers ownership of the inner MxEvent (GWC-07 / IPC-05): the
|
||||
/// returned reference is the same instance carried by the WorkerEvent, not a clone. The
|
||||
/// WorkerEvent is discarded after mapping and the distributor pump is its single consumer,
|
||||
/// so moving the inner event out is safe and avoids a per-event deep copy.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapEvent_TransfersOwnershipOfInnerEventWithoutCloning()
|
||||
{
|
||||
MxEvent innerEvent = new()
|
||||
{
|
||||
Family = MxEventFamily.OnDataChange,
|
||||
RawStatus = "OK",
|
||||
};
|
||||
WorkerEvent workerEvent = new() { Event = innerEvent };
|
||||
|
||||
MxEvent mapped = new MxAccessGrpcMapper().MapEvent(workerEvent);
|
||||
|
||||
Assert.Same(innerEvent, mapped);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a worker event with no public payload maps to the unspecified-family sentinel.</summary>
|
||||
[Fact]
|
||||
public void MapEvent_WhenEventMissing_ReturnsUnspecifiedFamilySentinel()
|
||||
{
|
||||
MxEvent mapped = new MxAccessGrpcMapper().MapEvent(new WorkerEvent());
|
||||
|
||||
Assert.Equal(MxEventFamily.Unspecified, mapped.Family);
|
||||
Assert.Equal("Worker event did not contain a public event payload.", mapped.RawStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ public sealed class GatewayMetricsTests
|
||||
metrics.EventReceived("session-1", "OnDataChange");
|
||||
metrics.EventReceived("session-1", "OnDataChange");
|
||||
metrics.SetWorkerEventQueueDepth(7);
|
||||
metrics.AdjustGrpcEventStreamQueueDepth(3);
|
||||
// GWC-15: the gRPC stream queue-depth gauge sums live backlog sources at collection time
|
||||
// rather than tracking a pushed running total. Register a source reporting 3.
|
||||
using IDisposable backlogSource = metrics.RegisterEventStreamBacklogSource(static () => 3);
|
||||
metrics.QueueOverflow("session-events");
|
||||
metrics.Fault("CommandTimeout");
|
||||
metrics.WorkerKilled("CommandTimeout");
|
||||
|
||||
Reference in New Issue
Block a user