@@ -55,41 +55,50 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target
5555 if ( inDegree [ e . to ] !== undefined ) inDegree [ e . to ] ++ ;
5656 } ) ;
5757
58- // We will do a modified iterative propagation to handle cycles softly.
59- // Instead of strict topological sort, we do a fixed number of iterations (e.g. 5 passes)
60- // For pure DAGs, it propagates cleanly. For cycles, it stabilizes.
61-
58+ // Iterative propagation with convergence detection to handle cycles properly
59+ // We iterate until traffic distribution stabilizes or we hit maxIterations
60+
6261 // Initial Load
6362 sources . forEach ( s => {
6463 if ( nodeMetrics [ s . id ] ) {
6564 nodeMetrics [ s . id ] . trafficIn += targetRps / sources . length ;
6665 }
6766 } ) ;
6867
69- // 10 passes is enough for most UI architectures
70- for ( let pass = 0 ; pass < 10 ; pass ++ ) {
71- // Reset edge flows before recalculating this pass's spread
68+ const maxIterations = 100 ;
69+ const epsilon = 0.01 ; // Convergence threshold: stop when total change < epsilon
70+ const sourceIds = new Set ( sources . map ( s => s . id ) ) ;
71+
72+ for ( let iteration = 0 ; iteration < maxIterations ; iteration ++ ) {
73+ // Store previous trafficIn values to measure convergence
74+ const prevTrafficIn : Record < string , number > = { } ;
75+ Object . keys ( nodeMetrics ) . forEach ( id => {
76+ prevTrafficIn [ id ] = nodeMetrics [ id ] . trafficIn ;
77+ } ) ;
78+
79+ // Reset edge flows before recalculating this iteration's spread
7280 edges . forEach ( e => { edgeMetrics [ e . id ] . trafficFlow = 0 ; } ) ;
73-
74- // We need a snapshot of trafficIn for the current frame to distribute it properly
75- const sourceIds = new Set ( sources . map ( s => s . id ) ) ;
76- const currentTrafficIn = Object . keys ( nodeMetrics ) . reduce ( ( acc , id ) => {
77- acc [ id ] = nodeMetrics [ id ] . trafficIn ;
78- // Clear trafficIn for non-sources so they can receive the fresh wave
79- if ( ! sourceIds . has ( id ) ) {
80- nodeMetrics [ id ] . trafficIn = 0 ;
81+
82+ // Create next iteration's trafficIn map, starting with sources
83+ const nextTrafficIn : Record < string , number > = { } ;
84+ sourceIds . forEach ( id => {
85+ nextTrafficIn [ id ] = targetRps / sources . length ;
86+ } ) ;
87+ nodes . forEach ( n => {
88+ if ( ! sourceIds . has ( n . id ) ) {
89+ nextTrafficIn [ n . id ] = 0 ;
8190 }
82- return acc ;
83- } , { } as Record < string , number > ) ;
91+ } ) ;
8492
93+ // Process each node: compute outFlow and distribute to children
8594 nodes . forEach ( node => {
8695 const metrics = nodeMetrics [ node . id ] ;
87- const processingTraffic = currentTrafficIn [ node . id ] ;
88-
96+ const processingTraffic = prevTrafficIn [ node . id ] ;
97+
8998 // Cap out at capacity
9099 const outFlow = Math . min ( processingTraffic , metrics . capacity ) ;
91100 metrics . trafficOut = outFlow ;
92-
101+
93102 // Update status
94103 if ( processingTraffic > metrics . capacity ) {
95104 metrics . status = 'bottlenecked' ;
@@ -105,12 +114,28 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target
105114 const flowPerEdge = outFlow / outgoingEdges . length ;
106115 outgoingEdges . forEach ( edge => {
107116 edgeMetrics [ edge . id ] . trafficFlow += flowPerEdge ;
108- if ( nodeMetrics [ edge . to ] ) {
109- nodeMetrics [ edge . to ] . trafficIn += flowPerEdge ;
117+ if ( nextTrafficIn [ edge . to ] !== undefined ) {
118+ nextTrafficIn [ edge . to ] += flowPerEdge ;
110119 }
111120 } ) ;
112121 }
113122 } ) ;
123+
124+ // Update nodeMetrics with nextTrafficIn
125+ Object . keys ( nextTrafficIn ) . forEach ( id => {
126+ nodeMetrics [ id ] . trafficIn = nextTrafficIn [ id ] ;
127+ } ) ;
128+
129+ // Measure total delta to check for convergence
130+ let totalDelta = 0 ;
131+ Object . keys ( nodeMetrics ) . forEach ( id => {
132+ totalDelta += Math . abs ( nodeMetrics [ id ] . trafficIn - prevTrafficIn [ id ] ) ;
133+ } ) ;
134+
135+ // Stop if converged
136+ if ( totalDelta < epsilon ) {
137+ break ;
138+ }
114139 }
115140
116141 // Evaluate Global Status
@@ -120,4 +145,4 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target
120145 else if ( bottleneckCount > 0 ) globalStatus = 'degraded' ;
121146
122147 return { nodeMetrics, edgeMetrics, globalStatus } ;
123- }
148+ }
0 commit comments