diff --git a/.jules/bolt.md b/.jules/bolt.md index 7e209ff..10dec3e 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -23,3 +23,6 @@ ## 2024-05-23 - Optimize LINQ Allocations in Network Simulation Engine **Learning:** Found multiple places in `NetworkSimulationEngine.cs` where LINQ `.Count()` and `.Select().Min()` inside inner loops on the simulation path were allocating delegates and enumerators on the hot path (e.g., inside `GetPathRemainingCapacity` and `CountBottleneckResources`). **Action:** Replaced these LINQ chains with `for` and `foreach` loops respectively to minimize garbage generation during greedy route allocation, reducing allocations during repeated graph evaluations. +## 2026-05-22 - O(N^2) Lookup inside Simulation Updates +**Learning:** In the Avalonia presentation layer, `ApplySimulationOutcomes` in `WorkspacePresentation` was performing an O(N^2) nested lookup by filtering `timeline.NodeStates` inside a loop that iterated over every node in the scene. For large networks, this repeated filtering could freeze the UI during heavy simulation updates. +**Action:** Pre-compute node states into a grouped lookup dictionary outside of the loop, reducing the update bottleneck to O(N + S). diff --git a/src/MedWNetworkSim.Presentation/WorkspacePresentation.cs b/src/MedWNetworkSim.Presentation/WorkspacePresentation.cs index 980ca43..8439dde 100644 --- a/src/MedWNetworkSim.Presentation/WorkspacePresentation.cs +++ b/src/MedWNetworkSim.Presentation/WorkspacePresentation.cs @@ -8797,23 +8797,30 @@ private void ApplySimulationOutcomes(IEnumerable allocations, T var nodesById = network.Nodes.ToDictionary(node => node.Id, Comparer); if (timeline is not null) { + // Bolt: Pre-compute node states to optimize O(N^2) timeline.NodeStates lookup + var nodeStatesByNodeId = timeline.NodeStates + .GroupBy(pair => pair.Key.NodeId, Comparer) + .ToDictionary(group => group.Key, group => group.ToList(), Comparer); + foreach (var node in Scene.Nodes) { if (!nodesById.TryGetValue(node.Id, out var nodeModel)) continue; - var state = timeline.NodeStates - .Where(pair => Comparer.Equals(pair.Key.NodeId, node.Id)) - .Select(pair => pair.Value) - .FirstOrDefault(); - var backlogByTraffic = timeline.NodeStates - .Where(pair => Comparer.Equals(pair.Key.NodeId, node.Id) && pair.Value.DemandBacklog > 0d) + + var nodeTrafficStates = nodeStatesByNodeId.GetValueOrDefault(node.Id) ?? []; + + var state = nodeTrafficStates.Select(pair => pair.Value).FirstOrDefault(); + + var backlogByTraffic = nodeTrafficStates + .Where(pair => pair.Value.DemandBacklog > 0d) .GroupBy(pair => pair.Key.TrafficType, pair => pair.Value.DemandBacklog, Comparer) .Select(group => new KeyValuePair(group.Key, group.Sum())) .ToList(); + var pressure = timeline.NodePressureById.GetValueOrDefault(node.Id); node.MetricsLabel = string.Empty; node.DetailLines = BuildNodeDetailLines(nodeModel, backlogByTraffic, pressure.Score > 0d ? pressure : null); UpdateSceneNodeLayout(node, nodeModel, pressure.Score > 0d ? pressure : null, graphRenderer.GetZoomTier(Viewport.Zoom)); - node.HasWarning = pressure.Score > 0d || state.DemandBacklog > 0d; + node.HasWarning = pressure.Score > 0d || (state.DemandBacklog > 0d); } } else