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:
@@ -16,6 +16,17 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
||||
{
|
||||
private const string HealthyStatus = "Healthy";
|
||||
|
||||
/// <summary>
|
||||
/// Minimum spacing between API key list reads. The list is a SQLite query whose
|
||||
/// content only changes when an operator creates, rotates, or revokes a key, so
|
||||
/// refreshing it on every ~1s snapshot tick buys nothing; the dashboard still sees
|
||||
/// a key change within this interval.
|
||||
/// </summary>
|
||||
private static readonly TimeSpan ApiKeySummaryRefreshInterval = TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <summary>Sentinel for "the API key summaries have never been refreshed".</summary>
|
||||
private const long NeverRefreshedTicks = long.MinValue;
|
||||
|
||||
private readonly ISessionRegistry _sessionRegistry;
|
||||
private readonly GatewayMetrics _metrics;
|
||||
private readonly IGatewayConfigurationProvider _configurationProvider;
|
||||
@@ -30,6 +41,13 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
||||
private readonly ILogger<DashboardSnapshotService> _logger;
|
||||
private readonly SemaphoreSlim _apiKeySummaryRefreshGate = new(1, 1);
|
||||
private IReadOnlyList<DashboardApiKeySummary> _apiKeySummaries = Array.Empty<DashboardApiKeySummary>();
|
||||
private long _apiKeySummariesRefreshedAtTicks = NeverRefreshedTicks;
|
||||
// The effective configuration is built from IOptions<GatewayOptions> and is startup-static:
|
||||
// the gateway binds options once at boot and never reloads them, so this projection cannot
|
||||
// change for the process lifetime. Build it once instead of re-projecting the whole option
|
||||
// tree on every snapshot tick. A racing first build is harmless — the projection is pure,
|
||||
// so either winner stores equivalent content.
|
||||
private EffectiveGatewayConfiguration? _effectiveConfiguration;
|
||||
// Memoizes ONLY the O(N) template/category breakdown against the cache sequence. The shared
|
||||
// library bumps Sequence only on a heavy refresh that replaces the object set, so an unchanged
|
||||
// sequence means the breakdown is unchanged and can be reused — keeping the ~1s snapshot tick
|
||||
@@ -100,10 +118,23 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
||||
Metrics: CreateMetricSummaries(metricsSnapshot),
|
||||
Faults: CreateFaultSummaries(sessions, generatedAt),
|
||||
ApiKeys: Volatile.Read(ref _apiKeySummaries),
|
||||
Configuration: _configurationProvider.GetEffectiveConfiguration(),
|
||||
Configuration: ResolveEffectiveConfiguration(),
|
||||
Galaxy: ResolveGalaxySummary());
|
||||
}
|
||||
|
||||
private EffectiveGatewayConfiguration ResolveEffectiveConfiguration()
|
||||
{
|
||||
EffectiveGatewayConfiguration? cached = Volatile.Read(ref _effectiveConfiguration);
|
||||
if (cached is not null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
EffectiveGatewayConfiguration configuration = _configurationProvider.GetEffectiveConfiguration();
|
||||
Volatile.Write(ref _effectiveConfiguration, configuration);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
private DashboardGalaxySummary ResolveGalaxySummary()
|
||||
{
|
||||
GalaxyHierarchyCacheEntry entry = _galaxyHierarchyCache.Current;
|
||||
@@ -255,6 +286,17 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
||||
|
||||
private async Task RefreshApiKeySummariesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
DateTimeOffset now = _timeProvider.GetUtcNow();
|
||||
long lastRefreshedAtTicks = Interlocked.Read(ref _apiKeySummariesRefreshedAtTicks);
|
||||
if (lastRefreshedAtTicks != NeverRefreshedTicks
|
||||
&& now.UtcTicks - lastRefreshedAtTicks < ApiKeySummaryRefreshInterval.Ticks)
|
||||
{
|
||||
// Inside the refresh window: reuse the cached summaries rather than
|
||||
// re-reading the API key table on this tick. Only a *successful* refresh
|
||||
// moves the timestamp, so a failed read is retried on the next tick.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await _apiKeySummaryRefreshGate.WaitAsync(0, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
@@ -278,6 +320,7 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
||||
.ToArray();
|
||||
|
||||
Volatile.Write(ref _apiKeySummaries, summaries);
|
||||
Interlocked.Exchange(ref _apiKeySummariesRefreshedAtTicks, now.UtcTicks);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user