From 85f29712f79fdd6bda91f96bba831b514441d3ad Mon Sep 17 00:00:00 2001 From: Michael Farb Date: Thu, 5 Dec 2019 15:19:20 -0500 Subject: [PATCH] [webapp] Add Latency Metrics - Add background echo requests for paths on `Apps`. - STMP echo latency results in purple next to PATHs list tree. - STMP traceroute latency results in purple next to PATHs list tree. - PATHs list tree shows Last RTT e2e latency results. - PATHs interfaces list tree shows Last RTT hop latency results. - Path graph arrows shows Min/Best RTT hop latency results. - Provides ability to test survey all paths using `Run Continuous`. - Closes #26, providing auto-path switching. - Fixes traceroute DB query bug which ignored some results. --- webapp/lib/echocont.go | 2 - webapp/lib/sciond.go | 1 + webapp/lib/traceroutecont.go | 5 +- webapp/models/traceroute.go | 160 ++++++++++---------- webapp/tests/asviz/bwtester-d.json | 16 ++ webapp/tests/asviz/echo-d.json | 15 ++ webapp/tests/asviz/traceroute-d.json | 54 +++++++ webapp/web/static/css/style-viz.css | 10 ++ webapp/web/static/js/asviz.js | 22 ++- webapp/web/static/js/tab-paths.js | 213 ++++++++++++++++++++++++--- webapp/web/static/js/tab-topocola.js | 52 ++++++- webapp/web/static/js/webapp.js | 187 +++++++++++++++++------ webapp/web/template/index.html | 12 +- webapp/webapp.go | 52 ++++--- 14 files changed, 625 insertions(+), 176 deletions(-) create mode 100644 webapp/tests/asviz/bwtester-d.json create mode 100644 webapp/tests/asviz/echo-d.json create mode 100644 webapp/tests/asviz/traceroute-d.json diff --git a/webapp/lib/echocont.go b/webapp/lib/echocont.go index f6913159a..e569c8321 100644 --- a/webapp/lib/echocont.go +++ b/webapp/lib/echocont.go @@ -42,8 +42,6 @@ func ExtractEchoRespData(resp string, d *model.EchoItem, start time.Time) { // store current epoch in ms d.Inserted = time.Now().UnixNano() / 1e6 - log.Info("resp response", "content", resp) - var data = make(map[string]float32) // -1 if no match for response time, indicating response timeout or packets out of order data["response_time"] = -1 diff --git a/webapp/lib/sciond.go b/webapp/lib/sciond.go index d5d5332ab..984122a72 100644 --- a/webapp/lib/sciond.go +++ b/webapp/lib/sciond.go @@ -52,6 +52,7 @@ var cNodes string var cGeoLoc string func returnError(w http.ResponseWriter, err error) { + log.Error("Error:", "err", err) fmt.Fprintf(w, `{"err":`+strconv.Quote(err.Error())+`}`) } diff --git a/webapp/lib/traceroutecont.go b/webapp/lib/traceroutecont.go index e964652f9..39c17dc8b 100644 --- a/webapp/lib/traceroutecont.go +++ b/webapp/lib/traceroutecont.go @@ -41,8 +41,6 @@ func ExtractTracerouteRespData(resp string, d *model.TracerouteItem, start time. // store current epoch in ms d.Inserted = time.Now().UnixNano() / 1e6 - //log.Info("resp response", "content", resp) - var path, err string pathNext := false r := strings.Split(resp, "\n") @@ -144,7 +142,7 @@ func GetTracerouteByTimeHandler(w http.ResponseWriter, r *http.Request, active b returnError(w, err) return } - // log.Debug("Requested data:", "tracerouteResults", tracerouteResults) + log.Debug("Requested data:", "tracerouteResults", tracerouteResults) tracerouteJSON, err := json.Marshal(tracerouteResults) if CheckError(err) { @@ -156,7 +154,6 @@ func GetTracerouteByTimeHandler(w http.ResponseWriter, r *http.Request, active b jsonBuf = append(jsonBuf, json...) jsonBuf = append(jsonBuf, []byte(`}`)...) - //log.Debug(string(jsonBuf)) // ensure % if any, is escaped correctly before writing to printf formatter fmt.Fprintf(w, strings.Replace(string(jsonBuf), "%", "%%", -1)) } diff --git a/webapp/models/traceroute.go b/webapp/models/traceroute.go index 2642bbe72..c33cb487a 100644 --- a/webapp/models/traceroute.go +++ b/webapp/models/traceroute.go @@ -138,10 +138,10 @@ func createTracerouteTable() error { CAddr TEXT, SIa TEXT, SAddr TEXT, - Timeout REAL, - CmdOutput TEXT, - Error TEXT, - Path TEXT + Timeout REAL, + CmdOutput TEXT, + Error TEXT, + Path TEXT ); ` _, err := db.Exec(sqlCreateTable) @@ -153,13 +153,13 @@ func createTrHopTable() error { CREATE TABLE IF NOT EXISTS trhops( Inserted BIGINT NOT NULL PRIMARY KEY, RunTimeKey BIGINT, - Ord INT, - HopIa TEXT, - HopAddr TEXT, + Ord INT, + HopIa TEXT, + HopAddr TEXT, IntfID INT, - RespTime1 REAL, - RespTime2 REAL, - RespTime3 REAL + RespTime1 REAL, + RespTime2 REAL, + RespTime3 REAL ); ` _, err := db.Exec(sqlCreateTable) @@ -170,16 +170,16 @@ func createTrHopTable() error { func StoreTracerouteItem(tr *TracerouteItem) error { sqlInsert := ` INSERT INTO traceroute( - Inserted, + Inserted, ActualDuration, CIa, CAddr, SIa, SAddr, - Timeout, - CmdOutput, - Error, - Path + Timeout, + CmdOutput, + Error, + Path ) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` stmt, err := db.Prepare(sqlInsert) @@ -206,15 +206,15 @@ func StoreTracerouteItem(tr *TracerouteItem) error { func StoreTrHopItem(hop *TrHopItem) error { sqlInsert := ` INSERT INTO trhops( - Inserted, + Inserted, RunTimeKey, Ord, - HopIa, - HopAddr, + HopIa, + HopAddr, IntfID, - RespTime1, - RespTime2, - RespTime3 + RespTime1, + RespTime2, + RespTime3 ) values(?, ?, ?, ?, ?, ?, ?, ?, ?) ` stmt, err := db.Prepare(sqlInsert) @@ -239,18 +239,18 @@ func StoreTrHopItem(hop *TrHopItem) error { // ReadTracerouteItemsAll operates on the DB to return all traceroute rows. func ReadTracerouteItemsAll() ([]TracerouteItem, error) { sqlReadAll := ` - SELECT - Inserted, - ActualDuration, - CIa, - CAddr, - SIa, - SAddr, - Timeout, - CmdOutput, - Error, - Path - FROM traceroute + SELECT + Inserted, + ActualDuration, + CIa, + CAddr, + SIa, + SAddr, + Timeout, + CmdOutput, + Error, + Path + FROM traceroute ORDER BY datetime(Inserted) DESC ` rows, err := db.Query(sqlReadAll) @@ -285,36 +285,36 @@ func ReadTracerouteItemsAll() ([]TracerouteItem, error) { // which are more recent than the 'since' epoch in ms. func ReadTracerouteItemsSince(since string) ([]TracerouteGraph, error) { sqlReadSince := ` - SELECT - a.Inserted, - a.ActualDuration, - h.Ord, - h.HopIa, - h.HopAddr, - h.IntfID, - h.RespTime1, - h.RespTime2, - h.RespTime3, - a.CmdOutput, - a.Error, - a.Path - FROM ( - SELECT - Inserted, - ActualDuration, - CIa, - CAddr, - SIa, - SAddr, - Timeout, - CmdOutput, - Error, - Path - FROM traceroute - WHERE Inserted > ? - ) AS a - INNER JOIN trhops AS h ON a.Inserted = h.RunTimeKey - ORDER BY datetime(a.Inserted) DESC + SELECT + a.Inserted, + a.ActualDuration, + h.Ord, + h.HopIa, + h.HopAddr, + h.IntfID, + h.RespTime1, + h.RespTime2, + h.RespTime3, + a.CmdOutput, + a.Error, + a.Path + FROM ( + SELECT + Inserted, + ActualDuration, + CIa, + CAddr, + SIa, + SAddr, + Timeout, + CmdOutput, + Error, + Path + FROM traceroute + WHERE Inserted > ? + ) AS a + INNER JOIN trhops AS h ON a.Inserted = h.RunTimeKey + ORDER BY datetime(a.Inserted) DESC ` rows, err := db.Query(sqlReadSince, since) if err != nil { @@ -345,7 +345,6 @@ func ReadTracerouteItemsSince(since string) ([]TracerouteGraph, error) { } var graphEntries []TracerouteGraph - var lastEntryInsertedTime int64 // Store the infos of the last hop var inserted int64 @@ -353,23 +352,8 @@ func ReadTracerouteItemsSince(since string) ([]TracerouteGraph, error) { var trhops []ReducedTrHopItem var cmdOutput, path, errors string - for _, hop := range result { - if lastEntryInsertedTime == 0 { - lastEntryInsertedTime = hop.Inserted - trhops = nil - } else if lastEntryInsertedTime != hop.Inserted { - trg := TracerouteGraph{ - Inserted: inserted, - ActualDuration: actualDuration, - TrHops: trhops, - CmdOutput: cmdOutput, - Error: errors, - Path: path} - - lastEntryInsertedTime = hop.Inserted - graphEntries = append(graphEntries, trg) - trhops = nil - } + for i := 0; i < len(result); i++ { + hop := result[i] inserted = hop.Inserted actualDuration = hop.ActualDuration @@ -384,6 +368,20 @@ func ReadTracerouteItemsSince(since string) ([]TracerouteGraph, error) { RespTime2: hop.RespTime2, RespTime3: hop.RespTime3} trhops = append(trhops, rth) + + // append hops group at the end of each group + if (i+1) == len(result) || result[i+1].Inserted != hop.Inserted { + trg := TracerouteGraph{ + Inserted: inserted, + ActualDuration: actualDuration, + TrHops: trhops, + CmdOutput: cmdOutput, + Error: errors, + Path: path} + + graphEntries = append(graphEntries, trg) + trhops = nil + } } return graphEntries, nil diff --git a/webapp/tests/asviz/bwtester-d.json b/webapp/tests/asviz/bwtester-d.json new file mode 100644 index 000000000..cbc37ce16 --- /dev/null +++ b/webapp/tests/asviz/bwtester-d.json @@ -0,0 +1,16 @@ +{ + "graph": [ + { + "Inserted": 1562683731800, + "ActualDuration": 5041, + "CSBandwidth": 80000, + "CSThroughput": 80000, + "SCBandwidth": 80000, + "SCThroughput": 80000, + "Error": "", + "Path": "Hops: [1-ff00:0:111 103>4094 1-ff00:0:112] Mtu: 1450", + "Log": "t=2019-07-09T10:48:46-0400 lvl=dbug msg=\"Path selection algorithm choice\" path=\"Hops: [1-ff00:0:111 103>4094 1-ff00:0:112] Mtu: 1450\" score=0.993\nt=2019-07-09T10:48:46-0400 lvl=dbug msg=\"Registered with dispatcher\" addr=\"1-ff00:0:111,[127.0.0.1]:30001 (UDP)\"\nClient DC \tNext Hop [127.0.0.74]:31070\tServer Host [127.0.0.2]:30101\nt=2019-07-09T10:48:46-0400 lvl=dbug msg=\"Registered with dispatcher\" addr=\"1-ff00:0:111,[127.0.0.1]:30002 (UDP)\"\n\nTest parameters:\nclientDCAddr -> serverDCAddr 1-ff00:0:111,[127.0.0.1]:30002 (UDP) -> 1-ff00:0:112,[127.0.0.2]:30101 (UDP)\nclient->server: 3 seconds, 1000 bytes, 30 packets\nserver->client: 3 seconds, 1000 bytes, 30 packets\n\nS->C results\nAttempted bandwidth: 80000 bps / 0.08 Mbps\nAchieved bandwidth: 80000 bps / 0.08 Mbps\nLoss rate: 0 %\nInterarrival time variance: 23ms, average interarrival time: 103ms\nInterarrival time min: 82ms, interarrival time max: 127ms\nWe need to sleep for 2 seconds before we can get the results\n\nC->S results\nAttempted bandwidth: 80000 bps / 0.08 Mbps\nAchieved bandwidth: 80000 bps / 0.08 Mbps\nLoss rate: 0 %\nInterarrival time variance: 23ms, average interarrival time: 103ms\nInterarrival time min: 82ms, interarrival time max: 126ms\n" + } + ], + "active": false +} diff --git a/webapp/tests/asviz/echo-d.json b/webapp/tests/asviz/echo-d.json new file mode 100644 index 000000000..2204c9e78 --- /dev/null +++ b/webapp/tests/asviz/echo-d.json @@ -0,0 +1,15 @@ +{ + "graph": [ + { + "Inserted": 1562683883823, + "ActualDuration": 263, + "ResponseTime": 13.352, + "RunTime": 226.525, + "PktLoss": 0, + "CmdOutput": "Using path:\n Hops: [1-ff00:0:111 103>4094 1-ff00:0:112] Mtu: 1450\n104 bytes from 1-ff00:0:112,[127.0.0.2] scmp_seq=0 time=13.352ms\n\n--- 1-ff00:0:112,[[127.0.0.2]] statistics ---\n1 packets transmitted, 1 received, 0% packet loss, time 226.525ms\n", + "Path": "Hops: [1-ff00:0:111 103>4094 1-ff00:0:112] Mtu: 1450", + "Error": "" + } + ], + "active": false +} diff --git a/webapp/tests/asviz/traceroute-d.json b/webapp/tests/asviz/traceroute-d.json new file mode 100644 index 000000000..d9fbd6343 --- /dev/null +++ b/webapp/tests/asviz/traceroute-d.json @@ -0,0 +1,54 @@ +{ + "graph": [ + { + "Inserted": 1570192000394, + "ActualDuration": 35, + "TrHops": [ + { + "HopIa": "1-ff00:0:111", + "HopAddr": "127.0.0.17", + "IntfID": 41, + "RespTime1": 0.558, + "RespTime2": 0.439, + "RespTime3": 0.41 + }, + { + "HopIa": "1-ff00:0:110", + "HopAddr": "127.0.0.9", + "IntfID": 1, + "RespTime1": 0.577, + "RespTime2": 0.597, + "RespTime3": 0.601 + }, + { + "HopIa": "1-ff00:0:110", + "HopAddr": "127.0.0.10", + "IntfID": 2, + "RespTime1": 0.797, + "RespTime2": 0.666, + "RespTime3": 0.771 + }, + { + "HopIa": "1-ff00:0:112", + "HopAddr": "127.0.0.25", + "IntfID": 1, + "RespTime1": 1.18, + "RespTime2": 1.922, + "RespTime3": 1.322 + }, + { + "HopIa": "1-ff00:0:112", + "HopAddr": "127.0.0.2", + "IntfID": -1, + "RespTime1": 1.107, + "RespTime2": 0.899, + "RespTime3": 0.835 + } + ], + "CmdOutput": "Available paths to 1-ff00:0:112\n[ 0] Hops: [1-ff00:0:111 41>1 1-ff00:0:110 2>1 1-ff00:0:112] Mtu: 1280\nChoose path: Using path:\n Hops: [1-ff00:0:111 41>1 1-ff00:0:110 2>1 1-ff00:0:112] Mtu: 1280\n0 1-ff00:0:111,[127.0.0.17] IfID=41 558µs 439µs 410µs\n1 1-ff00:0:110,[127.0.0.9] IfID=1 577µs 597µs 601µs\n2 1-ff00:0:110,[127.0.0.10] IfID=2 797µs 666µs 771µs\n3 1-ff00:0:112,[127.0.0.25] IfID=1 1.18ms 1.922ms 1.322ms\n4 1-ff00:0:112,[127.0.0.2] 1.107ms 899µs 835µs\n", + "Error": "", + "Path": "Hops: [1-ff00:0:111 41>1 1-ff00:0:110 2>1 1-ff00:0:112] Mtu: 1280" + } + ], + "active": false +} diff --git a/webapp/web/static/css/style-viz.css b/webapp/web/static/css/style-viz.css index 1baad7b07..1e8e3599f 100644 --- a/webapp/web/static/css/style-viz.css +++ b/webapp/web/static/css/style-viz.css @@ -245,11 +245,21 @@ ul.tree li.open>ul { ul.tree li a { color: black; text-decoration: none; +} + +.path-text { + color: white; padding-left: 5px; /* background shape */ padding-right: 5px; /* background shape */ border-radius: 10px; /* background shape */ } +.latency-text { + color: purple; + position: absolute; + right: 0; +} + ul.tree li a:before { height: 1em; padding: 0 .1em; diff --git a/webapp/web/static/js/asviz.js b/webapp/web/static/js/asviz.js index 3a082fb9e..27bee0db6 100644 --- a/webapp/web/static/js/asviz.js +++ b/webapp/web/static/js/asviz.js @@ -68,7 +68,9 @@ function setPaths(type, idx, open) { } else if (type == 'UP') { addSegments(resUp, idx, num, colorSegUp, type); } else if (type == 'PATH') { - addPaths(resPath, idx, num, colorPaths, type); + var latencies = getPathLatencyMin(formatPathString(resPath, idx, + type)); + addPaths(resPath, idx, num, colorPaths, type, latencies); } self.segType = type; self.segNum = idx; @@ -108,12 +110,26 @@ function formatPathString(res, idx, type) { return path; } +function formatPathStringAll(res, type) { + var paths = ""; + for (var i = 0; i < res.if_lists.length; i++) { + var path = formatPathString(res, i, type); + if (path != "") { + if (i > 0) { + paths += ","; + } + paths += formatPathString(res, i, type); + } + } + return paths; +} + /* * Adds D3 forwarding path links with arrows and a title to paths graph. */ -function addPaths(res, idx, num, color, type) { +function addPaths(res, idx, num, color, type, latencies) { if (graphPath) { - drawPath(res, idx, color); + drawPath(res, idx, color, latencies); if (res.if_lists[idx].expTime) { drawTitle(type + ' ' + num, color, res.if_lists[idx].expTime); } else { diff --git a/webapp/web/static/js/tab-paths.js b/webapp/web/static/js/tab-paths.js index 36c5711a4..901cc1251 100644 --- a/webapp/web/static/js/tab-paths.js +++ b/webapp/web/static/js/tab-paths.js @@ -17,7 +17,14 @@ var iaLabels; var iaLocations = []; var iaGeoLoc; var g = {}; -var jPathColors = []; +var jPathsAvailable = {}; + +var STAT = { + LAST : 0, + AVG : 1, + MIN : 2, + MAX : 3, +} function setupDebug(src, dst) { var src = $('#ia_cli').val(); @@ -40,12 +47,136 @@ function path_colors(n) { } function getPathColor(hops) { - var idx = jPathColors.indexOf(hops + ''); - if (idx < 0) { + if (!jPathsAvailable[hops]) { return cMissingPath; } else { - return path_colors(idx); + return jPathsAvailable[hops].color; + } +} + +/** + * Updates statistics. + */ +function updateStats(fStat, oldStat) { + var newStat = {} + newStat.Last = fStat; + newStat.Num = oldStat ? (oldStat.Num + 1) : 1; + newStat.Avg = oldStat ? (((oldStat.Avg * oldStat.Num) + fStat) / newStat.Num) + : fStat; + newStat.Min = oldStat ? Math.min(fStat, oldStat.Min) : fStat; + newStat.Max = oldStat ? Math.max(fStat, oldStat.Max) : fStat; + return newStat; +} + +function getPathLatencyLast(hops) { + return getPathLatency(hops, STAT.LAST); +} + +function getPathLatencyAvg(hops) { + return getPathLatency(hops, STAT.AVG); +} + +function getPathLatencyMin(hops) { + return getPathLatency(hops, STAT.MIN); +} + +function getPathLatencyMax(hops) { + return getPathLatency(hops, STAT.MAX); +} + +function getLatencyStat(latency, type) { + switch (type) { + case STAT.LAST: + return latency.Last; + case STAT.AVG: + return latency.Avg; + case STAT.MIN: + return latency.Min; + case STAT.MAX: + return latency.Max; + } +} + +function formatLatency(lat) { + // ignore 1ms negative margin of error + if (lat > -1 && lat < 0) { + return 0; + } else { + return parseFloat(lat).toFixed(0); + } +} + +/** + * Returns array of interface and full path latency stats. + */ +function getPathLatency(hops, type) { + var path = {}; + if (jPathsAvailable[hops]) { + path = jPathsAvailable[hops]; + } + var latencies = []; + for (var i = 0; i < path.interfaces.length; i++) { + if (path.interfaces[i].latency) { + latencies.push(getLatencyStat(path.interfaces[i].latency, type)); + } else { + latencies.push(undefined); + } + } + if (path.latency) { + latencies.push(getLatencyStat(path.latency, type)); + } else { + latencies.push(undefined); } + return latencies; +} + +function setEchoLatency(hops, latency) { + var path = {}; + if (jPathsAvailable[hops]) { + path = jPathsAvailable[hops]; + } + if (latency > 0) { + path.latency = updateStats(latency, path.latency); + jPathsAvailable[hops] = path; + } + return path; +} + +function setTracerouteLatency(hops, interfaces) { + var path = {}; + if (jPathsAvailable[hops]) { + path = jPathsAvailable[hops]; + } + for (var i = 0; i < interfaces.length; i++) { + var if_ = interfaces[i]; + if (i < interfaces.length - 1) { + path.interfaces[i].addr = if_.HopAddr; + if (if_.RespTime1 > 0) { + path.interfaces[i].latency = updateStats(if_.RespTime1, + path.interfaces[i].latency); + } + if (if_.RespTime2 > 0) { + path.interfaces[i].latency = updateStats(if_.RespTime2, + path.interfaces[i].latency); + } + if (if_.RespTime3 > 0) { + path.interfaces[i].latency = updateStats(if_.RespTime3, + path.interfaces[i].latency); + } + } else { + if (if_.RespTime1 > 0) { + path.latency = updateStats(if_.RespTime1, path.latency); + } + if (if_.RespTime2 > 0) { + path.latency = updateStats(if_.RespTime2, path.latency); + } + if (if_.RespTime3 > 0) { + path.latency = updateStats(if_.RespTime3, path.latency); + } + } + } + jPathsAvailable[hops] = path; + return path; } function isConfigComplete(data, textStatus, jqXHR) { @@ -390,11 +521,18 @@ function get_path_html(paths, csegs, usegs, dsegs, show_segs) { if_ = ent.Path.Interfaces; var hops = if_.length / 2; - var style = "style='background-color: " - + getPathColor(formatPathJson(paths, parseInt(p))) + "; '"; - html += "
  • PATH " + (parseInt(p) + 1) - + " " + hops + ""; + var pathStr = formatPathJson(paths, parseInt(p)); + var latencies = getPathLatencyLast(pathStr); + var latencyPath = latencies[latencies.length - 1]; + var latPathStr = latencyPath ? formatLatency(latencyPath) : ''; + var aStyle = "style='background-color:" + getPathColor(pathStr) + ";'"; + html += "
  • PATH " + + (parseInt(p) + 1) + " " + hops + + " " + + latPathStr + ""; exp.setUTCSeconds(ent.Path.ExpTime); html += ""; } @@ -676,6 +816,39 @@ function get_nonseg_links(paths, lType) { return hops; } +function addAvailablePaths(paths) { + Object.keys(jPathsAvailable).forEach(function(key) { + jPathsAvailable[key].listIdx = undefined; // reset + }); + if (!paths) { + return; + } + for (var idx = 0; idx < paths.length; idx++) { + var hops = formatPathJson(paths, idx, 'PATH'); + if (!jPathsAvailable[hops]) { + jPathsAvailable[hops] = {}; + } + // update path preserving old values + var path = jPathsAvailable[hops]; + var pathLen = Object.keys(jPathsAvailable).length; + path.interfaces = []; + var ifs = paths[idx].Entry.Path.Interfaces; + for (var i = 0; i < ifs.length; i++) { + var if_ = {}; + if_.ifid = ifs[i].IfID; + if_.isdas = iaRaw2Read(ifs[i].RawIsdas); + path.interfaces.push(if_); + } + path.expTime = paths[idx].Entry.Path.ExpTime; + path.mtu = paths[idx].Entry.Path.Mtu; + if (!path.color) { + path.color = path_colors(pathLen - 1); + } + path.listIdx = idx; + jPathsAvailable[hops] = path; + } +} + function requestPaths() { // make sure to get path topo after IAs are loaded var form_data = $('#command-form').serializeArray(); @@ -699,12 +872,7 @@ function requestPaths() { resDown = resSegs.down_segments; // store incoming paths - for (var idx = 0; idx < resPath.if_lists.length; idx++) { - var hops = formatPathString(resPath, idx, 'PATH'); - if (!jPathColors.includes(hops)) { - jPathColors.push(hops); - } - } + addAvailablePaths(data.paths); jTopo = get_json_path_links(resPath, resCore, resUp, resDown); $('#path-info').html( @@ -717,7 +885,18 @@ function requestPaths() { // setup path config based on defaults loaded setupDebug(); - ajaxConfig(); + + // request config/paths if none exists + if (!iaLabels || !iaGeoLoc || !iaLocations) { + ajaxConfig(); // ajaxConfig => loadPathData + } else { + loadPathData(g.src, g.dst); + } + + // auto survey latency if none exists + if (!$('#path-lat-0').text()) { + surveyEchoBackground(); + } // path info label switches $('#switch_as_names').change(function() { diff --git a/webapp/web/static/js/tab-topocola.js b/webapp/web/static/js/tab-topocola.js index bb40f4a0f..dbc7baa70 100644 --- a/webapp/web/static/js/tab-topocola.js +++ b/webapp/web/static/js/tab-topocola.js @@ -327,6 +327,19 @@ function update() { }); markerPath.exit().remove(); + svgPath.selectAll("text.latency").remove(); + var markerText = pathsg.selectAll("path.latency").data(markerLinks) + markerText.enter().append("text").attr("dy", ".35em").attr("text-anchor", + "middle").style("font-size", "12px").style("fill", "purple").attr( + "class", function(d) { + return "latency " + d.type; + }).attr("id", function(d) { + return d.id; + }).text(function(d) { + return d.latency ? formatLatency(d.latency) : ''; + }); + markerText.exit().remove(); + var node = circlesg.selectAll(".node").data(realGraphNodes, function(d) { return d.name; }) @@ -399,6 +412,13 @@ function update() { path.attr("d", linkStraight); markerPath.attr("d", linkArc); node.attr("transform", nodeTransform); + + markerText.attr("x", function(d) { + return ((d.source.x + d.target.x) / 2); + }).attr("y", function(d) { + return ((d.source.y + d.target.y) / 2); + }); + }); colaPath.start(50, 100, 200); @@ -591,7 +611,7 @@ function addFixedLabel(label, x, y, lastLabel) { /* * Post-rendering method to draw path arcs for the given path and color. */ -function drawPath(res, path, color) { +function drawPath(res, path, color, lats) { // get the index of the routes to render var routes = []; if (path < 0) { @@ -624,21 +644,45 @@ function drawPath(res, path, color) { graphPath.links = graphPath.links.filter(function(link) { return !link.path; }); + var fullLat = fullPathLatencies(lats); for (var i = 0; i < path_ids.length - 1; i++) { // prevent src == dst links from being formed if (path_ids[i] != path_ids[i + 1]) { - graphPath.links.push({ + var linkLat = undefined; + if (fullLat) { + // report latency difference between inter-AS links + linkLat = lats ? (lats[i + 1] - lats[i]) : undefined; + } + var link = { "color" : color, "path" : true, "source" : graphPath["ids"][path_ids[i]], "target" : graphPath["ids"][path_ids[i + 1]], - "type" : "PARENT" - }); + "type" : "PARENT", + "latency" : linkLat, + "id" : "path-lat-diff-" + path + "-" + i, // TODO + }; + graphPath.links.push(link); } } update(); } +/** + * Interrogate latencies for missing values. + */ +function fullPathLatencies(lats) { + if (!lats) { + return false; + } + for (var i = 0; i < lats.length; i++) { + if (!lats[i]) { + return false; + } + } + return true; +} + /* * Removes all path arcs from the graph. */ diff --git a/webapp/web/static/js/webapp.js b/webapp/web/static/js/webapp.js index f0f255518..981c6e8f2 100644 --- a/webapp/web/static/js/webapp.js +++ b/webapp/web/static/js/webapp.js @@ -46,7 +46,8 @@ var chartCS; var chartSC; var chartSE; var lastTime; -var lastTimeBwDb = new Date((new Date()).getTime() - (xAxisSec * 1000)); +var lastTimeBwDb = new Date((new Date()).getTime() - (xAxisSec * 1000)) + .getTime(); var dial_prop_all = { // dial constants @@ -108,6 +109,7 @@ function initBwGraphs() { updateBwInterval(); // charts update on tab switch + handleSwitchTabs(); // init $('a[data-toggle="tab"]').on('shown.bs.tab', function(e) { var name = $(e.target).attr("name"); if (name != "as-graphs" && name != "as-tab-pathtopo") { @@ -127,7 +129,7 @@ function initBwGraphs() { // setup interval to manage smooth ticking lastTime = (new Date()).getTime() - (ticks * tickMs) + xLeftTrimMs; manageTickData(); - manageTestData(); + // avoid manageTestData on startup before tests } function showOnlyConsoleGraphs(activeApp) { @@ -359,9 +361,6 @@ function manageTickData() { function manageTestData() { // setup interval to request data point updates, only in range - var now = (new Date()).getTime(); - maxTimeBwDb = (new Date(now - (xAxisSec * 1000))).getTime(); - lastTimeBwDb = (lastTimeBwDb < maxTimeBwDb ? maxTimeBwDb : lastTimeBwDb); clearInterval(intervalGraphData); // prevent overlap intervalGraphData = setInterval(function() { // update continuous test parameters @@ -369,7 +368,6 @@ function manageTestData() { if (checked) { command(true); } - now = (new Date()).getTime(); // update continuous results var form_data = { since : lastTimeBwDb @@ -386,26 +384,32 @@ function manageTestData() { } else if (activeApp == "traceroute") { requestTraceRouteByTime(form_data); } - lastTimeBwDb = now; }, dataIntervalMs); } +function manageTestEnd(d, appSel, since) { + if (d.active != null) { + if (d.active) { + enableTestControls(false); + lockTab(appSel); + failContinuousOff(); + } else { + enableTestControls(true); + releaseTabs(); + // waiting for reported data + if (d.graph != null) { + clearInterval(intervalGraphData); + } + } + } +} + function requestBwTestByTime(form_data) { $.post("/getbwbytime", form_data, function(json) { var d = JSON.parse(json); console.info('resp:', JSON.stringify(d)); if (d != null) { - if (d.active != null) { - if (d.active) { - enableTestControls(false); - lockTab("bwtester"); - failContinuousOff(); - } else { - enableTestControls(true); - releaseTabs(); - clearInterval(intervalGraphData); - } - } + manageTestEnd(d, "bwtester", form_data.since); if (d.graph != null) { // write data on graph for (var i = 0; i < d.graph.length; i++) { @@ -432,6 +436,7 @@ function requestBwTestByTime(form_data) { console.info(JSON.stringify(data)); console.info('continuous bwtester', 'duration:', d.graph[i].ActualDuration, 'ms'); + lastTimeBwDb = Math.max(lastTimeBwDb, d.graph[i].Inserted); // use the time the test began var time = d.graph[i].Inserted - d.graph[i].ActualDuration; updateBwGraph(data, time) @@ -446,17 +451,7 @@ function requestEchoByTime(form_data) { var d = JSON.parse(json); console.info('resp:', JSON.stringify(d)); if (d != null) { - if (d.active != null) { - if (d.active) { - enableTestControls(false); - lockTab("echo"); - failContinuousOff(); - } else { - enableTestControls(true); - releaseTabs(); - clearInterval(intervalGraphData); - } - } + manageTestEnd(d, "echo", form_data.since); if (d.graph != null) { // write data on graph for (var i = 0; i < d.graph.length; i++) { @@ -477,11 +472,22 @@ function requestEchoByTime(form_data) { data.runTime = d.graph[i].ActualDuration; } console.info(JSON.stringify(data)); - console.info('continous echo', 'duration:', + console.info('continuous echo', 'duration:', d.graph[i].ActualDuration, 'ms'); + lastTimeBwDb = Math.max(lastTimeBwDb, d.graph[i].Inserted); // use the time the test began var time = d.graph[i].Inserted - d.graph[i].ActualDuration; updatePingGraph(chartSE, data, time) + + // update latency stats, when valid, use average + if (d.graph[i].ResponseTime > 0) { + var path = setEchoLatency(d.graph[i].Path + .match("\\[.*]"), d.graph[i].ResponseTime); + if (path.latency) { + var latStr = formatLatency(path.latency.Last); + $('#path-lat-' + path.listIdx).html(latStr); + } + } } } } @@ -493,17 +499,7 @@ function requestTraceRouteByTime(form_data) { var d = JSON.parse(json); console.info('resp:', JSON.stringify(d)); if (d != null) { - if (d.active != null) { - $('#switch_cont').prop("checked", d.active); - if (d.active) { - enableTestControls(false); - lockTab("traceroute"); - } else { - enableTestControls(true); - releaseTabs(); - clearInterval(intervalGraphData); - } - } + manageTestEnd(d, "traceroute", form_data.since); if (d.graph != null) { // write data on graph for (var i = 0; i < d.graph.length; i++) { @@ -513,10 +509,109 @@ function requestTraceRouteByTime(form_data) { handleEndCmdDisplay(d.graph[i].CmdOutput); } - console.info('continous traceroute', 'duration:', + console.info('continuous traceroute', 'duration:', d.graph[i].ActualDuration, 'ms'); + lastTimeBwDb = Math.max(lastTimeBwDb, d.graph[i].Inserted); + // use the time the test began + var time = d.graph[i].Inserted - d.graph[i].ActualDuration; // TODO (mwfarb): implement traceroute graph + + // update latency stats, when valid, use average + var trhops = ((d.graph[i].Path.split('>').length) * 2) - 1; + if (!d.graph[i].TrHops + || d.graph[i].TrHops.length != trhops) { + console.error("Did not receive expected " + trhops + + " traceroute hops."); + continue; + } + var path = setTracerouteLatency(d.graph[i].Path + .match("\\[.*]"), d.graph[i].TrHops); + for (var i = 0; i < path.interfaces.length; i++) { + var if_ = path.interfaces[i]; + var if_prev = path.interfaces[i - 1]; + if (if_.latency) { + var latStr = formatLatency(if_.latency.Last); + $('#path-lat-' + path.listIdx + '-' + i).html( + latStr); + } + // update each inter-AS link with difference + if (i % 2 == 1) { + var diff = if_.latency.Min - if_prev.latency.Min; + var latStr = formatLatency(diff); + $('#path-lat-diff-' + path.listIdx + '-' + (i - 1)) + .html(latStr); + } + } + if (path.latency) { + var latStr = formatLatency(path.latency.Last); + $('#path-lat-' + path.listIdx).html(latStr); + } + } + } + } + }); +} + +var backgroundEcho; +function surveyEchoBackground() { + var lastTimeBkg = (new Date()).getTime(); + var form_data = $('#command-form').serializeArray(); + var interval = 0.1; + var count = 3; + form_data.push({ + name : "apps", + value : "echo" + }, { + name : "count", + value : count + }, { + name : "continuous", + value : true + }, { + name : "pathStr", + value : formatPathStringAll(resPath, 'PATH') + }, { + name : "interval", + value : interval + }); + // start background echo + console.info('req:', JSON.stringify(form_data)); + $.post('/command', form_data, function(resp, status, jqXHR) { + console.info('resp:', resp); + }).fail(function(error) { + showError(error.responseJSON); + }); + + clearInterval(backgroundEcho); + backgroundEcho = setInterval(function() { + reportEchoBackground({ + since : lastTimeBkg + }); + lastTimeBkg = (new Date()).getTime(); + }, dataIntervalMs); +} + +function reportEchoBackground(form_data) { + $.post("/getechobytime", form_data, function(json) { + var d = JSON.parse(json); + console.info('resp:', JSON.stringify(d)); + if (d != null) { + if (!d.active) { + // end background echo + clearInterval(backgroundEcho); + } + if (d.graph != null) { + for (var i = 0; i < d.graph.length; i++) { + // update latency stats, when valid, use average + if (d.graph[i].ResponseTime > 0) { + var path = setEchoLatency(d.graph[i].Path + .match("\\[.*]"), d.graph[i].ResponseTime); + if (path.latency) { + var latStr = formatLatency(path.latency.Last); + $('#path-lat-' + path.listIdx).html(latStr); + } + } } } } @@ -654,13 +749,19 @@ function command(continuous) { name : "continuous", value : continuous }); - if (self.segType == 'PATH') { // only full paths allowed + if (self.segType == 'PATH') { // single path open form_data.push({ name : "pathStr", value : formatPathString(resPath, self.segNum, self.segType) }); + } else if (continuous) { // all paths in survey + form_data.push({ + name : "pathStr", + value : formatPathStringAll(resPath, 'PATH') + }); } } + if (activeApp == "bwtester") { form_data.push({ name : "interval", diff --git a/webapp/web/template/index.html b/webapp/web/template/index.html index ca095757b..1521c7bf3 100644 --- a/webapp/web/template/index.html +++ b/webapp/web/template/index.html @@ -411,11 +411,17 @@

    SCIONLab Apps

    -

    - Available Paths hops -

    +

    + + Available Paths Hops RTT ms +
    +

    diff --git a/webapp/webapp.go b/webapp/webapp.go index bc89e3984..6091d2bea 100644 --- a/webapp/webapp.go +++ b/webapp/webapp.go @@ -394,10 +394,6 @@ func parseCmdItem2Cmd(dOrinial model.CmdItem, appSel string, pathStr string) []s bwSC := fmt.Sprintf("-sc=%d,%d,%d,%dbps", d.SCDuration/1000, d.SCPktSize, d.SCPackets, d.SCBandwidth) command = append(command, bwCS, bwSC) - if len(pathStr) > 0 { - // if path choice provided, use interactive mode - command = append(command, "-i") - } } isdCli, _ = strconv.Atoi(strings.Split(d.CIa, "-")[0]) @@ -414,10 +410,6 @@ func parseCmdItem2Cmd(dOrinial model.CmdItem, appSel string, pathStr string) []s optTimeout := fmt.Sprintf("-timeout=%fs", d.Timeout) optInterval := fmt.Sprintf("-interval=%fs", d.Interval) command = append(command, installpath, optApp, optRemote, optLocal, optCount, optTimeout, optInterval) - if len(pathStr) > 0 { - // if path choice provided, use interactive mode - command = append(command, "-i") - } isdCli, _ = strconv.Atoi(strings.Split(d.CIa, "-")[0]) case "traceroute": @@ -431,13 +423,13 @@ func parseCmdItem2Cmd(dOrinial model.CmdItem, appSel string, pathStr string) []s optRemote := fmt.Sprintf("-remote=%s,[%s]", d.SIa, d.SAddr) optTimeout := fmt.Sprintf("-timeout=%fs", d.Timeout) command = append(command, installpath, optApp, optRemote, optLocal, optTimeout) - if len(pathStr) > 0 { - // if path choice provided, use interactive mode - command = append(command, "-i") - } isdCli, _ = strconv.Atoi(strings.Split(d.CIa, "-")[0]) } + if len(pathStr) > 0 { + // if path choice provided, use interactive mode + command = append(command, "-i") + } if isdCli < 16 { // -sciondFromIA is better for localhost testing, with test isds command = append(command, "-sciondFromIA") @@ -452,6 +444,7 @@ func commandHandler(w http.ResponseWriter, r *http.Request) { appSel := r.PostFormValue("apps") continuous, _ := strconv.ParseBool(r.PostFormValue("continuous")) interval, _ := strconv.Atoi(r.PostFormValue("interval")) + count, _ := strconv.Atoi(r.PostFormValue("count")) if appSel == "" { fmt.Fprintf(w, "Unknown SCION client app. Is one selected?") return @@ -476,7 +469,8 @@ func commandHandler(w http.ResponseWriter, r *http.Request) { if !contCmdActive { // run continuous goroutine contCmdActive = true - go continuousCmd(t) + log.Warn("Launching continuousCmd", "count", count) + go continuousCmd(t, count) } else { // continuous goroutine running? if continuous { @@ -489,14 +483,18 @@ func commandHandler(w http.ResponseWriter, r *http.Request) { } } else { // single run - executeCommand(w, r) + pathStr := r.PostFormValue("pathStr") + executeCommand(w, r, pathStr) } } // Could either be bwtest, echo or traceroute -func continuousCmd(t contCmd) { +func continuousCmd(t contCmd, count int) { log.Info(fmt.Sprintf("Starting continuous %s...", t)) + pathIdx := 0 + attempts := 0 defer func() { + log.Warn("Ending continuousCmd", "count", count) log.Info(fmt.Sprintf("Ending continuous %s...", t)) }() for contCmdActive { @@ -506,9 +504,16 @@ func continuousCmd(t contCmd) { contCmdActive = false break } + r := contCmdRequest + thePath := "" + pathStr := r.PostFormValue("pathStr") + paths := strings.Split(pathStr, ",") + if pathIdx >= 0 && pathIdx < len(paths) { + thePath = paths[pathIdx] + } start := time.Now() - executeCommand(nil, r) + executeCommand(nil, r, thePath) // block on cmd output finish <-contCmdChanDone @@ -522,14 +527,23 @@ func continuousCmd(t contCmd) { } log.Info(fmt.Sprintf("Test took %d ms, sleeping for remaining interval: %d ms", elapsed.Nanoseconds()/1e6, remaining.Nanoseconds()/1e6)) + if pathIdx >= (len(paths) - 1) { // iterate all paths given + pathIdx = 0 + attempts++ + } else { + pathIdx++ + } + if count > 0 && attempts >= count { + log.Info("Continuous count reached ", "count", count, "attempts", attempts) + contCmdActive = false + } time.Sleep(remaining) } } -func executeCommand(w http.ResponseWriter, r *http.Request) { +func executeCommand(w http.ResponseWriter, r *http.Request, pathStr string) { r.ParseForm() appSel := r.PostFormValue("apps") - pathStr := r.PostFormValue("pathStr") d, addlOpt := parseRequest2CmdItem(r, appSel) command := parseCmdItem2Cmd(d, appSel, pathStr) if addlOpt != "" { @@ -707,7 +721,7 @@ func writeCmdOutput(w http.ResponseWriter, reader io.Reader, stdin io.WriteClose } if appSel == "traceroute" { - // parse traceroute data/error + // parse scmp traceroute data/error d, ok := d.(model.TracerouteItem) if !ok { log.Error("Parsing error, CmdItem category doesn't match its name")