From 66a3704ee49b3eab8ad3c59c061bd772facbe8bd Mon Sep 17 00:00:00 2001 From: D4VID Date: Sun, 2 Mar 2025 21:06:20 +0100 Subject: [PATCH] Implement proper algorithm for finding loops --- .../FancyInput/ContextMetadata.jecs | 1 - .../FancyInput/TriggerMetadata.jecs | 6 - .../languages/English/English_input.jecs | 5 - .../client/CriticalPathAnalyzerGameState.cs | 3 - .../CriticalPathAnalyzerTrigger.cs | 1 - .../client/tool/CriticalPathAnalyzerTool.cs | 25 +-- .../src/server/tool/ClusterNode.cs | 2 +- .../src/server/tool/ServerPathTracer.cs | 164 +++++++++++++----- .../shared/packets/s2c/AnalyzePathResponse.cs | 2 - 9 files changed, 125 insertions(+), 84 deletions(-) diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/FancyInput/ContextMetadata.jecs b/CriticalPathAnalyzer/CriticalPathAnalyzer/FancyInput/ContextMetadata.jecs index 1f57727..69626c4 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/FancyInput/ContextMetadata.jecs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/FancyInput/ContextMetadata.jecs @@ -10,4 +10,3 @@ CriticalPathAnalyzer.CriticalPathAnalyzerTool: Triggers: - CriticalPathAnalyzer.AnalyzePathStart - CriticalPathAnalyzer.AnalyzePathEnd - - CriticalPathAnalyzer.DisplayLoop diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/FancyInput/TriggerMetadata.jecs b/CriticalPathAnalyzer/CriticalPathAnalyzer/FancyInput/TriggerMetadata.jecs index 8de3166..18e78b9 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/FancyInput/TriggerMetadata.jecs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/FancyInput/TriggerMetadata.jecs @@ -16,9 +16,3 @@ CriticalPathAnalyzer.AnalyzePathEnd: DefaultBinding: Options: - O - -CriticalPathAnalyzer.DisplayLoop: - Heading: "CriticalPathAnalyzer" - DefaultBinding: - Options: - - P diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/languages/English/English_input.jecs b/CriticalPathAnalyzer/CriticalPathAnalyzer/languages/English/English_input.jecs index f044cb5..bcd1c59 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/languages/English/English_input.jecs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/languages/English/English_input.jecs @@ -14,8 +14,3 @@ FancyInput.Trigger.CriticalPathAnalyzer.AnalyzePathEnd: "Select end of path" FancyInput.Trigger.CriticalPathAnalyzer.AnalyzePathEnd.Description: """ Press to select the end of the path to be analyzed. """ - -FancyInput.Trigger.CriticalPathAnalyzer.DisplayLoop: "Display loop" -FancyInput.Trigger.CriticalPathAnalyzer.DisplayLoop.Description: """ - Highlights only clusters that are causing a loop. - """ diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerGameState.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerGameState.cs index d731641..a00838f 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerGameState.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/CriticalPathAnalyzerGameState.cs @@ -24,7 +24,6 @@ namespace CriticalPathAnalyzer.Client { UITrigger.Back, CriticalPathAnalyzerTrigger.AnalyzePathStart, CriticalPathAnalyzerTrigger.AnalyzePathEnd, - CriticalPathAnalyzerTrigger.DisplayLoop, }; /// @@ -44,8 +43,6 @@ namespace CriticalPathAnalyzer.Client { CriticalPathAnalyzerTool.SelectPathStart(); } else if (CustomInput.DownThisFrame(CriticalPathAnalyzerTrigger.AnalyzePathEnd)) { CriticalPathAnalyzerTool.SelectPathEnd(); - } else if (CustomInput.DownThisFrame(CriticalPathAnalyzerTrigger.DisplayLoop)) { - CriticalPathAnalyzerTool.DisplayLoop(); } } diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/keybindings/CriticalPathAnalyzerTrigger.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/keybindings/CriticalPathAnalyzerTrigger.cs index ac63061..8c652e5 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/keybindings/CriticalPathAnalyzerTrigger.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/keybindings/CriticalPathAnalyzerTrigger.cs @@ -5,6 +5,5 @@ namespace CriticalPathAnalyzer.Client.Keybindings { OpenPathAnalyzer, AnalyzePathStart, AnalyzePathEnd, - DisplayLoop, } } \ No newline at end of file diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/CriticalPathAnalyzerTool.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/CriticalPathAnalyzerTool.cs index 96ab410..73ce9f0 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/CriticalPathAnalyzerTool.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/client/tool/CriticalPathAnalyzerTool.cs @@ -20,7 +20,7 @@ namespace CriticalPathAnalyzer.Client.Tool { private static AnalyzePathResponse _response; private static readonly OutlineData StartOutline = new OutlineData(new Color24(0x00ff00)); - private static readonly OutlineData EndOutline = new OutlineData(new Color24(0xff0000)); + private static readonly OutlineData EndOutline = new OutlineData(new Color24(0x00aaff)); public static void Init(ILogicLogger logger) { _logger = logger; @@ -97,19 +97,6 @@ namespace CriticalPathAnalyzer.Client.Tool { } } - /// - /// Remove standard highlighting and only highlight clusters that form a loop. - /// - public static void DisplayLoop() { - if (_response == null) { - _logger.Error("Got no response - cannot display loop"); - return; - } - - PathHighlighter.RemoveHighlighting(); - PathHighlighter.HighlightWires(_response.LoopingClusters); - } - /// /// Send a request to the server to analyze the path between the selected start and end pegs. /// @@ -141,10 +128,14 @@ namespace CriticalPathAnalyzer.Client.Tool { _response = response; - _logger.Info($"Critical path length is: {_response.CriticalPathLength}"); - PathHighlighter.RemoveHighlighting(); - PathHighlighter.HighlightWires(_response.Clusters); + + if (_response.LoopingClusters.Count > 0) { + PathHighlighter.HighlightWires(_response.LoopingClusters); + } else { + _logger.Info($"Critical path length is: {_response.CriticalPathLength}"); + PathHighlighter.HighlightWires(_response.Clusters); + } } } } \ No newline at end of file diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/tool/ClusterNode.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/tool/ClusterNode.cs index dae59c5..5cececc 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/tool/ClusterNode.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/tool/ClusterNode.cs @@ -7,6 +7,6 @@ namespace CriticalPathAnalyzer.Server.Tool { public HashSet Clusters; // bidirectionally connected clusters without delay public int Time; // what point in time has been this cluster last updated public Dictionary NextNodes; // next node index / delay between them - public HashSet PrevNodeIndexes; // the last cluster that updated this one + public HashSet PrevNodeIndexes; // node indexes that update this one } } \ No newline at end of file diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/tool/ServerPathTracer.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/tool/ServerPathTracer.cs index cd949e9..45cf0ed 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/tool/ServerPathTracer.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/server/tool/ServerPathTracer.cs @@ -57,18 +57,17 @@ namespace CriticalPathAnalyzer.Server.Tool { var clusterNodes = new List(); var clusterToNodeMapping = new Dictionary(); // Cluster / index of node var queue = new Queue(); - var loopingNodes = new HashSet(); // An input peg, only has a single cluster. // An output peg however can be connected to multiple clusters. // It only makes sense to then select all these clusters as primary cluster. - var collectedClusters = new HashSet(); - CollectMainClusters(start, collectedClusters); + var startingClusters = new HashSet(); + CollectMainClusters(start, startingClusters); var endingClusters = new HashSet(); CollectMainClusters(end, endingClusters); - foreach (Cluster startingCluster in collectedClusters) { + foreach (Cluster startingCluster in startingClusters) { // ignore already mapped clusters if (clusterToNodeMapping.ContainsKey(startingCluster)) { continue; @@ -93,12 +92,112 @@ namespace CriticalPathAnalyzer.Server.Tool { queue.Enqueue(node); } - logger.Info($"collected {collectedClusters.Count} main clusters"); + logger.Info($"collected {startingClusters.Count} main clusters"); - int iters = 0; + // Backup starting clusters for later + List startingNodeIndexes = clusterNodes.Select(node => node.Index).ToList(); + + BuildClusterTree(queue, clusterToNodeMapping, clusterNodes); + + List loopingNodes = FindLoops(clusterNodes); + + logger.Info($"Found {loopingNodes.Count} looping nodes"); + + if (loopingNodes.Count == 0) { + PropagateTimeDelay(clusterNodes, startingNodeIndexes); + } + + int criticalPathLength = -1; + if (endingClusters.Count > 0) { + // Attempt to get the critical path length + // If the ending cluster has not been found, return -1 + Cluster endingCluster = endingClusters.First(); + if (clusterToNodeMapping.TryGetValue(endingCluster, out int nodeIndex)) { + criticalPathLength = clusterNodes[nodeIndex].Time; + } + } + + // Collect information about each cluster: + response = new AnalyzePathResponse() { + RequestGuid = requestGuid, + Clusters = clusterNodes.SelectMany(node => + node.Clusters.Select(cluster => CollectClusterInformation(cluster, node.Time))).ToList(), + CriticalPathLength = criticalPathLength, + LoopingClusters = loopingNodes.SelectMany(loopingNode => loopingNode.Clusters + .Select(cluster => CollectClusterInformation(cluster, 0))).ToList(), + }; + + logger.Info("Trace end"); + + return true; + } + + private static void PropagateTimeDelay(List clusterNodes, List startingNodeIndexes) { + var queue = new Queue(startingNodeIndexes); + + while (queue.TryDequeue(out int index)) { + ClusterNode node = clusterNodes[index]; + foreach ((int nextNodeIndex, int delay) in node.NextNodes) { + clusterNodes[nextNodeIndex].Time = node.Time + delay; + queue.Enqueue(nextNodeIndex); + } + } + } + + private static List FindLoops(List clusterNodesOrig) { + List clusterNodes = clusterNodesOrig.Select(node => new ClusterNode() { + Index = node.Index, + Clusters = node.Clusters, + NextNodes = node.NextNodes.ToDictionary(), + PrevNodeIndexes = node.PrevNodeIndexes.ToHashSet(), + Time = node.Time, + }).ToList(); + + var queue = new Queue(); + foreach (ClusterNode node in clusterNodes) { + if (node.PrevNodeIndexes.Count == 0) { + // source + queue.Enqueue(node); + } + + if (node.NextNodes.Count == 0) { + // source + queue.Enqueue(node); + } + } + + while (queue.TryDequeue(out ClusterNode node)) { + foreach ((int nextNodeIndex, int _) in node.NextNodes) { + ClusterNode nextNode = clusterNodes[nextNodeIndex]; + nextNode.PrevNodeIndexes.Remove(node.Index); + if (nextNode.PrevNodeIndexes.Count == 0) { + queue.Enqueue(nextNode); + } + } + + foreach (int prevNodeIndex in node.PrevNodeIndexes) { + ClusterNode prevNode = clusterNodes[prevNodeIndex]; + prevNode.NextNodes.Remove(node.Index); + if (prevNode.NextNodes.Count == 0) { + queue.Enqueue(prevNode); + } + } + } + + return clusterNodes.Where(node => node.PrevNodeIndexes.Count > 0 && node.NextNodes.Count > 0).ToList(); + } + + private static void BuildClusterTree(Queue queue, Dictionary clusterToNodeMapping, + List clusterNodes) { + ILogicLogger logger = CriticalPathAnalyzerServer.LoggerInstance; + int iterations = 0; while (queue.TryDequeue(out ClusterNode node)) { - iters++; - if (iters > 10000) { + iterations++; + if (iterations % 1000 == 0) { + logger.Info($"{iterations} iterations"); + } + + if (iterations > 100000) { logger.Error("Infinite iterations"); break; } @@ -124,71 +223,36 @@ namespace CriticalPathAnalyzer.Server.Tool { if (clusterToNodeMapping.TryGetValue(nextCluster, out int nextNodeIndex)) { ClusterNode nextNode = clusterNodes[nextNodeIndex]; // only add the link, don't propagate - if (!nextNode.PrevNodeIndexes.Add(node.Index)) { - logger.Warn("Loop detected"); - loopingNodes.Add(node.Index); - loopingNodes.Add(nextNode.Index); - // already been here - there is a cycle - // TODO: this doesn't work - if cluster A is updated by cluster B, - // TODO: and cluster B is updated by clusters C and D, it detects a cycle where isn't one - continue; - } - - // Replace the time with a later one and continue - int timeDelay = 1; + nextNode.PrevNodeIndexes.Add(node.Index); node.NextNodes.TryAdd(nextNodeIndex, 1); - nextNode.Time = node.Time + timeDelay; - queue.Enqueue(nextNode); } else { // var nextClusters = new HashSet(); // var twoWayConnectedClusters = new HashSet(); // GetLinkedClusters(cluster, nextClusters, twoWayConnectedClusters, GetFollowers); - int timeDelay = 1; nextNodeIndex = clusterNodes.Count; var nextNode = new ClusterNode() { Index = nextNodeIndex, Clusters = new HashSet() {nextCluster}, - Time = node.Time + timeDelay, + Time = 0, NextNodes = new Dictionary(), PrevNodeIndexes = new HashSet() {node.Index}, }; clusterNodes.Add(nextNode); - node.NextNodes.TryAdd(nextNodeIndex, timeDelay); + node.NextNodes.TryAdd(nextNodeIndex, 1); foreach (Cluster nextNodeCluster in nextNode.Clusters) { clusterToNodeMapping.Add(nextNodeCluster, nextNodeIndex); } + // propagate queue.Enqueue(nextNode); } } } - // Attempt to get the critical path length - // If the ending cluster has not been found, return -1 - Cluster endingCluster = endingClusters.First(); - int criticalPathLength = -1; - if (clusterToNodeMapping.TryGetValue(endingCluster, out int nodeIndex)) { - criticalPathLength = clusterNodes[nodeIndex].Time; - } - - // Collect information about each cluster: - response = new AnalyzePathResponse() { - RequestGuid = requestGuid, - Clusters = clusterNodes.SelectMany(node => - node.Clusters.Select(cluster => CollectClusterInformation(cluster, node.Time))).ToList(), - CriticalPathLength = criticalPathLength, - LoopingClusters = loopingNodes.SelectMany(loopingNodeIndex => clusterNodes[loopingNodeIndex].Clusters - .Select(cluster => CollectClusterInformation(cluster, 0))).ToList(), - // PerimeterComponents = perimeterComponents.ToList(), - // NextClusters = collectedNextClusters.Select(CollectClusterInformation).ToList(), - }; - - logger.Info("Trace end"); - - return true; + logger.Info($"Finished after {iterations} iterations"); } @@ -333,6 +397,10 @@ namespace CriticalPathAnalyzer.Server.Tool { return details; } + /// 0 - 360 + /// 0.0 - 1.0 + /// 0.0 - 1.0 + /// 0xrrggbb private static int HsvToRgb(int h, float s, float v) { s = Math.Clamp(s, 0, 1); v = Math.Clamp(v, 0, 1); diff --git a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/shared/packets/s2c/AnalyzePathResponse.cs b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/shared/packets/s2c/AnalyzePathResponse.cs index 473f20d..296b77e 100644 --- a/CriticalPathAnalyzer/CriticalPathAnalyzer/src/shared/packets/s2c/AnalyzePathResponse.cs +++ b/CriticalPathAnalyzer/CriticalPathAnalyzer/src/shared/packets/s2c/AnalyzePathResponse.cs @@ -13,8 +13,6 @@ namespace CriticalPathAnalyzer.Shared.Packets.S2C { [Key(2)] public int CriticalPathLength; [Key(3)] public List LoopingClusters; - // [Key(4)] public List PerimeterComponents; - // [Key(5)] public List NextClusters; } [MessagePackObject]