perf(dashboard): memoize Galaxy summary by cache sequence; document scope-provider identity invariant

This commit is contained in:
Joseph Doherty
2026-06-25 11:48:27 -04:00
parent 8e196a7c83
commit 80bf4acc4f
2 changed files with 35 additions and 1 deletions
@@ -30,6 +30,12 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
private readonly ILogger<DashboardSnapshotService> _logger;
private readonly SemaphoreSlim _apiKeySummaryRefreshGate = new(1, 1);
private IReadOnlyList<DashboardApiKeySummary> _apiKeySummaries = Array.Empty<DashboardApiKeySummary>();
// Memoizes the projected Galaxy summary against the immutable cache sequence. The shared
// library bumps Sequence only on a heavy refresh, so an unchanged sequence means the entry
// (and therefore its summary) is unchanged and the O(N) projection can be reused. This keeps
// the ~1s snapshot tick O(1) for Galaxy, restoring the pre-adoption behavior where the
// summary was computed once per refresh rather than once per tick.
private GalaxySummaryCache? _galaxySummaryCache;
/// <summary>Initializes a new instance of the DashboardSnapshotService class.</summary>
/// <param name="sessionRegistry">Registry of active gateway sessions.</param>
@@ -97,9 +103,30 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
Faults: CreateFaultSummaries(sessions, generatedAt),
ApiKeys: Volatile.Read(ref _apiKeySummaries),
Configuration: _configurationProvider.GetEffectiveConfiguration(),
Galaxy: DashboardGalaxyProjector.Project(_galaxyHierarchyCache.Current));
Galaxy: ResolveGalaxySummary());
}
private DashboardGalaxySummary ResolveGalaxySummary()
{
GalaxyHierarchyCacheEntry entry = _galaxyHierarchyCache.Current;
long sequence = entry.Sequence;
// Lock-free reuse: a matching sequence means the entry is unchanged, so the previously
// projected summary is still correct. A racing recompute for a new sequence is harmless —
// the projection is pure, so any winner stores identical content for that sequence.
GalaxySummaryCache? cached = Volatile.Read(ref _galaxySummaryCache);
if (cached is not null && cached.Sequence == sequence)
{
return cached.Summary;
}
DashboardGalaxySummary summary = DashboardGalaxyProjector.Project(entry);
Volatile.Write(ref _galaxySummaryCache, new GalaxySummaryCache(sequence, summary));
return summary;
}
private sealed record GalaxySummaryCache(long Sequence, DashboardGalaxySummary Summary);
/// <summary>
/// Watches dashboard snapshots at regular intervals asynchronously.
/// </summary>
@@ -11,6 +11,13 @@ public sealed class GatewayBrowseScopeProvider(IGatewayRequestIdentityAccessor i
/// <inheritdoc />
public IReadOnlyList<string>? ResolveBrowseSubtrees(ServerCallContext context)
{
// Invariant: the caller identity is the ambient one pushed by
// GatewayGrpcAuthorizationInterceptor earlier in this same call. That interceptor is
// registered globally and therefore runs before the library's galaxy service, so
// Current is populated for any authenticated galaxy RPC. If it is somehow absent we
// return empty BrowseSubtrees, which the library treats as "no scoping" (full
// hierarchy) — safe only because the global interceptor has already authenticated and
// authorized the request first. Do not reorder the interceptor without revisiting this.
ApiKeyConstraints constraints = identityAccessor.Current?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
return constraints.BrowseSubtrees;
}