Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
21 changes: 14 additions & 7 deletions src/MedWNetworkSim.Presentation/WorkspacePresentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8797,23 +8797,30 @@ private void ApplySimulationOutcomes(IEnumerable<RouteAllocation> 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<string, double>(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
Expand Down