From b9ad794dc55bebb2cb343c43da078435f29ab703 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Fri, 26 Jul 2024 09:53:46 -0500 Subject: [PATCH 01/28] Start tracking bytes/second over time per-flow. This is experimental; in theory it won't cause problems, but it needs to be thoroughly tested. --- .../lqosd/src/throughput_tracker/flow_data/flow_tracker.rs | 3 +++ src/rust/lqosd/src/throughput_tracker/tracking_data.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/rust/lqosd/src/throughput_tracker/flow_data/flow_tracker.rs b/src/rust/lqosd/src/throughput_tracker/flow_data/flow_tracker.rs index 943b3600..49c52484 100644 --- a/src/rust/lqosd/src/throughput_tracker/flow_data/flow_tracker.rs +++ b/src/rust/lqosd/src/throughput_tracker/flow_data/flow_tracker.rs @@ -41,6 +41,8 @@ pub struct FlowbeeLocalData { pub flags: u8, /// Recent RTT median pub rtt: [RttData; 2], + /// Throughput Buffer + pub throughput_buffer: Vec>, } impl From<&FlowbeeData> for FlowbeeLocalData { @@ -56,6 +58,7 @@ impl From<&FlowbeeData> for FlowbeeLocalData { tos: data.tos, flags: data.flags, rtt: [RttData::from_nanos(0); 2], + throughput_buffer: vec![ data.bytes_sent ], } } } \ No newline at end of file diff --git a/src/rust/lqosd/src/throughput_tracker/tracking_data.rs b/src/rust/lqosd/src/throughput_tracker/tracking_data.rs index 7467269d..a7e60ed9 100644 --- a/src/rust/lqosd/src/throughput_tracker/tracking_data.rs +++ b/src/rust/lqosd/src/throughput_tracker/tracking_data.rs @@ -200,6 +200,7 @@ impl ThroughputTracker { get_flowbee_event_count_and_reset(); let since_boot = Duration::from(now); let expire = (since_boot - Duration::from_secs(timeout_seconds)).as_nanos() as u64; + let zeroed = DownUpOrder::zeroed(); // Tracker for per-circuit RTT data. We're losing some of the smoothness by sampling // every flow; the idea is to combine them into a single entry for the circuit. This @@ -234,6 +235,8 @@ impl ThroughputTracker { this_flow.0.end_status = data.end_status; this_flow.0.tos = data.tos; this_flow.0.flags = data.flags; + let prev_bytes = this_flow.0.throughput_buffer.last().unwrap_or(&zeroed); + this_flow.0.throughput_buffer.push(data.bytes_sent.checked_sub_or_zero(*prev_bytes)); if let Some([up, down]) = rtt_samples.get(&key) { if up.as_nanos() != 0 { this_flow.0.rtt[0] = *up; From 403a0e0dcdbcb9eb60c0a7cfd50c62e9f4055d13 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Fri, 26 Jul 2024 10:18:05 -0500 Subject: [PATCH 02/28] We should now have a timeline of when retransmits happened. --- .../lqosd/src/throughput_tracker/flow_data/flow_tracker.rs | 6 ++++++ src/rust/lqosd/src/throughput_tracker/tracking_data.rs | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/src/rust/lqosd/src/throughput_tracker/flow_data/flow_tracker.rs b/src/rust/lqosd/src/throughput_tracker/flow_data/flow_tracker.rs index 49c52484..eb548562 100644 --- a/src/rust/lqosd/src/throughput_tracker/flow_data/flow_tracker.rs +++ b/src/rust/lqosd/src/throughput_tracker/flow_data/flow_tracker.rs @@ -43,6 +43,10 @@ pub struct FlowbeeLocalData { pub rtt: [RttData; 2], /// Throughput Buffer pub throughput_buffer: Vec>, + /// When did the retries happen? In nanoseconds since kernel boot + pub retry_times_down: Vec, + /// When did the retries happen? In nanoseconds since kernel boot + pub retry_times_up: Vec, } impl From<&FlowbeeData> for FlowbeeLocalData { @@ -59,6 +63,8 @@ impl From<&FlowbeeData> for FlowbeeLocalData { flags: data.flags, rtt: [RttData::from_nanos(0); 2], throughput_buffer: vec![ data.bytes_sent ], + retry_times_down: Vec::new(), + retry_times_up: Vec::new(), } } } \ No newline at end of file diff --git a/src/rust/lqosd/src/throughput_tracker/tracking_data.rs b/src/rust/lqosd/src/throughput_tracker/tracking_data.rs index a7e60ed9..eec3827e 100644 --- a/src/rust/lqosd/src/throughput_tracker/tracking_data.rs +++ b/src/rust/lqosd/src/throughput_tracker/tracking_data.rs @@ -227,6 +227,13 @@ impl ThroughputTracker { } else { // We have a valid flow, so it needs to be tracked if let Some(this_flow) = all_flows_lock.get_mut(&key) { + // If retransmits have changed, add the time to the retry list + if data.tcp_retransmits.down != this_flow.0.tcp_retransmits.down { + this_flow.0.retry_times_down.push(data.last_seen); + } + if data.tcp_retransmits.up != this_flow.0.tcp_retransmits.up { + this_flow.0.retry_times_up.push(data.last_seen); + } this_flow.0.last_seen = data.last_seen; this_flow.0.bytes_sent = data.bytes_sent; this_flow.0.packets_sent = data.packets_sent; From 48a6ca57046b7d9b3a465ac24df5b9e9c47460b3 Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Fri, 26 Jul 2024 10:45:04 -0500 Subject: [PATCH 03/28] Formatting fix --- src/rust/lqosd/src/throughput_tracker/tracking_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rust/lqosd/src/throughput_tracker/tracking_data.rs b/src/rust/lqosd/src/throughput_tracker/tracking_data.rs index eec3827e..9d37fdc8 100644 --- a/src/rust/lqosd/src/throughput_tracker/tracking_data.rs +++ b/src/rust/lqosd/src/throughput_tracker/tracking_data.rs @@ -229,7 +229,7 @@ impl ThroughputTracker { if let Some(this_flow) = all_flows_lock.get_mut(&key) { // If retransmits have changed, add the time to the retry list if data.tcp_retransmits.down != this_flow.0.tcp_retransmits.down { - this_flow.0.retry_times_down.push(data.last_seen); + this_flow.0.retry_times_down.push(data.last_seen); } if data.tcp_retransmits.up != this_flow.0.tcp_retransmits.up { this_flow.0.retry_times_up.push(data.last_seen); From b250196d07ade8bdf8ba91a3d12336d36836e14e Mon Sep 17 00:00:00 2001 From: Herbert Wolverson Date: Fri, 26 Jul 2024 14:09:50 -0500 Subject: [PATCH 04/28] First try at a sparkline based ASN explorer! --- .../src/node_manager/js_build/esbuild.sh | 2 +- .../node_manager/js_build/src/asn_explorer.js | 226 ++++++++++++++++++ src/rust/lqosd/src/node_manager/local_api.rs | 3 + .../node_manager/local_api/flow_explorer.rs | 58 +++++ .../node_manager/static2/asn_explorer.html | 11 + .../src/node_manager/static2/template.html | 6 + .../lqosd/src/node_manager/static_pages.rs | 1 + .../flow_data/flow_analysis/asn.rs | 9 + .../flow_data/flow_analysis/finished_flows.rs | 35 ++- .../flow_data/flow_analysis/mod.rs | 10 + .../src/throughput_tracker/flow_data/mod.rs | 3 +- 11 files changed, 360 insertions(+), 4 deletions(-) create mode 100644 src/rust/lqosd/src/node_manager/js_build/src/asn_explorer.js create mode 100644 src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs create mode 100644 src/rust/lqosd/src/node_manager/static2/asn_explorer.html diff --git a/src/rust/lqosd/src/node_manager/js_build/esbuild.sh b/src/rust/lqosd/src/node_manager/js_build/esbuild.sh index 3afb860f..f3072966 100755 --- a/src/rust/lqosd/src/node_manager/js_build/esbuild.sh +++ b/src/rust/lqosd/src/node_manager/js_build/esbuild.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -scripts=( index.js template.js login.js first-run.js shaped-devices.js tree.js help.js unknown-ips.js configuration.js circuit.js flow_map.js all_tree_sankey.js ) +scripts=( index.js template.js login.js first-run.js shaped-devices.js tree.js help.js unknown-ips.js configuration.js circuit.js flow_map.js all_tree_sankey.js asn_explorer.js ) for script in "${scripts[@]}" do echo "Building {$script}" diff --git a/src/rust/lqosd/src/node_manager/js_build/src/asn_explorer.js b/src/rust/lqosd/src/node_manager/js_build/src/asn_explorer.js new file mode 100644 index 00000000..897832b1 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/js_build/src/asn_explorer.js @@ -0,0 +1,226 @@ +import {clearDiv} from "./helpers/builders"; +import {scaleNanos, scaleNumber} from "./helpers/scaling"; + +let asnList = []; +let asnData = []; +let graphMinTime = Number.MAX_SAFE_INTEGER; +let graphMaxTime = Number.MIN_SAFE_INTEGER; + +function unixTimeToDate(unixTime) { + return new Date(unixTime * 1000); +} + +function asnDropdown() { + $.get("local-api/asnList", (data) => { + asnList = data; + + // Sort data by row.count, descending + data.sort((a, b) => { + return b.count - a.count; + }); + + // Build the dropdown + let parentDiv = document.createElement("div"); + parentDiv.classList.add("dropdown"); + let button = document.createElement("button"); + button.classList.add("btn", "btn-secondary", "dropdown-toggle"); + button.type = "button"; + button.innerHTML = "Select ASN"; + button.setAttribute("data-bs-toggle", "dropdown"); + button.setAttribute("aria-expanded", "false"); + parentDiv.appendChild(button); + let dropdownList = document.createElement("ul"); + dropdownList.classList.add("dropdown-menu"); + + // Add items + data.forEach((row) => { + let li = document.createElement("li"); + li.innerHTML = row.name + " (" + row.count + ")"; + li.classList.add("dropdown-item"); + li.onclick = () => { + selectAsn(row.asn); + }; + dropdownList.appendChild(li); + }); + + parentDiv.appendChild(dropdownList); + let target = document.getElementById("asnList"); + target.appendChild(parentDiv); + + if (data.length > 0) { + selectAsn(data[0].asn); + } + }); +} + +function selectAsn(asn) { + let targetAsn = asnList.find((row) => row.asn === asn); + if (targetAsn === undefined || targetAsn === null) { + console.error("Could not find ASN: " + asn); + return; + } + + let target = document.getElementById("asnDetails"); + + // Build the heading + let heading = document.createElement("h2"); + heading.innerText = "ASN #" + asn.toFixed(0) + " (" + targetAsn.name + ")"; + + // Get the flow data + $.get("local-api/flowTimeline/" + asn, (data) => { + asnData = data; + + // Sort data by row.start, ascending + data.sort((a, b) => { + return a.start - b.start; + }); + + // Build the flows display + let flowsDiv = document.createElement("div"); + let count = 0; + let minTime = Number.MAX_SAFE_INTEGER; + let maxTime = Number.MIN_SAFE_INTEGER; + data.forEach((row) => { + // Update min/max time + if (row.start < minTime) { + minTime = row.start; + } + if (row.end > maxTime) { + maxTime = row.end; + } + + let div = document.createElement("div"); + div.classList.add("row"); + + // Build the heading + let headingCol = document.createElement("div"); + headingCol.classList.add("col-1"); + + let ht = "

