perf(dcl): tag->instances reverse index replaces per-update all-instances scan (P3)

This commit is contained in:
Joseph Doherty
2026-07-09 00:48:22 -04:00
parent f294b46263
commit 1673268aee
2 changed files with 126 additions and 8 deletions
@@ -61,6 +61,18 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
/// </summary>
private readonly Dictionary<string, int> _tagSubscriberCount = new();
/// <summary>
/// Reverse index of which instances subscribe to each tag path: tagPath → set of
/// instanceUniqueName. Mirrors <see cref="_subscriptionsByInstance"/> inverted so the
/// <see cref="HandleTagValueReceived"/> hot path fans a value out to exactly the
/// interested instances in O(subscribers) instead of scanning every instance's tag set
/// on every tag update. Maintained via <see cref="IndexTag"/>/<see cref="UnindexTag"/>
/// at every <see cref="_subscriptionsByInstance"/> tag-set mutation; a tag key is dropped
/// when its instance set empties. Derived purely from <see cref="_subscriptionsByInstance"/>,
/// so it is preserved across reconnect exactly as that map is (see <see cref="ReSubscribeAll"/>).
/// </summary>
private readonly Dictionary<string, HashSet<string>> _instancesByTag = new();
/// <summary>
/// Tags whose path resolution failed and are awaiting retry.
/// </summary>
@@ -907,8 +919,11 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
// increments the reference count, so the count stays an accurate "number
// of distinct instances subscribed to this tag".
if (instanceTags.Add(result.TagPath))
{
_tagSubscriberCount[result.TagPath] =
_tagSubscriberCount.GetValueOrDefault(result.TagPath) + 1;
IndexTag(result.TagPath, instanceName);
}
// Re-check against current state: another subscribe may have resolved the
// same tag while this request's I/O was in flight.
@@ -1053,6 +1068,10 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
// Cleanup on Instance Actor stop
foreach (var tagPath in tags)
{
// Drop this instance from the fan-out reverse index (mirrors the
// _subscriptionsByInstance removal at the end of this method).
UnindexTag(tagPath, request.InstanceUniqueName);
// Drop this instance's reference; the tag is only
// released at the adapter when no other instance still subscribes to it.
// The reference count makes this O(1) instead of an O(instances) scan.
@@ -1585,6 +1604,10 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
_log.Info("[{0}] Re-subscribing {1} tags after reconnect", _connectionName, allTags.Count);
var self = Self;
// NOTE: do NOT clear _instancesByTag here. It is the inverse of
// _subscriptionsByInstance, which this method deliberately preserves as the durable
// source of truth across reconnect — clearing the index would silently stop the
// post-reconnect value fan-out for every already-subscribed tag.
_subscriptionIds.Clear();
_unresolvedTags.Clear();
_resolutionInFlight.Clear();
@@ -1710,6 +1733,31 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
}
/// <summary>
/// Adds <paramref name="instance"/> to the reverse index for <paramref name="tag"/>.
/// Call at every site that adds a tag to an instance's <see cref="_subscriptionsByInstance"/> set.
/// </summary>
private void IndexTag(string tag, string instance)
{
if (!_instancesByTag.TryGetValue(tag, out var insts))
{
insts = new HashSet<string>();
_instancesByTag[tag] = insts;
}
insts.Add(instance);
}
/// <summary>
/// Removes <paramref name="instance"/> from the reverse index for <paramref name="tag"/>,
/// dropping the tag key entirely when its instance set empties. Call at every site that
/// removes a tag from an instance's <see cref="_subscriptionsByInstance"/> set.
/// </summary>
private void UnindexTag(string tag, string instance)
{
if (_instancesByTag.TryGetValue(tag, out var insts) && insts.Remove(instance) && insts.Count == 0)
_instancesByTag.Remove(tag);
}
private void HandleTagValueReceived(TagValueReceived msg)
{
// Drop values delivered by a disposed adapter. After a
@@ -1722,16 +1770,17 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
return;
}
// Fan out to all subscribed instances
foreach (var (instanceName, tags) in _subscriptionsByInstance)
// Fan out to exactly the instances subscribed to this tag via the reverse index —
// O(subscribers of this tag) instead of scanning every instance's tag set per update.
if (_instancesByTag.TryGetValue(msg.TagPath, out var interestedInstances))
{
if (!tags.Contains(msg.TagPath))
continue;
if (_subscribers.TryGetValue(instanceName, out var subscriber))
foreach (var instanceName in interestedInstances)
{
subscriber.Tell(new TagValueUpdate(
_connectionName, msg.TagPath, msg.Value.Value, msg.Value.Quality, msg.Value.Timestamp));
if (_subscribers.TryGetValue(instanceName, out var subscriber))
{
subscriber.Tell(new TagValueUpdate(
_connectionName, msg.TagPath, msg.Value.Value, msg.Value.Quality, msg.Value.Timestamp));
}
}
}