perf(client): lazy child detection in BrowseAsync, NodeId-indexed UI notifications

R08 3.6: BrowseAsync no longer issues one HasChildren Browse per Object child; an
Object reports HasChildren (may have children) and is confirmed when expanded or
recursed. ISessionAdapter.HasChildrenAsync is removed. The UI subscriptions panel
routes each notification through a NodeId index kept in step with
ActiveSubscriptions (add/remove/replace/reset) instead of scanning every row.

Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 16:27:24 -04:00
parent a74a18e49a
commit 540b1632c5
8 changed files with 135 additions and 57 deletions
@@ -224,18 +224,15 @@ public sealed class OpcUaClientService : IOpcUaClientService
while (references.Count > 0)
{
// Child detection is lazy: an Object MAY have children and is confirmed when the caller expands or
// recurses into it. Probing each Object child with its own Browse made one level of N folders cost N + 1
// round trips, and a recursive subscribe over a 50k-node tree ~50k extra Browse calls.
foreach (var reference in references)
{
var childNodeId = ExpandedNodeId.ToNodeId(reference.NodeId, _session.NamespaceUris);
var hasChildren = reference.NodeClass == NodeClass.Object &&
await _session.HasChildrenAsync(childNodeId, ct);
results.Add(new BrowseResult(
reference.NodeId.ToString(),
reference.DisplayName?.Text ?? string.Empty,
reference.NodeClass.ToString(),
hasChildren));
}
reference.NodeClass == NodeClass.Object));
if (continuationPoint is not { Length: > 0 } pending) break;
(continuationPoint, references) = await _session.BrowseNextAsync(pending, ct);