From 74b2ce170b5962ec68657cb7ee778066e7599b47 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 11 Nov 2026 14:31:46 -0500 Subject: [PATCH] =?UTF-8?q?fix(opcua):=20#855=20perf=20review=204=20?= =?UTF-8?q?=E2=80=94=20registry=20sweep=20threshold=20capped;=20re-sweep?= =?UTF-8?q?=20at=20the=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The sweep threshold never rises above the 4096 cap (a threshold the count could not reach stopped sweeping, and sharing, for good); at the cap a sweep for dead shapes runs after every 256 refusals. - The per-report "queued" mark is taken after the filter group resolves, so a failure there leaves the item unmarked. - The bucket key also carries TypeDefinitionId and IndexRange. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk --- .../ProjectedEventDelivery.cs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ProjectedEventDelivery.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ProjectedEventDelivery.cs index 3f6733f74..0f1aa859d 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ProjectedEventDelivery.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ProjectedEventDelivery.cs @@ -199,6 +199,10 @@ internal static class ProjectedEventDelivery private static readonly Dictionary>> s_registry = new(StringComparer.Ordinal); private static int s_registered; private static int s_sweepAt = 1024; + private static int s_unsharedSinceSweep; + + /// At the cap, refusals between two sweeps for dead shapes. + private const int UnsharedResweepInterval = 256; /// Live distinct filter shapes kept for sharing; past this a new filter gets a group of its own (no sharing). internal const int MaxSharedFilterGroups = 4096; @@ -225,10 +229,9 @@ internal static class ProjectedEventDelivery var handler = s_eventQueue!(item); if (s_filterToUse!(item) is not EventFilter filter) return false; if (handler is null) return true; // Disabled: the SDK queues nothing (no handler) - if (!cache.TryMarkQueued(item)) return true; // the SDK's duplicate check (see EventDeliveryCache) - var locales = item.Session?.PreferredLocales; // string[]?, exactly what the SDK hands its FilterContext and TranslateValue var group = GroupOf(filter, server); + if (!cache.TryMarkQueued(item)) return true; // the SDK's duplicate check (see EventDeliveryCache) var projection = cache.ProjectionFor(group, LocalesKey(locales), () => new EventDeliveryCache.Projection( () => Passes(server, group, locales, e), () => Project(server, group, locales, e))); if (!projection.Passes || handler.SetQueueOverflowIfFull()) return true; @@ -313,9 +316,21 @@ internal static class ProjectedEventDelivery var created = new FilterGroup(f, server); if (s_registered >= MaxSharedFilterGroups) { - OtOpcUaTelemetry.ConditionEventFilterGroupsUnshared.Add(1); - Interlocked.Increment(ref s_unsharedGroups); - return created; + // Full: re-check for dead shapes after every UnsharedResweepInterval refusals, so a registry that filled + // once does not refuse sharing for good, and one that is genuinely full of live shapes is not swept on + // every new filter. + if (++s_unsharedSinceSweep >= UnsharedResweepInterval) + { + s_unsharedSinceSweep = 0; + SweepRegistry(); + } + + if (s_registered >= MaxSharedFilterGroups) + { + OtOpcUaTelemetry.ConditionEventFilterGroupsUnshared.Add(1); + Interlocked.Increment(ref s_unsharedGroups); + return created; + } } s_registered++; @@ -337,8 +352,9 @@ internal static class ProjectedEventDelivery } s_registered = live; - // Amortised: the next sweep only once as many shapes again have been registered (never more often than every 1024). - s_sweepAt = Math.Max(1024, live * 2); + // Amortised: the next sweep only once as many shapes again have been registered (never more often than every 1024), + // and never above the cap — a threshold the count can no longer reach would stop sweeping, and sharing, for good. + s_sweepAt = Math.Min(MaxSharedFilterGroups, Math.Max(1024, live * 2)); } private static long s_unsharedGroups; @@ -361,7 +377,7 @@ internal static class ProjectedEventDelivery var path = new List(); foreach (var name in clause.BrowsePath) path.Add(name.ToString()); - parts.Add(string.Join("/", path) + "@" + clause.AttributeId); + parts.Add(clause.TypeDefinitionId + ":" + string.Join("/", path) + "@" + clause.AttributeId + "[" + clause.IndexRange + "]"); } return string.Join("|", parts);