perf(abcip): cache the declaration-only UDT layout per member list
P11: AbCipUdtReadPlanner called AbCipUdtMemberLayout.TryBuild(parent.Members) for every member reference on every poll - O(members^2) per parent per tick, a dictionary allocation each time. GetOrBuild caches the layout (including a "not groupable" verdict) in a ConditionalWeakTable keyed by the member list's identity, which is immutable for the life of the tag table; a re-init's new lists replace the entries and the old ones are collected with them. Refs #737 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -25,6 +25,30 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
|
||||
/// </remarks>
|
||||
public static class AbCipUdtMemberLayout
|
||||
{
|
||||
/// <summary>Layouts already computed, keyed by the parent definition's member list (reference identity).</summary>
|
||||
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<IReadOnlyList<AbCipStructureMember>, LayoutBox> Cache = new();
|
||||
|
||||
/// <summary>Holds a computed layout, including a "not groupable" (<c>null</c>) verdict.</summary>
|
||||
private sealed class LayoutBox(IReadOnlyDictionary<string, int>? offsets)
|
||||
{
|
||||
public IReadOnlyDictionary<string, int>? Offsets { get; } = offsets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="TryBuild"/>, computed once per member list (review 07 P11: the read planner rebuilt the layout —
|
||||
/// a dictionary allocation and an O(members) walk — for every member reference on every poll, O(members²) per
|
||||
/// parent per tick). A parent definition's member list is immutable for the life of the tag table that holds
|
||||
/// it, so caching by list identity is exact; a re-init builds new lists and the old entries are collected with
|
||||
/// them (<see cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}"/>).
|
||||
/// </summary>
|
||||
/// <param name="members">The parent definition's declared members.</param>
|
||||
/// <returns>The member offsets, or <c>null</c> when the members cannot be laid out by declaration.</returns>
|
||||
public static IReadOnlyDictionary<string, int>? GetOrBuild(IReadOnlyList<AbCipStructureMember> members)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(members);
|
||||
return Cache.GetValue(members, m => new LayoutBox(TryBuild(m))).Offsets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to compute member offsets for the supplied declared members. Returns <c>null</c>
|
||||
/// if any member type is unsupported for declaration-only layout.
|
||||
|
||||
@@ -67,7 +67,7 @@ public static class AbCipUdtReadPlanner
|
||||
continue;
|
||||
}
|
||||
|
||||
var offsets = AbCipUdtMemberLayout.TryBuild(parent.Members);
|
||||
var offsets = AbCipUdtMemberLayout.GetOrBuild(parent.Members);
|
||||
if (offsets is null || !offsets.TryGetValue(memberName, out var offset))
|
||||
{
|
||||
fallback.Add(new AbCipUdtReadFallback(i, name));
|
||||
|
||||
@@ -99,4 +99,26 @@ public sealed class AbCipUdtMemberLayoutTests
|
||||
new AbCipStructureMember("Setpoints", AbCipDataType.Real, ElementCount: 4),
|
||||
}).ShouldBeNull();
|
||||
}
|
||||
|
||||
/// <summary>Review 07 P11: the layout is computed once per member list, not once per member per poll.</summary>
|
||||
[Fact]
|
||||
public void GetOrBuild_computes_a_layout_once_per_member_list()
|
||||
{
|
||||
IReadOnlyList<AbCipStructureMember> members =
|
||||
[
|
||||
new AbCipStructureMember("A", AbCipDataType.DInt),
|
||||
new AbCipStructureMember("B", AbCipDataType.Real),
|
||||
];
|
||||
var first = AbCipUdtMemberLayout.GetOrBuild(members);
|
||||
first.ShouldNotBeNull();
|
||||
first["B"].ShouldBe(4);
|
||||
AbCipUdtMemberLayout.GetOrBuild(members).ShouldBeSameAs(first);
|
||||
|
||||
// Positive control: a different (equal-content) list is its own entry, and a not-groupable verdict caches too.
|
||||
IReadOnlyList<AbCipStructureMember> copy = [.. members];
|
||||
AbCipUdtMemberLayout.GetOrBuild(copy).ShouldNotBeSameAs(first);
|
||||
IReadOnlyList<AbCipStructureMember> withBool = [new AbCipStructureMember("F", AbCipDataType.Bool)];
|
||||
AbCipUdtMemberLayout.GetOrBuild(withBool).ShouldBeNull();
|
||||
AbCipUdtMemberLayout.GetOrBuild(withBool).ShouldBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user