Improved current throughput gauge, with Insight support.

This commit is contained in:
Herbert Wolverson 2025-01-30 11:29:14 -06:00
parent 0fb0bed57d
commit d14db24f7b
10 changed files with 355 additions and 7 deletions

1
.gitignore vendored
View File

@ -123,3 +123,4 @@ src/ShapedDevices.csv.good
src/rust/lqosd/lts_keys.bin
src/rust/lqosd/src/node_manager/js_build/out
src/bin/dashboards
.aider*

View File

@ -1,5 +1,5 @@
import {BaseDashlet} from "./base_dashlet";
import {BitsPerSecondGauge} from "../graphs/bits_gauge";
import {scaleNumber} from "../lq_js_common/helpers/scaling";
export class ThroughputBpsDash extends BaseDashlet{
title() {
@ -16,18 +16,310 @@ export class ThroughputBpsDash extends BaseDashlet{
buildContainer() {
let base = super.buildContainer();
base.appendChild(this.graphDiv());
base.style.height = "270px";
base.style.overflow = "auto";
return base;
}
setup() {
super.setup();
this.graph = new BitsPerSecondGauge(this.graphDivId());
this.medians = null;
this.tickCount = 0;
this.busy = false;
this.upRing = [];
this.dlRing = [];
let target = document.getElementById(this.id);
// Create row
const row = document.createElement("div");
row.classList.add("row");
row.style.height = "100%";
// ---------------------
// LEFT COLUMN
// ---------------------
const colLeft = document.createElement("div");
colLeft.classList.add("col-auto", "text-center");
// Recent
const recentWrapper = document.createElement("div");
recentWrapper.classList.add("mb-3");
const recentDlHeader = document.createElement("div");
recentDlHeader.classList.add("stat-header");
recentDlHeader.textContent = "Download:";
const recentDlValue = document.createElement("div");
recentDlValue.classList.add("stat-value-big");
recentDlValue.textContent = "-";
recentDlValue.id = this.id + "_dl_bps";
const recentUp = document.createElement("div");
recentUp.classList.add("stat-header");
recentUp.textContent = "Upload:";
const recentUpValue = document.createElement("div");
recentUpValue.classList.add("stat-value-big");
recentUpValue.textContent = "-";
recentUpValue.id = this.id + "_up_bps";
recentWrapper.appendChild(recentDlHeader);
recentWrapper.appendChild(recentDlValue);
recentWrapper.appendChild(recentUp);
recentWrapper.appendChild(recentUpValue);
// Current
const currentWrapper = document.createElement("div");
const currentHeader = document.createElement("div");
currentHeader.classList.add("stat-header");
currentHeader.textContent = "Current:";
const currentDlValue = document.createElement("div");
currentDlValue.classList.add("fw-bold", "text-secondary");
currentDlValue.textContent = "-";
currentDlValue.id = this.id + "_cdl_bps";
const currentUlValue = document.createElement("div");
currentUlValue.classList.add("fw-bold", "text-secondary");
currentUlValue.textContent = "-";
currentUlValue.id = this.id + "_cul_bps";
currentWrapper.appendChild(currentHeader);
currentWrapper.appendChild(currentDlValue);
currentWrapper.appendChild(currentUlValue);
colLeft.appendChild(recentWrapper);
colLeft.appendChild(currentWrapper);
// ---------------------
// DIVIDER COLUMN
// ---------------------
const colDivider = document.createElement("div");
colDivider.classList.add("col-auto", "px-3");
const divider = document.createElement("div");
divider.classList.add("vertical-divider", "h-100");
colDivider.appendChild(divider);
// ---------------------
// RIGHT COLUMN
// ---------------------
const colRight = document.createElement("div");
colRight.classList.add("col-auto");
if (!window.hasLts) {
// No LTS for you
const yestWrapper = document.createElement("div");
yestWrapper.classList.add("mb-3");
const yestHeader = document.createElement("div");
yestHeader.classList.add("stat-header");
const yestValue = document.createElement("span");
yestValue.classList.add("fw-bold", "text-secondary");
yestValue.innerHTML = "<i class=\"fa fa-fw fa-centerline fa-line-chart nav-icon\"></i> Requires Insight";
yestWrapper.appendChild(yestHeader);
yestWrapper.appendChild(yestValue);
colRight.appendChild(yestWrapper);
} else {
// Yesterday
const yestWrapper = document.createElement("div");
yestWrapper.classList.add("mb-3");
const yestHeader = document.createElement("div");
yestHeader.classList.add("stat-header");
yestHeader.textContent = "This Time Yesterday:";
const yestValueDl = document.createElement("div");
yestValueDl.classList.add("fw-bold", "text-secondary");
const yestValueDlInner = document.createElement("span");
yestValueDlInner.textContent = "-";
yestValueDlInner.id = this.id + "_yest_dl_bps";
const YestSpanDl = document.createElement("span");
YestSpanDl.classList.add("small", "ms-2");
YestSpanDl.textContent = "-";
YestSpanDl.id = this.id + "_yest_dl_bps_span";
const yestValueUl = document.createElement("div");
yestValueUl.classList.add("fw-bold", "text-secondary");
const yestValueUlInner = document.createElement("span");
yestValueUlInner.textContent = "-";
yestValueUlInner.id = this.id + "_yest_ul_bps";
const YestSpanUl = document.createElement("span");
YestSpanUl.classList.add("small", "ms-2");
YestSpanUl.textContent = "-";
YestSpanUl.id = this.id + "_yest_ul_bps_span";
yestValueDl.appendChild(yestValueDlInner);
yestValueDl.appendChild(YestSpanDl);
yestValueUl.appendChild(yestValueUlInner);
yestValueUl.appendChild(YestSpanUl);
yestWrapper.appendChild(yestHeader);
yestWrapper.appendChild(yestValueDl);
yestWrapper.appendChild(yestValueUl);
// Last Week
const lastWeekWrapper = document.createElement("div");
const lastWeekHeader = document.createElement("div");
lastWeekHeader.classList.add("stat-header");
lastWeekHeader.textContent = "This Time Last Week:";
const lastWeekValueDl = document.createElement("div");
lastWeekValueDl.classList.add("fw-bold", "text-secondary");
const lastWeekValueDlInner = document.createElement("span");
lastWeekValueDlInner.textContent = "-";
lastWeekValueDlInner.id = this.id + "_last_dl_bps";
const lastWeekSpanDl = document.createElement("span");
lastWeekSpanDl.classList.add("small", "ms-2");
lastWeekSpanDl.textContent = "-";
lastWeekSpanDl.id = this.id + "_last_dl_bps_span";
const lastWeekValueUl = document.createElement("div");
lastWeekValueUl.classList.add("fw-bold", "text-secondary");
const lastWeekValueUlInner = document.createElement("span");
lastWeekValueUlInner.textContent = "-";
lastWeekValueUlInner.id = this.id + "_last_ul_bps";
const lastWeekSpanUl = document.createElement("span");
lastWeekSpanUl.classList.add("small", "ms-2");
lastWeekSpanUl.textContent = "-";
lastWeekSpanUl.id = this.id + "_last_ul_bps_span";
lastWeekValueDl.appendChild(lastWeekValueDlInner);
lastWeekValueDl.appendChild(lastWeekSpanDl);
lastWeekValueUl.appendChild(lastWeekValueUlInner);
lastWeekValueUl.appendChild(lastWeekSpanUl);
lastWeekWrapper.appendChild(lastWeekHeader);
lastWeekWrapper.appendChild(lastWeekValueDl);
lastWeekWrapper.appendChild(lastWeekValueUl);
colRight.appendChild(yestWrapper);
colRight.appendChild(lastWeekWrapper);
}
// ---------------------
// ASSEMBLE
// ---------------------
row.appendChild(colLeft);
row.appendChild(colDivider);
row.appendChild(colRight);
// Add it all
target.appendChild(row);
}
onMessage(msg) {
const RingSize = 10;
if (msg.event === "Throughput") {
this.graph.update(msg.data.bps.down, msg.data.bps.up, msg.data.max.down, msg.data.max.up);
this.tickCount++;
if (this.busy === false && (this.medians === null || this.tickCount > 300)) {
this.tickCount = 0;
this.busy = true;
$.get("/local-api/ltsRecentMedian", (m) => {
this.medians = m[0];
});
}
this.upRing.push(msg.data.bps.up);
this.dlRing.push(msg.data.bps.down);
if (this.upRing.length > RingSize) {
this.upRing.shift();
}
if (this.dlRing.length > RingSize) {
this.dlRing.shift();
}
// Get the median from upRing
let upMedian = 0;
if (this.upRing.length > 0) {
this.upRing.sort();
upMedian = this.upRing[Math.floor(this.upRing.length / 2)];
}
// Get the median from dlRing
let dlMedian = 0;
if (this.dlRing.length > 0) {
this.dlRing.sort();
dlMedian = this.dlRing[Math.floor(this.dlRing.length / 2)];
}
// Big numbers are smoothed medians
let dl = document.getElementById(this.id + "_dl_bps");
dl.textContent = scaleNumber(dlMedian, 0);
let ul = document.getElementById(this.id + "_up_bps");
ul.textContent = scaleNumber(upMedian, 0);
// Small numbers are current (jittery)
let cdl = document.getElementById(this.id + "_cdl_bps");
cdl.textContent = scaleNumber(msg.data.bps.down, 0);
let cul = document.getElementById(this.id + "_cul_bps");
cul.textContent = scaleNumber(msg.data.bps.up, 0);
// Update the yesterday values
if (this.medians !== null) {
document.getElementById(this.id + "_yest_dl_bps").textContent = scaleNumber(this.medians.yesterday[0], 0);
document.getElementById(this.id + "_yest_ul_bps").textContent = scaleNumber(this.medians.yesterday[1], 0);
let [yest_dl_color, yest_dl_icon, yest_dl_percent] = this.priorComparision(dlMedian, this.medians.yesterday[0]);
if (yest_dl_percent === null) {
document.getElementById(this.id + "_yest_dl_bps_span").innerHTML = "";
} else {
document.getElementById(this.id + "_yest_dl_bps_span").innerHTML = `<i class="fa ${yest_dl_icon} ${yest_dl_color}"></i> ${yest_dl_percent.toFixed(0)}%`;
}
let [yest_ul_color, yest_ul_icon, yest_ul_percent] = this.priorComparision(upMedian, this.medians.yesterday[1]);
if (yest_ul_percent === null) {
document.getElementById(this.id + "_yest_ul_bps_span").innerHTML = "";
} else {
document.getElementById(this.id + "_yest_ul_bps_span").innerHTML = `<i class="fa ${yest_ul_icon} ${yest_ul_color}"></i> ${yest_ul_percent.toFixed(0)}%`;
}
}
// Update the last week values
if (this.medians !== null) {
document.getElementById(this.id + "_last_dl_bps").textContent = scaleNumber(this.medians.last_week[0], 0);
document.getElementById(this.id + "_last_ul_bps").textContent = scaleNumber(this.medians.last_week[1], 0);
let [last_dl_color, last_dl_icon, last_dl_percent] = this.priorComparision(dlMedian, this.medians.last_week[0]);
if (last_dl_percent === null) {
document.getElementById(this.id + "_last_dl_bps_span").textContent = "";
} else {
document.getElementById(this.id + "_last_dl_bps_span").innerHTML = `<i class="fa ${last_dl_icon} ${last_dl_color}"></i> ${last_dl_percent.toFixed(0)}%`;
}
let [last_ul_color, last_ul_icon, last_ul_percent] = this.priorComparision(upMedian, this.medians.last_week[1]);
if (last_ul_percent === null) {
document.getElementById(this.id + "_last_ul_bps_span").textContent = "";
} else {
document.getElementById(this.id + "_last_ul_bps_span").innerHTML = `<i class="fa ${last_ul_icon} ${last_ul_color}"></i> ${last_ul_percent.toFixed(0)}%`;
}
}
}
}
priorComparision(current, previous) {
if (previous === 0) return ["", "", null];
let color = "text-success";
let icon = "fa-arrow-up";
let diff = current - previous;
if (diff < 0) {
color = "text-danger";
icon = "fa-arrow-down";
}
let percent = (diff / previous) * 100;
return [color, icon, percent];
}
}

View File

@ -73,6 +73,7 @@ pub fn local_api(shaper_query: tokio::sync::mpsc::Sender<ShaperQueryCommand>) ->
.route("/ltsWorst10Rtt/:seconds", get(lts::worst10_rtt_period))
.route("/ltsWorst10Rxmit/:seconds", get(lts::worst10_rxmit_period))
.route("/ltsTopFlows/:seconds", get(lts::top10_flows_period))
.route("/ltsRecentMedian", get(lts::recent_medians))
.layer(Extension(shaper_query))
.layer(CorsLayer::very_permissive())
.route_layer(axum::middleware::from_fn(auth_layer))

View File

@ -147,6 +147,12 @@ pub struct AsnFlowSizeWeb {
pub shaper_name: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RecentMedians {
pub yesterday: (i64, i64),
pub last_week: (i64, i64),
}
pub async fn last_24_hours()-> Result<Json<Vec<ThroughputData>>, StatusCode> {
let config = load_config().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let seconds = 24 * 60 * 60;
@ -301,6 +307,22 @@ pub async fn top10_flows_period(
Ok(Json(throughput))
}
pub async fn recent_medians(
Extension(shaper_query): Extension<tokio::sync::mpsc::Sender<ShaperQueryCommand>>,
)-> Result<Json<Vec<RecentMedians>>, StatusCode> {
tracing::error!("rtt_histo_period");
let (tx, rx) = tokio::sync::oneshot::channel();
shaper_query.send(ShaperQueryCommand::ShaperRecentMedian { reply: tx }).await.map_err(|_| {
warn!("Error sending flows period");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let throughput = rx.await.map_err(|e| {
warn!("Error getting flows: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(throughput))
}
pub async fn retransmits_period(Path(seconds): Path<i32>)-> Result<Json<Vec<RetransmitData>>, StatusCode> {
let config = load_config().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let url = format!("https://{}/shaper_api/totalRetransmits/{seconds}", config.long_term_stats.lts_url.clone().unwrap_or("insight.libreqos.com".to_string()));

View File

@ -4,7 +4,7 @@ use std::time::{Duration, Instant};
use serde::de::DeserializeOwned;
use tokio::sync::Mutex;
use tracing::{info, warn};
use crate::node_manager::local_api::lts::{AsnFlowSizeWeb, FlowCountViewWeb, FullPacketData, PercentShapedWeb, ShaperRttHistogramEntry, ThroughputData, Top10Circuit, Worst10RttCircuit, Worst10RxmitCircuit};
use crate::node_manager::local_api::lts::{AsnFlowSizeWeb, FlowCountViewWeb, FullPacketData, PercentShapedWeb, RecentMedians, ShaperRttHistogramEntry, ThroughputData, Top10Circuit, Worst10RttCircuit, Worst10RxmitCircuit};
use crate::node_manager::shaper_queries_actor::timed_cache::TimedCache;
const CACHE_DURATION: Duration = Duration::from_secs(60 * 5);
@ -20,6 +20,7 @@ pub enum CacheType {
WorstRtt,
WorstRxmit,
TopFlows,
RecentMedians,
}
impl CacheType {
@ -34,6 +35,7 @@ impl CacheType {
"worst_rtt" => Self::WorstRtt,
"worst_rxmit" => Self::WorstRxmit,
"top_flows" => Self::TopFlows,
"recent_median" => Self::RecentMedians,
_ => panic!("Unknown cache type: {}", tag),
}
}
@ -146,4 +148,10 @@ impl Cacheable for AsnFlowSizeWeb {
fn tag() -> CacheType {
CacheType::TopFlows
}
}
impl Cacheable for RecentMedians {
fn tag() -> CacheType {
CacheType::RecentMedians
}
}

View File

@ -1,4 +1,4 @@
use crate::node_manager::local_api::lts::{AsnFlowSizeWeb, FlowCountViewWeb, FullPacketData, PercentShapedWeb, ShaperRttHistogramEntry, ThroughputData, Top10Circuit, Worst10RttCircuit, Worst10RxmitCircuit};
use crate::node_manager::local_api::lts::{AsnFlowSizeWeb, FlowCountViewWeb, FullPacketData, PercentShapedWeb, RecentMedians, ShaperRttHistogramEntry, ThroughputData, Top10Circuit, Worst10RttCircuit, Worst10RxmitCircuit};
pub enum ShaperQueryCommand {
ShaperThroughput { seconds: i32, reply: tokio::sync::oneshot::Sender<Vec<ThroughputData>> },
@ -10,4 +10,5 @@ pub enum ShaperQueryCommand {
ShaperWorstRtt { seconds: i32, reply: tokio::sync::oneshot::Sender<Vec<Worst10RttCircuit>> },
ShaperWorstRxmit { seconds: i32, reply: tokio::sync::oneshot::Sender<Vec<Worst10RxmitCircuit>> },
ShaperTopFlows { seconds: i32, reply: tokio::sync::oneshot::Sender<Vec<AsnFlowSizeWeb>> },
ShaperRecentMedian { reply: tokio::sync::oneshot::Sender<Vec<RecentMedians>> },
}

View File

@ -4,7 +4,7 @@ use tokio::sync::broadcast::error::RecvError;
use tokio::time::error::Elapsed;
use tokio::time::timeout;
use tracing::{info, warn};
use crate::node_manager::local_api::lts::{AsnFlowSizeWeb, FlowCountViewWeb, FullPacketData, PercentShapedWeb, ShaperRttHistogramEntry, ThroughputData, Top10Circuit, Worst10RttCircuit, Worst10RxmitCircuit};
use crate::node_manager::local_api::lts::{AsnFlowSizeWeb, FlowCountViewWeb, FullPacketData, PercentShapedWeb, RecentMedians, ShaperRttHistogramEntry, ThroughputData, Top10Circuit, Worst10RttCircuit, Worst10RxmitCircuit};
use crate::node_manager::shaper_queries_actor::{remote_insight, ShaperQueryCommand};
use crate::node_manager::shaper_queries_actor::caches::Caches;
@ -103,6 +103,9 @@ pub async fn shaper_queries(mut rx: tokio::sync::mpsc::Receiver<ShaperQueryComma
ShaperQueryCommand::ShaperTopFlows { seconds, reply } => {
shaper_query!(AsnFlowSizeWeb, caches, seconds, reply, broadcast_rx, my_remote_insight.command(remote_insight::RemoteInsightCommand::ShaperTopFlows { seconds }));
}
ShaperQueryCommand::ShaperRecentMedian { reply } => {
shaper_query!(RecentMedians, caches, 0, reply, broadcast_rx, my_remote_insight.command(remote_insight::RemoteInsightCommand::ShaperRecentMedians));
}
}
info!("SQ Looping");
}

View File

@ -29,6 +29,7 @@ pub enum RemoteInsightCommand {
ShaperWorstRtt { seconds: i32 },
ShaperWorstRxmit { seconds: i32 },
ShaperTopFlows { seconds: i32 },
ShaperRecentMedians,
}
pub struct RemoteInsight {
@ -198,6 +199,10 @@ async fn run_remote_insight(
let msg = WsMessage::ShaperTopFlows { seconds }.to_bytes()?;
tx.send(tungstenite::Message::Binary(msg)).await?;
}
Some(RemoteInsightCommand::ShaperRecentMedians) => {
let msg = WsMessage::ShaperRecentMedian.to_bytes()?;
tx.send(tungstenite::Message::Binary(msg)).await?;
}
}
}
msg = read.next() => {

View File

@ -15,6 +15,7 @@ pub enum WsMessage {
ShaperWorstRtt { seconds: i32 },
ShaperWorstRxmit { seconds: i32 },
ShaperTopFlows { seconds: i32 },
ShaperRecentMedian,
// Responses
Hello { license_key: String, node_id: String },

View File

@ -196,3 +196,17 @@ table tr td a {
background-repeat: no-repeat;
background-size: 32px;
}
.stat-header {
font-size: 0.9rem;
color: var(--bs-tertiary-color);
margin-bottom: 0.3rem;
}
.stat-value-big {
font-size: 2rem;
font-weight: 700;
color: var(--bs-secondary);
}
.vertical-divider {
width: 1px;
background-color: var(--bs-border-color);
}