perf(dashboard): idle-gate the snapshot tick; cache static config; bound key-list refresh
The snapshot publisher broadcast to Clients.All on every ~1s tick forever, with zero viewers. Each tick cost a session-registry snapshot and sort, a metrics snapshot that copies dictionaries under the global metrics lock, a full rebuild of EffectiveGatewayConfiguration, and a SQLite read of the API key table. DashboardSnapshotHub now counts live connections into the singleton DashboardSnapshotHubConnectionCounter (clamped at zero, since SignalR can call OnDisconnectedAsync for a connection whose OnConnectedAsync faulted). The publisher drives the snapshot enumerator by hand instead of await foreach: with no connections it does not call MoveNextAsync at all, so the producing iterator stays suspended at its yield and no snapshot is built — the gate removes the build, not just the broadcast. It re-checks once a second, so the first viewer resumes the tick within about one interval; that viewer is seeded immediately by DashboardPageBase's synchronous GetSnapshot() and by the hub's OnConnectedAsync. Two per-tick costs are bounded independently of the gate: the effective configuration is startup-static (options are bound once at boot and never reloaded), so it is built once and cached; and the API key summaries refresh at most every 15s, since the list only changes when an operator creates, rotates, or revokes a key. Only a successful refresh restarts the interval, so a failed or timed-out read is still retried on the next tick with the previous summaries left on screen.
This commit is contained in:
@@ -9,8 +9,15 @@ namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
|
||||
/// immediately via <see cref="OnConnectedAsync"/>; subsequent refreshes are
|
||||
/// broadcast by <see cref="DashboardSnapshotPublisher"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Connections are counted into <see cref="DashboardSnapshotHubConnectionCounter"/>
|
||||
/// so <see cref="DashboardSnapshotPublisher"/> can stop building and broadcasting
|
||||
/// snapshots while nobody is watching.
|
||||
/// </remarks>
|
||||
[Authorize(Policy = DashboardAuthenticationDefaults.HubClientsPolicy)]
|
||||
public sealed class DashboardSnapshotHub(IDashboardSnapshotService snapshotService) : Hub
|
||||
public sealed class DashboardSnapshotHub(
|
||||
IDashboardSnapshotService snapshotService,
|
||||
DashboardSnapshotHubConnectionCounter connectionCounter) : Hub
|
||||
{
|
||||
/// <summary>Method name used to push snapshot updates to clients.</summary>
|
||||
public const string SnapshotMessage = "SnapshotUpdated";
|
||||
@@ -18,7 +25,57 @@ public sealed class DashboardSnapshotHub(IDashboardSnapshotService snapshotServi
|
||||
/// <inheritdoc />
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
// Count the viewer before seeding it, so the publisher resumes its tick
|
||||
// no later than the first snapshot this connection renders.
|
||||
connectionCounter.Increment();
|
||||
await Clients.Caller.SendAsync(SnapshotMessage, snapshotService.GetSnapshot()).ConfigureAwait(false);
|
||||
await base.OnConnectedAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
connectionCounter.Decrement();
|
||||
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process-wide count of live <see cref="DashboardSnapshotHub"/> connections.
|
||||
/// Registered as a singleton and read by <see cref="DashboardSnapshotPublisher"/>
|
||||
/// to idle-gate the snapshot tick: with no dashboard connected there is nothing
|
||||
/// to broadcast to, so no snapshot is built.
|
||||
/// </summary>
|
||||
public sealed class DashboardSnapshotHubConnectionCounter
|
||||
{
|
||||
private int _count;
|
||||
|
||||
/// <summary>Gets the number of live snapshot hub connections.</summary>
|
||||
public int Count => Volatile.Read(ref _count);
|
||||
|
||||
/// <summary>Records a new snapshot hub connection.</summary>
|
||||
/// <returns>The connection count after the increment.</returns>
|
||||
public int Increment()
|
||||
{
|
||||
return Interlocked.Increment(ref _count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a snapshot hub disconnection. The count is clamped at zero: SignalR
|
||||
/// can invoke <c>OnDisconnectedAsync</c> for a connection whose
|
||||
/// <c>OnConnectedAsync</c> faulted, and a negative count would idle-gate the
|
||||
/// publisher while viewers are still attached.
|
||||
/// </summary>
|
||||
/// <returns>The connection count after the decrement.</returns>
|
||||
public int Decrement()
|
||||
{
|
||||
int updated = Interlocked.Decrement(ref _count);
|
||||
if (updated >= 0)
|
||||
{
|
||||
return updated;
|
||||
}
|
||||
|
||||
Interlocked.CompareExchange(ref _count, 0, updated);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user