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
7 changes: 7 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,10 @@
## 2024-06-15 - Optimize Dictionary allocations in C# hot loops
**Learning:** In C#, LINQ `.ToDictionary` allocates a new dictionary, delegates, and enumerators. Using `.Any()` afterwards also introduces another O(N) pass.
**Action:** Replace `.ToDictionary` and subsequent `.Any()` combinations with a manual `Dictionary` pre-allocated by count, populated via a `foreach` loop, and track boolean flags (e.g., `hasFiniteEdges`) inside the same loop to avoid multiple iterations and delegate allocations.
## 2024-06-15 - Optimize Dictionary allocations in C# hot loops
**Learning:** In C#, LINQ `.ToDictionary` allocates a new dictionary, delegates, and enumerators. Using `.Any()` afterwards also introduces another O(N) pass.
**Action:** Replace `.ToDictionary` and subsequent `.Any()` combinations with a manual `Dictionary` pre-allocated by count, populated via a `foreach` loop, and track boolean flags (e.g., `hasFiniteEdges`) inside the same loop to avoid multiple iterations and delegate allocations.

## 2024-06-18 - Replacing LINQ ToDictionary causes correctness regression on math operations
**Learning:** When refactoring a LINQ `.GroupBy` and `.ToDictionary` call (like in `SummarizeLandedUnitCosts`), changing the calculation inside the aggregation loop (e.g. accidentally changing `DeliveredCostPerUnit * Quantity` to `TotalMovementCost`) breaks fundamental simulation results because these domain concepts are distinct.
**Action:** Be extremely cautious when rewriting aggregation logic in standard loops. Ensure the math inside the `else` / `TryGetValue` block precisely mirrors the original LINQ delegate calculation without substituting properties.
88 changes: 54 additions & 34 deletions src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,17 @@ 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() with manual foreach to avoid enumerator and delegate allocations
var definitionsByTraffic = new Dictionary<string, TrafficTypeDefinition>(Comparer);
foreach (var def in network.TrafficTypes)
{
if (!string.IsNullOrWhiteSpace(def.Name) && !definitionsByTraffic.ContainsKey(def.Name))
{
definitionsByTraffic[def.Name] = def;
}
}

var contexts = MixedRoutingAllocator.BuildStaticContexts(network, applyLocalAllocations: !hasRecipeDependencies).ToList();
// Bolt: Replaced LINQ .ToDictionary() with manual foreach to avoid enumerator and delegate allocations
var remainingCapacityByEdgeId = new Dictionary<string, double>(network.Edges.Count, Comparer);
Expand Down Expand Up @@ -71,16 +78,21 @@ public IReadOnlyList<TrafficSimulationOutcome> Simulate(NetworkModel network)

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 LINQ .ToDictionary() with manual loops to avoid enumerator and delegate allocations
var contextsByTraffic = new Dictionary<string, RoutingTrafficContext>(contexts.Count, Comparer);
var sourceUnitCosts = new Dictionary<string, Dictionary<string, double>>(contexts.Count, Comparer);
var landedUnitCosts = new Dictionary<string, Dictionary<string, double>>(contexts.Count, Comparer);
var trafficTypesList = new List<string>(contexts.Count);

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

var allocationOrder = BuildStaticRecipeCostOrder(network, trafficTypesList);

foreach (var trafficType in allocationOrder)
{
Expand Down Expand Up @@ -332,9 +344,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);
// Bolt: Replaced LINQ Select().ToDictionary() with a manual loop to avoid enumerator and delegate allocations
var simulationOrderList = layerResolver.GetSimulationOrder(network);
var order = new Dictionary<Guid, int>(simulationOrderList.Count);
for (var index = 0; index < simulationOrderList.Count; index++)
{
order[simulationOrderList[index].Id] = index;
}

