perf(templateengine): memoise CollisionDetector's composed-module walk per check (review 2026-10-21 04/P8b)
This commit is contained in:
@@ -32,6 +32,7 @@ public static class CollisionDetector
|
||||
// not-yet-saved templates carrying Id 0).
|
||||
var lookup = CycleDetector.BuildLookup(allTemplates);
|
||||
var allMembers = new List<ResolvedMember>();
|
||||
var moduleMemo = new Dictionary<int, IReadOnlyList<RelativeMember>>();
|
||||
|
||||
// Collect direct (top-level) members. Inherited placeholder rows are skipped:
|
||||
// the inheritance walk below re-adds them under the parent's origin.
|
||||
@@ -47,12 +48,12 @@ public static class CollisionDetector
|
||||
prefix: composition.InstanceName,
|
||||
lookup,
|
||||
allMembers,
|
||||
visited: new HashSet<int>());
|
||||
moduleMemo);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect inherited members (walk parent chain)
|
||||
CollectInheritedMembers(template, lookup, allMembers, new HashSet<int> { template.Id });
|
||||
CollectInheritedMembers(template, lookup, allMembers, moduleMemo, new HashSet<int> { template.Id });
|
||||
|
||||
// Detect duplicates by canonical name
|
||||
var collisions = new List<string>();
|
||||
@@ -83,9 +84,9 @@ public static class CollisionDetector
|
||||
/// <see cref="CollectInheritedMembers"/>. Counting both yielded two distinct
|
||||
/// origins for the same canonical name and a spurious collision, which blocked
|
||||
/// every attempt to add an attribute/composition to a derived template.
|
||||
/// Pass <c>false</c> only for the composed-module walk, where
|
||||
/// placeholder rows are the sole representation of a derived module's inherited
|
||||
/// members (that walk does not climb the composed template's parent chain).
|
||||
/// (The composed-module walk, <see cref="GetModuleMembers"/>, keeps placeholder rows
|
||||
/// instead: there they are the sole representation of a derived module's inherited
|
||||
/// members, because that walk does not climb the composed template's parent chain.)
|
||||
/// </param>
|
||||
private static void CollectDirectMembers(
|
||||
Template template,
|
||||
@@ -116,43 +117,115 @@ public static class CollisionDetector
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A composed module's member, named relative to the module: <see cref="RelativeName"/>
|
||||
/// is the member's path below the slot, <see cref="ModulePath"/> the nested-slot path
|
||||
/// of the module that declares it (<c>null</c> for the module's own direct members).
|
||||
/// </summary>
|
||||
private sealed record RelativeMember(string RelativeName, string MemberType, string? ModulePath);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a composed module's members — its own and its nested modules', recursively —
|
||||
/// under <paramref name="prefix"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// The module's member list is computed once per collision check and memoised by
|
||||
/// template id in <paramref name="moduleMemo"/> (review 2026-10-21 04/P8(b)): a
|
||||
/// module composed into several slots, or reached through several parents (a diamond),
|
||||
/// used to have its whole subtree re-walked once per path. Each further slot now only
|
||||
/// re-prefixes the memoised list. Every path still contributes its own canonical names
|
||||
/// — a repeatedly-composed template must be checked under every slot.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static void CollectComposedMembers(
|
||||
Template template,
|
||||
string prefix,
|
||||
Dictionary<int, Template> lookup,
|
||||
List<ResolvedMember> members,
|
||||
HashSet<int> visited)
|
||||
Dictionary<int, IReadOnlyList<RelativeMember>> moduleMemo)
|
||||
{
|
||||
// `visited` is the current RECURSION PATH, not a global visited-set: an id is
|
||||
// added on entry and REMOVED on exit (finally), so a template legitimately
|
||||
// composed into more than one slot is re-descended for each slot. The early
|
||||
// return fires only on a genuine cycle — an id already on the ACTIVE path.
|
||||
// (A global visited-set would blind the detector to every collision reachable
|
||||
// only through the second-and-later slot of a repeatedly-composed template.)
|
||||
if (!visited.Add(template.Id))
|
||||
return;
|
||||
var relative = GetModuleMembers(template, lookup, moduleMemo, new HashSet<int>(), out _);
|
||||
foreach (var m in relative)
|
||||
{
|
||||
var origin = m.ModulePath == null ? prefix : $"{prefix}.{m.ModulePath}";
|
||||
members.Add(new ResolvedMember($"{prefix}.{m.RelativeName}", m.MemberType, $"module '{origin}'"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes (or returns the memoised) module-relative member list of
|
||||
/// <paramref name="template"/> as a composed module.
|
||||
/// </summary>
|
||||
/// <param name="template">The composed module.</param>
|
||||
/// <param name="lookup">All templates by id.</param>
|
||||
/// <param name="moduleMemo">Per-check memo of complete (cycle-free) module member lists.</param>
|
||||
/// <param name="activePath">
|
||||
/// The ids on the current recursion PATH (not a global visited-set): a nested module
|
||||
/// already on the path is a genuine cycle and contributes nothing, exactly as before.
|
||||
/// </param>
|
||||
/// <param name="truncatedByCycle">
|
||||
/// True when a cycle cut this list short. Such a list depends on the path it was
|
||||
/// computed from, so it — and every ancestor list built from it — is not memoised.
|
||||
/// </param>
|
||||
/// <returns>The module's members, relative to the module.</returns>
|
||||
private static IReadOnlyList<RelativeMember> GetModuleMembers(
|
||||
Template template,
|
||||
Dictionary<int, Template> lookup,
|
||||
Dictionary<int, IReadOnlyList<RelativeMember>> moduleMemo,
|
||||
HashSet<int> activePath,
|
||||
out bool truncatedByCycle)
|
||||
{
|
||||
truncatedByCycle = false;
|
||||
if (moduleMemo.TryGetValue(template.Id, out var cached))
|
||||
return cached;
|
||||
|
||||
activePath.Add(template.Id);
|
||||
try
|
||||
{
|
||||
// Add direct members of this composed template with the prefix. Inherited
|
||||
// placeholder rows are KEPT here: the composed-module walk does not climb the
|
||||
// composed template's parent chain, so the placeholders are the only
|
||||
// representation of a derived module's inherited members.
|
||||
CollectDirectMembers(template, prefix, $"module '{prefix}'", members, skipInherited: false);
|
||||
var result = new List<RelativeMember>();
|
||||
|
||||
// Recurse into nested compositions
|
||||
// Direct members of the composed template. Inherited placeholder rows are
|
||||
// KEPT here: the composed-module walk does not climb the composed template's
|
||||
// parent chain, so the placeholders are the only representation of a derived
|
||||
// module's inherited members.
|
||||
foreach (var attr in template.Attributes)
|
||||
result.Add(new RelativeMember(attr.Name, "Attribute", null));
|
||||
foreach (var alarm in template.Alarms)
|
||||
result.Add(new RelativeMember(alarm.Name, "Alarm", null));
|
||||
foreach (var script in template.Scripts)
|
||||
result.Add(new RelativeMember(script.Name, "Script", null));
|
||||
|
||||
// Nested compositions.
|
||||
foreach (var composition in template.Compositions)
|
||||
{
|
||||
if (lookup.TryGetValue(composition.ComposedTemplateId, out var nested))
|
||||
if (!lookup.TryGetValue(composition.ComposedTemplateId, out var nested))
|
||||
continue;
|
||||
|
||||
if (activePath.Contains(nested.Id))
|
||||
{
|
||||
var nestedPrefix = $"{prefix}.{composition.InstanceName}";
|
||||
CollectComposedMembers(nested, nestedPrefix, lookup, members, visited);
|
||||
truncatedByCycle = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var slot = composition.InstanceName;
|
||||
var nestedMembers = GetModuleMembers(nested, lookup, moduleMemo, activePath, out var nestedTruncated);
|
||||
truncatedByCycle |= nestedTruncated;
|
||||
foreach (var m in nestedMembers)
|
||||
{
|
||||
result.Add(new RelativeMember(
|
||||
$"{slot}.{m.RelativeName}",
|
||||
m.MemberType,
|
||||
m.ModulePath == null ? slot : $"{slot}.{m.ModulePath}"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!truncatedByCycle)
|
||||
moduleMemo[template.Id] = result;
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
visited.Remove(template.Id);
|
||||
activePath.Remove(template.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +233,7 @@ public static class CollisionDetector
|
||||
Template template,
|
||||
Dictionary<int, Template> lookup,
|
||||
List<ResolvedMember> members,
|
||||
Dictionary<int, IReadOnlyList<RelativeMember>> moduleMemo,
|
||||
HashSet<int> visited)
|
||||
{
|
||||
if (!template.ParentTemplateId.HasValue)
|
||||
@@ -186,11 +260,11 @@ public static class CollisionDetector
|
||||
composition.InstanceName,
|
||||
lookup,
|
||||
members,
|
||||
new HashSet<int>());
|
||||
moduleMemo);
|
||||
}
|
||||
}
|
||||
|
||||
// Continue up the inheritance chain
|
||||
CollectInheritedMembers(parent, lookup, members, visited);
|
||||
CollectInheritedMembers(parent, lookup, members, moduleMemo, visited);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user