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 @@ -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<TKey> 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.
71 changes: 71 additions & 0 deletions patch.diff
Original file line number Diff line number Diff line change
@@ -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<string, TrafficTypeDefinition>(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<string, double>(network.Edges.Count, Comparer);
+ foreach (var edge in network.Edges)
+ {
+ remainingCapacityByEdgeId[edge.Id] = edge.Capacity ?? double.PositiveInfinity;
+ }
+
+ var remainingTranshipmentCapacityByNodeId = new Dictionary<string, double>(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<string, double>(Comparer),
- Comparer);
- var landedUnitCosts = contexts.ToDictionary(
- context => context.TrafficType,
- _ => new Dictionary<string, double>(Comparer),
- Comparer);
+ var contextsByTraffic = new Dictionary<string, RoutingTrafficContext>(contexts.Count, Comparer);
+ var trafficTypes = new List<string>(contexts.Count);
+ var sourceUnitCosts = new Dictionary<string, Dictionary<string, double>>(contexts.Count, Comparer);
+ var landedUnitCosts = new Dictionary<string, Dictionary<string, double>>(contexts.Count, Comparer);
+
+ foreach (var context in contexts)
+ {
+ contextsByTraffic[context.TrafficType] = context;
+ trafficTypes.Add(context.TrafficType);
+ sourceUnitCosts[context.TrafficType] = new Dictionary<string, double>(Comparer);
+ landedUnitCosts[context.TrafficType] = new Dictionary<string, double>(Comparer);
+ }
+
+ var allocationOrder = BuildStaticRecipeCostOrder(network, trafficTypes);

foreach (var trafficType in allocationOrder)
71 changes: 46 additions & 25 deletions src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,34 +39,51 @@ public IReadOnlyList<TrafficSimulationOutcome> 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<string, TrafficTypeDefinition>(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<string, double>(network.Edges.Count, Comparer);
foreach (var edge in network.Edges)
{
remainingCapacityByEdgeId[edge.Id] = edge.Capacity ?? double.PositiveInfinity;
}

var remainingTranshipmentCapacityByNodeId = new Dictionary<string, double>(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<string, double>(Comparer),
Comparer);
var landedUnitCosts = contexts.ToDictionary(
context => context.TrafficType,
_ => new Dictionary<string, double>(Comparer),
Comparer);
// Bolt: Replaced multiple LINQ ToDictionary allocations with a single manual initialization loop.
var contextsByTraffic = new Dictionary<string, RoutingTrafficContext>(contexts.Count, Comparer);
var trafficTypes = new List<string>(contexts.Count);
var sourceUnitCosts = new Dictionary<string, Dictionary<string, double>>(contexts.Count, Comparer);
var landedUnitCosts = new Dictionary<string, Dictionary<string, double>>(contexts.Count, Comparer);

foreach (var context in contexts)
{
contextsByTraffic[context.TrafficType] = context;
trafficTypes.Add(context.TrafficType);
sourceUnitCosts[context.TrafficType] = new Dictionary<string, double>(Comparer);
landedUnitCosts[context.TrafficType] = new Dictionary<string, double>(Comparer);
}

var allocationOrder = BuildStaticRecipeCostOrder(network, trafficTypes);

foreach (var trafficType in allocationOrder)
{
Expand Down Expand Up @@ -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<Guid, int>(orderedLayers.Count);
var index = 0;
foreach (var layer in orderedLayers)
{
order[layer.Id] = index++;
}

return new NetworkModel
{
Expand Down