From ef70214f797b4fa1bfc4e24dfdbef03a6907f021 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 17:38:34 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20LINQ=20Dictionar?= =?UTF-8?q?y=20Allocations=20in=20NetworkSimulationEngine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced expensive LINQ `ToDictionary`, `GroupBy`, and `Select` chains with explicit `foreach` loops and pre-sized dictionaries in `NetworkSimulationEngine.cs`. This significantly reduces heap allocations, delegate creation, and GC pressure during the static routing context compilation phase. --- .jules/bolt.md | 3 + patch.diff | 71 +++++++++++++++++++ .../Services/NetworkSimulationEngine.cs | 71 ++++++++++++------- 3 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 patch.diff diff --git a/.jules/bolt.md b/.jules/bolt.md index 36c1587..db4c090 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -79,3 +79,6 @@ ## $(date +%Y-%m-%d) - Preserve GroupBy Determinism when refactoring to Dictionary **Learning:** When refactoring LINQ `GroupBy` calls to manual `Dictionary` iterations to reduce allocation overhead, it is easy to accidentally introduce non-determinism. `Enumerable.GroupBy` yields elements in the exact order their keys first appeared, while `Dictionary` enumeration uses hash buckets (which are randomized per execution in modern .NET). This loss of determinism can be fatal in simulation engines. **Action:** When manually replacing `GroupBy` with a `Dictionary`, always track the order of keys explicitly as they are first encountered (e.g., using a `List orderedKeys`) and iterate over that list instead of the dictionary keys or values. +## $(date +%Y-%m-%d) - Eliminate LINQ Chains in NetworkSimulationEngine Hot Paths +**Learning:** In C#, replacing multiple LINQ `.ToDictionary()` allocations with single manual initialization loops and dictionary indexers drastically reduces hidden enumerator allocations and delegate GC overhead. This is especially true when `Select()` or `GroupBy()` are chained into dictionary creations within hot methods. Note that indexers (`dict[key] = val`) overwrite duplicates safely, while `ToDictionary` throws exceptions, making the manual loop slightly more resilient. +**Action:** When inspecting simulation hot paths or network parsing phases, aggressively seek out LINQ aggregation or dictionary building chains and hoist/flatten them into manually pre-sized `foreach` loops. diff --git a/patch.diff b/patch.diff new file mode 100644 index 0000000..a217d7a --- /dev/null +++ b/patch.diff @@ -0,0 +1,71 @@ +--- src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs ++++ src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs +@@ -40,31 +40,43 @@ + + var hasRecipeDependencies = HasStaticRecipeDependencies(network); +- var definitionsByTraffic = network.TrafficTypes +- .Where(definition => !string.IsNullOrWhiteSpace(definition.Name)) +- .GroupBy(definition => definition.Name, Comparer) +- .ToDictionary(group => group.Key, group => group.First(), Comparer); ++ ++ var definitionsByTraffic = new Dictionary(Comparer); ++ foreach (var definition in network.TrafficTypes) ++ { ++ if (!string.IsNullOrWhiteSpace(definition.Name) && !definitionsByTraffic.ContainsKey(definition.Name)) ++ { ++ definitionsByTraffic[definition.Name] = definition; ++ } ++ } ++ + var contexts = MixedRoutingAllocator.BuildStaticContexts(network, applyLocalAllocations: !hasRecipeDependencies).ToList(); +- var remainingCapacityByEdgeId = network.Edges.ToDictionary( +- edge => edge.Id, +- edge => edge.Capacity ?? double.PositiveInfinity, +- Comparer); +- var remainingTranshipmentCapacityByNodeId = network.Nodes.ToDictionary( +- node => node.Id, +- node => node.TranshipmentCapacity ?? double.PositiveInfinity, +- Comparer); ++ var remainingCapacityByEdgeId = new Dictionary(network.Edges.Count, Comparer); ++ foreach (var edge in network.Edges) ++ { ++ remainingCapacityByEdgeId[edge.Id] = edge.Capacity ?? double.PositiveInfinity; ++ } ++ ++ var remainingTranshipmentCapacityByNodeId = new Dictionary(network.Nodes.Count, Comparer); ++ foreach (var node in network.Nodes) ++ { ++ remainingTranshipmentCapacityByNodeId[node.Id] = node.TranshipmentCapacity ?? double.PositiveInfinity; ++ } ++ + var hasFiniteCapacities = network.Edges.Any(edge => edge.Capacity.HasValue) || + network.Nodes.Any(node => node.TranshipmentCapacity.HasValue); + + if (hasRecipeDependencies) + { +- var contextsByTraffic = contexts.ToDictionary(context => context.TrafficType, context => context, Comparer); +- var allocationOrder = BuildStaticRecipeCostOrder(network, contexts.Select(context => context.TrafficType).ToList()); +- var sourceUnitCosts = contexts.ToDictionary( +- context => context.TrafficType, +- _ => new Dictionary(Comparer), +- Comparer); +- var landedUnitCosts = contexts.ToDictionary( +- context => context.TrafficType, +- _ => new Dictionary(Comparer), +- Comparer); ++ var contextsByTraffic = new Dictionary(contexts.Count, Comparer); ++ var trafficTypes = new List(contexts.Count); ++ var sourceUnitCosts = new Dictionary>(contexts.Count, Comparer); ++ var landedUnitCosts = new Dictionary>(contexts.Count, Comparer); ++ ++ foreach (var context in contexts) ++ { ++ contextsByTraffic[context.TrafficType] = context; ++ trafficTypes.Add(context.TrafficType); ++ sourceUnitCosts[context.TrafficType] = new Dictionary(Comparer); ++ landedUnitCosts[context.TrafficType] = new Dictionary(Comparer); ++ } ++ ++ var allocationOrder = BuildStaticRecipeCostOrder(network, trafficTypes); + + foreach (var trafficType in allocationOrder) diff --git a/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs b/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs index 3aa9dbd..9c7efd9 100644 --- a/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs +++ b/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs @@ -39,34 +39,51 @@ public IReadOnlyList Simulate(NetworkModel network) } var hasRecipeDependencies = HasStaticRecipeDependencies(network); - var definitionsByTraffic = network.TrafficTypes - .Where(definition => !string.IsNullOrWhiteSpace(definition.Name)) - .GroupBy(definition => definition.Name, Comparer) - .ToDictionary(group => group.Key, group => group.First(), Comparer); + + // Bolt: Replaced LINQ ToDictionary chains with manual Dictionary initialization to avoid enumerator and delegate allocations on hot path. + var definitionsByTraffic = new Dictionary(Comparer); + foreach (var definition in network.TrafficTypes) + { + if (!string.IsNullOrWhiteSpace(definition.Name) && !definitionsByTraffic.ContainsKey(definition.Name)) + { + definitionsByTraffic.Add(definition.Name, definition); + } + } + var contexts = MixedRoutingAllocator.BuildStaticContexts(network, applyLocalAllocations: !hasRecipeDependencies).ToList(); - var remainingCapacityByEdgeId = network.Edges.ToDictionary( - edge => edge.Id, - edge => edge.Capacity ?? double.PositiveInfinity, - Comparer); - var remainingTranshipmentCapacityByNodeId = network.Nodes.ToDictionary( - node => node.Id, - node => node.TranshipmentCapacity ?? double.PositiveInfinity, - Comparer); + + var remainingCapacityByEdgeId = new Dictionary(network.Edges.Count, Comparer); + foreach (var edge in network.Edges) + { + remainingCapacityByEdgeId[edge.Id] = edge.Capacity ?? double.PositiveInfinity; + } + + var remainingTranshipmentCapacityByNodeId = new Dictionary(network.Nodes.Count, Comparer); + foreach (var node in network.Nodes) + { + remainingTranshipmentCapacityByNodeId[node.Id] = node.TranshipmentCapacity ?? double.PositiveInfinity; + } + var hasFiniteCapacities = network.Edges.Any(edge => edge.Capacity.HasValue) || network.Nodes.Any(node => node.TranshipmentCapacity.HasValue); if (hasRecipeDependencies) { - var contextsByTraffic = contexts.ToDictionary(context => context.TrafficType, context => context, Comparer); - var allocationOrder = BuildStaticRecipeCostOrder(network, contexts.Select(context => context.TrafficType).ToList()); - var sourceUnitCosts = contexts.ToDictionary( - context => context.TrafficType, - _ => new Dictionary(Comparer), - Comparer); - var landedUnitCosts = contexts.ToDictionary( - context => context.TrafficType, - _ => new Dictionary(Comparer), - Comparer); + // Bolt: Replaced multiple LINQ ToDictionary allocations with a single manual initialization loop. + var contextsByTraffic = new Dictionary(contexts.Count, Comparer); + var trafficTypes = new List(contexts.Count); + var sourceUnitCosts = new Dictionary>(contexts.Count, Comparer); + var landedUnitCosts = new Dictionary>(contexts.Count, Comparer); + + foreach (var context in contexts) + { + contextsByTraffic[context.TrafficType] = context; + trafficTypes.Add(context.TrafficType); + sourceUnitCosts[context.TrafficType] = new Dictionary(Comparer); + landedUnitCosts[context.TrafficType] = new Dictionary(Comparer); + } + + var allocationOrder = BuildStaticRecipeCostOrder(network, trafficTypes); foreach (var trafficType in allocationOrder) { @@ -318,9 +335,13 @@ private static string GetNodeLabel(NetworkModel network, string nodeId) private NetworkModel OrderNetworkForLayerProcessing(NetworkModel network) { - var order = layerResolver.GetSimulationOrder(network) - .Select((layer, index) => new { layer.Id, index }) - .ToDictionary(item => item.Id, item => item.index); + var orderedLayers = layerResolver.GetSimulationOrder(network); + var order = new Dictionary(orderedLayers.Count); + var index = 0; + foreach (var layer in orderedLayers) + { + order[layer.Id] = index++; + } return new NetworkModel {