Merge pull request #377 from pgreyson/master

Bug fixes for iperf display
diff --git a/web/ons-demo/RELEASE_NOTES.txt b/web/ons-demo/RELEASE_NOTES.txt
index 3616d22..19d5e58 100644
--- a/web/ons-demo/RELEASE_NOTES.txt
+++ b/web/ons-demo/RELEASE_NOTES.txt
@@ -1,3 +1,7 @@
+** April 12, 2013 **
+- fixed bug in iperf fetch
+- improved iperf logging
+
 ** April 11, 2013 **
 - Use timestamps for iperf display
 This elimates spurious gaps when server responds slowly. However, gaps still appear if the server drops buffers entirely
diff --git a/web/ons-demo/index.html b/web/ons-demo/index.html
index 460188f..ed31bee 100644
--- a/web/ons-demo/index.html
+++ b/web/ons-demo/index.html
@@ -76,6 +76,7 @@
 
 <script src="js/topologyactions.js"></script>
 <script src="js/topology.js"></script>
+<script src="js/iperf.js"></script>
 <script src="js/flows.js"></script>
 <script src="js/init.js"></script>
 <script src="js/app.js"></script>
diff --git a/web/ons-demo/js/flows.js b/web/ons-demo/js/flows.js
index e21af0d..9f72fe2 100644
--- a/web/ons-demo/js/flows.js
+++ b/web/ons-demo/js/flows.js
@@ -1,6 +1,9 @@
 function startFlowAnimation(flow) {
-	if (flow.select('animate').empty()) {
-		flow.append('svg:animate')
+//	console.log('starting animation for flow: ' + flow.flowId);
+
+	var flowSelection = d3.select(document.getElementById(makeFlowKey(flow)));
+	if (flowSelection.select('animate').empty()) {
+		flowSelection.append('svg:animate')
 			.attr('attributeName', 'stroke-dashoffset')
 			.attr('attributeType', 'xml')
 			.attr('from', '500')
@@ -11,7 +14,8 @@
 }
 
 function stopFlowAnimation(flow) {
-	flow.select('animate').remove();
+	var flowSelection = d3.select(document.getElementById(makeFlowKey(flow)));
+	flowSelection.select('animate').remove();
 }
 
 
@@ -24,7 +28,9 @@
 		}
 	});
 
-	var flows = flowLayer.selectAll('.flow').data(topologyFlows);
+	var flows = flowLayer.selectAll('.flow').data(topologyFlows, function (d) {
+		return d.flowId;
+	});
 
 	flows.enter().append("svg:path").attr('class', 'flow')
 		.attr('stroke-dasharray', '4, 10')
@@ -200,141 +206,6 @@
 	flows.exit().remove();
 }
 
