perf(dashboard): gate event mirror on live viewers — no clone, no send for unwatched sessions
DashboardEventBroadcaster.Publish ran a deep protobuf Clone (redaction is on by
default) and a group SendAsync for every event of every session, before anything
checked whether a dashboard client was actually watching. In the steady state the
session:{id} group is empty, so that work was thrown away per event.
SignalR does not expose group membership, so EventsHub now mirrors its own
add/remove into a singleton EventsHubViewerRegistry, and OnDisconnectedAsync
releases everything a dropped connection held (SessionDetailsPage disposes the
connection rather than unsubscribing). Publish returns early when the session has
no viewers, before the redaction clone. Watched sessions behave exactly as before.
Lazy mirror-lease start/stop was deliberately not attempted — it entangles the
dashboard with SessionEventDistributor subscribe lifetime for no saving beyond
this gate; recorded in docs/GatewayDashboardDesign.md.
This commit is contained in:
@@ -11,16 +11,64 @@ namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard;
|
||||
/// Verifies that <see cref="DashboardEventBroadcaster"/> honours
|
||||
/// <c>MxGateway:Dashboard:ShowTagValues</c> (SEC-25): tag values are stripped
|
||||
/// from the mirrored copy when the flag is off, present when it is on, and the
|
||||
/// shared source event is never mutated.
|
||||
/// shared source event is never mutated. Also verifies the viewer gate — the
|
||||
/// mirror does no work at all for a session nobody is watching.
|
||||
/// </summary>
|
||||
public sealed class DashboardEventBroadcasterTests
|
||||
{
|
||||
/// <summary>An unwatched session costs neither a redaction clone nor a send.</summary>
|
||||
[Fact]
|
||||
public void Publish_WithNoRegisteredViewers_DoesNotCloneOrSend()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, viewers);
|
||||
MxEvent source = BuildEventWithValue();
|
||||
|
||||
broadcaster.Publish("session-1", source);
|
||||
|
||||
Assert.Equal(0, hubContext.SendCount);
|
||||
Assert.Null(hubContext.LastArgument);
|
||||
}
|
||||
|
||||
/// <summary>A viewer on a different session does not open the gate for this one.</summary>
|
||||
[Fact]
|
||||
public void Publish_WithViewersOnAnotherSessionOnly_DoesNotSend()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
viewers.AddViewer("conn-1", "session-2");
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, viewers);
|
||||
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
|
||||
Assert.Equal(0, hubContext.SendCount);
|
||||
}
|
||||
|
||||
/// <summary>Once the last viewer leaves, the mirror stops sending again.</summary>
|
||||
[Fact]
|
||||
public void Publish_AfterLastViewerLeaves_StopsSending()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
viewers.AddViewer("conn-1", "session-1");
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, viewers);
|
||||
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
Assert.Equal(1, hubContext.SendCount);
|
||||
|
||||
viewers.RemoveViewer("conn-1", "session-1");
|
||||
broadcaster.Publish("session-1", BuildEventWithValue());
|
||||
|
||||
Assert.Equal(1, hubContext.SendCount);
|
||||
}
|
||||
|
||||
/// <summary>Values are stripped from the mirror when ShowTagValues is off; metadata survives.</summary>
|
||||
[Fact]
|
||||
public void Publish_WhenShowTagValuesFalse_RedactsValuesButKeepsMetadata()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false);
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, WatchedSession1());
|
||||
MxEvent source = BuildEventWithValue();
|
||||
|
||||
broadcaster.Publish("session-1", source);
|
||||
@@ -43,7 +91,7 @@ public sealed class DashboardEventBroadcasterTests
|
||||
public void Publish_WhenShowTagValuesFalse_DoesNotMutateSourceEvent()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false);
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: false, WatchedSession1());
|
||||
MxEvent source = BuildEventWithValue();
|
||||
|
||||
broadcaster.Publish("session-1", source);
|
||||
@@ -61,7 +109,7 @@ public sealed class DashboardEventBroadcasterTests
|
||||
public void Publish_WhenShowTagValuesTrue_KeepsValues()
|
||||
{
|
||||
CapturingHubContext hubContext = new();
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: true);
|
||||
DashboardEventBroadcaster broadcaster = Create(hubContext, showTagValues: true, WatchedSession1());
|
||||
MxEvent source = BuildEventWithValue();
|
||||
|
||||
broadcaster.Publish("session-1", source);
|
||||
@@ -73,7 +121,10 @@ public sealed class DashboardEventBroadcasterTests
|
||||
Assert.NotNull(sent.OnAlarmTransition.LimitValue);
|
||||
}
|
||||
|
||||
private static DashboardEventBroadcaster Create(CapturingHubContext hubContext, bool showTagValues)
|
||||
private static DashboardEventBroadcaster Create(
|
||||
CapturingHubContext hubContext,
|
||||
bool showTagValues,
|
||||
EventsHubViewerRegistry viewers)
|
||||
{
|
||||
GatewayOptions gatewayOptions = new()
|
||||
{
|
||||
@@ -82,10 +133,20 @@ public sealed class DashboardEventBroadcasterTests
|
||||
|
||||
return new DashboardEventBroadcaster(
|
||||
hubContext,
|
||||
viewers,
|
||||
Options.Create(gatewayOptions),
|
||||
NullLogger<DashboardEventBroadcaster>.Instance);
|
||||
}
|
||||
|
||||
/// <summary>A registry with one hub connection watching <c>session-1</c>.</summary>
|
||||
/// <returns>The populated registry.</returns>
|
||||
private static EventsHubViewerRegistry WatchedSession1()
|
||||
{
|
||||
EventsHubViewerRegistry viewers = new();
|
||||
viewers.AddViewer("conn-1", "session-1");
|
||||
return viewers;
|
||||
}
|
||||
|
||||
private static MxEvent BuildEventWithValue()
|
||||
{
|
||||
return new MxEvent
|
||||
@@ -117,6 +178,9 @@ public sealed class DashboardEventBroadcasterTests
|
||||
|
||||
/// <summary>Gets the first argument of the most recent send call.</summary>
|
||||
public object? LastArgument => _clients.GroupProxy.LastArgument;
|
||||
|
||||
/// <summary>Gets the number of send calls this fake has observed.</summary>
|
||||
public int SendCount => _clients.GroupProxy.SendCount;
|
||||
}
|
||||
|
||||
private sealed class CapturingHubClients : IHubClients
|
||||
@@ -148,6 +212,9 @@ public sealed class DashboardEventBroadcasterTests
|
||||
/// <summary>Gets the first argument of the most recent send call.</summary>
|
||||
public object? LastArgument { get; private set; }
|
||||
|
||||
/// <summary>Gets the number of send calls made through this proxy.</summary>
|
||||
public int SendCount { get; private set; }
|
||||
|
||||
/// <summary>Records the send call arguments and completes synchronously.</summary>
|
||||
/// <param name="method">The SignalR method name.</param>
|
||||
/// <param name="args">The method arguments.</param>
|
||||
@@ -155,6 +222,7 @@ public sealed class DashboardEventBroadcasterTests
|
||||
/// <returns>A completed task.</returns>
|
||||
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
|
||||
{
|
||||
SendCount++;
|
||||
LastArgument = args.Length > 0 ? args[0] : null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user