using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Opc.Ua;
using Opc.Ua.Server;
using SdkMonitoredItem = Opc.Ua.Server.MonitoredItem;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
///
/// Per-report state for delivering ONE condition event to many event items (Gitea #855 perf): the event's projected
/// fields computed once per distinct (EventFilter, preferred locales) instead of once per item, the SDK's
/// role-permission verdict once per (session, identity), and the set of items it was already queued to.
///
///
///
/// Why. Since #855 condition events are queued to folder items by the reporter under the node manager's
/// Lock. The SDK's MonitoredItem.QueueEvent(IFilterTarget) re-does, for every item, the where-clause
/// evaluation and the select-clause projection, and first scans the last ≤ 1000 queued events for a duplicate —
/// at a full queue that scan alone dominated (measured ~2.8 µs/item at QueueSize 10, ~10–16 µs at 1000, ~16–31 µs
/// at 10 000). The projection and verdicts are the same for every item that shares a filter and an identity.
///
///
/// What is reproduced, exactly. performs the SDK's QueueEvent(instance, false)
/// body (decompiled, SDK 2.0.1) under the item's own lock: event-item check; no queue handler (Disabled) ⇒ nothing;
/// duplicate ⇒ nothing; the filter in use (FilterToUse, the revised one); CanSendFilteredAlarm, which
/// without SupportsFilteredRetain on the condition is the where clause alone; SetQueueOverflowIfFull;
/// then GetEventFields (rejected select clauses ⇒ null, LocalizedText translated to the session's
/// preferred locales, Handle = the event, the item's ClientHandle) and the public
/// QueueEvent(EventFieldList). Each item gets its OWN (the SDK mutates a queued
/// list's ClientHandle on a modify); the field values are shared (immutable).
///
///
/// Duplicate check. The SDK compares the event INSTANCE against the last 1000 queued handles. A condition
/// snapshot reaches an item only through this per-report delivery, so "already queued in this report" is the same
/// question, asked of a set instead of the queue (it stays true after the event leaves the queue, where the SDK's
/// would not — a second delivery of one report to one item never happens on purpose).
///
///
/// Fallback. Anything outside that shape — the private members not found (an SDK upgrade; pinned by a guard
/// test), an item type that is not exactly the SDK's MonitoredItem, a filter in use that is not an
/// EventFilter, a condition with SupportsFilteredRetain, or an event that is not a condition snapshot —
/// goes through the SDK's own QueueEvent(IFilterTarget), unchanged.
///
///
internal sealed class EventDeliveryCache
{
private readonly object _sync = new();
private readonly HashSet _queued = new(System.Collections.Generic.ReferenceEqualityComparer.Instance);
private readonly Dictionary<(object? Session, object? Identity), bool> _roleVerdicts = new();
private readonly Dictionary<(FilterGroup Group, string Locales), Projection> _projections = new();
/// The SDK role-permission verdict for an item, computed once per (session, effective identity).
/// The event item.
/// The SDK check (ValidateEventRolePermissions) for that item.
/// when the item's identity may receive the event.
///
/// The SDK check builds an OperationContext from the item and asks ReceiveEvents on the event's type
/// and source for that context's identity and session — nothing else of the item takes part, so two items of one
/// session and identity always get the same answer for one event.
///
public bool RoleAllows(IEventMonitoredItem item, Func compute)
{
var key = ((object?)item.Session, (object?)item.EffectiveIdentity);
lock (_sync)
{
if (_roleVerdicts.TryGetValue(key, out var cached)) return cached;
}
var verdict = compute(item);
lock (_sync) _roleVerdicts[key] = verdict;
return verdict;
}
///
/// Queues the event to the item through the projected fast path when it has exactly the shape the SDK's
/// QueueEvent(IFilterTarget) would handle, else through that method itself.
///
/// The server (namespaces, type tree, resource manager).
/// The event item.
/// The condition event.
/// False routes it through the SDK's own QueueEvent (the node manager's test switch).
public void Queue(IServerInternal server, IEventMonitoredItem item, IFilterTarget e, bool projected)
{
if (!projected
|| e is not InstanceStateSnapshot { Handle: ConditionState condition }
|| condition.SupportsFilteredRetain?.Value == true
|| item.GetType() != typeof(SdkMonitoredItem)
|| !ProjectedEventDelivery.TryQueue(server, (SdkMonitoredItem)item, e, this))
{
item.QueueEvent(e);
}
}
/// Marks the item as queued for this report; false when it already was (the SDK's duplicate check).
internal bool TryMarkQueued(IEventMonitoredItem item)
{
lock (_sync) return _queued.Add(item);
}
/// The projection of the event for a filter group and locale list, computed on first use.
internal Projection ProjectionFor(FilterGroup group, string locales, Func compute)
{
lock (_sync)
{
if (_projections.TryGetValue((group, locales), out var cached)) return cached;
}
var projection = compute();
lock (_sync) _projections[(group, locales)] = projection;
return projection;
}
/// The event as one filter sees it: whether the where clause passes, and the selected field values.
/// The fields are shared by every item queued from this projection: is a read-only view, and the array behind it is never written after it is built.
internal sealed record Projection(bool Passes, ArrayOf Fields);
}
/// A set of value-equal s (one canonical representative), so they can share a projection.
internal sealed class FilterGroup(EventFilter representative)
{
public EventFilter Representative { get; } = representative;
}
/// The SDK internals the projected fast path reads, resolved once by reflection — see .
internal static class ProjectedEventDelivery
{
private static readonly Func? s_itemLock = FieldGetter("m_lock");
private static readonly Func? s_eventQueue = FieldGetter("m_eventQueueHandler");
private static readonly Func? s_filterToUse = PropertyGetter("FilterToUse");
private static long s_projectedQueued;
/// Events queued through the projected path since start (test observation: the fast path is really taken).
internal static long ProjectedQueued => Interlocked.Read(ref s_projectedQueued);
/// Whether every SDK member the fast path needs was found (pinned by a guard test).
internal static bool Available => s_itemLock is not null && s_eventQueue is not null && s_filterToUse is not null;
private static readonly ConditionalWeakTable s_groups = new();
private static readonly object s_registryLock = new();
private static readonly Dictionary> s_registry = new(StringComparer.Ordinal);
private static int s_registered;
/// Distinct filter shapes kept for sharing; past this a new filter gets a group of its own (no sharing).
internal const int MaxSharedFilterGroups = 4096;
///
/// The SDK's QueueEvent(instance, bypassFilter: false) for one condition snapshot, with the projection taken
/// from . Returns false (nothing done) when the item is not in a shape this reproduces.
///
internal static bool TryQueue(IServerInternal server, SdkMonitoredItem item, IFilterTarget e, EventDeliveryCache cache)
{
if (!Available || (item.MonitoredItemType & 2) == 0) return false;
// Everything below happens under the item's own lock, as in the SDK: a concurrent ModifyMonitoredItems (filter,
// client handle, queue) or SetMonitoringMode (queue handler) cannot interleave between the decision and the queueing.
var itemLock = s_itemLock!(item);
if (itemLock is null) return false;
using (itemLock.EnterScope())
{
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 projection = cache.ProjectionFor(GroupOf(filter), LocalesKey(locales), () => Project(server, filter, locales, e));
if (!projection.Passes || handler.SetQueueOverflowIfFull()) return true;
Interlocked.Increment(ref s_projectedQueued);
item.QueueEvent(new EventFieldList
{
ClientHandle = item.ClientHandle,
Handle = e,
EventFields = projection.Fields, // shared, read-only (see Projection)
});
return true;
}
}
/// The SDK's where-clause evaluation and GetEventFields projection, for one filter and locale list.
private static EventDeliveryCache.Projection Project(
IServerInternal server, EventFilter filter, string[]? locales, IFilterTarget e)
{
var context = new FilterContext(
server.NamespaceUris, server.TypeTree, locales!, server.Telemetry); // as the SDK: Session?.PreferredLocales, null included
var passes = filter.WhereClause.Evaluate(context, e);
var clauses = filter.SelectClauses;
var fields = new Variant[clauses.Count];
for (var i = 0; i < fields.Length; i++)
{
var clause = clauses[i];
if (clause is null || (!clause.Validated && ServiceResult.IsBad(clause.Validate(context, 0))))
{
fields[i] = Variant.Null; // a rejected select clause
continue;
}
var value = e.GetAttributeValue(context, clause.TypeDefinitionId, clause.BrowsePath, clause.AttributeId, clause.ParsedIndexRange);
if (value.IsNull)
{
fields[i] = Variant.Null;
continue;
}
if (value.TypeInfo.BuiltInType == BuiltInType.LocalizedText)
value = server.ResourceManager.TranslateValue(locales!, value);
fields[i] = value;
}
return new EventDeliveryCache.Projection(passes, (ArrayOf)fields);
}
/// The canonical group of a filter object: value-equal filters share one (decided once per filter object).
private static FilterGroup GroupOf(EventFilter filter) =>
s_groups.GetValue(filter, static f =>
{
var key = SelectKey(f);
lock (s_registryLock)
{
if (s_registry.TryGetValue(key, out var bucket))
{
foreach (var group in bucket)
{
if (Utils.IsEqual(group.Representative, f)) return group;
}
}
var created = new FilterGroup(f);
if (s_registered >= MaxSharedFilterGroups) return created;
s_registered++;
(bucket ??= s_registry[key] = []).Add(created);
return created;
}
});
/// A cheap bucket key over the select clauses (equality within a bucket is decided by Utils.IsEqual).
private static string SelectKey(EventFilter filter)
{
var parts = new List();
foreach (var clause in filter.SelectClauses)
{
if (clause is null)
{
parts.Add("-");
continue;
}
var path = new List();
foreach (var name in clause.BrowsePath) path.Add(name.ToString());
parts.Add(string.Join("/", path) + "@" + clause.AttributeId);
}
return string.Join("|", parts);
}
private static string LocalesKey(string[]? locales) =>
locales is null ? "\u0002" : string.Join("\u0001", locales);
private static Func? FieldGetter(string name) where T : class
{
var field = typeof(SdkMonitoredItem).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic);
if (field is null || !typeof(T).IsAssignableFrom(field.FieldType)) return null;
var item = Expression.Parameter(typeof(SdkMonitoredItem));
return Expression.Lambda>(Expression.Convert(Expression.Field(item, field), typeof(T)), item).Compile();
}
private static Func? PropertyGetter(string name) where T : class
{
var property = typeof(SdkMonitoredItem).GetProperty(name, BindingFlags.Instance | BindingFlags.NonPublic);
if (property?.GetMethod is null || !typeof(T).IsAssignableFrom(property.PropertyType)) return null;
return property.GetMethod.CreateDelegate>();
}
}