-function startIPerfForFlow(flow) {
-	var duration = 10000; // seconds
-	var interval = 100; // ms. this is defined by the server
-	var updateRate = 2000; // ms
-	var pointsToDisplay = 1000;
-
-	function makeGraph(iperfData) {
-		var d = 'M0,0';
-
-		var now = flow.iperfData.startTime + (Date.now() - flow.iperfData.localNow)/1000;
-
-		if (iperfData.samples && iperfData.samples.length) {
-
-			var lastX;
-			var i = iperfData.samples.length - 1;
-			while (i) {
-				var sample = iperfData.samples[i];
-
-				var x = (1000 - (now - sample.time)*10);
-				// workaround for discontinuity in iperf data
-				if (x < 0) {
-					i -= 1;
-					continue;
-				}
-
-				var y = 28 * sample.value/1000000;
-				if (y > 28) {
-					y = 28;
-				}
-				if (i == iperfData.samples.length - 1) {
-					d = 'M' + x + ',30';
-				}
-
-				// handle gaps
-				// 1.5 for rounding error
-				if (lastX && lastX - x > 1.5) {
-					d += 'L' + lastX + ',30';
-					d += 'M' + x + ',30'
-				}
-				lastX = x;
-
-				d += 'L' + x + ',' + (30-y);
-
-				i -= 1;
-			}
-			d += 'L' + lastX + ',30';
-		}
-		return d;
-	}
-
-	if (flow.flowId) {
-		console.log('starting iperf for: ' + flow.flowId);
-		startIPerf(flow, duration, updateRate/interval);
-		flow.iperfDisplayInterval = setInterval(function () {
-			if (flow.iperfData) {
-				var iperfPath = d3.select(document.getElementById(makeSelectedFlowKey(flow))).select('path');
-				iperfPath.attr('d', makeGraph(flow.iperfData));
-			}
-
-
-		}, interval);
-
-		var animationTimeout;
-		flow.iperfData = {
-			samples: []
-		}
-
-		var lastTime;
-		flow.iperfFetchInterval = setInterval(function () {
-			console.log('Requesting iperf data');
-			getIPerfData(flow, function (data) {
-				try {
-					var iperfData = JSON.parse(data);
-
-//				console.log(iperfData.timestamp);
-
-					// if the data is fresh
-					if (flow.iperfData.timestamp && iperfData.timestamp != flow.iperfData.timestamp) {
-
-						var flowSelection = d3.select(document.getElementById(makeFlowKey(flow)));
-						startFlowAnimation(flowSelection);
-						clearTimeout(animationTimeout);
-						// kill the animation if iperfdata stops flowing
-						animationTimeout = setTimeout(function () {
-							stopFlowAnimation(flowSelection);
-						}, updateRate*1.5);
-
-						var endTime = Math.floor(iperfData['end-time']*10)/10;
-
-						var startTime = endTime - (iperfData.samples.length * interval/1000);
-						// set now on the first buffer
-						if (!flow.iperfData.startTime) {
-							flow.iperfData.startTime = startTime;
-							flow.iperfData.localNow = Date.now();
-						}
-
-						console.log('iperf buffer start time: ' + startTime);
-						if (lastTime && (startTime - lastTime) > updateRate/1000) {
-							console.log('iperf buffer gap: ' + startTime + ',' + lastTime);
-						}
-						lastTime = startTime;
-
-						// clear out the old data
-						while (flow.iperfData.samples.length > pointsToDisplay + iperfData.samples.length) {
-							flow.iperfData.samples.shift();
-						}
-
-						// if the client gets too out of sync, resynchronize
-						var clientNow = flow.iperfData.startTime + (Date.now() - flow.iperfData.localNow)/1000;
-						if (Math.abs(clientNow - startTime) > (updateRate/1000) * 2) {
-							console.log('resynchronizing now: ' + clientNow + ' => ' + startTime);
-							flow.iperfData.startTime = startTime;
-							flow.iperfData.localNow = Date.now();
-						}
-
-						var time = startTime;
-						iperfData.samples.forEach(function (s) {
-							var sample = {
-								time: time,
-								value: s
-							};
-							flow.iperfData.samples.push(sample);
-							time += interval/1000;
-						});
-					}
-					flow.iperfData.timestamp = iperfData.timestamp;
-				} catch (e) {
-					console.log('bad iperf data: ' + data);
-				}
-//				console.log(data);
-			});
-		}, updateRate*.5); // over sample to avoid gaps
-	}
-}
-
 function updateSelectedFlows() {
 	// make sure that all of the selected flows are either
 	// 1) valid (meaning they are in the latest list of flows)
@@ -350,10 +221,13 @@
 			if (flow) {
 				var liveFlow = flowMap[makeFlowKey(flow)];
 				if (liveFlow) {
-					newSelectedFlows.push(liveFlow);
-					liveFlow.deletePending = flow.deletePending;
-					liveFlow.iperfFetchInterval = flow.iperfFetchInterval;
-					liveFlow.iperfDisplayInterval = flow.iperfDisplayInterval;
+					flow.flowId = liveFlow.flowId;
+					if (flow.createPending) {
+						startIPerfForFlow(flow);
+						flow.createPending = false;
+					}
+					flow.dataPath = liveFlow.dataPath;
+					newSelectedFlows.push(flow);
 				} else if (flow.createPending) {
 					newSelectedFlows.push(flow);
 				} else if (hasIPerf(flow)) {
@@ -395,19 +269,6 @@
 	}
 }
 
-function hasIPerf(flow) {
-	return flow && flow.iperfFetchInterval;
-}
-
-function clearIPerf(flow) {
-	console.log('clearing iperf interval for: ' + flow.flowId);
-	clearInterval(flow.iperfFetchInterval);
-	delete flow.iperfFetchInterval;
-	clearInterval(flow.iperfDisplayInterval);
-	delete flow.iperfDisplayInterval;
-	delete flow.iperfData;
-}
-
 function deselectFlow(flow, ifCreatePending) {
 	if (!flow) {
 		return;
diff --git a/web/ons-demo/js/iperf.js b/web/ons-demo/js/iperf.js
new file mode 100644
index 0000000..b1f964c
--- /dev/null
+++ b/web/ons-demo/js/iperf.js
@@ -0,0 +1,190 @@
+var enableIPerfLog = false;
+
+function iperfLog(message, flow) {
+	if (enableIPerfLog) {
+		console.log('flow: ' + flow.flowId + '===>' + message);
+	}
+}
+
+function hasIPerf(flow) {
+	return flow && flow.iperfFetchTimeout;
+}
+
+function clearIPerf(flow) {
+	iperfLog('clearing iperf interval for: ' + flow.flowId, flow);
+	clearTimeout(flow.iperfFetchTimeout);
+	delete flow.iperfFetchTimeout;
+	clearInterval(flow.iperfDisplayInterval);
+	delete flow.iperfDisplayInterval;
+	clearTimeout(flow.animationTimeout);
+	delete flow.animationTimeout;
+	stopFlowAnimation(flow);
+	delete flow.iperfData.timestamp;
+
+	delete flow.iperfData;
+}
+
+function startIPerfForFlow(flow) {
+	var duration = 10000; // seconds
+	var interval = 100; // ms. this is defined by the server
+	var updateRate = 3000; // ms
+	var pointsToDisplay = 1000;
+
+	function makeGraph(iperfData) {
+		var d = 'M0,0';
+
+		var now = flow.iperfData.startTime + (Date.now() - flow.iperfData.localNow)/1000;
+
+		if (iperfData.samples && iperfData.samples.length) {
+
+			var lastX;
+			var i = iperfData.samples.length - 1;
+			while (i) {
+				var sample = iperfData.samples[i];
+
+				var x = (1000 - (now - sample.time)*10);
+				// workaround for discontinuity in iperf data
+				if (x < 0) {
+					i -= 1;
+					continue;
+				}
+
+				var y = 28 * sample.value/1000000;
+				if (y > 28) {
+					y = 28;
+				}
+				if (i == iperfData.samples.length - 1) {
+					d = 'M' + x + ',30';
+				}
+
+				// handle gaps
+				// 1.5 for rounding error
+				if (lastX && lastX - x > 1.5) {
+					d += 'L' + lastX + ',30';
+					d += 'M' + x + ',30'
+				}
+				lastX = x;
+
+				d += 'L' + x + ',' + (30-y);
+
+				i -= 1;
+			}
+			d += 'L' + lastX + ',30';
+		}
+		return d;
+	}
+
+	if (flow.flowId) {
+		iperfLog('starting iperf', flow);
+		startIPerf(flow, duration, updateRate/interval);
+
+		flow.iperfDisplayInterval = setInterval(function () {
+			if (flow.iperfData) {
+				var iperfPath = d3.select(document.getElementById(makeSelectedFlowKey(flow))).select('path');
+				flow.iperfData.samples.sort(function (a, b) {
+					return a.time - b.time;
+				});
+				iperfPath.attr('d', makeGraph(flow.iperfData));
+			}
+
+
+		}, interval);
+
+		var animationTimeout;
+		flow.iperfData = {
+			samples: []
+		}
+
+		function resetFlowAnimationTimeout() {
+			clearTimeout(flow.animationTimeout);
+			// kill the animation if iperfdata stops flowing
+			flow.animationTimeout = setTimeout(function () {
+				stopFlowAnimation(flow);
+			}, updateRate*1.5);
+		}
+
+		var lastTime;
+		function fetchData() {
+			iperfLog('Requesting iperf data', flow);
+			var fetchTime = Date.now();
+			getIPerfData(flow, function (data) {
+				var requestTime = Date.now() - fetchTime;
+				var requestTimeMessage = 'iperf request completed in: ' + requestTime + 'ms';
+				if (requestTime > 1000) {
+					requestTimeMessage = requestTimeMessage.toUpperCase();
+				}
+				iperfLog(requestTimeMessage, flow);
+
+				if (!flow.iperfData) {
+					iperfLog('iperf session closed', flow);
+					return;
+				}
+
+				try {
+					var iperfData = JSON.parse(data);
+
+//				iperfLog(iperfData.timestamp, flow);
+
+					// if the data is fresh
+					if (!(flow.iperfData.timestamp && iperfData.timestamp != flow.iperfData.timestamp)) {
+						if (!flow.iperfData.timestamp) {
+							iperfLog('received first iperf buffer', flow);
+						} else {
+							iperfLog('received duplicate iperf buffer with timestamp: ' + iperfData.timestamp, flow);
+						}
+					} else {
+						iperfLog('received new iperf buffer with timstamp: ' + iperfData.timestamp, flow);
+						startFlowAnimation(flow);
+						resetFlowAnimationTimeout();
+
+						var endTime = Math.floor(iperfData['end-time']*10)/10;
+
+						var startTime = endTime - (iperfData.samples.length * interval/1000);
+						// set now on the first buffer
+						if (!flow.iperfData.startTime) {
+							flow.iperfData.startTime = startTime;
+							flow.iperfData.localNow = Date.now();
+						}
+
+						iperfLog('iperf buffer start time: ' + startTime, flow);
+						if (lastTime && (startTime - lastTime) > updateRate/1000) {
+							iperfLog('iperf buffer gap: ' + (startTime - lastTime), flow);
+						}
+						lastTime = startTime;
+
+						// clear out the old data
+						while (flow.iperfData.samples.length > pointsToDisplay + iperfData.samples.length) {
+							flow.iperfData.samples.shift();
+						}
+
+						// if the client gets too out of sync, resynchronize
+						var clientNow = flow.iperfData.startTime + (Date.now() - flow.iperfData.localNow)/1000;
+						if (Math.abs(clientNow - startTime) > (updateRate/1000) * 2) {
+							iperfLog('resynchronizing now: ' + clientNow + ' => ' + startTime, flow);
+							flow.iperfData.startTime = startTime;
+							flow.iperfData.localNow = Date.now();
+						}
+
+						var time = startTime;
+						iperfData.samples.forEach(function (s) {
+							var sample = {
+								time: time,
+								value: s
+							};
+							flow.iperfData.samples.push(sample);
+							time += interval/1000;
+						});
+					}
+					flow.iperfData.timestamp = iperfData.timestamp;
+				} catch (e) {
+					iperfLog('bad iperf data: ' + data, flow);
+				}
+				flow.iperfFetchTimeout = setTimeout(fetchData, updateRate*.25); // over sample to avoid gaps
+//				iperfLog(data, flow);
+			});
+		}
+
+		// wait a buffer to make sure the old iperf session gets cleared out
+		flow.iperfFetchTimeout = setTimeout(fetchData, updateRate);
+	}
+}
\ No newline at end of file
diff --git a/web/ons-demo/js/map.js b/web/ons-demo/js/map.js
index ad590cb..256162d 100644
--- a/web/ons-demo/js/map.js
+++ b/web/ons-demo/js/map.js
@@ -344,10 +344,7 @@
 }
 
 function switchesEnter(switches) {
-	switchLayer.selectAll('g').data(switches, function (d) {
-		return d.dpid;
-	})
-		.enter()
+	switches.enter()
 			.append('svg:g')
 				.attr("id", function (d) {
 					return d.dpid;
@@ -363,9 +360,7 @@
 
 
 function switchesUpdate(switches) {
-	switchLayer.selectAll('g').data(switches, function (d) {
-		return d.dpid;
-	}).each(function (d) {
+	switches.each(function (d) {
 			// if there's a pending state changed and then the state changes, clear the pending class
 			var circle = d3.select(this);
 			if (d.state === 'ACTIVE' && circle.classed('inactive') ||
@@ -392,8 +387,10 @@
 	var aggregationSwitches = makeSwitchesModel(model.aggregationSwitches, 'aggregation');
 	var edgeSwitches = makeSwitchesModel(model.edgeSwitches, 'edge');
 
-	var switches = coreSwitches.concat(aggregationSwitches).concat(edgeSwitches);
-
+	var switches = 	switchLayer.selectAll('g')
+		.data(coreSwitches.concat(aggregationSwitches).concat(edgeSwitches), function (d) {
+			return d.dpid;
+		});
 	switchesEnter(switches)
 	switchesUpdate(switches);