fix(opcua): #855 perf review 3 — weak filter-group registry; where clause validated; sharing pinned

- The filter-group registry holds groups weakly (the filter->group table is
  the only strong reference), sweeps dead entries amortised and counts only
  live shapes against the 4096 cap; filters past the cap are counted on the
  new meter otopcua.opcua.condition_events.filter_groups_unshared.
- A group validates its where clause too (restored, never-validated filters
  evaluate like validated ones).
- Test: value-equal filters cost one projection per locale set per report.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
Joseph Doherty
2026-11-11 14:20:19 -05:00
parent b77bb2ded1
commit 733dfe94da
4 changed files with 93 additions and 8 deletions
@@ -295,6 +295,39 @@ public sealed class ProjectedEventDeliveryWireTests
Narrow(800).WhereClause.GetHashCode().ShouldNotBe(Narrow(900).WhereClause.GetHashCode());
}
/// <summary>
/// Sharing really happens (not only equivalence): three items with value-equal filters on one session and one on a
/// session with other locales cost TWO projections per report (one per locale set), not four.
/// </summary>
[Fact]
public async Task Value_equal_filters_share_one_projection_per_locale_set()
{
var ct = TestContext.Current.CancellationToken;
await using var rig = await ServerEventRig.StartAsync("Projected855Share", encrypted: false, ct);
rig.KeyRoles = [];
rig.MaterialiseNative();
rig.Enforce(false, [ServerEventRig.Key()], KeyEquipmentGrant(EqA));
using var session = await rig.ConnectAsync(anonymous: false, ct);
var events = new RecordedEvents();
var subscription = await RecordedEvents.SubscribeAsync(session, events, ct);
var eqA = ServerEventRig.UnsNodeId(session, EqA);
var items = Enumerable.Range(0, 3).Select(_ => RecordedEvents.AddEventItem(subscription, eqA)).ToList();
await subscription.ApplyChangesAsync(ct);
using var german = await rig.ConnectAsync(anonymous: false, ct, preferredLocales: ["de-DE"]);
var germanEvents = new RecordedEvents();
var germanSubscription = await RecordedEvents.SubscribeAsync(german, germanEvents, ct);
var g1 = RecordedEvents.AddEventItem(germanSubscription, ServerEventRig.UnsNodeId(german, EqA));
await germanSubscription.ApplyChangesAsync(ct);
var before = rig.Nm.ProjectionsCreated;
const int Reports = 5;
for (var i = 0; i < Reports; i++) rig.Project(native: true, State($"x{i}"));
(rig.Nm.ProjectionsCreated - before).ShouldBe(2 * Reports, "one projection per (filter group, locale set) per report");
await RecordedEvents.WaitUntilAsync(() => items.All(m => events.Has(m, $"x{Reports - 1}")) && germanEvents.Has(g1, $"x{Reports - 1}"));
}
private static AlarmConditionSnapshot State(string message) =>
new(true, false, false, true, AlarmShelvingKind.Unshelved, 700, message);