fix(opcua): #855 perf review 2 — where clause in the group bucket key; pin dedupe, modify and keys
- Filter-group buckets include the where clause's (value-based) hash, so per-source filters sharing a select list are not all scanned under Lock. - Locale keys are cached per session array. - Tests: a notifier reached twice in one walk (Line above two wired equipment) gets each event once on both paths (fails without the per-report set); queued events re-projected by a modify are identical on both paths; locale keys unambiguous and where clauses hash by value. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
@@ -33,8 +33,11 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;
|
||||
/// <para>
|
||||
/// <b>Duplicate check.</b> 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).
|
||||
/// question, asked of a set instead of the queue. It matters: a walk can reach one notifier twice (a Line folder above
|
||||
/// two equipment folders the alarm is wired into), and the SDK's scan suppressed that second copy too. The set
|
||||
/// stays true after the event has left the queue, where the SDK's scan would not — a second visit within one walk
|
||||
/// happens long before anything is published; at a full DiscardOldest=false queue the SDK also counted that second
|
||||
/// visit as one more discarded event, this counts it once.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Fallback.</b> Anything outside that shape — the private members not found (an SDK upgrade; pinned by a guard
|
||||
@@ -275,7 +278,9 @@ internal static class ProjectedEventDelivery
|
||||
private static FilterGroup GroupOf(EventFilter filter, IServerInternal server) =>
|
||||
s_groups.GetValue(filter, f =>
|
||||
{
|
||||
var key = SelectKey(f);
|
||||
// The where clause takes part in the bucket (its hash is value-based), so filters that share a select list but
|
||||
// differ in their where clauses (per-source filters) do not all land in one bucket scanned under Lock.
|
||||
var key = SelectKey(f) + "#" + f.WhereClause?.GetHashCode().ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
lock (s_registryLock)
|
||||
{
|
||||
if (s_registry.TryGetValue(key, out var bucket))
|
||||
@@ -325,9 +330,14 @@ internal static class ProjectedEventDelivery
|
||||
}
|
||||
|
||||
/// <summary>An unambiguous key for a locale list (length-prefixed entries; null distinct from empty).</summary>
|
||||
private static string LocalesKey(string[]? locales)
|
||||
private static readonly ConditionalWeakTable<string[], string> s_localeKeys = new();
|
||||
|
||||
/// <summary>An unambiguous key for a locale list, cached per array (a session's array is fixed for its lifetime).</summary>
|
||||
internal static string LocalesKey(string[]? locales) =>
|
||||
locales is null ? "null" : s_localeKeys.GetValue(locales, static l => BuildLocalesKey(l));
|
||||
|
||||
private static string BuildLocalesKey(string[] locales)
|
||||
{
|
||||
if (locales is null) return "null";
|
||||
var key = new System.Text.StringBuilder().Append(locales.Length).Append(':');
|
||||
foreach (var locale in locales) key.Append(locale?.Length ?? -1).Append(':').Append(locale).Append(';');
|
||||
return key.ToString();
|
||||
|
||||
Reference in New Issue
Block a user