fix bug in iperf update and improve logging
diff --git a/web/ons-demo/js/iperf.js b/web/ons-demo/js/iperf.js
new file mode 100644
index 0000000..c2abaa7
--- /dev/null
+++ b/web/ons-demo/js/iperf.js
@@ -0,0 +1,178 @@
+var enableIPerfLog = false;
+
+function iperfLog(message) {
+	if (enableIPerfLog) {
+		console.log(message);
+	}
+}
+
+function hasIPerf(flow) {
+	return flow && flow.iperfFetchTimeout;
+}
+
+function clearIPerf(flow) {
+	iperfLog('clearing iperf interval for: ' + flow.flowId);
+	clearTimeout(flow.iperfFetchTimeout);
+	delete flow.iperfFetchTimeout;
+	clearInterval(flow.iperfDisplayInterval);
+	delete flow.iperfDisplayInterval;
+	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 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;
+		function fetchData() {
+			iperfLog('Requesting iperf data');
+			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);
+
+				if (!flow.iperfData) {
+					iperfLog('iperf session closed for flow: ' + flow.id);
+					return;
+				}
+
+				try {
+					var iperfData = JSON.parse(data);
+
+//				iperfLog(iperfData.timestamp);
+
+					// if the data is fresh
+					if (!(flow.iperfData.timestamp && iperfData.timestamp != flow.iperfData.timestamp)) {
+						if (!flow.iperfData.timestamp) {
+							iperfLog('received first iperf buffer');
+						} else {
+							iperfLog('received duplicate iperf buffer with timestamp: ' + iperfData.timestamp);
+						}
+					} else {
+						iperfLog('received new iperf buffer with timstamp: ' + 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();
+						}
+
+						iperfLog('iperf buffer start time: ' + startTime);
+						if (lastTime && (startTime - lastTime) > updateRate/1000) {
+							iperfLog('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) {
+							iperfLog('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) {
+					iperfLog('bad iperf data: ' + data);
+				}
+				flow.iperfFetchTimeout = setTimeout(fetchData, updateRate*.25); // over sample to avoid gaps
+//				iperfLog(data);
+			});
+		}
+		fetchData();
+
+	}
+}
\ No newline at end of file