From 80bf4acc4f6127615fba56efadb811da551477c3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 25 Jun 2026 11:48:27 -0400 Subject: [PATCH] perf(dashboard): memoize Galaxy summary by cache sequence; document scope-provider identity invariant --- .../Dashboard/DashboardSnapshotService.cs | 29 ++++++++++++++++++- .../GatewayBrowseScopeProvider.cs | 7 +++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSnapshotService.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSnapshotService.cs index ad5a482..0489bfd 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSnapshotService.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardSnapshotService.cs @@ -30,6 +30,12 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService private readonly ILogger _logger; private readonly SemaphoreSlim _apiKeySummaryRefreshGate = new(1, 1); private IReadOnlyList _apiKeySummaries = Array.Empty(); + // 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; /// Initializes a new instance of the DashboardSnapshotService class. /// Registry of active gateway sessions. @@ -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); + /// /// Watches dashboard snapshots at regular intervals asynchronously. /// diff --git a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs index e651cfd..88cec3e 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Security/Authorization/GatewayBrowseScopeProvider.cs @@ -11,6 +11,13 @@ public sealed class GatewayBrowseScopeProvider(IGatewayRequestIdentityAccessor i /// public IReadOnlyList? 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; }