" + scaleNumber(row.total_bytes.down, 0) + " / " + scaleNumber(row.total_bytes.up); + + if (row.rtt[0] !== undefined) { + ht += "
RTT: " + scaleNanos(row.rtt[0].nanoseconds, 0); + } else { + ht += "
RTT: -"; + } + if (row.rtt[1] !== undefined) { + ht += " / " + scaleNanos(row.rtt[1].nanoseconds, 0); + } + ht += "

"; + headingCol.innerHTML = ht; + div.appendChild(headingCol); + + // Build a canvas div, we'll decorate this later + let canvasCol = document.createElement("div"); + canvasCol.classList.add("col-11"); + let canvas = document.createElement("canvas"); + canvas.id = "flowCanvas" + count; + canvas.style.width = "100%"; + canvas.style.height = "50px"; + canvasCol.appendChild(canvas); + div.appendChild(canvasCol); + + flowsDiv.appendChild(div); + count++; + }); + + // Store the global time range + graphMinTime = minTime; + graphMaxTime = maxTime; + + // Apply the data to the page + clearDiv(target); + target.appendChild(heading); + target.appendChild(flowsDiv); + + // Wait for the page to render before drawing the graphs + requestAnimationFrame(() => { + setTimeout(() => { + drawTimeline(); + }); + }); + }); +} + +function timeToX(time, width) { + let range = graphMaxTime - graphMinTime; + let offset = time - graphMinTime; + return (offset / range) * width; +} + +function drawTimeline() { + var style = getComputedStyle(document.body) + let regionBg = style.getPropertyValue('--bs-tertiary-bg'); + let lineColor = style.getPropertyValue('--bs-primary'); + + for (let i=0; i { + // Start at y/2, end at y + ctx.beginPath(); + ctx.moveTo(timeToX(time, width), height / 2); + ctx.lineTo(timeToX(time, width), height); + ctx.stroke(); + }); + row.retransmit_times_up.forEach((time) => { + // Start at 0, end at y/2 + ctx.beginPath(); + ctx.moveTo(timeToX(time, width), 0); + ctx.lineTo(timeToX(time, width), height / 2); + ctx.stroke(); + }); + + // Find the max of row.throughput.down and row.throughput.up + let maxThroughputDown = 0; + let maxThroughputUp = 0; + row.throughput.forEach((value) => { + if (value.down > maxThroughputDown) { + maxThroughputDown = value.down; + } + if (value.up > maxThroughputUp) { + maxThroughputUp = value.up; + } + }); + + // Draw a throughput down line. Y from y/2 to height, scaled to maxThroughputDown + ctx.strokeStyle = lineColor; + ctx.beginPath(); + let duration = row.end - row.start; + let numberOfSamples = row.throughput.length; + let startX = timeToX(row.start, width); + let endX = timeToX(row.end, width); + let sampleWidth = (endX - startX) / numberOfSamples; + let x = timeToX(row.start, width); + row.throughput.forEach((value, index) => { + let downPercent = value.down / maxThroughputDown; + let downHeight = downPercent * (height / 2); + let y = height - downHeight; + ctx.moveTo(x, y); + + let upPercent = value.up / maxThroughputUp; + let upHeight = upPercent * (height / 2); + ctx.lineTo(x, upHeight); + + x += sampleWidth; + }); + ctx.stroke(); + + } +} + +asnDropdown(); diff --git a/src/rust/lqosd/src/node_manager/local_api.rs b/src/rust/lqosd/src/node_manager/local_api.rs index 07fc7173..f8b0ad72 100644 --- a/src/rust/lqosd/src/node_manager/local_api.rs +++ b/src/rust/lqosd/src/node_manager/local_api.rs @@ -13,6 +13,7 @@ mod circuit; mod packet_analysis; mod flow_map; mod warnings; +mod flow_explorer; use axum::Router; use axum::routing::{get, post}; @@ -48,5 +49,7 @@ pub fn local_api() -> Router { .route("/pcapDump/:id", get(packet_analysis::pcap_dump)) .route("/flowMap", get(flow_map::flow_lat_lon)) .route("/globalWarnings", get(warnings::get_global_warnings)) + .route("/asnList", get(flow_explorer::asn_list)) + .route("/flowTimeline/:asn_id", get(flow_explorer::flow_timeline)) .route_layer(axum::middleware::from_fn(auth_layer)) } \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs b/src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs new file mode 100644 index 00000000..dc8ba177 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/local_api/flow_explorer.rs @@ -0,0 +1,58 @@ +use std::time::Duration; +use axum::extract::Path; +use axum::Json; +use serde::Serialize; +use lqos_utils::units::DownUpOrder; +use lqos_utils::unix_time::{time_since_boot, unix_now}; +use crate::throughput_tracker::flow_data::{AsnListEntry, RECENT_FLOWS, RttData}; + +pub async fn asn_list() -> Json> { + Json(RECENT_FLOWS.asn_list()) +} + +#[derive(Serialize)] +pub struct FlowTimeline { + start: u64, + end: u64, + duration_nanos: u64, + throughput: Vec>, + tcp_retransmits: DownUpOrder, + rtt: [RttData; 2], + retransmit_times_down: Vec, + retransmit_times_up: Vec, + total_bytes: DownUpOrder, +} + +pub async fn flow_timeline(Path(asn_id): Path) -> Json> { + let time_since_boot = time_since_boot().unwrap(); + let since_boot = Duration::from(time_since_boot); + let boot_time = unix_now().unwrap() - since_boot.as_secs(); + + let all_flows_for_asn = RECENT_FLOWS.all_flows_for_asn(asn_id); + + let flows = all_flows_for_asn + .iter() + .map(|flow| { + + FlowTimeline { + start: boot_time + Duration::from_nanos(flow.1.start_time).as_secs(), + end: boot_time + Duration::from_nanos(flow.1.last_seen).as_secs(), + duration_nanos: flow.1.last_seen - flow.1.start_time, + tcp_retransmits: flow.1.tcp_retransmits.clone(), + throughput: flow.1.throughput_buffer.clone(), + rtt: flow.1.rtt.clone(), + retransmit_times_down: flow.1.retry_times_down + .iter() + .map(|t| boot_time + *t) + .collect(), + retransmit_times_up: flow.1.retry_times_up + .iter() + .map(|t| boot_time + *t) + .collect(), + total_bytes: flow.1.bytes_sent.clone(), + } + }) + .collect::>(); + + Json(flows) +} \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/static2/asn_explorer.html b/src/rust/lqosd/src/node_manager/static2/asn_explorer.html new file mode 100644 index 00000000..835c7785 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/static2/asn_explorer.html @@ -0,0 +1,11 @@ +
+
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/static2/template.html b/src/rust/lqosd/src/node_manager/static2/template.html index 50b8f769..7659138c 100644 --- a/src/rust/lqosd/src/node_manager/static2/template.html +++ b/src/rust/lqosd/src/node_manager/static2/template.html @@ -77,6 +77,12 @@ Tree Overview + +