return new NetworkModel
{
Expand Down Expand Up @@ -736,24 +752,28 @@ private static double ResolveBaseProductionCost(NodeTrafficProfile? profile, Tra

private static Dictionary<string, double> SummarizeLandedUnitCosts(IEnumerable<RouteAllocation> allocations)
{
return allocations
.GroupBy(allocation => allocation.ConsumerNodeId, Comparer)
.ToDictionary(
group => group.Key,
group =>
{
// Bolt: Accumulate quantity and total cost in a single loop to avoid multiple O(N) LINQ enumerations and delegate allocations
var quantity = 0d;
var totalCost = 0d;
foreach (var allocation in group)
{
quantity += allocation.Quantity;
totalCost += allocation.DeliveredCostPerUnit * allocation.Quantity;
}
// Bolt: Replaced GroupBy and ToDictionary with manual Dictionary accumulations to avoid all delegate and enumerator overhead
var costsByConsumer = new Dictionary<string, (double Quantity, double TotalCost)>(Comparer);

foreach (var allocation in allocations)
{
if (costsByConsumer.TryGetValue(allocation.ConsumerNodeId, out var current))
{
costsByConsumer[allocation.ConsumerNodeId] = (current.Quantity + allocation.Quantity, current.TotalCost + allocation.DeliveredCostPerUnit * allocation.Quantity);
}
else
{
costsByConsumer[allocation.ConsumerNodeId] = (allocation.Quantity, allocation.DeliveredCostPerUnit * allocation.Quantity);
}
}

return quantity > Epsilon ? totalCost / quantity : 0d;
},
Comparer);
var result = new Dictionary<string, double>(costsByConsumer.Count, Comparer);
foreach (var pair in costsByConsumer)
{
result[pair.Key] = pair.Value.Quantity > Epsilon ? pair.Value.TotalCost / pair.Value.Quantity : 0d;
}

return result;
}

private static TrafficContext BuildContext(NetworkModel network, TrafficTypeDefinition definition)
Expand Down
34 changes: 30 additions & 4 deletions src/MedWNetworkSim.App/Services/TemporalNetworkSimulationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ public TemporalSimulationState Initialize(NetworkModel network)
network = executionCache.GetStaticContext(network).EffectiveNetwork;

var state = new TemporalSimulationState();
var definitionsByTraffic = network.TrafficTypes.ToDictionary(definition => definition.Name, definition => definition, Comparer);

// Bolt: Replaced LINQ .ToDictionary() with manual foreach to avoid enumerator and delegate allocations
var definitionsByTraffic = new Dictionary<string, TrafficTypeDefinition>(network.TrafficTypes.Count, Comparer);
foreach (var def in network.TrafficTypes)
{
definitionsByTraffic[def.Name] = def;
}

foreach (var node in network.Nodes)
{
foreach (var profile in node.TrafficProfiles)
Expand Down Expand Up @@ -1060,6 +1067,25 @@ private static void ApplyLocalAllocations(

private static RoutingTrafficContext ToRoutingContext(TemporalTrafficContext context)
{
// Bolt: Replaced LINQ .ToDictionary() with manual foreach to avoid enumerator and delegate allocations
var supply = new Dictionary<string, double>(context.Supply.Count, Comparer);
foreach (var pair in context.Supply)
{
supply[pair.Key] = pair.Value;
}

var supplyUnitCosts = new Dictionary<string, double>(context.SupplyUnitCosts.Count, Comparer);
foreach (var pair in context.SupplyUnitCosts)
{
supplyUnitCosts[pair.Key] = pair.Value;
}

var demand = new Dictionary<string, double>(context.Demand.Count, Comparer);
foreach (var pair in context.Demand)
{
demand[pair.Key] = pair.Value;
}

return new RoutingTrafficContext
{
TrafficType = context.TrafficType,
Expand All @@ -1072,9 +1098,9 @@ private static RoutingTrafficContext ToRoutingContext(TemporalTrafficContext con
Seed = context.Seed,
NodesById = context.NodesById,
ProfilesByNodeId = context.ProfilesByNodeId,
Supply = context.Supply.ToDictionary(pair => pair.Key, pair => pair.Value, Comparer),
SupplyUnitCosts = context.SupplyUnitCosts.ToDictionary(pair => pair.Key, pair => pair.Value, Comparer),
Demand = context.Demand.ToDictionary(pair => pair.Key, pair => pair.Value, Comparer),
Supply = supply,
SupplyUnitCosts = supplyUnitCosts,
Demand = demand,
MeetingDemandEligibleNodeIds = context.MeetingDemandEligibleNodeIds
};
}
Expand Down