perf(acl): O(n) browse/translate filtering compaction

This commit is contained in:
Joseph Doherty
2026-08-16 04:03:32 -04:00
parent 4b6a93bb78
commit 541a223b96
2 changed files with 172 additions and 11 deletions
@@ -2943,22 +2943,36 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
base.Browse(context, ref continuationPoint, references);
var returned = references.Count - before;
// Forward two-pointer compaction over the appended range, NOT a per-denial RemoveAt. Removing
// from the middle shifts every later element down one slot, so denying a large prefix of a wide
// browse costs O(kept × removed) element moves; copying each survivor down to a write cursor and
// trimming the tail afterwards is one pass. The tail trim removes from the END, where RemoveAt
// moves nothing.
var filtered = 0;
for (var ii = references.Count - 1; ii >= before; ii--)
var write = before;
for (var ii = before; ii < references.Count; ii++)
{
// ExpandedNodeId → NodeId only makes sense for a reference into our own server; a reference
// to another server's node is left alone, as this manager has no grants that could speak to it.
var target = references[ii].NodeId;
if (target is null || target.IsAbsolute) continue;
var keep = target is null
|| target.IsAbsolute
|| ExpandedNodeId.ToNodeId(target, Server.NamespaceUris) is not { } targetId
|| !OwnsNamespace(targetId)
|| Permits(identity, OpcUaOperation.Browse, targetId);
var targetId = ExpandedNodeId.ToNodeId(target, Server.NamespaceUris);
if (targetId is null || !OwnsNamespace(targetId)) continue;
if (Permits(identity, OpcUaOperation.Browse, targetId)) continue;
if (!keep)
{
filtered++;
continue;
}
references.RemoveAt(ii);
filtered++;
if (write != ii) references[write] = references[ii];
write++;
}
for (var ii = references.Count - 1; ii >= write; ii--) references.RemoveAt(ii);
LogAclFiltering("Browse", identity, filtered, returned);
}
@@ -2995,14 +3009,24 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
var before = targetIds.Count;
base.TranslateBrowsePath(context, sourceHandle, relativePath, targetIds, unresolvedTargetIds);
for (var ii = targetIds.Count - 1; ii >= before; ii--)
// Forward two-pointer compaction over the appended range — same reasoning as Browse above: a
// mid-list RemoveAt per denial is O(kept × removed) element moves, one compacting pass plus a
// tail trim is O(n).
var write = before;
for (var ii = before; ii < targetIds.Count; ii++)
{
var targetId = ExpandedNodeId.ToNodeId(targetIds[ii], Server.NamespaceUris);
if (targetId is null || !OwnsNamespace(targetId)) continue;
if (Permits(identity, OpcUaOperation.Browse, targetId)) continue;
var keep = targetId is null
|| !OwnsNamespace(targetId)
|| Permits(identity, OpcUaOperation.Browse, targetId);
targetIds.RemoveAt(ii);
if (!keep) continue;
if (write != ii) targetIds[write] = targetIds[ii];
write++;
}
for (var ii = targetIds.Count - 1; ii >= write; ii--) targetIds.RemoveAt(ii);
}
/// <inheritdoc />