diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/circuit_capacity_dash.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/circuit_capacity_dash.js new file mode 100644 index 00000000..222ba6e8 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/circuit_capacity_dash.js @@ -0,0 +1,75 @@ +import {BaseDashlet} from "./base_dashlet"; +import {clearDashDiv, simpleRow, simpleRowHtml, theading} from "../helpers/builders"; +import {scaleNumber, scaleNanos, formatRtt} from "../helpers/scaling"; +import {redactCell} from "../helpers/redact"; + +export class CircuitCapacityDash extends BaseDashlet { + constructor(slot) { + super(slot); + } + + title() { + return "Circuits At Capacity"; + } + + tooltip() { + return "
Circuits at Capacity

Customer circuits using close to their maximum capacities, and possibly in need of an upsell.

"; + } + + subscribeTo() { + return [ "CircuitCapacity" ]; + } + + buildContainer() { + let base = super.buildContainer(); + base.style.height = "250px"; + base.style.overflow = "auto"; + return base; + } + + setup() { + super.setup(); + } + + onMessage(msg) { + if (msg.event === "CircuitCapacity") { + let target = document.getElementById(this.id); + + let table = document.createElement("table"); + table.classList.add("table", "table-striped", "small"); + let thead = document.createElement("thead"); + thead.classList.add("small"); + thead.appendChild(theading("Circuit")); + thead.appendChild(theading("% Utilization (DL)")); + thead.appendChild(theading("% Utilization (UL)")); + thead.appendChild(theading("RTT")); + table.appendChild(thead); + let tbody = document.createElement("tbody"); + msg.data.forEach((c) => { + if (c.capacity[0] < 0.9 && c.capacity[1] < 0.9) { + return; + } + let row = document.createElement("tr"); + row.classList.add("small"); + + let linkCol = document.createElement("td"); + let link = document.createElement("a"); + link.href = "circuit.html?id=" + encodeURI(c.circuit_id); + link.innerText = c.circuit_name; + redactCell(link); + linkCol.appendChild(link); + row.appendChild(linkCol); + + row.appendChild(simpleRow((c.capacity[0]*100).toFixed(0))); + row.appendChild(simpleRow((c.capacity[1]*100).toFixed(0))); + row.appendChild(simpleRowHtml(formatRtt(c.rtt))); + tbody.appendChild(row); + }) + table.appendChild(tbody); + + // Display it + clearDashDiv(this.id, target); + target.appendChild(table); + } + } +} \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/dashlet_index.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/dashlet_index.js index eebfa821..a3a23777 100644 --- a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/dashlet_index.js +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/dashlet_index.js @@ -18,6 +18,8 @@ import {TopTreeSummary} from "./top_tree_summary"; import {CombinedTopDashlet} from "./combined_top_dash"; import {RttHisto3dDash} from "./rtt_histo3d_dash"; import {QueueStatsTotalDash} from "./queue_stats_total"; +import {TreeCapacityDash} from "./tree_capacity_dash"; +import {CircuitCapacityDash} from "./circuit_capacity_dash"; export const DashletMenu = [ { name: "Throughput Bits/Second", tag: "throughputBps", size: 3 }, @@ -40,6 +42,8 @@ export const DashletMenu = [ { name: "Combined Top 10 Box", tag: "combinedTop10", size: 3 }, { name: "Total Cake Stats", tag: "totalCakeStats", size: 3 }, { name: "Round-Trip Time Histogram 3D", tag: "rttHistogram3D", size: 12 }, + { name: "Circuits At Capacity", tag: "circuitCapacity", size: 6 }, + { name: "Tree Nodes At Capacity", tag: "treeCapacity", size: 6 }, ]; export function widgetFactory(widgetName, count) { @@ -65,6 +69,8 @@ export function widgetFactory(widgetName, count) { case "treeSummary" : widget = new TopTreeSummary(count); break; case "combinedTop10" : widget = new CombinedTopDashlet(count); break; case "totalCakeStats" : widget = new QueueStatsTotalDash(count); break; + case "circuitCapacity" : widget = new CircuitCapacityDash(count); break; + case "treeCapacity" : widget = new TreeCapacityDash(count); break; default: { console.log("I don't know how to construct a widget of type [" + widgetName + "]"); return null; diff --git a/src/rust/lqosd/src/node_manager/js_build/src/dashlets/tree_capacity_dash.js b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/tree_capacity_dash.js new file mode 100644 index 00000000..9887b887 --- /dev/null +++ b/src/rust/lqosd/src/node_manager/js_build/src/dashlets/tree_capacity_dash.js @@ -0,0 +1,79 @@ +import {BaseDashlet} from "./base_dashlet"; +import {clearDashDiv, simpleRow, simpleRowHtml, theading} from "../helpers/builders"; +import {scaleNumber, scaleNanos, formatRtt} from "../helpers/scaling"; + +export class TreeCapacityDash extends BaseDashlet { + constructor(slot) { + super(slot); + } + + title() { + return "Tree Nodes At Capacity"; + } + + tooltip() { + return "
Tree Nodes at Capacity

Distribution Nodes approaching their maximum capacity, possibly in need of an upgrade or a better shaping policy.

"; + } + + subscribeTo() { + return [ "TreeCapacity" ]; + } + + buildContainer() { + let base = super.buildContainer(); + base.style.height = "250px"; + base.style.overflow = "auto"; + return base; + } + + setup() { + super.setup(); + } + + onMessage(msg) { + if (msg.event === "TreeCapacity") { + //console.log(msg.data); + let target = document.getElementById(this.id); + + let table = document.createElement("table"); + table.classList.add("table", "table-striped", "small"); + let thead = document.createElement("thead"); + thead.classList.add("small"); + thead.appendChild(theading("Node")); + thead.appendChild(theading("% Utilization (DL)")); + thead.appendChild(theading("% Utilization (UL)")); + thead.appendChild(theading("RTT")); + table.appendChild(thead); + let tbody = document.createElement("tbody"); + + msg.data.forEach((node) => { + if (node.max_down === 0 || node.max_up === 0) { + // No divisions by zero + return; + } + let down = node.down / node.max_down; + let up = node.up / node.max_up; + + if (down < 0.75 && up < 0.75) { + // Not at capacity + return; + } + + let row = document.createElement("tr"); + row.classList.add("small"); + + row.appendChild(simpleRow(node.name)); + row.appendChild(simpleRow((down*100).toFixed(0))); + row.appendChild(simpleRow((up*100).toFixed(0))); + row.appendChild(simpleRowHtml(formatRtt(node.rtt))); + + tbody.appendChild(row); + }); + table.appendChild(tbody); + + // Display it + clearDashDiv(this.id, target); + target.appendChild(table); + } + } +} \ No newline at end of file diff --git a/src/rust/lqosd/src/node_manager/ws/ticker/circuit_capacity.rs b/src/rust/lqosd/src/node_manager/ws/ticker/circuit_capacity.rs index 8d6781d4..53cac2a2 100644 --- a/src/rust/lqosd/src/node_manager/ws/ticker/circuit_capacity.rs +++ b/src/rust/lqosd/src/node_manager/ws/ticker/circuit_capacity.rs @@ -33,13 +33,13 @@ pub async fn circuit_capacity(channels: Arc) { THROUGHPUT_TRACKER.raw_data.iter().for_each(|c| { if let Some(circuit_id) = &c.circuit_id { if let Some(accumulator) = circuits.get_mut(circuit_id) { - accumulator.bytes += c.bytes; + accumulator.bytes += c.bytes_per_second; if let Some(latency) = c.median_latency() { accumulator.median_rtt = latency; } } else { circuits.insert(circuit_id.clone(), CircuitAccumulator { - bytes: c.bytes, + bytes: c.bytes_per_second, median_rtt: c.median_latency().unwrap_or(0.0), }); } @@ -51,9 +51,9 @@ pub async fn circuit_capacity(channels: Arc) { let shaped_devices = SHAPED_DEVICES.read().unwrap(); circuits.iter().filter_map(|(circuit_id, accumulator)| { if let Some(device) = shaped_devices.devices.iter().find(|sd| sd.circuit_id == *circuit_id) { - let down_mbps = accumulator.bytes.down as f64 * 8.0 / 1_000_000.0; + let down_mbps = (accumulator.bytes.down as f64 * 8.0) / 1_000_000.0; let down = down_mbps / device.download_max_mbps as f64; - let up_mbps = accumulator.bytes.up as f64 * 8.0 / 1_000_000.0; + let up_mbps = (accumulator.bytes.up as f64 * 8.0) / 1_000_000.0; let up = up_mbps / device.upload_max_mbps as f64; Some(Capacity {