From 2048f9c57db8032802beb226ab36df8026c49426 Mon Sep 17 00:00:00 2001 From: ryyst Date: Sun, 2 Jun 2024 19:38:39 +0300 Subject: [PATCH] TEMP --- app/collector.py | 12 +- app/db.py | 23 ++-- app/static/index.js | 315 ++++++++++++++++++++++++++++---------------- 3 files changed, 222 insertions(+), 128 deletions(-) diff --git a/app/collector.py b/app/collector.py index 1c5baf3..cb713c7 100755 --- a/app/collector.py +++ b/app/collector.py @@ -8,6 +8,8 @@ from .db import Database def parse_traceroute_output(data: str, origin: str): + # TODO: data validation + lines = data.strip().split("\n") target = lines[0].split()[2] @@ -15,21 +17,26 @@ def parse_traceroute_output(data: str, origin: str): trace = {"target": target, "created": created, "origin": origin, "hops": []} - prev_latency = 0 + prev_latency = None for line in lines[1:]: hop_info = line.split() print("LINE:", hop_info) try: + # Regular lines. number, name, ip, latency, _ = hop_info + latency = float(latency) hop = { "created": created, "number": number, "name": name, "ip": ip.strip("()"), - "latency": float(latency), + "latency": latency, + "link_latency": round(latency if prev_latency else latency, 3), } + prev_latency = latency except ValueError: + # Asterisks, no data found for hop. number, name = hop_info hop = { "created": created, @@ -37,6 +44,7 @@ def parse_traceroute_output(data: str, origin: str): "name": name, "ip": None, "latency": None, + "link_latency": "?", } trace["hops"].append(hop) diff --git a/app/db.py b/app/db.py index c1b3265..228fe73 100644 --- a/app/db.py +++ b/app/db.py @@ -36,6 +36,7 @@ class Database: name TEXT, ip TEXT, latency TEXT, + link_latency TEXT, FOREIGN KEY(trace_id) REFERENCES Traces(id) ); @@ -59,7 +60,12 @@ class Database: trace = dict(t) self.cursor.execute( - "SELECT number, name, ip, latency FROM Hops WHERE trace_id = ? ORDER BY number ASC", + """ + SELECT number, name, ip, latency, link_latency + FROM Hops + WHERE trace_id = ? + ORDER BY number ASC + """, (trace["id"],), ) hops = self.cursor.fetchall() @@ -78,7 +84,7 @@ class Database: for hop in trace["hops"]: self.cursor.execute( - "INSERT OR IGNORE INTO Hops (trace_id, created, number, name, ip, latency) VALUES (?, ?, ?, ?, ?, ?)", + "INSERT OR IGNORE INTO Hops (trace_id, created, number, name, ip, latency, link_latency) VALUES (?, ?, ?, ?, ?, ?, ?)", ( trace_id, hop["created"], @@ -86,6 +92,7 @@ class Database: hop["name"], hop["ip"], hop["latency"], + hop["link_latency"], ), ) @@ -97,18 +104,6 @@ class Database: (name, ip, latency), ) - def create_latency(self, link_id, timestamp, link_latency): - self.cursor.execute( - "INSERT INTO Latency (link_id, timestamp, latency_ms) VALUES (?, ?, ?)", - (link_id, timestamp, link_latency), - ) - - def create_path(self, node, target, json): - self.cursor.execute( - "INSERT OR IGNORE INTO Paths (node, target, hops_json) VALUES (?, ?, ?)", - (node, target, json), - ) - def ensure_table_setup(): db = Database() diff --git a/app/static/index.js b/app/static/index.js index d9e8b95..58a95ce 100644 --- a/app/static/index.js +++ b/app/static/index.js @@ -130,133 +130,219 @@ const drag = (simulation) => { // return svg.node(); // }; -const drawChart2 = (data) => { - // Data parsing - const nodes = Array.from( - new Set(data.flatMap((l) => [l.source, l.target])), - (id) => ({ id }), - ); - const links = data.map((d) => Object.create(d)); +// const drawChart2 = (data) => { +// // Data parsing +// const nodes = Array.from( +// new Set(data.flatMap((l) => [l.source, l.target])), +// (id) => ({ id }), +// ); +// const links = data.map((d) => Object.create(d)); +// +// // Styles +// const width = window.visualViewport.width; +// const height = window.visualViewport.height - 10; +// +// const colors = Array.from(new Set(data.map((d) => d.traceId))); +// const color = d3.scaleOrdinal(colors, d3.schemeCategory10); +// +// const simulation = d3 +// .forceSimulation(nodes) +// .force( +// "link", +// d3.forceLink(links).id((d) => d.id), +// ) +// .force("charge", d3.forceManyBody().strength(-400)) +// .force("x", d3.forceX()) +// .force("y", d3.forceY()); +// +// // Canvas settings +// const svg = d3 +// .create("svg") +// .attr("width", width) +// .attr("height", height) +// .attr("viewBox", [-width / 2, -height / 2, width, height]) +// .attr("style", "max-width: 100%; height: auto; font: 14px monospace;"); +// +// // Pre-type arrowheads, as they don't inherit styles. +// svg +// .append("defs") +// .selectAll("marker") +// .data(colors) +// .join("marker") +// .attr("id", (d) => `arrow-${d}`) +// .attr("viewBox", "0 -5 10 10") +// .attr("refX", 15) +// .attr("refY", -0.5) +// .attr("markerWidth", 5) +// .attr("markerHeight", 5) +// .attr("orient", "auto") +// .append("path") +// .attr("fill", color) +// .attr("d", "M0,-5L10,0L0,5"); +// +// const link = svg +// .append("g") +// .attr("fill", "none") +// .attr("stroke-width", 1.5) +// .selectAll("path") +// .data(links) +// .join("path") +// .attr("stroke", (d) => color(d.traceId)) +// .attr( +// "marker-end", +// (d) => `url(${new URL(`#arrow-${d.traceId}`, location)})`, +// ); +// +// const node = svg +// .append("g") +// .attr("fill", "currentColor") +// .attr("stroke-linecap", "round") +// .attr("stroke-linejoin", "round") +// .selectAll("g") +// .data(nodes) +// .join("g") +// .call(drag(simulation)); +// +// // Node "icon" +// node +// .append("circle") +// .attr("stroke", "white") +// .attr("stroke-width", 1.5) +// .attr("r", 5); +// +// // Node text +// node +// .append("text") +// .attr("x", 8) +// .attr("y", 4) +// .text((d) => (d.id.endsWith("*") ? "*" : d.id)) +// .clone(true) +// .lower() +// .attr("fill", "black") +// .attr("stroke", "white") +// .attr("stroke-width", 2); +// +// simulation.on("tick", () => { +// link.attr("d", linkArc); +// // link +// // .attr("x1", (d) => d.source.x) +// // .attr("y1", (d) => d.source.y) +// // .attr("x2", (d) => d.target.x) +// // .attr("y2", (d) => d.target.y); +// node.attr("transform", (d) => `translate(${d.x},${d.y})`); +// }); +// +// // invalidation.then(() => simulation.stop()); +// +// return Object.assign(svg.node(), { +// scales: { color }, +// }); +// }; - // Styles - const width = window.visualViewport.width; - const height = window.visualViewport.height - 10; - - const colors = Array.from(new Set(data.map((d) => d.traceId))); - const color = d3.scaleOrdinal(colors, d3.schemeCategory10); - - const simulation = d3 - .forceSimulation(nodes) - .force( - "link", - d3.forceLink(links).id((d) => d.id), - ) - .force("charge", d3.forceManyBody().strength(-400)) - .force("x", d3.forceX()) - .force("y", d3.forceY()); - - // Canvas settings - const svg = d3 - .create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [-width / 2, -height / 2, width, height]) - .attr("style", "max-width: 100%; height: auto; font: 14px monospace;"); - - // Pre-type arrowheads, as they don't inherit styles. - svg - .append("defs") - .selectAll("marker") - .data(colors) - .join("marker") - .attr("id", (d) => `arrow-${d}`) - .attr("viewBox", "0 -5 10 10") - .attr("refX", 15) - .attr("refY", -0.5) - .attr("markerWidth", 5) - .attr("markerHeight", 5) - .attr("orient", "auto") - .append("path") - .attr("fill", color) - .attr("d", "M0,-5L10,0L0,5"); - - const link = svg - .append("g") - .attr("fill", "none") - .attr("stroke-width", 1.5) - .selectAll("path") - .data(links) - .join("path") - .attr("stroke", (d) => color(d.traceId)) - .attr( - "marker-end", - (d) => `url(${new URL(`#arrow-${d.traceId}`, location)})`, - ); - - const node = svg - .append("g") - .attr("fill", "currentColor") - .attr("stroke-linecap", "round") - .attr("stroke-linejoin", "round") - .selectAll("g") - .data(nodes) - .join("g") - .call(drag(simulation)); - - // Node "icon" - node - .append("circle") - .attr("stroke", "white") - .attr("stroke-width", 1.5) - .attr("r", 5); - - // Node text - node - .append("text") - .attr("x", 8) - .attr("y", 4) - .text((d) => (d.id.endsWith("*") ? "*" : d.id)) - .clone(true) - .lower() - .attr("fill", "black") - .attr("stroke", "white") - .attr("stroke-width", 2); - - simulation.on("tick", () => { - link.attr("d", linkArc); - // link - // .attr("x1", (d) => d.source.x) - // .attr("y1", (d) => d.source.y) - // .attr("x2", (d) => d.target.x) - // .attr("y2", (d) => d.target.y); - node.attr("transform", (d) => `translate(${d.x},${d.y})`); - }); - - // invalidation.then(() => simulation.stop()); - - return Object.assign(svg.node(), { scales: { color } }); -}; +const state = {}; const drawSigma = (data) => { // Create a graphology graph const graph = new graphology.MultiDirectedGraph(); - data.nodes.forEach((n, i) => { - console.log("Node:", n); + const setHoveredNode = (node) => { + if (node) { + const props = graph.getNodeAttributes(node); + state.hoveredNode = node; + state.hoveredTrace = props.traceId; + + // Compute the partial that we need to re-render to optimize the refresh + const nodes = graph.filterNodes((n) => { + const np = graph.getNodeAttributes(n); + return np.traceId === state.hoveredTrace; + }); + + console.log("Nämä?", nodes); + + const nodesIndex = new Set(nodes); + const edges = graph.filterEdges((e) => + graph.extremities(e).some((n) => nodesIndex.has(n)), + ); + + // Refresh rendering + renderer.refresh({ + partialGraph: { + nodes, + edges, + }, + // We don't touch the graph data so we can skip its reindexation + skipIndexation: true, + }); + } else { + state.hoveredNode = undefined; + state.hoveredTrace = undefined; + + // Refresh rendering + renderer.refresh({ + // We don't touch the graph data so we can skip its reindexation + skipIndexation: true, + }); + } + }; + + data.nodes.forEach((n) => { try { graph.addNode(n.id, n); } catch (e) { - console.log("Node add:", e); + // Duplicate node found, which is correct in our data scenario. } }); - data.links.forEach((l, i) => { + data.links.forEach((l) => { graph.addEdge(l.source, l.target, l); }); // Instantiate sigma.js and render the graph const renderer = new Sigma(graph, document.getElementById("container"), { - labelThreshold: -10000, + // labelThreshold: -10000, + renderEdgeLabels: true, + }); + + // Bind graph interactions: + renderer.on("enterNode", ({ node }) => { + console.log("enter"); + setHoveredNode(node); + }); + renderer.on("leaveNode", () => { + console.log("leave"); + setHoveredNode(undefined); + }); + + // Render nodes accordingly to the internal state: + // 1. If a node is selected, it is highlighted + // 2. If there is query, all non-matching nodes are greyed + // 3. If there is a hovered node, all non-neighbor nodes are greyed + renderer.setSetting("nodeReducer", (node, data) => { + const res = { ...data }; + const props = graph.getNodeAttributes(node); + + if (state.hoveredTrace && props.traceId !== state.hoveredTrace) { + res.label = ""; + res.color = "#f6f6f6"; + } + + return res; + }); + + // Render edges accordingly to the internal state: + // 1. If a node is hovered, the edge is hidden if it is not connected to the + // node + // 2. If there is a query, the edge is only visible if it connects two + // suggestions + renderer.setSetting("edgeReducer", (edge, data) => { + const res = { ...data }; + const props = graph.getEdgeAttributes(edge); + + if (state.hoveredTrace && props.traceId !== state.hoveredTrace) { + res.hidden = true; + } + + return res; }); }; @@ -291,6 +377,7 @@ const parseNodesAndLinks = (traces) => { label: id.endsWith("*") ? "*" : id, x: trace.id, y: hop.number / 2, + traceId: trace.id, size: 9, labelSize: 30, color: color(trace.id), @@ -299,12 +386,12 @@ const parseNodesAndLinks = (traces) => { if (prevId) { // New link links.push({ - label: "asd", + label: hop.link_latency, source: prevId, target: id, traceId: trace.id, origin: trace.origin, - size: 1, + size: 3, color: color(trace.id), }); } @@ -319,6 +406,7 @@ const parseNodesAndLinks = (traces) => { nodes.push({ id: trace.id, label: trace.target, + traceId: trace.id, x: trace.id, y: (latestNumber + 1) / 2, size: 8, @@ -326,6 +414,9 @@ const parseNodesAndLinks = (traces) => { }); links.push({ + label: "-", + size: 8, + traceId: trace.id, source: prevId, target: trace.id, });