perf(worker): reverse tag index + memoized views + indexed removals in the handle registry
This commit is contained in:
@@ -4,27 +4,52 @@ using System.Linq;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the server, item, and advice handles owned by one MXAccess session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The registry is STA-confined by contract: every mutation and every query
|
||||
/// runs on the session's dedicated STA thread (see
|
||||
/// <see cref="MxAccessStaSession"/>, which marshals all registry access
|
||||
/// through that thread), so there is no locking here and the memoized views
|
||||
/// and secondary indexes below need none either.
|
||||
/// </remarks>
|
||||
public sealed class MxAccessHandleRegistry
|
||||
{
|
||||
private readonly Dictionary<int, RegisteredServerHandle> serverHandles = new();
|
||||
private readonly Dictionary<long, RegisteredItemHandle> itemHandles = new();
|
||||
private readonly Dictionary<AdviceHandleKey, RegisteredAdviceHandle> adviceHandles = new();
|
||||
|
||||
// Secondary indexes. Every register/remove method below maintains these so
|
||||
// lookups by tag and removals stay proportional to what is actually removed
|
||||
// instead of scanning a whole primary table.
|
||||
private readonly Dictionary<ItemDefinitionKey, List<int>> itemHandlesByDefinition = new();
|
||||
private readonly Dictionary<int, HashSet<long>> itemKeysByServer = new();
|
||||
private readonly Dictionary<long, List<AdviceHandleKey>> adviceKeysByItem = new();
|
||||
private readonly Dictionary<int, HashSet<long>> advisedItemKeysByServer = new();
|
||||
|
||||
// Memoized materializations of the three sorted views. Each is dropped by
|
||||
// the mutation that invalidates it, so repeated reads between mutations
|
||||
// hand back the same array instead of re-sorting and re-copying the table.
|
||||
private RegisteredServerHandle[]? serverHandlesView;
|
||||
private RegisteredItemHandle[]? itemHandlesView;
|
||||
private RegisteredAdviceHandle[]? adviceHandlesView;
|
||||
|
||||
/// <summary>Gets a read-only list of registered server handles ordered by handle value.</summary>
|
||||
public IReadOnlyList<RegisteredServerHandle> ServerHandles => serverHandles
|
||||
public IReadOnlyList<RegisteredServerHandle> ServerHandles => serverHandlesView ??= serverHandles
|
||||
.Values
|
||||
.OrderBy(handle => handle.ServerHandle)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>Gets a read-only list of registered item handles ordered by server handle then item handle.</summary>
|
||||
public IReadOnlyList<RegisteredItemHandle> ItemHandles => itemHandles
|
||||
public IReadOnlyList<RegisteredItemHandle> ItemHandles => itemHandlesView ??= itemHandles
|
||||
.Values
|
||||
.OrderBy(handle => handle.ServerHandle)
|
||||
.ThenBy(handle => handle.ItemHandle)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>Gets a read-only list of registered advice handles ordered by server handle, item handle, and advice kind.</summary>
|
||||
public IReadOnlyList<RegisteredAdviceHandle> AdviceHandles => adviceHandles
|
||||
public IReadOnlyList<RegisteredAdviceHandle> AdviceHandles => adviceHandlesView ??= adviceHandles
|
||||
.Values
|
||||
.OrderBy(handle => handle.ServerHandle)
|
||||
.ThenBy(handle => handle.ItemHandle)
|
||||
@@ -39,28 +64,38 @@ public sealed class MxAccessHandleRegistry
|
||||
string clientName)
|
||||
{
|
||||
serverHandles[serverHandle] = new RegisteredServerHandle(serverHandle, clientName);
|
||||
serverHandlesView = null;
|
||||
}
|
||||
|
||||
/// <summary>Unregisters a server handle and all associated item and advice handles from the registry.</summary>
|
||||
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||
public void UnregisterServerHandle(int serverHandle)
|
||||
{
|
||||
serverHandles.Remove(serverHandle);
|
||||
|
||||
foreach (long key in itemHandles
|
||||
.Where(pair => pair.Value.ServerHandle == serverHandle)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray())
|
||||
if (serverHandles.Remove(serverHandle))
|
||||
{
|
||||
itemHandles.Remove(key);
|
||||
serverHandlesView = null;
|
||||
}
|
||||
|
||||
foreach (AdviceHandleKey key in adviceHandles
|
||||
.Where(pair => pair.Value.ServerHandle == serverHandle)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray())
|
||||
// Per-server indexes: the removal cost is proportional to the handles
|
||||
// this server actually owns, not to the size of the whole table.
|
||||
if (itemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? ownedItemKeys))
|
||||
{
|
||||
adviceHandles.Remove(key);
|
||||
itemKeysByServer.Remove(serverHandle);
|
||||
|
||||
foreach (long itemKey in ownedItemKeys)
|
||||
{
|
||||
RemoveItemEntry(itemKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (advisedItemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? advisedItemKeys))
|
||||
{
|
||||
advisedItemKeysByServer.Remove(serverHandle);
|
||||
|
||||
foreach (long itemKey in advisedItemKeys)
|
||||
{
|
||||
RemoveAdviceEntries(itemKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +120,32 @@ public sealed class MxAccessHandleRegistry
|
||||
string itemContext,
|
||||
bool hasItemContext)
|
||||
{
|
||||
itemHandles[CreateItemKey(serverHandle, itemHandle)] = new RegisteredItemHandle(
|
||||
long itemKey = CreateItemKey(serverHandle, itemHandle);
|
||||
|
||||
if (itemHandles.TryGetValue(itemKey, out RegisteredItemHandle? existing))
|
||||
{
|
||||
// Re-registering the same handle can carry a different item
|
||||
// definition; drop the stale reverse-index entry first.
|
||||
RemoveDefinitionIndexEntry(existing);
|
||||
}
|
||||
|
||||
itemHandles[itemKey] = new RegisteredItemHandle(
|
||||
serverHandle,
|
||||
itemHandle,
|
||||
itemDefinition,
|
||||
itemContext,
|
||||
hasItemContext);
|
||||
|
||||
AddDefinitionIndexEntry(serverHandle, itemHandle, itemDefinition);
|
||||
|
||||
if (!itemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? ownedItemKeys))
|
||||
{
|
||||
ownedItemKeys = new HashSet<long>();
|
||||
itemKeysByServer[serverHandle] = ownedItemKeys;
|
||||
}
|
||||
|
||||
ownedItemKeys.Add(itemKey);
|
||||
itemHandlesView = null;
|
||||
}
|
||||
|
||||
/// <summary>Removes an item handle and all associated advice handles from the registry.</summary>
|
||||
@@ -100,7 +155,19 @@ public sealed class MxAccessHandleRegistry
|
||||
int serverHandle,
|
||||
int itemHandle)
|
||||
{
|
||||
itemHandles.Remove(CreateItemKey(serverHandle, itemHandle));
|
||||
long itemKey = CreateItemKey(serverHandle, itemHandle);
|
||||
|
||||
if (RemoveItemEntry(itemKey)
|
||||
&& itemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? ownedItemKeys))
|
||||
{
|
||||
ownedItemKeys.Remove(itemKey);
|
||||
|
||||
if (ownedItemKeys.Count == 0)
|
||||
{
|
||||
itemKeysByServer.Remove(serverHandle);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveAdviceHandles(serverHandle, itemHandle);
|
||||
}
|
||||
|
||||
@@ -115,6 +182,34 @@ public sealed class MxAccessHandleRegistry
|
||||
return itemHandles.ContainsKey(CreateItemKey(serverHandle, itemHandle));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item handles registered under the specified server handle for
|
||||
/// the specified item definition, ordered by item handle.
|
||||
/// </summary>
|
||||
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||
/// <param name="itemDefinition">Item definition name, compared ordinally.</param>
|
||||
/// <returns>
|
||||
/// The matching item handles in ascending order — the same order a scan of
|
||||
/// <see cref="ItemHandles"/> would visit them — or an empty list when the
|
||||
/// definition is not registered for that server. MXAccess allows the same
|
||||
/// tag to be added more than once under one server handle, so this is a
|
||||
/// list rather than a single handle. The returned list is a live view of
|
||||
/// the index and is only valid until the next registry mutation.
|
||||
/// </returns>
|
||||
public IReadOnlyList<int> GetItemHandlesForDefinition(
|
||||
int serverHandle,
|
||||
string itemDefinition)
|
||||
{
|
||||
if (itemHandlesByDefinition.TryGetValue(
|
||||
new ItemDefinitionKey(serverHandle, itemDefinition),
|
||||
out List<int>? handles))
|
||||
{
|
||||
return handles;
|
||||
}
|
||||
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
|
||||
/// <summary>Registers an advice handle with the registry.</summary>
|
||||
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||
/// <param name="itemHandle">Handle returned by the worker.</param>
|
||||
@@ -125,10 +220,36 @@ public sealed class MxAccessHandleRegistry
|
||||
MxAccessAdviceKind adviceKind)
|
||||
{
|
||||
AdviceHandleKey key = new(serverHandle, itemHandle, adviceKind);
|
||||
|
||||
if (!adviceHandles.ContainsKey(key))
|
||||
{
|
||||
// Only index a key the primary table does not already hold, so the
|
||||
// per-item list never accumulates duplicates.
|
||||
long itemKey = CreateItemKey(serverHandle, itemHandle);
|
||||
|
||||
if (!adviceKeysByItem.TryGetValue(itemKey, out List<AdviceHandleKey>? itemAdviceKeys))
|
||||
{
|
||||
itemAdviceKeys = new List<AdviceHandleKey>(2);
|
||||
adviceKeysByItem[itemKey] = itemAdviceKeys;
|
||||
}
|
||||
|
||||
itemAdviceKeys.Add(key);
|
||||
|
||||
if (!advisedItemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? advisedItemKeys))
|
||||
{
|
||||
advisedItemKeys = new HashSet<long>();
|
||||
advisedItemKeysByServer[serverHandle] = advisedItemKeys;
|
||||
}
|
||||
|
||||
advisedItemKeys.Add(itemKey);
|
||||
}
|
||||
|
||||
adviceHandles[key] = new RegisteredAdviceHandle(
|
||||
serverHandle,
|
||||
itemHandle,
|
||||
adviceKind);
|
||||
|
||||
adviceHandlesView = null;
|
||||
}
|
||||
|
||||
/// <summary>Removes all advice handles for the specified server and item handles from the registry.</summary>
|
||||
@@ -138,12 +259,17 @@ public sealed class MxAccessHandleRegistry
|
||||
int serverHandle,
|
||||
int itemHandle)
|
||||
{
|
||||
foreach (AdviceHandleKey key in adviceHandles
|
||||
.Where(pair => pair.Value.ServerHandle == serverHandle && pair.Value.ItemHandle == itemHandle)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray())
|
||||
long itemKey = CreateItemKey(serverHandle, itemHandle);
|
||||
RemoveAdviceEntries(itemKey);
|
||||
|
||||
if (advisedItemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? advisedItemKeys))
|
||||
{
|
||||
adviceHandles.Remove(key);
|
||||
advisedItemKeys.Remove(itemKey);
|
||||
|
||||
if (advisedItemKeys.Count == 0)
|
||||
{
|
||||
advisedItemKeysByServer.Remove(serverHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +293,81 @@ public sealed class MxAccessHandleRegistry
|
||||
return ((long)serverHandle << 32) | (uint)itemHandle;
|
||||
}
|
||||
|
||||
/// <summary>Removes one item from the primary table and the definition index.</summary>
|
||||
/// <param name="itemKey">Packed server/item key produced by <see cref="CreateItemKey"/>.</param>
|
||||
/// <returns><see langword="true"/> if an item was removed; otherwise, <see langword="false"/>.</returns>
|
||||
private bool RemoveItemEntry(long itemKey)
|
||||
{
|
||||
if (!itemHandles.TryGetValue(itemKey, out RegisteredItemHandle? registered))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
itemHandles.Remove(itemKey);
|
||||
RemoveDefinitionIndexEntry(registered);
|
||||
itemHandlesView = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Removes every advice handle recorded for one item from the primary table and the per-item index.</summary>
|
||||
/// <param name="itemKey">Packed server/item key produced by <see cref="CreateItemKey"/>.</param>
|
||||
private void RemoveAdviceEntries(long itemKey)
|
||||
{
|
||||
if (!adviceKeysByItem.TryGetValue(itemKey, out List<AdviceHandleKey>? itemAdviceKeys))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
adviceKeysByItem.Remove(itemKey);
|
||||
|
||||
foreach (AdviceHandleKey key in itemAdviceKeys)
|
||||
{
|
||||
adviceHandles.Remove(key);
|
||||
}
|
||||
|
||||
adviceHandlesView = null;
|
||||
}
|
||||
|
||||
private void AddDefinitionIndexEntry(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
string itemDefinition)
|
||||
{
|
||||
ItemDefinitionKey definitionKey = new(serverHandle, itemDefinition);
|
||||
|
||||
if (!itemHandlesByDefinition.TryGetValue(definitionKey, out List<int>? handles))
|
||||
{
|
||||
handles = new List<int>(1);
|
||||
itemHandlesByDefinition[definitionKey] = handles;
|
||||
}
|
||||
|
||||
// Kept sorted so callers see the same order a scan of ItemHandles would.
|
||||
int index = handles.BinarySearch(itemHandle);
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
handles.Insert(~index, itemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDefinitionIndexEntry(RegisteredItemHandle registered)
|
||||
{
|
||||
ItemDefinitionKey definitionKey = new(registered.ServerHandle, registered.ItemDefinition);
|
||||
|
||||
if (!itemHandlesByDefinition.TryGetValue(definitionKey, out List<int>? handles))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
handles.Remove(registered.ItemHandle);
|
||||
|
||||
if (handles.Count == 0)
|
||||
{
|
||||
itemHandlesByDefinition.Remove(definitionKey);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct AdviceHandleKey : IEquatable<AdviceHandleKey>
|
||||
{
|
||||
private readonly int serverHandle;
|
||||
@@ -214,4 +415,50 @@ public sealed class MxAccessHandleRegistry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverse-index key: a server handle plus an item definition, compared
|
||||
/// ordinally to match the tag comparison the read path performs.
|
||||
/// </summary>
|
||||
private readonly struct ItemDefinitionKey : IEquatable<ItemDefinitionKey>
|
||||
{
|
||||
private readonly int serverHandle;
|
||||
private readonly string itemDefinition;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ItemDefinitionKey"/> struct.</summary>
|
||||
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||
/// <param name="itemDefinition">Item definition name from MXAccess.</param>
|
||||
public ItemDefinitionKey(
|
||||
int serverHandle,
|
||||
string itemDefinition)
|
||||
{
|
||||
this.serverHandle = serverHandle;
|
||||
this.itemDefinition = itemDefinition;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(ItemDefinitionKey other)
|
||||
{
|
||||
return serverHandle == other.serverHandle
|
||||
&& string.Equals(itemDefinition, other.itemDefinition, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ItemDefinitionKey other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hashCode = serverHandle;
|
||||
hashCode = (hashCode * 397) ^ (itemDefinition ?? string.Empty).GetHashCode();
|
||||
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -990,23 +990,20 @@ public sealed class MxAccessSession : IDisposable
|
||||
out int itemHandle,
|
||||
out MxAccessValueCache.CachedValue cachedValue)
|
||||
{
|
||||
// Linear scan — bulk-read sizes are small in practice and the registry
|
||||
// is keyed by handle, not by tag. If profiling ever shows this hot, a
|
||||
// reverse tag→handle map can be added on the registry side.
|
||||
foreach (RegisteredItemHandle registered in handleRegistry.ItemHandles)
|
||||
// Dictionary probe, not a scan: the registry keeps a reverse
|
||||
// (server handle, item definition) → item handle index, so a bulk read
|
||||
// no longer walks the whole item table once per tag. The index matches
|
||||
// the tag ordinally and hands the handles back in ascending order — the
|
||||
// same order the previous linear scan over the sorted ItemHandles view
|
||||
// visited them — so the first advised-and-cached candidate still wins.
|
||||
IReadOnlyList<int> candidates = handleRegistry.GetItemHandlesForDefinition(serverHandle, tagAddress);
|
||||
|
||||
for (int index = 0; index < candidates.Count; index++)
|
||||
{
|
||||
if (registered.ServerHandle != serverHandle)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int candidate = candidates[index];
|
||||
|
||||
if (!string.Equals(registered.ItemDefinition, tagAddress, StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!handleRegistry.ContainsAdviceHandle(serverHandle, registered.ItemHandle, MxAccessAdviceKind.Plain)
|
||||
&& !handleRegistry.ContainsAdviceHandle(serverHandle, registered.ItemHandle, MxAccessAdviceKind.Supervisory))
|
||||
if (!handleRegistry.ContainsAdviceHandle(serverHandle, candidate, MxAccessAdviceKind.Plain)
|
||||
&& !handleRegistry.ContainsAdviceHandle(serverHandle, candidate, MxAccessAdviceKind.Supervisory))
|
||||
{
|
||||
// Tag is added but not advised — no fresh OnDataChange will
|
||||
// arrive without us advising. Fall through to the snapshot
|
||||
@@ -1014,9 +1011,9 @@ public sealed class MxAccessSession : IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
if (valueCache.TryGet(serverHandle, registered.ItemHandle, out cachedValue))
|
||||
if (valueCache.TryGet(serverHandle, candidate, out cachedValue))
|
||||
{
|
||||
itemHandle = registered.ItemHandle;
|
||||
itemHandle = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user