perf(mqtt): suppress unchanged-value republish fan-out

This commit is contained in:
Joseph Doherty
2026-08-15 21:42:00 -04:00
parent 8d91833fed
commit 2c708f5560
2 changed files with 126 additions and 1 deletions
@@ -1,3 +1,4 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Globalization;
@@ -748,8 +749,39 @@ public sealed class MqttSubscriptionManager
snapshot.StatusCode);
}
// Unchanged-value suppression. Every poll driver gets this dedupe for free at the edge from
// PollGroupEngine; MQTT is push-based, so a broker that timer-republishes an identical payload
// would otherwise re-trigger the ENTIRE downstream fan-out (actor hop → dependency mux → OPC UA
// sink write) per message. The cache is still freshened — a read must serve the current
// timestamp — and only the notification is skipped. "Cached but not raised" is already a valid
// state on this path (see Raise: an authored-but-unsubscribed tag does exactly that).
var prior = Values.Read(def.Name);
var unchanged = prior.StatusCode == snapshot.StatusCode && !ValuesAreDifferent(prior.Value, snapshot.Value);
Values.Update(def.Name, snapshot);
Raise(def.Name, snapshot);
if (!unchanged) Raise(def.Name, snapshot);
}
/// <summary>
/// Returns <c>true</c> when <paramref name="previous"/> and <paramref name="current"/> represent
/// different values. Mirrors <c>PollGroupEngine.ValuesAreDifferent</c> — deliberately duplicated
/// rather than shared, because that one is private to the poll-loop engine this push-based driver
/// does not run through.
/// </summary>
/// <remarks>
/// Arrays are compared structurally: a JSON array payload decodes to a fresh array instance on
/// every message, so reference equality would report every republish as a change and suppress
/// nothing at all for exactly the tags whose payloads are largest.
/// </remarks>
/// <param name="previous">The last cached value for the reference.</param>
/// <param name="current">The value just extracted from the inbound message.</param>
/// <returns><c>true</c> when the two differ.</returns>
private static bool ValuesAreDifferent(object? previous, object? current)
{
if (previous is Array prevArr && current is Array currArr)
return !StructuralComparisons.StructuralEqualityComparer.Equals(prevArr, currArr);
return !Equals(previous, current);
}
/// <summary>