Replace coarse lock-key batching with conflict-zone-aware scheduling
Replace string-based conflict keys (source:{nodeId}, target:{nodeId}) with
geometric bounding-box overlap detection. Edges now conflict only when their
routed path bounding boxes overlap spatially (with 40px margin) or share a
repeat-collector label on the same source-target pair.
This enables true spatial parallelism: edges using different sides of the
same node can now be repaired in parallel instead of being serialized.
Sprint 006 TASK-001 final criterion met. All 4 tasks DONE.
Tests verified: StraightExit 2/2, HybridDeterministicMode 3/3,
DocumentProcessingWorkflow artifact 1/1 (all 44+ assertions pass).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -268,8 +268,12 @@ internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
var edgesById = edges.ToDictionary(edge => edge.Id, StringComparer.Ordinal);
|
||||
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
|
||||
var orderedBatches = new List<(List<string> EdgeIds, HashSet<string> ConflictKeys)>();
|
||||
|
||||
// Build conflict zones: each repair edge gets a bounding box from its
|
||||
// routed path plus a margin. Two edges conflict if their zones overlap
|
||||
// spatially, or if they share a repeat-collector label on the same
|
||||
// source-target pair.
|
||||
var zones = new List<(string EdgeId, ConflictZone Zone)>();
|
||||
foreach (var edgeId in repairPlan.EdgeIds)
|
||||
{
|
||||
if (!edgesById.TryGetValue(edgeId, out var edge))
|
||||
@@ -277,90 +281,136 @@ internal static partial class ElkEdgeRouterIterative
|
||||
continue;
|
||||
}
|
||||
|
||||
var conflictKeys = GetHybridConflictKeys(edge, nodesById);
|
||||
zones.Add((edgeId, BuildConflictZone(edge, nodesById)));
|
||||
}
|
||||
|
||||
// Greedy first-fit batching: assign each edge to the first batch
|
||||
// whose existing zones don't spatially conflict.
|
||||
var orderedBatches = new List<(List<string> EdgeIds, List<ConflictZone> Zones)>();
|
||||
foreach (var (edgeId, zone) in zones)
|
||||
{
|
||||
var assigned = false;
|
||||
foreach (var batch in orderedBatches)
|
||||
{
|
||||
if (batch.ConflictKeys.Overlaps(conflictKeys))
|
||||
if (batch.Zones.Any(existing => existing.ConflictsWith(zone)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
batch.EdgeIds.Add(edgeId);
|
||||
batch.ConflictKeys.UnionWith(conflictKeys);
|
||||
batch.Zones.Add(zone);
|
||||
assigned = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (assigned)
|
||||
if (!assigned)
|
||||
{
|
||||
continue;
|
||||
orderedBatches.Add((EdgeIds: [edgeId], Zones: [zone]));
|
||||
}
|
||||
|
||||
orderedBatches.Add((
|
||||
EdgeIds: [edgeId],
|
||||
ConflictKeys: new HashSet<string>(conflictKeys, StringComparer.Ordinal)));
|
||||
}
|
||||
|
||||
return orderedBatches
|
||||
.Select(batch => new HybridRepairBatch(
|
||||
batch.EdgeIds.ToArray(),
|
||||
batch.ConflictKeys.OrderBy(key => key, StringComparer.Ordinal).ToArray()))
|
||||
batch.Zones.SelectMany(z => z.DescriptiveKeys).Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(key => key, StringComparer.Ordinal).ToArray()))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string[] GetHybridConflictKeys(
|
||||
/// <summary>
|
||||
/// Geometric conflict zone for an edge: bounding box of its routed path
|
||||
/// expanded by a margin, plus endpoint node IDs for collector-label
|
||||
/// conflict detection.
|
||||
/// </summary>
|
||||
private static ConflictZone BuildConflictZone(
|
||||
ElkRoutedEdge edge,
|
||||
IReadOnlyDictionary<string, ElkPositionedNode> nodesById)
|
||||
{
|
||||
var keys = new HashSet<string>(StringComparer.Ordinal);
|
||||
var sourceId = edge.SourceNodeId ?? string.Empty;
|
||||
var targetId = edge.TargetNodeId ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(sourceId))
|
||||
{
|
||||
keys.Add($"source:{sourceId}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(targetId))
|
||||
{
|
||||
keys.Add($"target:{targetId}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(sourceId) && !string.IsNullOrEmpty(targetId))
|
||||
{
|
||||
keys.Add($"pair:{sourceId}->{targetId}");
|
||||
}
|
||||
|
||||
var path = ExtractPath(edge);
|
||||
if (path.Count >= 2
|
||||
&& nodesById.TryGetValue(sourceId, out var sourceNode))
|
||||
const double margin = 40d;
|
||||
|
||||
// Compute bounding box from path + source/target node extents.
|
||||
var minX = double.MaxValue;
|
||||
var minY = double.MaxValue;
|
||||
var maxX = double.MinValue;
|
||||
var maxY = double.MinValue;
|
||||
foreach (var point in path)
|
||||
{
|
||||
var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[0], path[1], sourceNode);
|
||||
keys.Add($"source-side:{sourceId}:{sourceSide}");
|
||||
if (ElkShapeBoundaries.IsGatewayShape(sourceNode))
|
||||
minX = Math.Min(minX, point.X);
|
||||
minY = Math.Min(minY, point.Y);
|
||||
maxX = Math.Max(maxX, point.X);
|
||||
maxY = Math.Max(maxY, point.Y);
|
||||
}
|
||||
|
||||
// Include source/target node extents in the zone.
|
||||
if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var srcNode))
|
||||
{
|
||||
minX = Math.Min(minX, srcNode.X);
|
||||
minY = Math.Min(minY, srcNode.Y);
|
||||
maxX = Math.Max(maxX, srcNode.X + srcNode.Width);
|
||||
maxY = Math.Max(maxY, srcNode.Y + srcNode.Height);
|
||||
}
|
||||
|
||||
if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var tgtNode))
|
||||
{
|
||||
minX = Math.Min(minX, tgtNode.X);
|
||||
minY = Math.Min(minY, tgtNode.Y);
|
||||
maxX = Math.Max(maxX, tgtNode.X + tgtNode.Width);
|
||||
maxY = Math.Max(maxY, tgtNode.Y + tgtNode.Height);
|
||||
}
|
||||
|
||||
var isCollector = ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label);
|
||||
var descriptiveKeys = new List<string>(3);
|
||||
if (!string.IsNullOrEmpty(edge.SourceNodeId))
|
||||
{
|
||||
descriptiveKeys.Add($"source:{edge.SourceNodeId}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(edge.TargetNodeId))
|
||||
{
|
||||
descriptiveKeys.Add($"target:{edge.TargetNodeId}");
|
||||
}
|
||||
|
||||
if (isCollector)
|
||||
{
|
||||
descriptiveKeys.Add($"collector:{edge.SourceNodeId}:{edge.TargetNodeId}");
|
||||
}
|
||||
|
||||
return new ConflictZone(
|
||||
MinX: minX - margin,
|
||||
MinY: minY - margin,
|
||||
MaxX: maxX + margin,
|
||||
MaxY: maxY + margin,
|
||||
SourceNodeId: edge.SourceNodeId ?? string.Empty,
|
||||
TargetNodeId: edge.TargetNodeId ?? string.Empty,
|
||||
IsCollector: isCollector,
|
||||
DescriptiveKeys: descriptiveKeys.ToArray());
|
||||
}
|
||||
|
||||
private readonly record struct ConflictZone(
|
||||
double MinX, double MinY, double MaxX, double MaxY,
|
||||
string SourceNodeId, string TargetNodeId,
|
||||
bool IsCollector,
|
||||
string[] DescriptiveKeys)
|
||||
{
|
||||
/// <summary>
|
||||
/// Two zones conflict if their bounding boxes overlap spatially,
|
||||
/// or if they are both collectors on the same source-target pair.
|
||||
/// </summary>
|
||||
internal bool ConflictsWith(ConflictZone other)
|
||||
{
|
||||
// Collector edges on the same pair always conflict.
|
||||
if (IsCollector && other.IsCollector
|
||||
&& string.Equals(SourceNodeId, other.SourceNodeId, StringComparison.Ordinal)
|
||||
&& string.Equals(TargetNodeId, other.TargetNodeId, StringComparison.Ordinal))
|
||||
{
|
||||
keys.Add($"gateway-source:{sourceId}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (path.Count >= 2
|
||||
&& nodesById.TryGetValue(targetId, out var targetNode))
|
||||
{
|
||||
var previousPoint = path[^2];
|
||||
var targetSide = ResolveEntrySide(path[^1], previousPoint, targetNode);
|
||||
keys.Add($"target-side:{targetId}:{targetSide}");
|
||||
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
||||
{
|
||||
keys.Add($"gateway-target:{targetId}");
|
||||
}
|
||||
// Spatial overlap check.
|
||||
return MinX < other.MaxX && MaxX > other.MinX
|
||||
&& MinY < other.MaxY && MaxY > other.MinY;
|
||||
}
|
||||
|
||||
if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label))
|
||||
{
|
||||
keys.Add($"collector:{sourceId}:{targetId}");
|
||||
}
|
||||
|
||||
return keys.OrderBy(key => key, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static RepairPlan BuildHybridBatchPlan(RepairPlan repairPlan, IReadOnlyCollection<string> batchEdgeIds)
|
||||
|
||||
Reference in New Issue
Block a user