diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs index 6349a953..756d2977 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs @@ -20,7 +20,9 @@ internal sealed class GalaxyBrowseSession : IBrowseSession private readonly GalaxyRepositoryClient _client; private readonly ConcurrentDictionary _byTagName = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _byGobjectId = new(); - private bool _disposed; + private volatile bool _disposed; + private readonly SemaphoreSlim _rootGate = new(1, 1); + private HashSet _hasChildrenSet = new(); /// Opaque token identifying this session in the AdminUI registry. public Guid Token { get; } = Guid.NewGuid(); @@ -48,25 +50,37 @@ internal sealed class GalaxyBrowseSession : IBrowseSession public async Task> RootAsync(CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); - var all = await _client.DiscoverHierarchyAsync( - new DiscoverHierarchyOptions { IncludeAttributes = false }, cancellationToken) - .ConfigureAwait(false); - - // Populate caches so later ExpandAsync calls can resolve children in-memory. - foreach (var obj in all) + await _rootGate.WaitAsync(cancellationToken).ConfigureAwait(false); + try { - _byTagName[obj.TagName] = obj; - _byGobjectId[obj.GobjectId] = obj; + var all = await _client.DiscoverHierarchyAsync( + new DiscoverHierarchyOptions { IncludeAttributes = false }, cancellationToken) + .ConfigureAwait(false); + + // Populate caches so later ExpandAsync calls can resolve children in-memory. + foreach (var obj in all) + { + _byTagName[obj.TagName] = obj; + _byGobjectId[obj.GobjectId] = obj; + } + + // Precompute the set of GobjectIds that appear as a parent — used by + // Project to compute HasChildrenHint in O(1) instead of O(n²). + _hasChildrenSet = new HashSet(_byGobjectId.Values.Select(o => o.ParentGobjectId)); + + // Roots are objects whose parent isn't part of the returned set (typically + // ParentGobjectId == 0 for WinPlatforms / top-level Areas). + var roots = all + .Where(o => o.ParentGobjectId == 0 || !_byGobjectId.ContainsKey(o.ParentGobjectId)) + .ToList(); + + LastUsedUtc = DateTime.UtcNow; + return Project(roots); + } + finally + { + _rootGate.Release(); } - - // Roots are objects whose parent isn't part of the returned set (typically - // ParentGobjectId == 0 for WinPlatforms / top-level Areas). - var roots = all - .Where(o => o.ParentGobjectId == 0 || !_byGobjectId.ContainsKey(o.ParentGobjectId)) - .ToList(); - - LastUsedUtc = DateTime.UtcNow; - return Project(roots); } /// @@ -146,7 +160,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession _byGobjectId[obj.GobjectId] = obj; var displayName = !string.IsNullOrEmpty(obj.ContainedName) ? obj.ContainedName : obj.TagName; - var hasChildrenHint = _byGobjectId.Values.Any(o => o.ParentGobjectId == obj.GobjectId); + var hasChildrenHint = _hasChildrenSet.Contains(obj.GobjectId); result.Add(new BrowseNode( NodeId: obj.TagName, DisplayName: displayName, @@ -179,6 +193,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession { if (_disposed) return; _disposed = true; + _rootGate.Dispose(); try { await _session.DisposeAsync().ConfigureAwait(false);