Merge pull request #279 from LibreQoE/the-funnel

Merge the funnel branch
This commit is contained in:
Herbert "TheBracket 2023-03-09 09:12:36 -06:00 committed by GitHub
commit 4f9bc7e867
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1534 additions and 1041 deletions

40
src/rust/Cargo.lock generated
View File

@ -587,9 +587,9 @@ dependencies = [
[[package]] [[package]]
name = "csv" name = "csv"
version = "1.2.0" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af91f40b7355f82b0a891f50e70399475945bb0b0da4f1700ce60761c9d3e359" checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad"
dependencies = [ dependencies = [
"csv-core", "csv-core",
"itoa", "itoa",
@ -615,6 +615,19 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "dashmap"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
dependencies = [
"cfg-if",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]] [[package]]
name = "default-net" name = "default-net"
version = "0.12.0" version = "0.12.0"
@ -1116,9 +1129,9 @@ dependencies = [
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.5" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.45.0", "windows-sys 0.45.0",
@ -1330,6 +1343,7 @@ name = "lqos_config"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"csv", "csv",
"dashmap",
"ip_network", "ip_network",
"ip_network_table", "ip_network_table",
"log", "log",
@ -1348,13 +1362,11 @@ dependencies = [
"anyhow", "anyhow",
"default-net", "default-net",
"jemallocator", "jemallocator",
"lazy_static",
"lqos_bus", "lqos_bus",
"lqos_config", "lqos_config",
"lqos_utils", "lqos_utils",
"nix", "nix",
"once_cell", "once_cell",
"parking_lot",
"rocket", "rocket",
"rocket_async_compression", "rocket_async_compression",
"sysinfo", "sysinfo",
@ -1378,15 +1390,14 @@ name = "lqos_queue_tracker"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"criterion", "criterion",
"lazy_static", "dashmap",
"log", "log",
"log-once", "log-once",
"lqos_bus", "lqos_bus",
"lqos_config", "lqos_config",
"lqos_sys", "lqos_sys",
"lqos_utils", "lqos_utils",
"parking_lot", "once_cell",
"rayon",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
@ -1435,6 +1446,7 @@ name = "lqosd"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dashmap",
"env_logger", "env_logger",
"jemallocator", "jemallocator",
"log", "log",
@ -1445,8 +1457,6 @@ dependencies = [
"lqos_utils", "lqos_utils",
"nix", "nix",
"once_cell", "once_cell",
"parking_lot",
"rayon",
"serde", "serde",
"serde_json", "serde_json",
"signal-hook", "signal-hook",
@ -2200,18 +2210,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.152" version = "1.0.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" checksum = "3a382c72b4ba118526e187430bb4963cd6d55051ebf13d9b25574d379cc98d20"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.152" version = "1.0.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" checksum = "1ef476a5790f0f6decbc66726b6e5d63680ed518283e64c7df415989d880954f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -112,11 +112,27 @@ pub enum BusRequest {
ValidateShapedDevicesCsv, ValidateShapedDevicesCsv,
/// Request details of part of the network tree /// Request details of part of the network tree
GetNetworkMap{ GetNetworkMap {
/// The parent of the map to retrieve /// The parent of the map to retrieve
parent: usize parent: usize,
}, },
/// Retrieves the top N queues from the root level, and summarizes
/// the others as "other"
TopMapQueues(usize),
/// Retrieve node names from network.json
GetNodeNamesFromIds(Vec<usize>),
/// Retrieve stats for all queues above a named circuit id
GetFunnel {
/// Circuit being analyzed, as the named circuit id
target: String,
},
/// Obtain the lqosd statistics
GetLqosStats,
/// If running on Equinix (the `equinix_test` feature is enabled), /// If running on Equinix (the `equinix_test` feature is enabled),
/// display a "run bandwidht test" link. /// display a "run bandwidht test" link.
#[cfg(feature = "equinix_tests")] #[cfg(feature = "equinix_tests")]

View File

@ -70,5 +70,16 @@ pub enum BusResponse {
RawQueueData(String), RawQueueData(String),
/// Results from network map queries /// Results from network map queries
NetworkMap(Vec<(usize, lqos_config::NetworkJsonNode)>), NetworkMap(Vec<(usize, lqos_config::NetworkJsonTransport)>),
/// Named nodes from network.json
NodeNames(Vec<(usize, String)>),
/// Statistics from lqosd
LqosdStats{
/// Number of bus requests handled
bus_requests: u64,
/// Us to poll hosts
time_to_poll_hosts: u64,
}
} }

View File

@ -5,7 +5,7 @@ use thiserror::Error;
/// Provides consistent handling of TC handle types. /// Provides consistent handling of TC handle types.
#[derive( #[derive(
Copy, Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq, Hash
)] )]
pub struct TcHandle(u32); pub struct TcHandle(u32);

View File

@ -14,3 +14,4 @@ ip_network = "0"
sha2 = "0" sha2 = "0"
uuid = { version = "1", features = ["v4", "fast-rng" ] } uuid = { version = "1", features = ["v4", "fast-rng" ] }
log = "0" log = "0"
dashmap = "5"

View File

@ -9,16 +9,16 @@
mod authentication; mod authentication;
mod etc; mod etc;
mod libre_qos_config; mod libre_qos_config;
mod network_json;
mod program_control; mod program_control;
mod shaped_devices; mod shaped_devices;
mod network_json;
pub use authentication::{UserRole, WebUsers}; pub use authentication::{UserRole, WebUsers};
pub use etc::{BridgeConfig, BridgeInterface, BridgeVlan, EtcLqos, Tunables}; pub use etc::{BridgeConfig, BridgeInterface, BridgeVlan, EtcLqos, Tunables};
pub use libre_qos_config::LibreQoSConfig; pub use libre_qos_config::LibreQoSConfig;
pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport};
pub use program_control::load_libreqos; pub use program_control::load_libreqos;
pub use shaped_devices::{ConfigShapedDevices, ShapedDevice}; pub use shaped_devices::{ConfigShapedDevices, ShapedDevice};
pub use network_json::{NetworkJson, NetworkJsonNode};
/// Used as a constant in determining buffer preallocation /// Used as a constant in determining buffer preallocation
pub const SUPPORTED_CUSTOMERS: usize = 16_000_000; pub const SUPPORTED_CUSTOMERS: usize = 16_000_000;

View File

@ -1,15 +1,16 @@
use crate::etc; use crate::etc;
use dashmap::DashSet;
use log::{error, info, warn}; use log::{error, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use std::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf}, sync::atomic::AtomicU64,
}; };
use thiserror::Error; use thiserror::Error;
/// Describes a node in the network map tree. /// Describes a node in the network map tree.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug)]
pub struct NetworkJsonNode { pub struct NetworkJsonNode {
/// The node name, as it appears in `network.json` /// The node name, as it appears in `network.json`
pub name: String, pub name: String,
@ -18,12 +19,12 @@ pub struct NetworkJsonNode {
pub max_throughput: (u32, u32), // In mbps pub max_throughput: (u32, u32), // In mbps
/// Current throughput (in bytes/second) at this node /// Current throughput (in bytes/second) at this node
pub current_throughput: (u64, u64), // In bytes pub current_throughput: (AtomicU64, AtomicU64), // In bytes
/// Approximate RTTs reported for this level of the tree. /// Approximate RTTs reported for this level of the tree.
/// It's never going to be as statistically accurate as the actual /// It's never going to be as statistically accurate as the actual
/// numbers, being based on medians. /// numbers, being based on medians.
pub rtts: Vec<f32>, pub rtts: DashSet<u16>,
/// A list of indices in the `NetworkJson` vector of nodes /// A list of indices in the `NetworkJson` vector of nodes
/// linking to parent nodes /// linking to parent nodes
@ -33,12 +34,51 @@ pub struct NetworkJsonNode {
pub immediate_parent: Option<usize>, pub immediate_parent: Option<usize>,
} }
impl NetworkJsonNode {
/// Make a deep copy of a `NetworkJsonNode`, converting atomics
/// into concrete values.
pub fn clone_to_transit(&self) -> NetworkJsonTransport {
NetworkJsonTransport {
name: self.name.clone(),
max_throughput: self.max_throughput,
current_throughput: (
self.current_throughput.0.load(std::sync::atomic::Ordering::Relaxed),
self.current_throughput.1.load(std::sync::atomic::Ordering::Relaxed),
),
rtts: self.rtts.iter().map(|n| *n as f32 / 100.0).collect(),
parents: self.parents.clone(),
immediate_parent: self.immediate_parent,
}
}
}
/// A "transport-friendly" version of `NetworkJsonNode`. Designed
/// to be quickly cloned from original nodes and efficiently
/// transmitted/received.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NetworkJsonTransport {
/// Display name
pub name: String,
/// Max throughput for node (not clamped)
pub max_throughput: (u32, u32),
/// Current node throughput
pub current_throughput: (u64, u64),
/// Set of RTT data
pub rtts: Vec<f32>,
/// Node indices of parents
pub parents: Vec<usize>,
/// The immediate parent node in the tree
pub immediate_parent: Option<usize>,
}
/// Holder for the network.json representation. /// Holder for the network.json representation.
/// This is condensed into a single level vector with index-based referencing /// This is condensed into a single level vector with index-based referencing
/// for easy use in funnel calculations. /// for easy use in funnel calculations.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug)]
pub struct NetworkJson { pub struct NetworkJson {
nodes: Vec<NetworkJsonNode>, /// Nodes that make up the tree, flattened and referenced by index number.
/// TODO: We should add a primary key to nodes in network.json.
pub nodes: Vec<NetworkJsonNode>,
} }
impl Default for NetworkJson { impl Default for NetworkJson {
@ -77,10 +117,10 @@ impl NetworkJson {
let mut nodes = vec![NetworkJsonNode { let mut nodes = vec![NetworkJsonNode {
name: "Root".to_string(), name: "Root".to_string(),
max_throughput: (0, 0), max_throughput: (0, 0),
current_throughput: (0, 0), current_throughput: (AtomicU64::new(0), AtomicU64::new(0)),
parents: Vec::new(), parents: Vec::new(),
immediate_parent: None, immediate_parent: None,
rtts: Vec::new(), rtts: DashSet::new(),
}]; }];
if !Self::exists() { if !Self::exists() {
return Err(NetworkJsonError::FileNotFound); return Err(NetworkJsonError::FileNotFound);
@ -114,19 +154,22 @@ impl NetworkJson {
pub fn get_cloned_entry_by_index( pub fn get_cloned_entry_by_index(
&self, &self,
index: usize, index: usize,
) -> Option<NetworkJsonNode> { ) -> Option<NetworkJsonTransport> {
self.nodes.get(index).cloned() self.nodes.get(index).map(|n| n.clone_to_transit())
} }
/// Retrieve a cloned copy of all children with a parent containing a specific /// Retrieve a cloned copy of all children with a parent containing a specific
/// node index. /// node index.
pub fn get_cloned_children(&self, index: usize) -> Vec<(usize, NetworkJsonNode)> { pub fn get_cloned_children(
&self,
index: usize,
) -> Vec<(usize, NetworkJsonTransport)> {
self self
.nodes .nodes
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_i,n)| n.immediate_parent == Some(index)) .filter(|(_i, n)| n.immediate_parent == Some(index))
.map(|(i, n)| (i, n.clone())) .map(|(i, n)| (i, n.clone_to_transit()))
.collect() .collect()
} }
@ -144,28 +187,42 @@ impl NetworkJson {
} }
/// Sets all current throughput values to zero /// Sets all current throughput values to zero
pub fn zero_throughput_and_rtt(&mut self) { /// Note that due to interior mutability, this does not require mutable
self.nodes.iter_mut().for_each(|n| { /// access.
n.current_throughput = (0, 0); pub fn zero_throughput_and_rtt(&self) {
self.nodes.iter().for_each(|n| {
n.current_throughput.0.store(0, std::sync::atomic::Ordering::Relaxed);
n.current_throughput.1.store(0, std::sync::atomic::Ordering::Relaxed);
n.rtts.clear(); n.rtts.clear();
}); });
} }
/// Add throughput numbers to node entries /// Add throughput numbers to node entries. Note that this does *not* require
/// mutable access due to atomics and interior mutability - so it is safe to use
/// a read lock.
pub fn add_throughput_cycle( pub fn add_throughput_cycle(
&mut self, &self,
targets: &[usize], targets: &[usize],
bytes: (u64, u64), bytes: (u64, u64),
median_rtt: f32,
) { ) {
for idx in targets { for idx in targets {
// Safety first: use "get" to ensure that the node exists // Safety first: use "get" to ensure that the node exists
if let Some(node) = self.nodes.get_mut(*idx) { if let Some(node) = self.nodes.get(*idx) {
node.current_throughput.0 += bytes.0; node.current_throughput.0.fetch_add(bytes.0, std::sync::atomic::Ordering::Relaxed);
node.current_throughput.1 += bytes.1; node.current_throughput.1.fetch_add(bytes.1, std::sync::atomic::Ordering::Relaxed);
if median_rtt > 0.0 { } else {
node.rtts.push(median_rtt); warn!("No network tree entry for index {idx}");
} }
}
}
/// Record RTT time in the tree. Note that due to interior mutability,
/// this does not require mutable access.
pub fn add_rtt_cycle(&self, targets: &[usize], rtt: f32) {
for idx in targets {
// Safety first: use "get" to ensure that the node exists
if let Some(node) = self.nodes.get(*idx) {
node.rtts.insert((rtt * 100.0) as u16);
} else { } else {
warn!("No network tree entry for index {idx}"); warn!("No network tree entry for index {idx}");
} }
@ -193,22 +250,28 @@ fn recurse_node(
immediate_parent: usize, immediate_parent: usize,
) { ) {
info!("Mapping {name} from network.json"); info!("Mapping {name} from network.json");
let mut parents = parents.to_vec();
let my_id = if name != "children" {
parents.push(nodes.len());
nodes.len()
} else {
nodes.len() - 1
};
let node = NetworkJsonNode { let node = NetworkJsonNode {
parents: parents.to_vec(), parents: parents.to_vec(),
max_throughput: ( max_throughput: (
json_to_u32(json.get("downloadBandwidthMbps")), json_to_u32(json.get("downloadBandwidthMbps")),
json_to_u32(json.get("uploadBandwidthMbps")), json_to_u32(json.get("uploadBandwidthMbps")),
), ),
current_throughput: (0, 0), current_throughput: (AtomicU64::new(0), AtomicU64::new(0)),
name: name.to_string(), name: name.to_string(),
immediate_parent: Some(immediate_parent), immediate_parent: Some(immediate_parent),
rtts: Vec::new(), rtts: DashSet::new(),
}; };
let my_id = nodes.len(); if node.name != "children" {
nodes.push(node); nodes.push(node);
let mut parents = parents.to_vec(); }
parents.push(my_id);
// Recurse children // Recurse children
for (key, value) in json.iter() { for (key, value) in json.iter() {

View File

@ -10,8 +10,6 @@ equinix_tests = []
[dependencies] [dependencies]
rocket = { version = "0.5.0-rc.2", features = [ "json", "msgpack", "uuid" ] } rocket = { version = "0.5.0-rc.2", features = [ "json", "msgpack", "uuid" ] }
rocket_async_compression = "0.2.0" rocket_async_compression = "0.2.0"
lazy_static = "1.4"
parking_lot = "0.12"
lqos_bus = { path = "../lqos_bus" } lqos_bus = { path = "../lqos_bus" }
lqos_config = { path = "../lqos_config" } lqos_config = { path = "../lqos_config" }
lqos_utils = { path = "../lqos_utils" } lqos_utils = { path = "../lqos_utils" }

View File

@ -1,7 +1,8 @@
use std::sync::Mutex;
use anyhow::Error; use anyhow::Error;
use lazy_static::*;
use lqos_config::{UserRole, WebUsers}; use lqos_config::{UserRole, WebUsers};
use parking_lot::Mutex; use once_cell::sync::Lazy;
use rocket::serde::{json::Json, Deserialize, Serialize}; use rocket::serde::{json::Json, Deserialize, Serialize};
use rocket::{ use rocket::{
http::{Cookie, CookieJar, Status}, http::{Cookie, CookieJar, Status},
@ -9,9 +10,8 @@ use rocket::{
Request, Request,
}; };
lazy_static! { static WEB_USERS: Lazy<Mutex<Option<WebUsers>>> =
static ref WEB_USERS: Mutex<Option<WebUsers>> = Mutex::new(None); Lazy::new(|| Mutex::new(None));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthGuard { pub enum AuthGuard {
@ -27,7 +27,7 @@ impl<'r> FromRequest<'r> for AuthGuard {
async fn from_request( async fn from_request(
request: &'r Request<'_>, request: &'r Request<'_>,
) -> Outcome<Self, Self::Error> { ) -> Outcome<Self, Self::Error> {
let mut lock = WEB_USERS.lock(); let mut lock = WEB_USERS.lock().unwrap();
if lock.is_none() { if lock.is_none() {
if WebUsers::does_users_file_exist().unwrap() { if WebUsers::does_users_file_exist().unwrap() {
*lock = Some(WebUsers::load_or_create().unwrap()); *lock = Some(WebUsers::load_or_create().unwrap());
@ -82,7 +82,7 @@ pub fn create_first_user(
if WebUsers::does_users_file_exist().unwrap() { if WebUsers::does_users_file_exist().unwrap() {
return Json("ERROR".to_string()); return Json("ERROR".to_string());
} }
let mut lock = WEB_USERS.lock(); let mut lock = WEB_USERS.lock().unwrap();
let mut users = WebUsers::load_or_create().unwrap(); let mut users = WebUsers::load_or_create().unwrap();
users.allow_anonymous(info.allow_anonymous).unwrap(); users.allow_anonymous(info.allow_anonymous).unwrap();
let token = users let token = users
@ -102,7 +102,7 @@ pub struct LoginAttempt {
#[post("/api/login", data = "<info>")] #[post("/api/login", data = "<info>")]
pub fn login(cookies: &CookieJar, info: Json<LoginAttempt>) -> Json<String> { pub fn login(cookies: &CookieJar, info: Json<LoginAttempt>) -> Json<String> {
let mut lock = WEB_USERS.lock(); let mut lock = WEB_USERS.lock().unwrap();
if lock.is_none() && WebUsers::does_users_file_exist().unwrap() { if lock.is_none() && WebUsers::does_users_file_exist().unwrap() {
*lock = Some(WebUsers::load_or_create().unwrap()); *lock = Some(WebUsers::load_or_create().unwrap());
} }
@ -126,7 +126,7 @@ pub fn admin_check(auth: AuthGuard) -> Json<bool> {
#[get("/api/username")] #[get("/api/username")]
pub fn username(_auth: AuthGuard, cookies: &CookieJar) -> Json<String> { pub fn username(_auth: AuthGuard, cookies: &CookieJar) -> Json<String> {
if let Some(token) = cookies.get("User-Token") { if let Some(token) = cookies.get("User-Token") {
let lock = WEB_USERS.lock(); let lock = WEB_USERS.lock().unwrap();
if let Some(users) = &*lock { if let Some(users) = &*lock {
return Json(users.get_username(token.value())); return Json(users.get_username(token.value()));
} }

View File

@ -1,8 +1,8 @@
use crate::{auth_guard::AuthGuard, cache_control::NoCache}; use crate::{auth_guard::AuthGuard, cache_control::NoCache};
use default_net::get_interfaces; use default_net::get_interfaces;
use lqos_bus::{bus_request, BusRequest}; use lqos_bus::{bus_request, BusRequest, BusResponse};
use lqos_config::{EtcLqos, LibreQoSConfig, Tunables}; use lqos_config::{EtcLqos, LibreQoSConfig, Tunables};
use rocket::{fs::NamedFile, serde::json::Json}; use rocket::{fs::NamedFile, serde::{json::Json, Serialize}};
// Note that NoCache can be replaced with a cache option // Note that NoCache can be replaced with a cache option
// once the design work is complete. // once the design work is complete.
@ -76,3 +76,23 @@ pub async fn update_lqos_tuning(
Json("OK".to_string()) Json("OK".to_string())
} }
#[derive(Serialize, Clone, Default)]
#[serde(crate = "rocket::serde")]
pub struct LqosStats {
pub bus_requests_since_start: u64,
pub time_to_poll_hosts_us: u64,
}
#[get("/api/stats")]
pub async fn stats() -> NoCache<Json<LqosStats>> {
for msg in bus_request(vec![BusRequest::GetLqosStats]).await.unwrap() {
if let BusResponse::LqosdStats { bus_requests, time_to_poll_hosts } = msg {
return NoCache::new(Json(LqosStats {
bus_requests_since_start: bus_requests,
time_to_poll_hosts_us: time_to_poll_hosts
}));
}
}
NoCache::new(Json(LqosStats::default()))
}

View File

@ -9,8 +9,8 @@ mod unknown_devices;
use rocket_async_compression::Compression; use rocket_async_compression::Compression;
mod auth_guard; mod auth_guard;
mod config_control; mod config_control;
mod queue_info;
mod network_tree; mod network_tree;
mod queue_info;
// Use JemAllocator only on supported platforms // Use JemAllocator only on supported platforms
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
@ -46,14 +46,12 @@ fn rocket() -> _ {
static_pages::klingon, static_pages::klingon,
// API calls // API calls
tracker::current_throughput, tracker::current_throughput,
tracker::throughput_ring,
tracker::cpu_usage, tracker::cpu_usage,
tracker::ram_usage, tracker::ram_usage,
tracker::top_10_downloaders, tracker::top_10_downloaders,
tracker::worst_10_rtt, tracker::worst_10_rtt,
tracker::rtt_histogram, tracker::rtt_histogram,
tracker::host_counts, tracker::host_counts,
tracker::busy_quantile,
shaped_devices::all_shaped_devices, shaped_devices::all_shaped_devices,
shaped_devices::shaped_devices_count, shaped_devices::shaped_devices_count,
shaped_devices::shaped_devices_range, shaped_devices::shaped_devices_range,
@ -80,10 +78,16 @@ fn rocket() -> _ {
static_pages::login_page, static_pages::login_page,
auth_guard::username, auth_guard::username,
network_tree::tree_entry, network_tree::tree_entry,
network_tree::tree_clients,
network_tree::network_tree_summary,
network_tree::node_names,
network_tree::funnel_for_queue,
config_control::stats,
// Supporting files // Supporting files
static_pages::bootsrap_css, static_pages::bootsrap_css,
static_pages::plotly_js, static_pages::plotly_js,
static_pages::jquery_js, static_pages::jquery_js,
static_pages::msgpack_js,
static_pages::bootsrap_js, static_pages::bootsrap_js,
static_pages::tinylogo, static_pages::tinylogo,
static_pages::favicon, static_pages::favicon,

View File

@ -1,8 +1,13 @@
use lqos_bus::{bus_request, BusRequest, BusResponse}; use std::net::IpAddr;
use lqos_config::NetworkJsonNode;
use rocket::{fs::NamedFile, serde::json::Json};
use crate::cache_control::NoCache; use lqos_bus::{bus_request, BusRequest, BusResponse};
use lqos_config::NetworkJsonTransport;
use rocket::{
fs::NamedFile,
serde::{json::Json, Serialize, msgpack::MsgPack},
};
use crate::{cache_control::NoCache, tracker::SHAPED_DEVICES};
// Note that NoCache can be replaced with a cache option // Note that NoCache can be replaced with a cache option
// once the design work is complete. // once the design work is complete.
@ -14,7 +19,7 @@ pub async fn tree_page<'a>() -> NoCache<Option<NamedFile>> {
#[get("/api/network_tree/<parent>")] #[get("/api/network_tree/<parent>")]
pub async fn tree_entry( pub async fn tree_entry(
parent: usize, parent: usize,
) -> NoCache<Json<Vec<(usize, NetworkJsonNode)>>> { ) -> NoCache<MsgPack<Vec<(usize, NetworkJsonTransport)>>> {
let responses = let responses =
bus_request(vec![BusRequest::GetNetworkMap { parent }]).await.unwrap(); bus_request(vec![BusRequest::GetNetworkMap { parent }]).await.unwrap();
let result = match &responses[0] { let result = match &responses[0] {
@ -22,5 +27,105 @@ pub async fn tree_entry(
_ => Vec::new(), _ => Vec::new(),
}; };
NoCache::new(MsgPack(result))
}
#[get("/api/network_tree_summary")]
pub async fn network_tree_summary(
) -> NoCache<MsgPack<Vec<(usize, NetworkJsonTransport)>>> {
let responses =
bus_request(vec![BusRequest::TopMapQueues(4)]).await.unwrap();
let result = match &responses[0] {
BusResponse::NetworkMap(nodes) => nodes.to_owned(),
_ => Vec::new(),
};
NoCache::new(MsgPack(result))
}
#[derive(Serialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct CircuitThroughput {
pub id: String,
pub name: String,
pub traffic: (u64, u64),
pub limit: (u64, u64),
}
#[get("/api/tree_clients/<parent>")]
pub async fn tree_clients(
parent: String,
) -> NoCache<MsgPack<Vec<CircuitThroughput>>> {
let mut result = Vec::new();
for msg in
bus_request(vec![BusRequest::GetHostCounter]).await.unwrap().iter()
{
let devices = SHAPED_DEVICES.read().unwrap();
if let BusResponse::HostCounters(hosts) = msg {
for (ip, down, up) in hosts.iter() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => *ip,
};
if let Some(c) = devices.trie.longest_match(lookup) {
if devices.devices[*c.1].parent_node == parent {
result.push(CircuitThroughput {
id: devices.devices[*c.1].circuit_id.clone(),
name: devices.devices[*c.1].circuit_name.clone(),
traffic: (*down, *up),
limit: (
devices.devices[*c.1].download_max_mbps as u64,
devices.devices[*c.1].upload_max_mbps as u64,
),
});
}
}
}
}
}
NoCache::new(MsgPack(result))
}
#[post("/api/node_names", data = "<nodes>")]
pub async fn node_names(
nodes: Json<Vec<usize>>,
) -> NoCache<Json<Vec<(usize, String)>>> {
let mut result = Vec::new();
for msg in bus_request(vec![BusRequest::GetNodeNamesFromIds(nodes.0)])
.await
.unwrap()
.iter()
{
if let BusResponse::NodeNames(map) = msg {
result.extend_from_slice(map);
}
}
NoCache::new(Json(result))
}
#[get("/api/funnel_for_queue/<circuit_id>")]
pub async fn funnel_for_queue(
circuit_id: String,
) -> NoCache<Json<Vec<(usize, NetworkJsonTransport)>>> {
let mut result = Vec::new();
let target = SHAPED_DEVICES
.read()
.unwrap()
.devices
.iter()
.find(|d| d.circuit_id == circuit_id)
.as_ref()
.unwrap()
.parent_node
.clone();
for msg in
bus_request(vec![BusRequest::GetFunnel { target }]).await.unwrap().iter()
{
if let BusResponse::NetworkMap(map) = msg {
result.extend_from_slice(map);
}
}
NoCache::new(Json(result)) NoCache::new(Json(result))
} }

View File

@ -29,8 +29,12 @@ pub async fn circuit_info(
circuit_id: String, circuit_id: String,
_auth: AuthGuard, _auth: AuthGuard,
) -> NoCache<Json<CircuitInfo>> { ) -> NoCache<Json<CircuitInfo>> {
if let Some(device) = if let Some(device) = SHAPED_DEVICES
SHAPED_DEVICES.read().devices.iter().find(|d| d.circuit_id == circuit_id) .read()
.unwrap()
.devices
.iter()
.find(|d| d.circuit_id == circuit_id)
{ {
let result = CircuitInfo { let result = CircuitInfo {
name: device.circuit_name.clone(), name: device.circuit_name.clone(),
@ -63,7 +67,7 @@ pub async fn current_circuit_throughput(
bus_request(vec![BusRequest::GetHostCounter]).await.unwrap().iter() bus_request(vec![BusRequest::GetHostCounter]).await.unwrap().iter()
{ {
if let BusResponse::HostCounters(hosts) = msg { if let BusResponse::HostCounters(hosts) = msg {
let devices = SHAPED_DEVICES.read(); let devices = SHAPED_DEVICES.read().unwrap();
for (ip, down, up) in hosts.iter() { for (ip, down, up) in hosts.iter() {
let lookup = match ip { let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(), IpAddr::V4(ip) => ip.to_ipv6_mapped(),

View File

@ -13,12 +13,12 @@ static RELOAD_REQUIRED: AtomicBool = AtomicBool::new(false);
pub fn all_shaped_devices( pub fn all_shaped_devices(
_auth: AuthGuard, _auth: AuthGuard,
) -> NoCache<Json<Vec<ShapedDevice>>> { ) -> NoCache<Json<Vec<ShapedDevice>>> {
NoCache::new(Json(SHAPED_DEVICES.read().devices.clone())) NoCache::new(Json(SHAPED_DEVICES.read().unwrap().devices.clone()))
} }
#[get("/api/shaped_devices_count")] #[get("/api/shaped_devices_count")]
pub fn shaped_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> { pub fn shaped_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
NoCache::new(Json(SHAPED_DEVICES.read().devices.len())) NoCache::new(Json(SHAPED_DEVICES.read().unwrap().devices.len()))
} }
#[get("/api/shaped_devices_range/<start>/<end>")] #[get("/api/shaped_devices_range/<start>/<end>")]
@ -27,7 +27,7 @@ pub fn shaped_devices_range(
end: usize, end: usize,
_auth: AuthGuard, _auth: AuthGuard,
) -> NoCache<Json<Vec<ShapedDevice>>> { ) -> NoCache<Json<Vec<ShapedDevice>>> {
let reader = SHAPED_DEVICES.read(); let reader = SHAPED_DEVICES.read().unwrap();
let result: Vec<ShapedDevice> = let result: Vec<ShapedDevice> =
reader.devices.iter().skip(start).take(end).cloned().collect(); reader.devices.iter().skip(start).take(end).cloned().collect();
NoCache::new(Json(result)) NoCache::new(Json(result))
@ -39,7 +39,7 @@ pub fn shaped_devices_search(
_auth: AuthGuard, _auth: AuthGuard,
) -> NoCache<Json<Vec<ShapedDevice>>> { ) -> NoCache<Json<Vec<ShapedDevice>>> {
let term = term.trim().to_lowercase(); let term = term.trim().to_lowercase();
let reader = SHAPED_DEVICES.read(); let reader = SHAPED_DEVICES.read().unwrap();
let result: Vec<ShapedDevice> = reader let result: Vec<ShapedDevice> = reader
.devices .devices
.iter() .iter()

View File

@ -104,6 +104,11 @@ pub async fn jquery_js<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/jquery.min.js").await.ok()) LongCache::new(NamedFile::open("static/vendor/jquery.min.js").await.ok())
} }
#[get("/vendor/msgpack.min.js")]
pub async fn msgpack_js<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/msgpack.min.js").await.ok())
}
#[get("/vendor/bootstrap.bundle.min.js")] #[get("/vendor/bootstrap.bundle.min.js")]
pub async fn bootsrap_js<'a>() -> LongCache<Option<NamedFile>> { pub async fn bootsrap_js<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new( LongCache::new(

View File

@ -1,22 +0,0 @@
use lazy_static::*;
use lqos_bus::IpStats;
use parking_lot::RwLock;
lazy_static! {
pub static ref TOP_10_DOWNLOADERS: RwLock<Vec<IpStats>> =
RwLock::new(Vec::with_capacity(10));
}
lazy_static! {
pub static ref WORST_10_RTT: RwLock<Vec<IpStats>> =
RwLock::new(Vec::with_capacity(10));
}
lazy_static! {
pub static ref RTT_HISTOGRAM: RwLock<Vec<u32>> =
RwLock::new(Vec::with_capacity(100));
}
lazy_static! {
pub static ref HOST_COUNTS: RwLock<(u32, u32)> = RwLock::new((0, 0));
}

View File

@ -3,11 +3,7 @@
//! of the system. //! of the system.
mod cpu_ram; mod cpu_ram;
mod lqosd_stats;
mod shaped_devices; mod shaped_devices;
mod throughput;
pub use cpu_ram::*; pub use cpu_ram::*;
pub use lqosd_stats::*;
pub use shaped_devices::*; pub use shaped_devices::*;
pub use throughput::*;

View File

@ -1,18 +1,9 @@
use lazy_static::*;
use lqos_bus::IpStats;
use lqos_config::ConfigShapedDevices; use lqos_config::ConfigShapedDevices;
use parking_lot::RwLock; use once_cell::sync::Lazy;
use std::sync::RwLock;
lazy_static! { /// Global storage of the shaped devices csv data.
/// Global storage of the shaped devices csv data. /// Updated by the file system watcher whenever
/// Updated by the file system watcher whenever /// the underlying file changes.
/// the underlying file changes. pub static SHAPED_DEVICES: Lazy<RwLock<ConfigShapedDevices>> =
pub static ref SHAPED_DEVICES : RwLock<ConfigShapedDevices> = RwLock::new(ConfigShapedDevices::default()); Lazy::new(|| RwLock::new(ConfigShapedDevices::default()));
}
lazy_static! {
/// Global storage of the shaped devices csv data.
/// Updated by the file system watcher whenever
/// the underlying file changes.
pub static ref UNKNOWN_DEVICES : RwLock<Vec<IpStats>> = RwLock::new(Vec::new());
}

View File

@ -1,63 +0,0 @@
use lazy_static::*;
use parking_lot::RwLock;
use rocket::serde::Serialize;
lazy_static! {
/// Global storage of the current throughput counter.
pub static ref CURRENT_THROUGHPUT : RwLock<ThroughputPerSecond> = RwLock::new(ThroughputPerSecond::default());
}
lazy_static! {
/// Global storage of the last N seconds throughput buffer.
pub static ref THROUGHPUT_BUFFER : RwLock<ThroughputRingbuffer> = RwLock::new(ThroughputRingbuffer::new());
}
/// Stores total system throughput per second.
#[derive(Debug, Clone, Copy, Serialize, Default)]
#[serde(crate = "rocket::serde")]
pub struct ThroughputPerSecond {
pub bits_per_second: (u64, u64),
pub packets_per_second: (u64, u64),
pub shaped_bits_per_second: (u64, u64),
}
/// How many entries (at one per second) should we keep in the
/// throughput ringbuffer?
const RINGBUFFER_SAMPLES: usize = 300;
/// Stores Throughput samples in a ringbuffer, continually
/// updating. There are always RINGBUFFER_SAMPLES available,
/// allowing for non-allocating/non-growing storage of
/// throughput for the dashboard summaries.
pub struct ThroughputRingbuffer {
readings: Vec<ThroughputPerSecond>,
next: usize,
}
impl ThroughputRingbuffer {
fn new() -> Self {
Self {
readings: vec![ThroughputPerSecond::default(); RINGBUFFER_SAMPLES],
next: 0,
}
}
pub fn store(&mut self, reading: ThroughputPerSecond) {
self.readings[self.next] = reading;
self.next += 1;
self.next %= RINGBUFFER_SAMPLES;
}
pub fn get_result(&self) -> Vec<ThroughputPerSecond> {
let mut result = Vec::with_capacity(RINGBUFFER_SAMPLES);
for i in self.next..RINGBUFFER_SAMPLES {
result.push(self.readings[i]);
}
for i in 0..self.next {
result.push(self.readings[i]);
}
result
}
}

View File

@ -3,7 +3,6 @@
//! when there are multiple clients. //! when there are multiple clients.
use super::cache::*; use super::cache::*;
use anyhow::Result; use anyhow::Result;
use lqos_bus::{bus_request, BusRequest, BusResponse, IpStats};
use lqos_config::ConfigShapedDevices; use lqos_config::ConfigShapedDevices;
use lqos_utils::file_watcher::FileWatcher; use lqos_utils::file_watcher::FileWatcher;
use nix::sys::{ use nix::sys::{
@ -11,7 +10,7 @@ use nix::sys::{
timerfd::{ClockId, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags}, timerfd::{ClockId, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags},
}; };
use rocket::tokio::task::spawn_blocking; use rocket::tokio::task::spawn_blocking;
use std::{net::IpAddr, sync::atomic::AtomicBool}; use std::{sync::atomic::AtomicBool};
/// Once per second, update CPU and RAM usage and ask /// Once per second, update CPU and RAM usage and ask
/// `lqosd` for updated system statistics. /// `lqosd` for updated system statistics.
@ -69,10 +68,6 @@ pub async fn update_tracking() {
.store(sys.used_memory(), std::sync::atomic::Ordering::Relaxed); .store(sys.used_memory(), std::sync::atomic::Ordering::Relaxed);
TOTAL_RAM TOTAL_RAM
.store(sys.total_memory(), std::sync::atomic::Ordering::Relaxed); .store(sys.total_memory(), std::sync::atomic::Ordering::Relaxed);
let error = get_data_from_server().await; // Ignoring errors to keep running
if let Err(error) = error {
error!("Error in usage update loop: {:?}", error);
}
monitor_busy.store(false, std::sync::atomic::Ordering::Relaxed); monitor_busy.store(false, std::sync::atomic::Ordering::Relaxed);
} }
@ -95,10 +90,10 @@ fn load_shaped_devices() {
let shaped_devices = ConfigShapedDevices::load(); let shaped_devices = ConfigShapedDevices::load();
if let Ok(new_file) = shaped_devices { if let Ok(new_file) = shaped_devices {
info!("ShapedDevices.csv loaded"); info!("ShapedDevices.csv loaded");
*SHAPED_DEVICES.write() = new_file; *SHAPED_DEVICES.write().unwrap() = new_file;
} else { } else {
warn!("ShapedDevices.csv failed to load, see previous error messages. Reverting to empty set."); warn!("ShapedDevices.csv failed to load, see previous error messages. Reverting to empty set.");
*SHAPED_DEVICES.write() = ConfigShapedDevices::default(); *SHAPED_DEVICES.write().unwrap() = ConfigShapedDevices::default();
} }
} }
@ -123,77 +118,3 @@ fn watch_for_shaped_devices_changing() -> Result<()> {
info!("ShapedDevices watcher returned: {result:?}"); info!("ShapedDevices watcher returned: {result:?}");
} }
} }
/// Requests data from `lqosd` and stores it in local
/// caches.
async fn get_data_from_server() -> Result<()> {
// Send request to lqosd
let requests = vec![
BusRequest::GetCurrentThroughput,
BusRequest::GetTopNDownloaders { start: 0, end: 10 },
BusRequest::GetWorstRtt { start: 0, end: 10 },
BusRequest::RttHistogram,
BusRequest::AllUnknownIps,
];
for r in bus_request(requests).await?.iter() {
match r {
BusResponse::CurrentThroughput {
bits_per_second,
packets_per_second,
shaped_bits_per_second,
} => {
{
let mut lock = CURRENT_THROUGHPUT.write();
lock.bits_per_second = *bits_per_second;
lock.packets_per_second = *packets_per_second;
} // Lock scope
{
let mut lock = THROUGHPUT_BUFFER.write();
lock.store(ThroughputPerSecond {
packets_per_second: *packets_per_second,
bits_per_second: *bits_per_second,
shaped_bits_per_second: *shaped_bits_per_second,
});
}
}
BusResponse::TopDownloaders(stats) => {
*TOP_10_DOWNLOADERS.write() = stats.clone();
}
BusResponse::WorstRtt(stats) => {
*WORST_10_RTT.write() = stats.clone();
}
BusResponse::RttHistogram(stats) => {
*RTT_HISTOGRAM.write() = stats.clone();
}
BusResponse::AllUnknownIps(unknowns) => {
*HOST_COUNTS.write() = (unknowns.len() as u32, 0);
let cfg = SHAPED_DEVICES.read();
let really_unknown: Vec<IpStats> = unknowns
.iter()
.filter(|ip| {
if let Ok(ip) = ip.ip_address.parse::<IpAddr>() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => ip,
};
cfg.trie.longest_match(lookup).is_none()
} else {
false
}
})
.cloned()
.collect();
*HOST_COUNTS.write() = (really_unknown.len() as u32, 0);
*UNKNOWN_DEVICES.write() = really_unknown;
}
BusResponse::NotReadyYet => {
warn!("Host system isn't ready to answer all queries yet.");
}
// Default
_ => {}
}
}
Ok(())
}

View File

@ -1,18 +1,15 @@
mod cache; mod cache;
mod cache_manager; mod cache_manager;
use std::net::IpAddr;
use self::cache::{ use self::cache::{
CPU_USAGE, CURRENT_THROUGHPUT, HOST_COUNTS, NUM_CPUS, RAM_USED, CPU_USAGE, NUM_CPUS, RAM_USED, TOTAL_RAM,
RTT_HISTOGRAM, THROUGHPUT_BUFFER, TOP_10_DOWNLOADERS, TOTAL_RAM,
WORST_10_RTT,
}; };
use crate::{auth_guard::AuthGuard, tracker::cache::ThroughputPerSecond}; use crate::{auth_guard::AuthGuard, cache_control::NoCache};
pub use cache::{SHAPED_DEVICES, UNKNOWN_DEVICES}; pub use cache::SHAPED_DEVICES;
pub use cache_manager::update_tracking; pub use cache_manager::update_tracking;
use lazy_static::lazy_static; use lqos_bus::{bus_request, BusRequest, BusResponse, IpStats, TcHandle};
use lqos_bus::{IpStats, TcHandle}; use rocket::serde::{Deserialize, Serialize, msgpack::MsgPack};
use lqos_config::LibreQoSConfig;
use parking_lot::Mutex;
use rocket::serde::{json::Json, Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
@ -41,6 +38,7 @@ impl From<&IpStats> for IpStatsWithPlan {
if !result.circuit_id.is_empty() { if !result.circuit_id.is_empty() {
if let Some(circuit) = SHAPED_DEVICES if let Some(circuit) = SHAPED_DEVICES
.read() .read()
.unwrap()
.devices .devices
.iter() .iter()
.find(|sd| sd.circuit_id == result.circuit_id) .find(|sd| sd.circuit_id == result.circuit_id)
@ -50,8 +48,7 @@ impl From<&IpStats> for IpStatsWithPlan {
} else { } else {
&circuit.circuit_name &circuit.circuit_name
}; };
result.ip_address = result.ip_address = format!("{} ({})", name, result.ip_address);
format!("{} ({})", name, result.ip_address);
result.plan = (circuit.download_max_mbps, circuit.download_min_mbps); result.plan = (circuit.download_max_mbps, circuit.download_min_mbps);
} }
} }
@ -60,91 +57,130 @@ impl From<&IpStats> for IpStatsWithPlan {
} }
} }
#[get("/api/current_throughput")] /// Stores total system throughput per second.
pub fn current_throughput(_auth: AuthGuard) -> Json<ThroughputPerSecond> { #[derive(Debug, Clone, Copy, Serialize, Default)]
let result = *CURRENT_THROUGHPUT.read(); #[serde(crate = "rocket::serde")]
Json(result) pub struct ThroughputPerSecond {
pub bits_per_second: (u64, u64),
pub packets_per_second: (u64, u64),
pub shaped_bits_per_second: (u64, u64),
} }
#[get("/api/throughput_ring")] #[get("/api/current_throughput")]
pub fn throughput_ring(_auth: AuthGuard) -> Json<Vec<ThroughputPerSecond>> { pub async fn current_throughput(
let result = THROUGHPUT_BUFFER.read().get_result(); _auth: AuthGuard,
Json(result) ) -> NoCache<MsgPack<ThroughputPerSecond>> {
let mut result = ThroughputPerSecond::default();
if let Ok(messages) =
bus_request(vec![BusRequest::GetCurrentThroughput]).await
{
for msg in messages {
if let BusResponse::CurrentThroughput {
bits_per_second,
packets_per_second,
shaped_bits_per_second,
} = msg
{
result.bits_per_second = bits_per_second;
result.packets_per_second = packets_per_second;
result.shaped_bits_per_second = shaped_bits_per_second;
}
}
}
NoCache::new(MsgPack(result))
} }
#[get("/api/cpu")] #[get("/api/cpu")]
pub fn cpu_usage(_auth: AuthGuard) -> Json<Vec<u32>> { pub fn cpu_usage(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u32>>> {
let usage: Vec<u32> = CPU_USAGE let usage: Vec<u32> = CPU_USAGE
.iter() .iter()
.take(NUM_CPUS.load(std::sync::atomic::Ordering::Relaxed)) .take(NUM_CPUS.load(std::sync::atomic::Ordering::Relaxed))
.map(|cpu| cpu.load(std::sync::atomic::Ordering::Relaxed)) .map(|cpu| cpu.load(std::sync::atomic::Ordering::Relaxed))
.collect(); .collect();
Json(usage) NoCache::new(MsgPack(usage))
} }
#[get("/api/ram")] #[get("/api/ram")]
pub fn ram_usage(_auth: AuthGuard) -> Json<Vec<u64>> { pub fn ram_usage(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u64>>> {
let ram_usage = RAM_USED.load(std::sync::atomic::Ordering::Relaxed); let ram_usage = RAM_USED.load(std::sync::atomic::Ordering::Relaxed);
let total_ram = TOTAL_RAM.load(std::sync::atomic::Ordering::Relaxed); let total_ram = TOTAL_RAM.load(std::sync::atomic::Ordering::Relaxed);
Json(vec![ram_usage, total_ram]) NoCache::new(MsgPack(vec![ram_usage, total_ram]))
} }
#[get("/api/top_10_downloaders")] #[get("/api/top_10_downloaders")]
pub fn top_10_downloaders(_auth: AuthGuard) -> Json<Vec<IpStatsWithPlan>> { pub async fn top_10_downloaders(_auth: AuthGuard) -> NoCache<MsgPack<Vec<IpStatsWithPlan>>> {
let tt: Vec<IpStatsWithPlan> = if let Ok(messages) = bus_request(vec![BusRequest::GetTopNDownloaders { start: 0, end: 10 }]).await
TOP_10_DOWNLOADERS.read().iter().map(|tt| tt.into()).collect(); {
Json(tt) for msg in messages {
if let BusResponse::TopDownloaders(stats) = msg {
let result = stats.iter().map(|tt| tt.into()).collect();
return NoCache::new(MsgPack(result));
}
}
}
NoCache::new(MsgPack(Vec::new()))
} }
#[get("/api/worst_10_rtt")] #[get("/api/worst_10_rtt")]
pub fn worst_10_rtt(_auth: AuthGuard) -> Json<Vec<IpStatsWithPlan>> { pub async fn worst_10_rtt(_auth: AuthGuard) -> NoCache<MsgPack<Vec<IpStatsWithPlan>>> {
let tt: Vec<IpStatsWithPlan> = if let Ok(messages) = bus_request(vec![BusRequest::GetWorstRtt { start: 0, end: 10 }]).await
WORST_10_RTT.read().iter().map(|tt| tt.into()).collect(); {
Json(tt) for msg in messages {
if let BusResponse::WorstRtt(stats) = msg {
let result = stats.iter().map(|tt| tt.into()).collect();
return NoCache::new(MsgPack(result));
}
}
}
NoCache::new(MsgPack(Vec::new()))
} }
#[get("/api/rtt_histogram")] #[get("/api/rtt_histogram")]
pub fn rtt_histogram(_auth: AuthGuard) -> Json<Vec<u32>> { pub async fn rtt_histogram(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u32>>> {
Json(RTT_HISTOGRAM.read().clone()) if let Ok(messages) = bus_request(vec![BusRequest::RttHistogram]).await
{
for msg in messages {
if let BusResponse::RttHistogram(stats) = msg {
let result = stats;
return NoCache::new(MsgPack(result));
}
}
}
NoCache::new(MsgPack(Vec::new()))
} }
#[get("/api/host_counts")] #[get("/api/host_counts")]
pub fn host_counts(_auth: AuthGuard) -> Json<(u32, u32)> { pub async fn host_counts(_auth: AuthGuard) -> NoCache<MsgPack<(u32, u32)>> {
let shaped_reader = SHAPED_DEVICES.read(); let mut host_counts = (0, 0);
let n_devices = shaped_reader.devices.len(); if let Ok(messages) = bus_request(vec![BusRequest::AllUnknownIps]).await {
let host_counts = HOST_COUNTS.read(); for msg in messages {
if let BusResponse::AllUnknownIps(unknowns) = msg {
let really_unknown: Vec<IpStats> = unknowns
.iter()
.filter(|ip| {
if let Ok(ip) = ip.ip_address.parse::<IpAddr>() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => ip,
};
SHAPED_DEVICES.read().unwrap().trie.longest_match(lookup).is_none()
} else {
false
}
})
.cloned()
.collect();
host_counts = (really_unknown.len() as u32, 0);
}
}
}
let n_devices = SHAPED_DEVICES.read().unwrap().devices.len();
let unknown = host_counts.0 - host_counts.1; let unknown = host_counts.0 - host_counts.1;
Json((n_devices as u32, unknown)) NoCache::new(MsgPack((n_devices as u32, unknown)))
}
lazy_static! {
static ref CONFIG: Mutex<LibreQoSConfig> =
Mutex::new(lqos_config::LibreQoSConfig::load().unwrap());
}
#[get("/api/busy_quantile")]
pub fn busy_quantile(_auth: AuthGuard) -> Json<Vec<(u32, u32)>> {
let (down_capacity, up_capacity) = {
let lock = CONFIG.lock();
(
lock.total_download_mbps as f64 * 1_000_000.0,
lock.total_upload_mbps as f64 * 1_000_000.0,
)
};
let throughput = THROUGHPUT_BUFFER.read().get_result();
let mut result = vec![(0, 0); 10];
throughput.iter().for_each(|tp| {
let (down, up) = tp.bits_per_second;
let (down, up) = (down * 8, up * 8);
//println!("{down_capacity}, {up_capacity}, {down}, {up}");
let (down, up) = (
if down_capacity > 0.0 { down as f64 / down_capacity } else { 0.0 },
if up_capacity > 0.0 { up as f64 / up_capacity } else { 0.0 },
);
let (down, up) = ((down * 10.0) as usize, (up * 10.0) as usize);
result[usize::min(9, down)].0 += 1;
result[usize::min(0, up)].1 += 1;
});
Json(result)
} }

View File

@ -1,35 +1,65 @@
use std::net::IpAddr;
use crate::{ use crate::{
auth_guard::AuthGuard, cache_control::NoCache, tracker::UNKNOWN_DEVICES, auth_guard::AuthGuard, cache_control::NoCache, tracker::SHAPED_DEVICES
}; };
use lqos_bus::IpStats; use lqos_bus::{IpStats, bus_request, BusRequest, BusResponse};
use rocket::serde::json::Json; use rocket::serde::json::Json;
async fn unknown_devices() -> Vec<IpStats> {
if let Ok(messages) = bus_request(vec![BusRequest::AllUnknownIps]).await {
for msg in messages {
if let BusResponse::AllUnknownIps(unknowns) = msg {
let cfg = SHAPED_DEVICES.read().unwrap();
let really_unknown: Vec<IpStats> = unknowns
.iter()
.filter(|ip| {
if let Ok(ip) = ip.ip_address.parse::<IpAddr>() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => ip,
};
cfg.trie.longest_match(lookup).is_none()
} else {
false
}
})
.cloned()
.collect();
return really_unknown;
}
}
}
Vec::new()
}
#[get("/api/all_unknown_devices")] #[get("/api/all_unknown_devices")]
pub fn all_unknown_devices(_auth: AuthGuard) -> NoCache<Json<Vec<IpStats>>> { pub async fn all_unknown_devices(_auth: AuthGuard) -> NoCache<Json<Vec<IpStats>>> {
NoCache::new(Json(UNKNOWN_DEVICES.read().clone())) NoCache::new(Json(unknown_devices().await))
} }
#[get("/api/unknown_devices_count")] #[get("/api/unknown_devices_count")]
pub fn unknown_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> { pub async fn unknown_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
NoCache::new(Json(UNKNOWN_DEVICES.read().len())) NoCache::new(Json(unknown_devices().await.len()))
} }
#[get("/api/unknown_devices_range/<start>/<end>")] #[get("/api/unknown_devices_range/<start>/<end>")]
pub fn unknown_devices_range( pub async fn unknown_devices_range(
start: usize, start: usize,
end: usize, end: usize,
_auth: AuthGuard, _auth: AuthGuard,
) -> NoCache<Json<Vec<IpStats>>> { ) -> NoCache<Json<Vec<IpStats>>> {
let reader = UNKNOWN_DEVICES.read(); let reader = unknown_devices().await;
let result: Vec<IpStats> = let result: Vec<IpStats> =
reader.iter().skip(start).take(end).cloned().collect(); reader.iter().skip(start).take(end).cloned().collect();
NoCache::new(Json(result)) NoCache::new(Json(result))
} }
#[get("/api/unknown_devices_csv")] #[get("/api/unknown_devices_csv")]
pub fn unknown_devices_csv(_auth: AuthGuard) -> NoCache<String> { pub async fn unknown_devices_csv(_auth: AuthGuard) -> NoCache<String> {
let mut result = String::new(); let mut result = String::new();
let reader = UNKNOWN_DEVICES.read(); let reader = unknown_devices().await;
for unknown in reader.iter() { for unknown in reader.iter() {
result += &format!("{}\n", unknown.ip_address); result += &format!("{}\n", unknown.ip_address);

View File

@ -9,7 +9,7 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script> <script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">
@ -25,10 +25,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a> <a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
</li> </li>
<li class="nav-item" id="currentLogin"></li> <li class="nav-item">
<!--<li class="nav-item"> <a class="nav-link" href="/tree?parent=0"><i class="fa fa-globe"></i> Tree</a>
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a> </li>
</li>-->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a> <a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li> </li>
@ -39,6 +38,7 @@
</div> </div>
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth Test</a> <a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth Test</a>
</li> </li>
@ -68,6 +68,9 @@
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="pills-tins-tab" data-bs-toggle="pill" data-bs-target="#pills-tins" type="button" role="tab" aria-controls="pills-profile" aria-selected="false">All Tins</button> <button class="nav-link" id="pills-tins-tab" data-bs-toggle="pill" data-bs-target="#pills-tins" type="button" role="tab" aria-controls="pills-profile" aria-selected="false">All Tins</button>
</li> </li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-funnel-tab" data-bs-toggle="pill" data-bs-target="#pills-funnel" type="button" role="tab" aria-controls="pills-funnel" aria-selected="false">Queue Funnel</button>
</li>
</ul> </ul>
</div> </div>
<div class="col-sm-2"> <div class="col-sm-2">
@ -139,7 +142,7 @@
</div> </div>
</div> </div>
<div class="tab-pane fade" id="pills-tins" role="tabpanel" aria-labelledby="pills-tins-tab" tabindex="0"> <div class="tab-pane fade" id="pills-tins" role="tabpanel" aria-labelledby="pills-tins-tab" tabindex="1">
<div class="row" class="mtop4"> <div class="row" class="mtop4">
<div class="col-sm-6"> <div class="col-sm-6">
<div class="card bg-light"> <div class="card bg-light">
@ -181,6 +184,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="pills-funnel" role="tabpanel" aria-labelledby="pills-funnel-tab" tabindex="2">
</div>
</div> </div>
</div> </div>
@ -450,6 +456,63 @@
setTimeout(getThroughput, 1000); setTimeout(getThroughput, 1000);
} }
let funnels = new MultiRingBuffer(300);
let rtts = {};
let circuitId = "";
function getFunnel(c) {
circuitId = encodeURI(c);
$.get("/api/funnel_for_queue/" + circuitId, (data) => {
let html = "";
for (let i=0; i<data.length; ++i) {
funnels.push(data[i][0], data[i][1].current_throughput[0]*8, data[i][1].current_throughput[1]*8);
rtts[data[i][0]] = new RttHistogram();
let row = "<div class='row row220'>";
row += "<div class='col-sm-6'>";
row += "<div class='card bg-light'>";
row += "<h5 class='card-title'><i class='fa fa-hourglass'></i> <a class='redact' href='/tree?parent=" + data[i][0] + "'>" + redactText(data[i][1].name) + " Throughput</a></h5>";
row += "<div id='tp" + data[i][0] + "' class='graph98 graph150'></div>";
row += "</div>";
row += "</div>";
row += "<div class='col-sm-6'>";
row += "<div class='card bg-light'>";
row += "<h5 class='card-title redact'><i class='fa fa-bar-chart'></i> " + redactText(data[i][1].name) + " TCP RTT</h5>";
row += "<div id='rtt" + data[i][0] + "' class='graph98 graph150'></div>";
row += "</div>";
row += "</div>";
row += "</div>";
html += row;
}
$("#pills-funnel").html(html);
setTimeout(plotFunnels, 1000);
});
}
function plotFunnels() {
$.get("/api/funnel_for_queue/" + encodeURI(circuitId), (data) => {
for (let i=0; i<data.length; ++i) {
funnels.push(data[i][0], data[i][1].current_throughput[0]*8, data[i][1].current_throughput[1]*8);
for (const [k, v] of Object.entries(funnels.data)) {
let target_div = "tp" + k;
let graphData = v.toScatterGraphData();
let graph = document.getElementById(target_div);
Plotly.newPlot(graph, graphData, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true, title: "Time since now (seconds)"} }, { responsive: true });
}
rtts[data[i][0]].clear();
for (let j=0; j<data[i][1].rtts.length; j++) {
rtts[data[i][0]].push(data[i][1].rtts[j]);
}
rtts[data[i][0]].plot("rtt" + data[i][0]);
}
});
setTimeout(plotFunnels, 1000);
}
function start() { function start() {
colorReloadButton(); colorReloadButton();
updateHostCounts(); updateHostCounts();
@ -459,6 +522,7 @@
$.get("/api/watch_circuit/" + params.id, () => { $.get("/api/watch_circuit/" + params.id, () => {
pollQueue(); pollQueue();
getThroughput(); getThroughput();
getFunnel(params.id);
}); });
} }

View File

@ -9,7 +9,7 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script> <script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">
@ -25,10 +25,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a> <a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
</li> </li>
<li class="nav-item" id="currentLogin"></li> <li class="nav-item">
<!--<li class="nav-item"> <a class="nav-link" href="/tree?parent=0"><i class="fa fa-globe"></i> Tree</a>
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a> </li>
</li>-->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a> <a class="nav-link" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li> </li>
@ -39,6 +38,7 @@
</div> </div>
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item ms-auto"> <li class="nav-item ms-auto">
<a class="nav-link active" href="/config"><i class="fa fa-gear"></i> Configuration</a> <a class="nav-link active" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li> </li>

View File

@ -9,7 +9,7 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="vendor/bootstrap.bundle.min.js"></script> <script defer src="vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">

View File

@ -9,7 +9,7 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script> <script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">

View File

@ -1,11 +1,49 @@
function msgPackGet(url, success) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "arraybuffer";
xhr.onload = () => {
var data = xhr.response;
let decoded = msgpack.decode(new Uint8Array(data));
success(decoded);
};
xhr.send(null);
}
const NetTrans = {
"name": 0,
"max_throughput": 1,
"current_throughput": 2,
"rtts": 3,
"parents": 4,
"immediate_parent": 5
}
const Circuit = {
"id" : 0,
"name" : 1,
"traffic": 2,
"limit": 3,
}
const IpStats = {
"ip_address": 0,
"bits_per_second": 1,
"packets_per_second": 2,
"median_tcp_rtt": 3,
"tc_handle": 4,
"circuit_id": 5,
"plan": 6,
}
function metaverse_color_ramp(n) { function metaverse_color_ramp(n) {
if (n <= 9) { if (n <= 9) {
return "#32b08c"; return "#32b08c";
} else if (n <= 20) { } else if (n <= 20) {
return "#ffb94a"; return "#ffb94a";
} else if (n <=50) { } else if (n <= 50) {
return "#f95f53"; return "#f95f53";
} else if (n <=70) { } else if (n <= 70) {
return "#bf3d5e"; return "#bf3d5e";
} else { } else {
return "#dc4e58"; return "#dc4e58";
@ -49,9 +87,9 @@ function deleteAllCookies() {
function cssrules() { function cssrules() {
var rules = {}; var rules = {};
for (var i=0; i<document.styleSheets.length; ++i) { for (var i = 0; i < document.styleSheets.length; ++i) {
var cssRules = document.styleSheets[i].cssRules; var cssRules = document.styleSheets[i].cssRules;
for (var j=0; j<cssRules.length; ++j) for (var j = 0; j < cssRules.length; ++j)
rules[cssRules[j].selectorText] = cssRules[j]; rules[cssRules[j].selectorText] = cssRules[j];
} }
return rules; return rules;
@ -65,7 +103,7 @@ function css_getclass(name) {
} }
function updateHostCounts() { function updateHostCounts() {
$.get("/api/host_counts", (hc) => { msgPackGet("/api/host_counts", (hc) => {
$("#shapedCount").text(hc[0]); $("#shapedCount").text(hc[0]);
$("#unshapedCount").text(hc[1]); $("#unshapedCount").text(hc[1]);
setTimeout(updateHostCounts, 5000); setTimeout(updateHostCounts, 5000);
@ -80,7 +118,7 @@ function updateHostCounts() {
$("#currentLogin").html(html); $("#currentLogin").html(html);
}); });
$("#startTest").on('click', () => { $("#startTest").on('click', () => {
$.get("/api/run_btest", () => {}); $.get("/api/run_btest", () => { });
}); });
} }
@ -88,9 +126,9 @@ function colorReloadButton() {
$("body").append(reloadModal); $("body").append(reloadModal);
$("#btnReload").on('click', () => { $("#btnReload").on('click', () => {
$.get("/api/reload_libreqos", (result) => { $.get("/api/reload_libreqos", (result) => {
const myModal = new bootstrap.Modal(document.getElementById('reloadModal'), {focus: true}); const myModal = new bootstrap.Modal(document.getElementById('reloadModal'), { focus: true });
$("#reloadLibreResult").text(result); $("#reloadLibreResult").text(result);
myModal.show(); myModal.show();
}); });
}); });
$.get("/api/reload_required", (req) => { $.get("/api/reload_required", (req) => {
@ -150,7 +188,7 @@ function redactText(text) {
if (!isRedacted()) return text; if (!isRedacted()) return text;
let redacted = ""; let redacted = "";
let sum = 0; let sum = 0;
for(let i = 0; i < text.length; i++){ for (let i = 0; i < text.length; i++) {
let code = text.charCodeAt(i); let code = text.charCodeAt(i);
sum += code; sum += code;
} }
@ -160,13 +198,13 @@ function redactText(text) {
function scaleNumber(n) { function scaleNumber(n) {
if (n > 1000000000000) { if (n > 1000000000000) {
return (n/1000000000000).toFixed(2) + "T"; return (n / 1000000000000).toFixed(2) + "T";
} else if (n > 1000000000) { } else if (n > 1000000000) {
return (n/1000000000).toFixed(2) + "G"; return (n / 1000000000).toFixed(2) + "G";
} else if (n > 1000000) { } else if (n > 1000000) {
return (n/1000000).toFixed(2) + "M"; return (n / 1000000).toFixed(2) + "M";
} else if (n > 1000) { } else if (n > 1000) {
return (n/1000).toFixed(2) + "K"; return (n / 1000).toFixed(2) + "K";
} }
return n; return n;
} }
@ -188,4 +226,146 @@ const reloadModal = `
</div> </div>
</div> </div>
</div> </div>
</div>`; </div>`;
// MultiRingBuffer provides an interface for storing multiple ring-buffers
// of performance data, with a view to them ending up on the same graph.
class MultiRingBuffer {
constructor(capacity) {
this.capacity = capacity;
this.data = {};
}
push(id, download, upload) {
if (!this.data.hasOwnProperty(id)) {
this.data[id] = new RingBuffer(this.capacity);
}
this.data[id].push(download, upload);
}
plotStackedBars(target_div, rootName) {
let graphData = [];
for (const [k, v] of Object.entries(this.data)) {
if (k != rootName) {
let y = v.sortedY;
let dn = { x: v.x_axis, y: y.down, name: k + "_DL", type: 'scatter', stackgroup: 'dn' };
let up = { x: v.x_axis, y: y.up, name: k + "_UL", type: 'scatter', stackgroup: 'up' };
graphData.push(dn);
graphData.push(up);
}
}
let graph = document.getElementById(target_div);
Plotly.newPlot(
graph,
graphData,
{
margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 },
yaxis: { automargin: true },
xaxis: { automargin: true, title: "Time since now (seconds)" },
showlegend: false,
},
{ responsive: true, displayModeBar: false });
}
plotTotalThroughput(target_div) {
let graph = document.getElementById(target_div);
let total = this.data['total'].sortedY();
let shaped = this.data['shaped'].sortedY();
let x = this.data['total'].x_axis;
let data = [
{x: x, y:total.down, name: 'Download', type: 'scatter', marker: {color: 'rgb(255,160,122)'}},
{x: x, y:total.up, name: 'Upload', type: 'scatter', marker: {color: 'rgb(255,160,122)'}},
{x: x, y:shaped.down, name: 'Shaped Download', type: 'scatter', fill: 'tozeroy', marker: {color: 'rgb(124,252,0)'}},
{x: x, y:shaped.up, name: 'Shaped Upload', type: 'scatter', fill: 'tozeroy', marker: {color: 'rgb(124,252,0)'}},
];
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true, title: "Time since now (seconds)"} }, { responsive: true });
}
}
class RingBuffer {
constructor(capacity) {
this.capacity = capacity;
this.head = capacity - 1;
this.download = [];
this.upload = [];
this.x_axis = [];
for (var i = 0; i < capacity; ++i) {
this.download.push(0.0);
this.upload.push(0.0);
this.x_axis.push(i);
}
}
push(download, upload) {
this.download[this.head] = download;
this.upload[this.head] = 0.0 - upload;
this.head += 1;
this.head %= this.capacity;
}
sortedY() {
let result = {
down: [],
up: [],
};
for (let i=this.head; i<this.capacity; i++) {
result.down.push(this.download[i]);
result.up.push(this.upload[i]);
}
for (let i=0; i < this.head; i++) {
result.down.push(this.download[i]);
result.up.push(this.upload[i]);
}
return result;
}
toScatterGraphData() {
let y = this.sortedY();
let GraphData = [
{ x: this.x_axis, y: y.down, name: 'Download', type: 'scatter' },
{ x: this.x_axis, y: y.up, name: 'Upload', type: 'scatter' },
];
return GraphData;
}
}
class RttHistogram {
constructor() {
this.entries = []
this.x = [];
for (let i = 0; i < 20; ++i) {
this.entries.push(i);
this.x.push(i * 10);
}
}
clear() {
for (let i = 0; i < 20; ++i) {
this.entries[i] = 0;
}
}
push(rtt) {
let band = Math.floor(rtt / 10.0);
if (band > 19) {
band = 19;
}
this.entries[band] += 1;
}
pushBand(band, n) {
this.entries[band] += n;
}
plot(target_div) {
let gData = [
{ x: this.x, y: this.entries, type: 'bar', marker: { color: this.x, colorscale: 'RdBu' } }
]
let graph = document.getElementById(target_div);
Plotly.newPlot(graph, gData, { margin: { l: 0, r: 0, b: 35, t: 0 }, xaxis: { title: 'TCP Round-Trip Time (ms)' } }, { responsive: true });
}
}

View File

@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -9,15 +10,19 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script> <script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" />&nbsp;LibreQoS</a> <a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
@ -25,28 +30,32 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/"><i class="fa fa-home"></i> Dashboard</a> <a class="nav-link active" aria-current="page" href="/"><i class="fa fa-home"></i> Dashboard</a>
</li> </li>
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-globe"></i> Network Layout</a> <a class="nav-link" href="/tree?parent=0"><i class="fa fa-globe"></i> Tree</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a> <a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a> <a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li> </li>
</ul> </ul>
</div> </div>
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth Test</a> <a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth
Test</a>
</li> </li>
<li class="nav-item ms-auto"> <li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a> <a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li> </li>
<li> <li>
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a> <a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
LibreQoS</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -54,227 +63,200 @@
<div id="container" class="pad4"> <div id="container" class="pad4">
<!-- Dashboard Row 1 --> <!-- Dashboard Row 1 -->
<div class="row mbot8"> <div class="row mbot8">
<!-- THROUGHPUT --> <!-- THROUGHPUT -->
<div class="col-sm-4"> <div class="col-sm-4">
<div class="card bg-light"> <div class="card bg-light">
<div class="card-body"> <div class="card-body">
<h5 class="card-title"><i class="fa fa-bolt"></i> Current Throughput</h5> <h5 class="card-title"><i class="fa fa-bolt"></i> Current Throughput</h5>
<table class="table"> <table class="table">
<tr> <tr>
<td class="bold">Packets/Second</td> <td class="bold">Packets/Second</td>
<td id="ppsDown"></td> <td id="ppsDown"></td>
<td id="ppsUp"></td> <td id="ppsUp"></td>
</tr> </tr>
<tr> <tr>
<td class="bold">Bits/Second</td> <td class="bold">Bits/Second</td>
<td id="bpsDown"></td> <td id="bpsDown"></td>
<td id="bpsUp"></td> <td id="bpsUp"></td>
</tr> </tr>
</table> </table>
</div>
</div>
</div>
<!-- RAM INFO -->
<div class="col-sm-2">
<div class="card bg-light d-none d-lg-block">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-database"></i> Memory Status</h5>
<div id="ram" class="graph98"></div>
</div>
</div>
</div>
<!-- CPU INFO -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-microchip"></i> CPU Status</h5>
<div id="cpu" class="graph98"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- RAM INFO --> <!-- Dashboard Row 2 -->
<div class="col-sm-2"> <div class="row mbot8 row220">
<div class="card bg-light d-none d-lg-block"> <!-- 5 minutes of throughput -->
<div class="card-body"> <div class="col-sm-4">
<h5 class="card-title"><i class="fa fa-database"></i> Memory Status</h5> <div class="card bg-light">
<div id="ram" class="graph98"></div> <div class="card-body">
<h5 class="card-title"><i class="fa fa-hourglass"></i> Last 5 Minutes</h5>
<div id="tpGraph" class="graph98 graph150"></div>
</div>
</div>
</div>
<!-- RTT Histogram -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bar-chart"></i> TCP Round-Trip Time Histogram</h5>
<div id="rttHistogram" class="graph98 graph150"></div>
</div>
</div>
</div>
<!-- Site Funnel -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-globe"></i> Site Funnel</h5>
<div id="siteFunnel" class="graph98 graph150"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- CPU INFO --> <!-- Dashboard Row 3 -->
<div class="col-sm-6"> <div class="row">
<div class="card bg-light"> <!-- Top 10 downloaders -->
<div class="card-body"> <div class="col-sm-6">
<h5 class="card-title"><i class="fa fa-microchip"></i> CPU Status</h5> <div class="card bg-light">
<div id="cpu" class="graph98"></div> <div class="card-body">
<h5 class="card-title"><i class='fa fa-arrow-down'></i> Top 10 Downloaders</h5>
<div id="top10dl"></div>
</div>
</div> </div>
</div> </div>
</div>
</div>
<!-- Dashboard Row 2 --> <!-- Worst 10 RTT -->
<div class="row mbot8 row220"> <div class="col-sm-6">
<!-- 5 minutes of throughput --> <div class="card bg-light">
<div class="col-sm-4"> <div class="card-body">
<div class="card bg-light"> <h5 class="card-title"><i class='fa fa-exclamation'></i> Worst 10 RTT</h5>
<div class="card-body"> <div id="worstRtt"></div>
<h5 class="card-title"><i class="fa fa-hourglass"></i> Last 5 Minutes</h5> </div>
<div id="tpGraph" class="graph98 graph150"></div>
</div> </div>
</div> </div>
</div> </div>
<!-- RTT Histogram -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bar-chart"></i> TCP Round-Trip Time Histogram</h5>
<div id="rttHistogram" class="graph98 graph150"></div>
</div>
</div>
</div>
<!-- Idle/Activity Quantiles -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bar-chart"></i> Utilization Quantiles</h5>
<div id="capacityHistogram" class="graph98 graph150"></div>
</div>
</div>
</div>
</div>
<!-- Dashboard Row 3 -->
<div class="row">
<!-- Top 10 downloaders -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class='fa fa-arrow-down'></i> Top 10 Downloaders</h5>
<div id="top10dl"></div>
</div>
</div>
</div>
<!-- Worst 10 RTT -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class='fa fa-exclamation'></i> Worst 10 RTT</h5>
<div id="worstRtt"></div>
</div>
</div>
</div>
</div>
</div> </div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer> <footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script> <script>
var throughput = new MultiRingBuffer(300);
function updateCurrentThroughput() { function updateCurrentThroughput() {
$.get("/api/current_throughput", (tp) => { msgPackGet("/api/current_throughput", (tp) => {
$("#ppsDown").text(scaleNumber(tp.packets_per_second[0])); const bits = 0;
$("#ppsUp").text(scaleNumber(tp.packets_per_second[1])); const packets = 1;
$("#bpsDown").text(scaleNumber(tp.bits_per_second[0])); const shaped = 2;
$("#bpsUp").text(scaleNumber(tp.bits_per_second[1])); $("#ppsDown").text(scaleNumber(tp[packets][0]));
setTimeout(updateCurrentThroughput, 1000); $("#ppsUp").text(scaleNumber(tp[packets][1]));
$("#bpsDown").text(scaleNumber(tp[bits][0]));
$("#bpsUp").text(scaleNumber(tp[bits][1]));
throughput.push("pps", tp[1][0], tp[packets][1]);
throughput.push("total", tp[bits][0], tp[bits][1]);
throughput.push("shaped", tp[shaped][0], tp[shaped][1]);
throughput.plotTotalThroughput("tpGraph");
}); });
} }
function updateThroughputGraph() { var funnelData = new MultiRingBuffer(300);
$.get("/api/throughput_ring", (tp) => {
let graph = document.getElementById("tpGraph");
let x = [];
let y = []; // Down
let y2 = []; // Up
let y3 = []; // Shaped Down
let y4 = []; // Shaped Up
for (i=0; i<300; i++) {
x.push(i);
y.push(tp[i].bits_per_second[0]);
y2.push(0.0 - tp[i].bits_per_second[1]);
y3.push(tp[i].shaped_bits_per_second[0]);
y4.push(0.0 - tp[i].shaped_bits_per_second[1]);
}
let data = [
{x: x, y:y, name: 'Download', type: 'scatter'},
{x: x, y:y2, name: 'Upload', type: 'scatter'},
{x: x, y:y3, name: 'Shaped Download', type: 'scatter', fill: 'tozeroy'},
{x: x, y:y4, name: 'Shaped Upload', type: 'scatter', fill: 'tozeroy'},
];
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true, title: "Time since now (seconds)"} }, { responsive: true });
//console.log(tp);
setTimeout(updateThroughputGraph, 1000);
});
}
function updateThroughputQuantile() { function updateSiteFunnel() {
$.get("/api/busy_quantile", (tp) => { msgPackGet("/api/network_tree_summary/", (data) => {
//console.log(tp); let table = "<table class='table' style='font-size: 8pt;'>";
let graph = document.getElementById("capacityHistogram"); for (let i = 0; i < data.length; ++i) {
let x1 = []; let name = data[i][1][NetTrans.name];
let x2 = []; if (name.length > 20) {
let y1 = []; name = name.substring(0, 20) + "...";
let y2 = [];
for (let i=0; i<10; i++) {
x1.push(i*10);
x2.push(i*10);
if (i > 0) {
y1.push(tp[i][0]);
y2.push(tp[i][1]);
} else {
y1.push(0);
y2.push(0);
} }
table += "<tr>";
table += "<td class='redact'>" + redactText(name) + "</td>";
table += "<td>" + scaleNumber(data[i][1][NetTrans.current_throughput][0] * 8) + "</td>";
table += "<td>" + scaleNumber(data[i][1][NetTrans.current_throughput][1] * 8) + "</td>";
table += "</tr>";
} }
let data = [ table += "</table>";
{x: x1, y:y1, type: 'bar', name: 'Download'}, $("#siteFunnel").html(table);
{x: x1, y:y2, type: 'bar', name: 'Upload'},
];
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: '# Samples' }, xaxis: {automargin: true, title: "% utilization"} }, { responsive: true });
setTimeout(updateThroughputQuantile, 1000);
}); });
} }
function updateCpu() { function updateCpu() {
$.get("/api/cpu", (cpu) => { msgPackGet("/api/cpu", (cpu) => {
let graph = document.getElementById("cpu"); let graph = document.getElementById("cpu");
let x = []; let x = [];
let y = []; let y = [];
let colors = []; let colors = [];
for (i=0; i<cpu.length; i++) { for (i = 0; i < cpu.length; i++) {
x.push(i); x.push(i);
y.push(cpu[i]); y.push(cpu[i]);
colors.push(cpu[i]); colors.push(cpu[i]);
} }
colors.push(100); // 1 extra colors entry to force color scaling colors.push(100); // 1 extra colors entry to force color scaling
let data = [ {x: x, y:y, type: 'bar', marker: { color:colors, colorscale: 'Jet' } } ]; let data = [{ x: x, y: y, type: 'bar', marker: { color: colors, colorscale: 'Jet' } }];
Plotly.newPlot(graph, data, { Plotly.newPlot(graph, data, {
margin: { l:0,r:0,b:15,t:0 }, margin: { l: 0, r: 0, b: 15, t: 0 },
yaxis: { automargin: true, autorange: false, range: [0.0, 100.0 ]}, yaxis: { automargin: true, autorange: false, range: [0.0, 100.0] },
}, },
{ responsive: true }); { responsive: true });
setTimeout(updateCpu, 2000);
}); });
} }
function updateRam() { function updateRam() {
$.get("/api/ram", (ram) => { msgPackGet("/api/ram", (ram) => {
let graph = document.getElementById("ram"); let graph = document.getElementById("ram");
let data = [ { let data = [{
values: [ram[0], ram[1]-ram[0]], values: [ram[0], ram[1] - ram[0]],
labels: ['Used', 'Available'], labels: ['Used', 'Available'],
type: 'pie' type: 'pie'
} ]; }];
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:12 }, showlegend: false }, { responsive: true }); Plotly.newPlot(graph, data, { margin: { l: 0, r: 0, b: 0, t: 12 }, showlegend: false }, { responsive: true });
setTimeout(updateRam, 30000);
}); });
} }
function updateNTable(target, tt) { function updateNTable(target, tt) {
let html = "<table class='table'>"; let html = "<table class='table'>";
html += "<thead><th>IP Address</th><th>DL ⬇️</th><th>UL ⬆️</th><th>RTT (ms)</th><th>Shaped</th></thead>"; html += "<thead><th>IP Address</th><th>DL ⬇️</th><th>UL ⬆️</th><th>RTT (ms)</th><th>Shaped</th></thead>";
for (let i=0; i<tt.length; i++) { for (let i = 0; i < tt.length; i++) {
let color = color_ramp(tt[i].median_tcp_rtt); let color = color_ramp(tt[i][IpStats.median_tcp_rtt]);
html += "<tr style='background-color: " + color + "'>"; html += "<tr style='background-color: " + color + "'>";
if (tt[i].circuit_id != "") { if (tt[i][IpStats.circuit_id] != "") {
html += "<td><a class='redact' href='/circuit_queue?id=" + encodeURI(tt[i].circuit_id) + "'>" + redactText(tt[i].ip_address) + "</td>"; html += "<td><a class='redact' href='/circuit_queue?id=" + encodeURI(tt[i][IpStats.circuit_id]) + "'>" + redactText(tt[i][IpStats.ip_address]) + "</td>";
} else { } else {
html += "<td><span class='redact'>" + redactText(tt[i].ip_address) + "</span></td>"; html += "<td><span class='redact'>" + redactText(tt[i][IpStats.ip_address]) + "</span></td>";
} }
html += "<td>" + scaleNumber(tt[i].bits_per_second[0]) + "</td>"; html += "<td>" + scaleNumber(tt[i][IpStats.bits_per_second][0]) + "</td>";
html += "<td>" + scaleNumber(tt[i].bits_per_second[1]) + "</td>"; html += "<td>" + scaleNumber(tt[i][IpStats.bits_per_second][1]) + "</td>";
html += "<td>" + tt[i].median_tcp_rtt.toFixed(2) + "</td>"; html += "<td>" + tt[i][IpStats.median_tcp_rtt].toFixed(2) + "</td>";
if (tt[i].tc_handle !=0) { if (tt[i].tc_handle != 0) {
html += "<td><i class='fa fa-check-circle'></i> (" + tt[i].plan[0] + "/" + tt[i].plan[1] + ")</td>"; html += "<td><i class='fa fa-check-circle'></i> (" + tt[i][IpStats.plan][0] + "/" + tt[i][IpStats.plan][1] + ")</td>";
} else { } else {
//html += "<td><a class='btn btn-small btn-success' href='/shaped-add?ip=" + tt[i].ip_address + "'>Add Shaper</a></td>"; //html += "<td><a class='btn btn-small btn-success' href='/shaped-add?ip=" + tt[i].ip_address + "'>Add Shaper</a></td>";
html += "<td>Not Shaped</td>" html += "<td>Not Shaped</td>"
@ -286,57 +268,72 @@
} }
function updateTop10() { function updateTop10() {
$.get("/api/top_10_downloaders", (tt) => { msgPackGet("/api/top_10_downloaders", (tt) => {
updateNTable('#top10dl', tt); updateNTable('#top10dl', tt);
setTimeout(updateTop10, 5000);
}); });
} }
function updateWorst10() { function updateWorst10() {
$.get("/api/worst_10_rtt", (tt) => { msgPackGet("/api/worst_10_rtt", (tt) => {
updateNTable('#worstRtt', tt); updateNTable('#worstRtt', tt);
setTimeout(updateWorst10, 5000);
}); });
} }
var rttGraph = new RttHistogram();
function updateHistogram() { function updateHistogram() {
$.get("/api/rtt_histogram", (rtt) => { msgPackGet("/api/rtt_histogram", (rtt) => {
let graph = document.getElementById("rttHistogram"); rttGraph.clear();
let x = []; for (let i = 0; i < rtt.length; i++) {
let y = []; rttGraph.pushBand(i, rtt[i]);
for (let i=0; i<rtt.length; i++) {
x.push(i*10.0);
y.push(rtt[i]);
} }
let data = [ rttGraph.plot("rttHistogram");
{x:x, y:y, type: 'bar', marker: { color:x, colorscale: 'RdBu' } }
]
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:35,t:0 }, xaxis: { title: 'TCP Round-Trip Time (ms)' } }, { responsive: true });
setTimeout(updateHistogram, 5000);
}); });
} }
var tickCount = 0;
function OneSecondCadence() {
updateCurrentThroughput();
updateSiteFunnel();
if (tickCount % 5 == 0) {
updateHistogram();
updateWorst10();
updateTop10();
}
if (tickCount % 10 == 0) {
updateCpu();
updateRam();
}
tickCount++;
setTimeout(OneSecondCadence, 1000);
}
function start() { function start() {
if (isRedacted()) { if (isRedacted()) {
console.log("Redacting"); //console.log("Redacting");
//css_getclass(".redact").style.filter = "blur(4px)"; //css_getclass(".redact").style.filter = "blur(4px)";
css_getclass(".redact").style.fontFamily = "klingon"; css_getclass(".redact").style.fontFamily = "klingon";
} }
colorReloadButton(); colorReloadButton();
updateCurrentThroughput(); updateCurrentThroughput();
updateThroughputGraph();
updateCpu(); updateCpu();
updateRam(); updateRam();
updateTop10(); updateTop10();
updateWorst10(); updateWorst10();
updateHistogram(); updateHistogram();
updateHostCounts(); updateHostCounts();
updateThroughputQuantile(); updateSiteFunnel();
OneSecondCadence();
} }
$(document).ready(start); $(document).ready(start);
</script> </script>
</body> </body>
</html>
</html>

View File

@ -9,7 +9,7 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script> <script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">
@ -25,10 +25,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a> <a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
</li> </li>
<li class="nav-item" id="currentLogin"></li> <li class="nav-item">
<!--<li class="nav-item"> <a class="nav-link" href="/tree?parent=0"><i class="fa fa-globe"></i> Tree</a>
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a> </li>
</li>-->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a> <a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li> </li>
@ -39,6 +38,7 @@
</div> </div>
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item ms-auto"> <li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a> <a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li> </li>

View File

@ -9,7 +9,7 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script> <script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">
@ -25,10 +25,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a> <a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
</li> </li>
<li class="nav-item" id="currentLogin"></li> <li class="nav-item">
<!--<li class="nav-item"> <a class="nav-link" href="/tree?parent=0"><i class="fa fa-globe"></i> Tree</a>
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a> </li>
</li>-->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a> <a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li> </li>
@ -39,6 +38,7 @@
</div> </div>
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth Test</a> <a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth Test</a>
</li> </li>

View File

@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -9,44 +10,52 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script> <script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" />&nbsp;LibreQoS</a> <a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/"><i class="fa fa-home"></i> Dashboard</a> <a class="nav-link" aria-current="page" href="/"><i class="fa fa-home"></i> Dashboard</a>
</li>
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-globe"></i> Network Layout</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a> <a class="nav-link active" href="/tree?parent=0"><i class="fa fa-globe"></i> Tree</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a> <a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li> </li>
</ul> </ul>
</div> </div>
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth Test</a> <a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth
Test</a>
</li> </li>
<li class="nav-item ms-auto"> <li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a> <a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li> </li>
<li> <li>
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a> <a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
LibreQoS</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -54,76 +63,232 @@
<div id="container" class="pad4"> <div id="container" class="pad4">
<div class="row top-shunt"> <div class="row mbot8 row220">
<div class="col-sm-12 bg-light center-txt"> <!-- 5 minutes of throughput -->
THIS NODE <!--
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-hourglass"></i> Last 5 Minutes</h5>
<div id="tpGraph" class="graph98 graph150"></div>
</div>
</div>
</div>
-->
<!-- RTT Histogram -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bar-chart"></i> TCP Round-Trip Time Histogram</h5>
<div id="rttHistogram" class="graph98 graph150"></div>
</div>
</div>
</div>
<!-- Info -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-globe"></i> <span id="nodeName"
style="font-weight: bold;" class='redact'></span></h5>
<strong>DL Limit</strong>: <span id="nodeDL"></span><br />
<strong>UL Limit</strong>: <span id="nodeUL"></span><br />
<div id="breadcrumbs"></div>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: 4px;">
<!-- List of network circuits -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-globe"></i> Child Nodes</h5>
<div id="treeList"></div>
</div>
</div>
</div>
<!-- List of client circuits -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-users"></i> Attached Clients</h5>
<div id="clientList"></div>
</div>
</div>
</div> </div>
</div> </div>
<div class="row">
<div class="col-sm-12 bg-light center-txt">
<div id="treeList"></div>
</div>
</div>
</div> <footer>&copy; 2022-2023, LibreQoE LLC</footer>
<footer>&copy; 2022-2023, LibreQoE LLC</footer> <script>
let node = 0;
let buffers = new MultiRingBuffer(300);
let rtt_histo = new RttHistogram();
<script> function bgColor(traffic, limit) {
let node = 0; if (limit == 0) {
return "#ddffdd";
function getTree() {
$.get("/api/network_tree/" + node, (data) => {
//console.log(data);
let tbl = "<table class='table'>";
tbl += "<thead><th>Circuit</th><th>Limit</th><th>Download</th><th>Upload</th><th>RTT Latency</th></thead>";
for (let i=1; i<data.length; ++i) {
tbl += "<tr>";
tbl += "<td style='width: 20%'><a href='/tree?parent=" + encodeURI(data[i][0]) + "'>" + data[i][1].name + "</a></td>";
if (data[i][1].max_throughput[0]==0 && data[i][1].max_throughput[1] == 0) {
tbl += "<td>No Limit</td>";
} else {
let down = scaleNumber(data[i][1].max_throughput[0] * 1000000);
let up = scaleNumber(data[i][1].max_throughput[1] * 1000000);
tbl += "<td>" + down + " / " + up + "</td>";
}
let down = scaleNumber(data[i][1].current_throughput[0] * 8);
let up = scaleNumber(data[i][1].current_throughput[1] * 8);
tbl += "<td>" + down + "</td>";
tbl += "<td>" + up + "</td>";
let rtt = "-";
if (data[i][1].rtts.length > 0) {
let sum = 0;
for (let j=0; j<data[i][1].rtts.length; ++j) {
sum += data[i][1].rtts[j];
}
sum /= data[i][1].rtts.length;
rtt = sum.toFixed(2) + " ms";
}
tbl += "<td>" + rtt + "</td>";
tbl += "</tr>";
} }
tbl += "</table>"; let usage = (traffic * 8) / (limit * 1000000);
$("#treeList").html(tbl); if (usage < 0.25) { return "#ddffdd" }
else if (usage < 0.5) { return "#aaffaa" }
else if (usage < 0.75) { return "#ffa500" }
else { return "#ffdddd" }
}
function getClients(rootName) {
msgPackGet("/api/tree_clients/" + encodeURI(rootName), (data) => {
let tbl = "<table class='table table-striped'>";
tbl += "<thead><th>Circuit</th><th>Limit</th><th>⬇️ DL</th><th>⬆️ UL</th></thead>";
for (let i = 0; i < data.length; ++i) {
let nodeDL = scaleNumber(data[i][Circuit.limit][0] * 1000000);
let nodeUL = scaleNumber(data[i][Circuit.limit[1]] * 1000000);
if (nodeDL == "0") nodeDL = "Unlimited";
if (nodeUL == "0") nodeUL = "Unlimited";
tbl += "<tr>";
let displayName = data[i][Circuit.name];
if (displayName.length > 30) displayName = displayName.substring(0, 30) + "...";
tbl += "<td class='redact'><a href='/circuit_queue?id=" + encodeURI(data[i][Circuit.id]) + "'>" + redactText(displayName) + "</a></td>";
tbl += "<td>" + nodeDL + " / " + nodeUL + "</td>";
let upbg = bgColor(data[i][Circuit.traffic][1], data[i][Circuit.limit][1]);
let dnbg = bgColor(data[i][Circuit.traffic][0], data[0][Circuit.limit][1]);
tbl += "<td style='background-color: " + dnbg + "'>" + scaleNumber(data[i][Circuit.traffic][0] * 8) + "</td>";
tbl += "<td style='background-color: " + upbg + "'>" + scaleNumber(data[i][Circuit.traffic][1] * 8) + "</td>";
buffers.push(nodeName, data[i][Circuit.traffic][0] * 8, data[i][Circuit.traffic][1] * 8);
}
tbl += "</table>";
$("#clientList").html(tbl);
});
}
let filled_root = false;
function getTree() {
msgPackGet("/api/network_tree/" + node, (data) => {
rtt_histo.clear();
//console.log(data);
// Setup "this node"
let rootName = data[0][1][NetTrans.name];
if (!filled_root) {
$("#nodeName").text(redactText(rootName));
let nodeDL = scaleNumber(data[0][1][NetTrans.max_throughput][0] * 1000000);
let nodeUL = scaleNumber(data[0][1][NetTrans.max_throughput][1] * 1000000);
if (nodeDL == "0") nodeDL = "Unlimited";
if (nodeUL == "0") nodeUL = "Unlimited";
$("#nodeDL").text(nodeDL);
$("#nodeUL").text(nodeUL);
$.ajax({
type: "POST",
url: "/api/node_names",
data: JSON.stringify(data[0][1][NetTrans.parents]),
success: (nodeNames) => {
let breadcrumbs = "<nav aria-label='breadcrumb'>";
breadcrumbs += "<ol class='breadcrumb'>";
for (let i=0; i<data[0][1][NetTrans.parents].length; ++i) {
let bcid = data[0][1][NetTrans.parents][i];
if (bcid != node) {
let n = nodeNames.find(e => e[0] == data[0][1][NetTrans.parents][i])[1];
breadcrumbs += "<li class='breadcrumb-item redact'>";
breadcrumbs += "<a href='/tree?parent=" + data[0][1][NetTrans.parents][i] + "'>";
breadcrumbs += redactText(n);
breadcrumbs += "</a></li>";
}
}
breadcrumbs += "<li class='breadcrumb-item active redact' aria-current='page'>";
breadcrumbs += redactText(rootName);
breadcrumbs += "</li>";
breadcrumbs += "</ol>";
breadcrumbs += "</nav>";
$("#breadcrumbs").html(breadcrumbs);
}
});
filled_root = true;
}
getClients(rootName);
// Throughput graph
buffers.push(rootName, data[0][1][NetTrans.current_throughput][0] * 8, data[0][1][NetTrans.current_throughput][1] * 8);
// Build the table & update node buffers
let tbl = "<table class='table table-striped'>";
tbl += "<thead><th>Site</th><th>Limit</th><th>⬇️ DL</th><th>⬆️ UL</th><th>RTT Latency</th></thead>";
for (let i = 1; i < data.length; ++i) {
let nodeName = data[i][1][NetTrans.name];
buffers.push(nodeName, data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
tbl += "<tr>";
tbl += "<td class='redact'><a href='/tree?parent=" + encodeURI(data[i][0]) + "'>" + redactText(nodeName) + "</a></td>";
if (data[i][1][NetTrans.max_throughput][0] == 0 && data[i][1][NetTrans.max_throughput][1] == 0) {
tbl += "<td>No Limit</td>";
} else {
let down = scaleNumber(data[i][1][NetTrans.max_throughput][0] * 1000000);
let up = scaleNumber(data[i][1][NetTrans.max_throughput][1] * 1000000);
tbl += "<td>" + down + " / " + up + "</td>";
}
let down = scaleNumber(data[i][1][NetTrans.current_throughput][0] * 8);
let up = scaleNumber(data[i][1][NetTrans.current_throughput][1] * 8);
let dbg = bgColor(data[i][1][NetTrans.current_throughput][0], data[i][1][NetTrans.max_throughput][0]);
let ubg = bgColor(data[i][1][NetTrans.current_throughput][0], data[i][1][NetTrans.max_throughput][0]);
tbl += "<td style='background-color: " + dbg + "'>" + down + "</td>";
tbl += "<td style='background-color: " + ubg + "'>" + up + "</td>";
let rtt = "-";
if (data[i][1][NetTrans.rtts].length > 0) {
let sum = 0;
for (let j = 0; j < data[i][1][NetTrans.rtts].length; ++j) {
sum += data[i][1][NetTrans.rtts][j];
}
sum /= data[i][1][NetTrans.rtts].length;
rtt = sum.toFixed(2) + " ms";
rtt_histo.push(sum);
}
tbl += "<td>" + rtt + "</td>";
tbl += "</tr>";
}
tbl += "</table>";
$("#treeList").html(tbl);
// Build the stacked chart
//buffers.plotStackedBars("tpGraph", rootName);
// Build the RTT histo
rtt_histo.plot("rttHistogram");
});
if (isRedacted()) {
//console.log("Redacting");
//css_getclass(".redact").style.filter = "blur(4px)";
css_getclass(".redact").style.fontFamily = "klingon";
}
setTimeout(getTree, 1000);
}
function start() {
for (let i = 0; i < 20; ++i) rtt_histo.push(0);
colorReloadButton();
updateHostCounts();
getTree();
}
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
}); });
setTimeout(getTree, 1000); node = params.parent;
}
function start() { $(document).ready(start);
colorReloadButton(); </script>
updateHostCounts();
getTree();
}
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
node = params.parent;
$(document).ready(start);
</script>
</body> </body>
</html>
</html>

View File

@ -9,7 +9,7 @@
<title>LibreQoS - Local Node Manager</title> <title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script> <script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script> <script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script> <script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script> <script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head> </head>
<body class="bg-secondary"> <body class="bg-secondary">
@ -25,10 +25,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a> <a class="nav-link" href="/"><i class="fa fa-home"></i> Dashboard</a>
</li> </li>
<li class="nav-item" id="currentLogin"></li> <li class="nav-item">
<!--<li class="nav-item"> <a class="nav-link" href="/tree?parent=0"><i class="fa fa-globe"></i> Tree</a>
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</a> </li>
</li>-->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a> <a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li> </li>
@ -39,6 +38,7 @@
</div> </div>
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item ms-auto"> <li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a> <a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li> </li>

File diff suppressed because one or more lines are too long

View File

@ -2,8 +2,8 @@ use lqos_bus::{BusRequest, BusResponse, TcHandle};
use lqos_utils::hex_string::read_hex_string; use lqos_utils::hex_string::read_hex_string;
use nix::libc::getpid; use nix::libc::getpid;
use pyo3::{ use pyo3::{
exceptions::PyOSError, pyclass, pyfunction, pymodule, types::PyModule, exceptions::PyOSError, pyclass, pyfunction, pymethods, pymodule,
wrap_pyfunction, PyResult, Python, pymethods, types::PyModule, wrap_pyfunction, PyResult, Python,
}; };
use std::{ use std::{
fs::{remove_file, File}, fs::{remove_file, File},
@ -158,7 +158,13 @@ impl BatchedCommands {
Ok(Self { batch: Vec::new() }) Ok(Self { batch: Vec::new() })
} }
pub fn add_ip_mapping(&mut self, ip: String, classid: String, cpu: String, upload: bool) -> PyResult<()> { pub fn add_ip_mapping(
&mut self,
ip: String,
classid: String,
cpu: String,
upload: bool,
) -> PyResult<()> {
let request = parse_add_ip(&ip, &classid, &cpu, upload); let request = parse_add_ip(&ip, &classid, &cpu, upload);
if let Ok(request) = request { if let Ok(request) = request {
self.batch.push(request); self.batch.push(request);

View File

@ -13,10 +13,9 @@ lqos_sys = { path = "../lqos_sys" }
lqos_utils = { path = "../lqos_utils" } lqos_utils = { path = "../lqos_utils" }
log = "0" log = "0"
log-once = "0.4.0" log-once = "0.4.0"
lazy_static = "1.4"
parking_lot = "0"
tokio = { version = "1", features = [ "full", "parking_lot" ] } tokio = { version = "1", features = [ "full", "parking_lot" ] }
rayon = "1" once_cell = "1"
dashmap = "5"
[dev-dependencies] [dev-dependencies]
criterion = { version = "0", features = [ "html_reports"] } criterion = { version = "0", features = [ "html_reports"] }

View File

@ -3,9 +3,8 @@ use lqos_bus::BusResponse;
pub fn get_raw_circuit_data(circuit_id: &str) -> BusResponse { pub fn get_raw_circuit_data(circuit_id: &str) -> BusResponse {
still_watching(circuit_id); still_watching(circuit_id);
let reader = CIRCUIT_TO_QUEUE.read(); if let Some(circuit) = CIRCUIT_TO_QUEUE.get(circuit_id) {
if let Some(circuit) = reader.get(circuit_id) { if let Ok(json) = serde_json::to_string(circuit.value()) {
if let Ok(json) = serde_json::to_string(circuit) {
BusResponse::RawQueueData(json) BusResponse::RawQueueData(json)
} else { } else {
BusResponse::RawQueueData(String::new()) BusResponse::RawQueueData(String::new())

View File

@ -1,9 +1,6 @@
use dashmap::DashMap;
use once_cell::sync::Lazy;
use crate::queue_store::QueueStore; use crate::queue_store::QueueStore;
use lazy_static::*;
use parking_lot::RwLock;
use std::collections::HashMap;
lazy_static! { pub(crate) static CIRCUIT_TO_QUEUE: Lazy<DashMap<String, QueueStore>> =
pub(crate) static ref CIRCUIT_TO_QUEUE: RwLock<HashMap<String, QueueStore>> = Lazy::new(DashMap::new);
RwLock::new(HashMap::new());
}

View File

@ -1,10 +1,6 @@
use lazy_static::*;
use std::sync::atomic::AtomicU64; use std::sync::atomic::AtomicU64;
lazy_static! { pub(crate) static QUEUE_MONITOR_INTERVAL: AtomicU64 = AtomicU64::new(1000);
pub(crate) static ref QUEUE_MONITOR_INTERVAL: AtomicU64 =
AtomicU64::new(1000);
}
pub fn set_queue_refresh_interval(interval_ms: u64) { pub fn set_queue_refresh_interval(interval_ms: u64) {
QUEUE_MONITOR_INTERVAL QUEUE_MONITOR_INTERVAL

View File

@ -1,19 +1,16 @@
use std::sync::RwLock;
use crate::queue_structure::{ use crate::queue_structure::{
queue_network::QueueNetwork, queue_node::QueueNode, read_queueing_structure, queue_network::QueueNetwork, queue_node::QueueNode, read_queueing_structure,
}; };
use lazy_static::*;
use log::{error, info}; use log::{error, info};
use lqos_utils::file_watcher::FileWatcher; use lqos_utils::file_watcher::FileWatcher;
use parking_lot::RwLock; use once_cell::sync::Lazy;
use thiserror::Error; use thiserror::Error;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
lazy_static! { pub(crate) static QUEUE_STRUCTURE: Lazy<RwLock<QueueStructure>> =
/// Global storage of the shaped devices csv data. Lazy::new(|| RwLock::new(QueueStructure::new()));
/// Updated by the file system watcher whenever
/// the underlying file changes.
pub(crate) static ref QUEUE_STRUCTURE : RwLock<QueueStructure> = RwLock::new(QueueStructure::new());
}
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct QueueStructure { pub(crate) struct QueueStructure {
@ -48,7 +45,7 @@ pub async fn spawn_queue_structure_monitor() {
fn update_queue_structure() { fn update_queue_structure() {
info!("queueingStructure.json reloaded"); info!("queueingStructure.json reloaded");
QUEUE_STRUCTURE.write().update(); QUEUE_STRUCTURE.write().unwrap().update();
} }
/// Fires up a Linux file system watcher than notifies /// Fires up a Linux file system watcher than notifies

View File

@ -1,7 +1,7 @@
use lqos_utils::hex_string::read_hex_string;
use super::QueueStructureError; use super::QueueStructureError;
use log::error; use log::error;
use lqos_bus::TcHandle; use lqos_bus::TcHandle;
use lqos_utils::hex_string::read_hex_string;
use serde_json::Value; use serde_json::Value;
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]

View File

@ -5,7 +5,6 @@ use crate::{
use log::{info, warn}; use log::{info, warn};
use lqos_config::LibreQoSConfig; use lqos_config::LibreQoSConfig;
use lqos_utils::fdtimer::periodic; use lqos_utils::fdtimer::periodic;
use rayon::prelude::{IntoParallelRefMutIterator, ParallelIterator};
mod reader; mod reader;
mod watched_queues; mod watched_queues;
use self::watched_queues::expire_watched_queues; use self::watched_queues::expire_watched_queues;
@ -13,8 +12,7 @@ use watched_queues::WATCHED_QUEUES;
pub use watched_queues::{add_watched_queue, still_watching}; pub use watched_queues::{add_watched_queue, still_watching};
fn track_queues() { fn track_queues() {
let mut watching = WATCHED_QUEUES.write(); if WATCHED_QUEUES.is_empty() {
if watching.is_empty() {
//info!("No queues marked for read."); //info!("No queues marked for read.");
return; // There's nothing to do - bail out fast return; // There's nothing to do - bail out fast
} }
@ -24,7 +22,7 @@ fn track_queues() {
return; return;
} }
let config = config.unwrap(); let config = config.unwrap();
watching.par_iter_mut().for_each(|q| { WATCHED_QUEUES.iter_mut().for_each(|q| {
let (circuit_id, download_class, upload_class) = q.get(); let (circuit_id, download_class, upload_class) = q.get();
let (download, upload) = if config.on_a_stick_mode { let (download, upload) = if config.on_a_stick_mode {
@ -50,13 +48,12 @@ fn track_queues() {
if let Ok(download) = download { if let Ok(download) = download {
if let Ok(upload) = upload { if let Ok(upload) = upload {
let mut mapping = CIRCUIT_TO_QUEUE.write(); if let Some(mut circuit) = CIRCUIT_TO_QUEUE.get_mut(circuit_id) {
if let Some(circuit) = mapping.get_mut(circuit_id) {
circuit.update(&download[0], &upload[0]); circuit.update(&download[0], &upload[0]);
} else { } else {
// It's new: insert it // It's new: insert it
if !download.is_empty() && !upload.is_empty() { if !download.is_empty() && !upload.is_empty() {
mapping.insert( CIRCUIT_TO_QUEUE.insert(
circuit_id.to_string(), circuit_id.to_string(),
QueueStore::new(download[0].clone(), upload[0].clone()), QueueStore::new(download[0].clone(), upload[0].clone()),
); );
@ -74,7 +71,6 @@ fn track_queues() {
} }
}); });
std::mem::drop(watching); // Release the lock
expire_watched_queues(); expire_watched_queues();
} }

View File

@ -1,15 +1,14 @@
use crate::queue_structure::QUEUE_STRUCTURE; use crate::queue_structure::QUEUE_STRUCTURE;
use lazy_static::*; use dashmap::DashMap;
use log::{info, warn}; use log::{info, warn};
use lqos_bus::TcHandle; use lqos_bus::TcHandle;
use lqos_utils::unix_time::unix_now; use lqos_utils::unix_time::unix_now;
use parking_lot::RwLock; use once_cell::sync::Lazy;
lazy_static! { pub(crate) static WATCHED_QUEUES: Lazy<DashMap<String, WatchedQueue>> =
pub(crate) static ref WATCHED_QUEUES: RwLock<Vec<WatchedQueue>> = Lazy::new(DashMap::new);
RwLock::new(Vec::new());
}
#[derive(PartialEq, Eq, Hash)]
pub(crate) struct WatchedQueue { pub(crate) struct WatchedQueue {
circuit_id: String, circuit_id: String,
expires_unix_time: u64, expires_unix_time: u64,
@ -35,13 +34,12 @@ pub fn add_watched_queue(circuit_id: &str) {
//info!("Watching queue {circuit_id}"); //info!("Watching queue {circuit_id}");
let max = unsafe { lqos_sys::libbpf_num_possible_cpus() } * 2; let max = unsafe { lqos_sys::libbpf_num_possible_cpus() } * 2;
{ {
let read_lock = WATCHED_QUEUES.read(); if WATCHED_QUEUES.contains_key(circuit_id) {
if read_lock.iter().any(|q| q.circuit_id == circuit_id) {
warn!("Queue {circuit_id} is already being watched. Duplicate ignored."); warn!("Queue {circuit_id} is already being watched. Duplicate ignored.");
return; // No duplicates, please return; // No duplicates, please
} }
if read_lock.len() > max as usize { if WATCHED_QUEUES.len() > max as usize {
warn!( warn!(
"Watching too many queues - didn't add {circuit_id} to watch list." "Watching too many queues - didn't add {circuit_id} to watch list."
); );
@ -49,7 +47,7 @@ pub fn add_watched_queue(circuit_id: &str) {
} }
} }
if let Some(queues) = &QUEUE_STRUCTURE.read().maybe_queues { if let Some(queues) = &QUEUE_STRUCTURE.read().unwrap().maybe_queues {
if let Some(circuit) = queues.iter().find(|c| { if let Some(circuit) = queues.iter().find(|c| {
c.circuit_id.is_some() && c.circuit_id.as_ref().unwrap() == circuit_id c.circuit_id.is_some() && c.circuit_id.as_ref().unwrap() == circuit_id
}) { }) {
@ -60,7 +58,7 @@ pub fn add_watched_queue(circuit_id: &str) {
upload_class: circuit.up_class_id, upload_class: circuit.up_class_id,
}; };
WATCHED_QUEUES.write().push(new_watch); WATCHED_QUEUES.insert(circuit.circuit_id.as_ref().unwrap().clone(), new_watch);
//info!("Added {circuit_id} to watched queues. Now watching {} queues.", WATCHED_QUEUES.read().len()); //info!("Added {circuit_id} to watched queues. Now watching {} queues.", WATCHED_QUEUES.read().len());
} else { } else {
warn!("No circuit ID of {circuit_id}"); warn!("No circuit ID of {circuit_id}");
@ -71,19 +69,16 @@ pub fn add_watched_queue(circuit_id: &str) {
} }
pub(crate) fn expire_watched_queues() { pub(crate) fn expire_watched_queues() {
let mut lock = WATCHED_QUEUES.write();
let now = unix_now().unwrap_or(0); let now = unix_now().unwrap_or(0);
lock.retain(|w| w.expires_unix_time > now); WATCHED_QUEUES.retain(|_,w| w.expires_unix_time > now);
} }
pub fn still_watching(circuit_id: &str) { pub fn still_watching(circuit_id: &str) {
let mut lock = WATCHED_QUEUES.write(); if let Some(mut q) = WATCHED_QUEUES.get_mut(circuit_id) {
if let Some(q) = lock.iter_mut().find(|q| q.circuit_id == circuit_id) {
//info!("Still watching circuit: {circuit_id}"); //info!("Still watching circuit: {circuit_id}");
q.refresh_timer(); q.refresh_timer();
} else { } else {
info!("Still watching circuit, but it had expired: {circuit_id}"); info!("Still watching circuit, but it had expired: {circuit_id}");
std::mem::drop(lock);
add_watched_queue(circuit_id); add_watched_queue(circuit_id);
} }
} }

View File

@ -59,9 +59,9 @@ impl XdpIpAddress {
/// Convers an `XdpIpAddress` type to a Rust `IpAddr` type, using /// Convers an `XdpIpAddress` type to a Rust `IpAddr` type, using
/// the in-build mapped function for squishing IPv4 into IPv6 /// the in-build mapped function for squishing IPv4 into IPv6
pub fn as_ipv6(&self) -> Ipv6Addr { pub fn as_ipv6(&self) -> Ipv6Addr {
if self.is_v4() if self.is_v4() {
{ Ipv4Addr::new(self.0[12], self.0[13], self.0[14], self.0[15])
Ipv4Addr::new(self.0[12], self.0[13], self.0[14], self.0[15]).to_ipv6_mapped() .to_ipv6_mapped()
} else { } else {
Ipv6Addr::new( Ipv6Addr::new(
BigEndian::read_u16(&self.0[0..2]), BigEndian::read_u16(&self.0[0..2]),
@ -78,8 +78,7 @@ impl XdpIpAddress {
/// Converts an `XdpIpAddress` type to a Rust `IpAddr` type /// Converts an `XdpIpAddress` type to a Rust `IpAddr` type
pub fn as_ip(&self) -> IpAddr { pub fn as_ip(&self) -> IpAddr {
if self.is_v4() if self.is_v4() {
{
// It's an IPv4 Address // It's an IPv4 Address
IpAddr::V4(Ipv4Addr::new(self.0[12], self.0[13], self.0[14], self.0[15])) IpAddr::V4(Ipv4Addr::new(self.0[12], self.0[13], self.0[14], self.0[15]))
} else { } else {

View File

@ -1,38 +1,38 @@
use log::error; use log::error;
use thiserror::Error; use thiserror::Error;
/// `read_hex_string` converts a string from C-friendly Hex format /// `read_hex_string` converts a string from C-friendly Hex format
/// (e.g. `0xC12`) into a hexadecimal `u32`. /// (e.g. `0xC12`) into a hexadecimal `u32`.
/// ///
/// ## Parameters /// ## Parameters
/// ///
/// * `s`: the string to attempt to parse. /// * `s`: the string to attempt to parse.
/// ///
/// ## Returns /// ## Returns
/// ///
/// Either a converted `u32` or a `HexParseError`. /// Either a converted `u32` or a `HexParseError`.
/// ///
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// use lqos_utils::hex_string::read_hex_string; /// use lqos_utils::hex_string::read_hex_string;
/// assert_eq!(read_hex_string("0x12AD").unwrap(), 4781); /// assert_eq!(read_hex_string("0x12AD").unwrap(), 4781);
/// ``` /// ```
pub fn read_hex_string(s: &str) -> Result<u32, HexParseError> { pub fn read_hex_string(s: &str) -> Result<u32, HexParseError> {
let result = u32::from_str_radix(&s.replace("0x", ""), 16); let result = u32::from_str_radix(&s.replace("0x", ""), 16);
match result { match result {
Ok(data) => Ok(data), Ok(data) => Ok(data),
Err(e) => { Err(e) => {
error!("Unable to convert {s} to a u32"); error!("Unable to convert {s} to a u32");
error!("{:?}", e); error!("{:?}", e);
Err(HexParseError::ParseError) Err(HexParseError::ParseError)
}
} }
} }
}
/// `HexParseError` is an error type defining what can go wrong /// `HexParseError` is an error type defining what can go wrong
/// parsing a string into a `u32` hex number. /// parsing a string into a `u32` hex number.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum HexParseError { pub enum HexParseError {
#[error("Unable to decode string into valid hex")] #[error("Unable to decode string into valid hex")]
ParseError, ParseError,
@ -55,4 +55,4 @@ mod tests {
assert!(read_hex_string("0xG00F").is_err()); assert!(read_hex_string("0xG00F").is_err());
assert!(read_hex_string("G00F").is_err()); assert!(read_hex_string("G00F").is_err());
} }
} }

View File

@ -1,7 +1,7 @@
mod commands; mod commands;
pub mod fdtimer; pub mod fdtimer;
pub mod file_watcher; pub mod file_watcher;
pub mod hex_string;
pub mod packet_scale; pub mod packet_scale;
mod string_table_enum; mod string_table_enum;
pub mod unix_time; pub mod unix_time;
pub mod hex_string;

View File

@ -15,7 +15,6 @@ lqos_queue_tracker = { path = "../lqos_queue_tracker" }
lqos_utils = { path = "../lqos_utils" } lqos_utils = { path = "../lqos_utils" }
tokio = { version = "1", features = [ "full", "parking_lot" ] } tokio = { version = "1", features = [ "full", "parking_lot" ] }
once_cell = "1.17.1" once_cell = "1.17.1"
parking_lot = "0.12"
lqos_bus = { path = "../lqos_bus" } lqos_bus = { path = "../lqos_bus" }
signal-hook = "0.3" signal-hook = "0.3"
serde_json = "1" serde_json = "1"
@ -23,9 +22,9 @@ serde = { version = "1.0", features = ["derive"] }
env_logger = "0" env_logger = "0"
log = "0" log = "0"
nix = "0" nix = "0"
rayon = "1"
sysinfo = "0" sysinfo = "0"
dashmap = "5"
# Support JemAlloc on supported platforms # Support JemAlloc on supported platforms
[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies] [target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies]
jemallocator = "0.5" jemallocator = "0.5"

View File

@ -11,7 +11,7 @@ pub fn lqos_daht_test() -> BusResponse {
true, true,
std::sync::atomic::Ordering::Relaxed, std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed, std::sync::atomic::Ordering::Relaxed,
) == Ok(true) ) == Ok(false)
{ {
let result = Command::new("/bin/ssh") let result = Command::new("/bin/ssh")
.args(["-t", "lqtest@lqos.taht.net", "\"/home/lqtest/bin/v6vsv4.sh\""]) .args(["-t", "lqtest@lqos.taht.net", "\"/home/lqtest/bin/v6vsv4.sh\""])

View File

@ -24,7 +24,9 @@ use signal_hook::{
consts::{SIGHUP, SIGINT, SIGTERM}, consts::{SIGHUP, SIGINT, SIGTERM},
iterator::Signals, iterator::Signals,
}; };
use stats::{BUS_REQUESTS, TIME_TO_POLL_HOSTS};
use tokio::join; use tokio::join;
mod stats;
// Use JemAllocator only on supported platforms // Use JemAllocator only on supported platforms
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
@ -120,6 +122,7 @@ fn handle_bus_requests(
) { ) {
for req in requests.iter() { for req in requests.iter() {
//println!("Request: {:?}", req); //println!("Request: {:?}", req);
BUS_REQUESTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
responses.push(match req { responses.push(match req {
BusRequest::Ping => lqos_bus::BusResponse::Ack, BusRequest::Ping => lqos_bus::BusResponse::Ack,
BusRequest::GetCurrentThroughput => { BusRequest::GetCurrentThroughput => {
@ -160,10 +163,25 @@ fn handle_bus_requests(
BusRequest::RequestLqosEquinixTest => lqos_daht_test::lqos_daht_test(), BusRequest::RequestLqosEquinixTest => lqos_daht_test::lqos_daht_test(),
BusRequest::ValidateShapedDevicesCsv => { BusRequest::ValidateShapedDevicesCsv => {
validation::validate_shaped_devices_csv() validation::validate_shaped_devices_csv()
}, }
BusRequest::GetNetworkMap { parent } => { BusRequest::GetNetworkMap { parent } => {
shaped_devices_tracker::get_one_network_map_layer(*parent) shaped_devices_tracker::get_one_network_map_layer(*parent)
}, }
BusRequest::TopMapQueues(n_queues) => {
shaped_devices_tracker::get_top_n_root_queues(*n_queues)
}
BusRequest::GetNodeNamesFromIds(nodes) => {
shaped_devices_tracker::map_node_names(nodes)
}
BusRequest::GetFunnel { target: parent } => {
shaped_devices_tracker::get_funnel(parent)
}
BusRequest::GetLqosStats => {
BusResponse::LqosdStats {
bus_requests: BUS_REQUESTS.load(std::sync::atomic::Ordering::Relaxed),
time_to_poll_hosts: TIME_TO_POLL_HOSTS.load(std::sync::atomic::Ordering::Relaxed),
}
}
}); });
} }
} }

View File

@ -1,10 +1,10 @@
use anyhow::Result; use anyhow::Result;
use log::{error, info, warn}; use log::{error, info, warn};
use lqos_bus::BusResponse; use lqos_bus::BusResponse;
use lqos_config::ConfigShapedDevices; use lqos_config::{ConfigShapedDevices, NetworkJsonTransport};
use lqos_utils::file_watcher::FileWatcher; use lqos_utils::file_watcher::FileWatcher;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use parking_lot::RwLock; use std::sync::RwLock;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
mod netjson; mod netjson;
pub use netjson::*; pub use netjson::*;
@ -17,11 +17,12 @@ fn load_shaped_devices() {
let shaped_devices = ConfigShapedDevices::load(); let shaped_devices = ConfigShapedDevices::load();
if let Ok(new_file) = shaped_devices { if let Ok(new_file) = shaped_devices {
info!("ShapedDevices.csv loaded"); info!("ShapedDevices.csv loaded");
*SHAPED_DEVICES.write() = new_file; *SHAPED_DEVICES.write().unwrap() = new_file;
crate::throughput_tracker::THROUGHPUT_TRACKER.write().refresh_circuit_ids(); crate::throughput_tracker::THROUGHPUT_TRACKER
.refresh_circuit_ids();
} else { } else {
warn!("ShapedDevices.csv failed to load, see previous error messages. Reverting to empty set."); warn!("ShapedDevices.csv failed to load, see previous error messages. Reverting to empty set.");
*SHAPED_DEVICES.write() = ConfigShapedDevices::default(); *SHAPED_DEVICES.write().unwrap() = ConfigShapedDevices::default();
} }
} }
@ -55,7 +56,7 @@ fn watch_for_shaped_devices_changing() -> Result<()> {
} }
pub fn get_one_network_map_layer(parent_idx: usize) -> BusResponse { pub fn get_one_network_map_layer(parent_idx: usize) -> BusResponse {
let net_json = NETWORK_JSON.read(); let net_json = NETWORK_JSON.read().unwrap();
if let Some(parent) = net_json.get_cloned_entry_by_index(parent_idx) { if let Some(parent) = net_json.get_cloned_entry_by_index(parent_idx) {
let mut nodes = vec![(parent_idx, parent)]; let mut nodes = vec![(parent_idx, parent)];
nodes.extend_from_slice(&net_json.get_cloned_children(parent_idx)); nodes.extend_from_slice(&net_json.get_cloned_children(parent_idx));
@ -63,4 +64,68 @@ pub fn get_one_network_map_layer(parent_idx: usize) -> BusResponse {
} else { } else {
BusResponse::Fail("No such node".to_string()) BusResponse::Fail("No such node".to_string())
} }
} }
pub fn get_top_n_root_queues(n_queues: usize) -> BusResponse {
let net_json = NETWORK_JSON.read().unwrap();
if let Some(parent) = net_json.get_cloned_entry_by_index(0) {
let mut nodes = vec![(0, parent)];
nodes.extend_from_slice(&net_json.get_cloned_children(0));
// Remove the top-level entry for root
nodes.remove(0);
// Sort by total bandwidth (up + down) descending
nodes.sort_by(|a, b| {
let total_a = a.1.current_throughput.0 + a.1.current_throughput.1;
let total_b = b.1.current_throughput.0 + b.1.current_throughput.1;
total_b.cmp(&total_a)
});
// Summarize everything after n_queues
if nodes.len() > n_queues {
let mut other_bw = (0, 0);
nodes.drain(n_queues..).for_each(|n| {
other_bw.0 += n.1.current_throughput.0;
other_bw.1 += n.1.current_throughput.1;
});
nodes.push((
0,
NetworkJsonTransport {
name: "Others".into(),
max_throughput: (0, 0),
current_throughput: other_bw,
rtts: Vec::new(),
parents: Vec::new(),
immediate_parent: None,
},
));
}
BusResponse::NetworkMap(nodes)
} else {
BusResponse::Fail("No such node".to_string())
}
}
pub fn map_node_names(nodes: &[usize]) -> BusResponse {
let mut result = Vec::new();
let reader = NETWORK_JSON.read().unwrap();
nodes.iter().for_each(|id| {
if let Some(node) = reader.nodes.get(*id) {
result.push((*id, node.name.clone()));
}
});
BusResponse::NodeNames(result)
}
pub fn get_funnel(circuit_id: &str) -> BusResponse {
let reader = NETWORK_JSON.read().unwrap();
if let Some(index) = reader.get_index_for_name(circuit_id) {
// Reverse the scanning order and skip the last entry (the parent)
let mut result = Vec::new();
for idx in reader.nodes[index].parents.iter().rev().skip(1) {
result.push((*idx, reader.nodes[*idx].clone_to_transit()));
}
return BusResponse::NetworkMap(result);
}
BusResponse::Fail("Unknown Node".into())
}

View File

@ -1,12 +1,13 @@
use log::{info, error, warn}; use anyhow::Result;
use log::{error, info, warn};
use lqos_config::NetworkJson; use lqos_config::NetworkJson;
use lqos_utils::file_watcher::FileWatcher; use lqos_utils::file_watcher::FileWatcher;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use parking_lot::RwLock; use std::sync::RwLock;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use anyhow::Result;
pub static NETWORK_JSON: Lazy<RwLock<NetworkJson>> = Lazy::new(|| RwLock::new(NetworkJson::default())); pub static NETWORK_JSON: Lazy<RwLock<NetworkJson>> =
Lazy::new(|| RwLock::new(NetworkJson::default()));
pub async fn network_json_watcher() { pub async fn network_json_watcher() {
spawn_blocking(|| { spawn_blocking(|| {
@ -18,30 +19,29 @@ pub async fn network_json_watcher() {
/// Fires up a Linux file system watcher than notifies /// Fires up a Linux file system watcher than notifies
/// when `network.json` changes, and triggers a reload. /// when `network.json` changes, and triggers a reload.
fn watch_for_network_json_changing() -> Result<()> { fn watch_for_network_json_changing() -> Result<()> {
let watch_path = NetworkJson::path(); let watch_path = NetworkJson::path();
if watch_path.is_err() { if watch_path.is_err() {
error!("Unable to generate path for network.json"); error!("Unable to generate path for network.json");
return Err(anyhow::Error::msg( return Err(anyhow::Error::msg("Unable to create path for network.json"));
"Unable to create path for network.json",
));
}
let watch_path = watch_path.unwrap();
let mut watcher = FileWatcher::new("network.json", watch_path);
watcher.set_file_exists_callback(load_network_json);
watcher.set_file_created_callback(load_network_json);
watcher.set_file_changed_callback(load_network_json);
loop {
let result = watcher.watch();
info!("network.json watcher returned: {result:?}");
}
} }
let watch_path = watch_path.unwrap();
fn load_network_json() {
let njs = NetworkJson::load(); let mut watcher = FileWatcher::new("network.json", watch_path);
if let Ok(njs) = njs { watcher.set_file_exists_callback(load_network_json);
*NETWORK_JSON.write() = njs; watcher.set_file_created_callback(load_network_json);
} else { watcher.set_file_changed_callback(load_network_json);
warn!("Unable to load network.json"); loop {
} let result = watcher.watch();
} info!("network.json watcher returned: {result:?}");
}
}
fn load_network_json() {
let njs = NetworkJson::load();
if let Ok(njs) = njs {
let mut write_lock = NETWORK_JSON.write().unwrap();
*write_lock = njs;
} else {
warn!("Unable to load network.json");
}
}

View File

@ -0,0 +1,4 @@
use std::sync::atomic::AtomicU64;
pub static BUS_REQUESTS: AtomicU64 = AtomicU64::new(0);
pub static TIME_TO_POLL_HOSTS: AtomicU64 = AtomicU64::new(0);

View File

@ -1,17 +1,20 @@
mod throughput_entry; mod throughput_entry;
mod tracking_data; mod tracking_data;
use crate::{throughput_tracker::tracking_data::ThroughputTracker, shaped_devices_tracker::NETWORK_JSON}; use crate::{
shaped_devices_tracker::NETWORK_JSON,
throughput_tracker::tracking_data::ThroughputTracker, stats::TIME_TO_POLL_HOSTS,
};
use log::{info, warn}; use log::{info, warn};
use lqos_bus::{BusResponse, IpStats, TcHandle, XdpPpingResult}; use lqos_bus::{BusResponse, IpStats, TcHandle, XdpPpingResult};
use lqos_sys::XdpIpAddress; use lqos_sys::XdpIpAddress;
use lqos_utils::{fdtimer::periodic, unix_time::time_since_boot}; use lqos_utils::{fdtimer::periodic, unix_time::time_since_boot};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use parking_lot::RwLock;
use std::time::Duration; use std::time::Duration;
const RETIRE_AFTER_SECONDS: u64 = 30; const RETIRE_AFTER_SECONDS: u64 = 30;
pub static THROUGHPUT_TRACKER: Lazy<RwLock<ThroughputTracker>> = Lazy::new(|| RwLock::new(ThroughputTracker::new())); pub static THROUGHPUT_TRACKER: Lazy<ThroughputTracker> =
Lazy::new(ThroughputTracker::new);
pub fn spawn_throughput_monitor() { pub fn spawn_throughput_monitor() {
info!("Starting the bandwidth monitor thread."); info!("Starting the bandwidth monitor thread.");
@ -20,24 +23,28 @@ pub fn spawn_throughput_monitor() {
std::thread::spawn(move || { std::thread::spawn(move || {
periodic(interval_ms, "Throughput Monitor", &mut || { periodic(interval_ms, "Throughput Monitor", &mut || {
let mut throughput = THROUGHPUT_TRACKER.write(); let start = std::time::Instant::now();
let mut net_json = NETWORK_JSON.write(); {
throughput.copy_previous_and_reset_rtt(&mut net_json); let net_json = NETWORK_JSON.read().unwrap();
throughput.apply_new_throughput_counters(); net_json.zero_throughput_and_rtt();
throughput.apply_rtt_data(); } // Scope to end the lock
throughput.update_totals(&mut net_json); THROUGHPUT_TRACKER.copy_previous_and_reset_rtt();
throughput.next_cycle(); THROUGHPUT_TRACKER.apply_new_throughput_counters();
THROUGHPUT_TRACKER.apply_rtt_data();
THROUGHPUT_TRACKER.update_totals();
THROUGHPUT_TRACKER.next_cycle();
let duration_ms = start.elapsed().as_micros();
TIME_TO_POLL_HOSTS.store(duration_ms as u64, std::sync::atomic::Ordering::Relaxed);
}); });
}); });
} }
pub fn current_throughput() -> BusResponse { pub fn current_throughput() -> BusResponse {
let (bits_per_second, packets_per_second, shaped_bits_per_second) = { let (bits_per_second, packets_per_second, shaped_bits_per_second) = {
let tp = THROUGHPUT_TRACKER.read();
( (
tp.bits_per_second(), THROUGHPUT_TRACKER.bits_per_second(),
tp.packets_per_second(), THROUGHPUT_TRACKER.packets_per_second(),
tp.shaped_bits_per_second(), THROUGHPUT_TRACKER.shaped_bits_per_second(),
) )
}; };
BusResponse::CurrentThroughput { BusResponse::CurrentThroughput {
@ -49,9 +56,8 @@ pub fn current_throughput() -> BusResponse {
pub fn host_counters() -> BusResponse { pub fn host_counters() -> BusResponse {
let mut result = Vec::new(); let mut result = Vec::new();
let tp = THROUGHPUT_TRACKER.read(); THROUGHPUT_TRACKER.raw_data.iter().for_each(|v| {
tp.raw_data.iter().for_each(|(k, v)| { let ip = v.key().as_ip();
let ip = k.as_ip();
let (down, up) = v.bytes_per_second; let (down, up) = v.bytes_per_second;
result.push((ip, down, up)); result.push((ip, down, up));
}); });
@ -67,14 +73,14 @@ type TopList = (XdpIpAddress, (u64, u64), (u64, u64), f32, TcHandle, String);
pub fn top_n(start: u32, end: u32) -> BusResponse { pub fn top_n(start: u32, end: u32) -> BusResponse {
let mut full_list: Vec<TopList> = { let mut full_list: Vec<TopList> = {
let tp = THROUGHPUT_TRACKER.read(); let tp_cycle = THROUGHPUT_TRACKER.cycle.load(std::sync::atomic::Ordering::Relaxed);
tp.raw_data THROUGHPUT_TRACKER.raw_data
.iter() .iter()
.filter(|(ip, _)| !ip.as_ip().is_loopback()) .filter(|v| !v.key().as_ip().is_loopback())
.filter(|(_, d)| retire_check(tp.cycle, d.most_recent_cycle)) .filter(|d| retire_check(tp_cycle, d.most_recent_cycle))
.map(|(ip, te)| { .map(|te| {
( (
*ip, *te.key(),
te.bytes_per_second, te.bytes_per_second,
te.packets_per_second, te.packets_per_second,
te.median_latency(), te.median_latency(),
@ -112,15 +118,15 @@ pub fn top_n(start: u32, end: u32) -> BusResponse {
pub fn worst_n(start: u32, end: u32) -> BusResponse { pub fn worst_n(start: u32, end: u32) -> BusResponse {
let mut full_list: Vec<TopList> = { let mut full_list: Vec<TopList> = {
let tp = THROUGHPUT_TRACKER.read(); let tp_cycle = THROUGHPUT_TRACKER.cycle.load(std::sync::atomic::Ordering::Relaxed);
tp.raw_data THROUGHPUT_TRACKER.raw_data
.iter() .iter()
.filter(|(ip, _)| !ip.as_ip().is_loopback()) .filter(|v| !v.key().as_ip().is_loopback())
.filter(|(_, d)| retire_check(tp.cycle, d.most_recent_cycle)) .filter(|d| retire_check(tp_cycle, d.most_recent_cycle))
.filter(|(_, te)| te.median_latency() > 0.0) .filter(|te| te.median_latency() > 0.0)
.map(|(ip, te)| { .map(|te| {
( (
*ip, *te.key(),
te.bytes_per_second, te.bytes_per_second,
te.packets_per_second, te.packets_per_second,
te.median_latency(), te.median_latency(),
@ -157,15 +163,15 @@ pub fn worst_n(start: u32, end: u32) -> BusResponse {
} }
pub fn best_n(start: u32, end: u32) -> BusResponse { pub fn best_n(start: u32, end: u32) -> BusResponse {
let mut full_list: Vec<TopList> = { let mut full_list: Vec<TopList> = {
let tp = THROUGHPUT_TRACKER.read(); let tp_cycle = THROUGHPUT_TRACKER.cycle.load(std::sync::atomic::Ordering::Relaxed);
tp.raw_data THROUGHPUT_TRACKER.raw_data
.iter() .iter()
.filter(|(ip, _)| !ip.as_ip().is_loopback()) .filter(|v| !v.key().as_ip().is_loopback())
.filter(|(_, d)| retire_check(tp.cycle, d.most_recent_cycle)) .filter(|d| retire_check(tp_cycle, d.most_recent_cycle))
.filter(|(_, te)| te.median_latency() > 0.0) .filter(|te| te.median_latency() > 0.0)
.map(|(ip, te)| { .map(|te| {
( (
*ip, *te.key(),
te.bytes_per_second, te.bytes_per_second,
te.packets_per_second, te.packets_per_second,
te.median_latency(), te.median_latency(),
@ -203,12 +209,12 @@ pub fn best_n(start: u32, end: u32) -> BusResponse {
} }
pub fn xdp_pping_compat() -> BusResponse { pub fn xdp_pping_compat() -> BusResponse {
let raw = THROUGHPUT_TRACKER.read(); let raw_cycle = THROUGHPUT_TRACKER.cycle.load(std::sync::atomic::Ordering::Relaxed);
let result = raw let result = THROUGHPUT_TRACKER
.raw_data .raw_data
.iter() .iter()
.filter(|(_, d)| retire_check(raw.cycle, d.most_recent_cycle)) .filter(|d| retire_check(raw_cycle, d.most_recent_cycle))
.filter_map(|(_ip, data)| { .filter_map(|data| {
if data.tc_handle.as_u32() > 0 { if data.tc_handle.as_u32() > 0 {
let mut valid_samples: Vec<u32> = let mut valid_samples: Vec<u32> =
data.recent_rtt_data.iter().filter(|d| **d > 0).copied().collect(); data.recent_rtt_data.iter().filter(|d| **d > 0).copied().collect();
@ -242,11 +248,11 @@ pub fn xdp_pping_compat() -> BusResponse {
pub fn rtt_histogram() -> BusResponse { pub fn rtt_histogram() -> BusResponse {
let mut result = vec![0; 20]; let mut result = vec![0; 20];
let reader = THROUGHPUT_TRACKER.read(); let reader_cycle = THROUGHPUT_TRACKER.cycle.load(std::sync::atomic::Ordering::Relaxed);
for (_, data) in reader for data in THROUGHPUT_TRACKER
.raw_data .raw_data
.iter() .iter()
.filter(|(_, d)| retire_check(reader.cycle, d.most_recent_cycle)) .filter(|d| retire_check(reader_cycle, d.most_recent_cycle))
{ {
let valid_samples: Vec<u32> = let valid_samples: Vec<u32> =
data.recent_rtt_data.iter().filter(|d| **d > 0).copied().collect(); data.recent_rtt_data.iter().filter(|d| **d > 0).copied().collect();
@ -265,11 +271,11 @@ pub fn rtt_histogram() -> BusResponse {
pub fn host_counts() -> BusResponse { pub fn host_counts() -> BusResponse {
let mut total = 0; let mut total = 0;
let mut shaped = 0; let mut shaped = 0;
let tp = THROUGHPUT_TRACKER.read(); let tp_cycle = THROUGHPUT_TRACKER.cycle.load(std::sync::atomic::Ordering::Relaxed);
tp.raw_data THROUGHPUT_TRACKER.raw_data
.iter() .iter()
.filter(|(_, d)| retire_check(tp.cycle, d.most_recent_cycle)) .filter(|d| retire_check(tp_cycle, d.most_recent_cycle))
.for_each(|(_, d)| { .for_each(|d| {
total += 1; total += 1;
if d.tc_handle.as_u32() != 0 { if d.tc_handle.as_u32() != 0 {
shaped += 1; shaped += 1;
@ -294,15 +300,14 @@ pub fn all_unknown_ips() -> BusResponse {
let five_minutes_ago_nanoseconds = five_minutes_ago.as_nanos(); let five_minutes_ago_nanoseconds = five_minutes_ago.as_nanos();
let mut full_list: Vec<FullList> = { let mut full_list: Vec<FullList> = {
let tp = THROUGHPUT_TRACKER.read(); THROUGHPUT_TRACKER.raw_data
tp.raw_data
.iter() .iter()
.filter(|(ip, _)| !ip.as_ip().is_loopback()) .filter(|v| !v.key().as_ip().is_loopback())
.filter(|(_, d)| d.tc_handle.as_u32() == 0) .filter(|d| d.tc_handle.as_u32() == 0)
.filter(|(_, d)| d.last_seen as u128 > five_minutes_ago_nanoseconds) .filter(|d| d.last_seen as u128 > five_minutes_ago_nanoseconds)
.map(|(ip, te)| { .map(|te| {
( (
*ip, *te.key(),
te.bytes, te.bytes,
te.packets, te.packets,
te.median_latency(), te.median_latency(),

View File

@ -1,18 +1,16 @@
use crate::shaped_devices_tracker::SHAPED_DEVICES; use std::sync::atomic::AtomicU64;
use crate::shaped_devices_tracker::{SHAPED_DEVICES, NETWORK_JSON};
use super::{throughput_entry::ThroughputEntry, RETIRE_AFTER_SECONDS}; use super::{throughput_entry::ThroughputEntry, RETIRE_AFTER_SECONDS};
use dashmap::DashMap;
use lqos_bus::TcHandle; use lqos_bus::TcHandle;
use lqos_config::NetworkJson;
use lqos_sys::{rtt_for_each, throughput_for_each, XdpIpAddress}; use lqos_sys::{rtt_for_each, throughput_for_each, XdpIpAddress};
use rayon::prelude::{IntoParallelRefMutIterator, ParallelIterator};
use std::collections::HashMap;
pub struct ThroughputTracker { pub struct ThroughputTracker {
pub(crate) cycle: u64, pub(crate) cycle: AtomicU64,
pub(crate) raw_data: HashMap<XdpIpAddress, ThroughputEntry>, pub(crate) raw_data: DashMap<XdpIpAddress, ThroughputEntry>,
pub(crate) bytes_per_second: (u64, u64), pub(crate) bytes_per_second: (AtomicU64, AtomicU64),
pub(crate) packets_per_second: (u64, u64), pub(crate) packets_per_second: (AtomicU64, AtomicU64),
pub(crate) shaped_bytes_per_second: (u64, u64), pub(crate) shaped_bytes_per_second: (AtomicU64, AtomicU64),
} }
impl ThroughputTracker { impl ThroughputTracker {
@ -21,26 +19,21 @@ impl ThroughputTracker {
// maximums.h (MAX_TRACKED_IPS), so we grab it // maximums.h (MAX_TRACKED_IPS), so we grab it
// from there via the C API. // from there via the C API.
Self { Self {
cycle: RETIRE_AFTER_SECONDS, cycle: AtomicU64::new(RETIRE_AFTER_SECONDS),
raw_data: HashMap::with_capacity(lqos_sys::max_tracked_ips()), raw_data: DashMap::with_capacity(lqos_sys::max_tracked_ips()),
bytes_per_second: (0, 0), bytes_per_second: (AtomicU64::new(0), AtomicU64::new(0)),
packets_per_second: (0, 0), packets_per_second: (AtomicU64::new(0), AtomicU64::new(0)),
shaped_bytes_per_second: (0, 0), shaped_bytes_per_second: (AtomicU64::new(0), AtomicU64::new(0)),
} }
} }
pub(crate) fn copy_previous_and_reset_rtt( pub(crate) fn copy_previous_and_reset_rtt(&self) {
&mut self,
netjson: &mut NetworkJson,
) {
// Zero the previous funnel hierarchy current numbers
netjson.zero_throughput_and_rtt();
// Copy previous byte/packet numbers and reset RTT data // Copy previous byte/packet numbers and reset RTT data
// We're using Rayon's "par_iter_mut" to spread the operation across // We're using Rayon's "par_iter_mut" to spread the operation across
// all CPU cores. // all CPU cores.
self.raw_data.par_iter_mut().for_each(|(_k, v)| { let self_cycle = self.cycle.load(std::sync::atomic::Ordering::Relaxed);
if v.first_cycle < self.cycle { self.raw_data.iter_mut().for_each(|mut v| {
if v.first_cycle < self_cycle {
v.bytes_per_second.0 = v.bytes_per_second.0 =
u64::checked_sub(v.bytes.0, v.prev_bytes.0).unwrap_or(0); u64::checked_sub(v.bytes.0, v.prev_bytes.0).unwrap_or(0);
v.bytes_per_second.1 = v.bytes_per_second.1 =
@ -53,8 +46,8 @@ impl ThroughputTracker {
v.prev_packets = v.packets; v.prev_packets = v.packets;
} }
// Roll out stale RTT data // Roll out stale RTT data
if self.cycle > RETIRE_AFTER_SECONDS if self_cycle > RETIRE_AFTER_SECONDS
&& v.last_fresh_rtt_data_cycle < self.cycle - RETIRE_AFTER_SECONDS && v.last_fresh_rtt_data_cycle < self_cycle - RETIRE_AFTER_SECONDS
{ {
v.recent_rtt_data = [0; 60]; v.recent_rtt_data = [0; 60];
} }
@ -64,7 +57,7 @@ impl ThroughputTracker {
fn lookup_circuit_id(xdp_ip: &XdpIpAddress) -> Option<String> { fn lookup_circuit_id(xdp_ip: &XdpIpAddress) -> Option<String> {
let mut circuit_id = None; let mut circuit_id = None;
let lookup = xdp_ip.as_ipv6(); let lookup = xdp_ip.as_ipv6();
let cfg = SHAPED_DEVICES.read(); let cfg = SHAPED_DEVICES.read().unwrap();
if let Some((_, id)) = cfg.trie.longest_match(lookup) { if let Some((_, id)) = cfg.trie.longest_match(lookup) {
circuit_id = Some(cfg.devices[*id].circuit_id.clone()); circuit_id = Some(cfg.devices[*id].circuit_id.clone());
} }
@ -76,12 +69,14 @@ impl ThroughputTracker {
circuit_id: Option<String>, circuit_id: Option<String>,
) -> Option<String> { ) -> Option<String> {
if let Some(circuit_id) = circuit_id { if let Some(circuit_id) = circuit_id {
let shaped = SHAPED_DEVICES.read(); let shaped = SHAPED_DEVICES.read().unwrap();
shaped let parent_name = shaped
.devices .devices
.iter() .iter()
.find(|d| d.circuit_id == circuit_id) .find(|d| d.circuit_id == circuit_id)
.map(|device| device.parent_node.clone()) .map(|device| device.parent_node.clone());
//println!("{parent_name:?}");
parent_name
} else { } else {
None None
} }
@ -91,26 +86,28 @@ impl ThroughputTracker {
circuit_id: Option<String>, circuit_id: Option<String>,
) -> Option<Vec<usize>> { ) -> Option<Vec<usize>> {
if let Some(parent) = Self::get_node_name_for_circuit_id(circuit_id) { if let Some(parent) = Self::get_node_name_for_circuit_id(circuit_id) {
let lock = crate::shaped_devices_tracker::NETWORK_JSON.read(); let lock = crate::shaped_devices_tracker::NETWORK_JSON.read().unwrap();
lock.get_parents_for_circuit_id(&parent) lock.get_parents_for_circuit_id(&parent)
} else { } else {
None None
} }
} }
pub(crate) fn refresh_circuit_ids(&mut self) { pub(crate) fn refresh_circuit_ids(&self) {
self.raw_data.par_iter_mut().for_each(|(ip, data)| { self.raw_data.iter_mut().for_each(|mut data| {
data.circuit_id = Self::lookup_circuit_id(ip); data.circuit_id = Self::lookup_circuit_id(data.key());
data.network_json_parents = data.network_json_parents =
Self::lookup_network_parents(data.circuit_id.clone()); Self::lookup_network_parents(data.circuit_id.clone());
}); });
} }
pub(crate) fn apply_new_throughput_counters(&mut self) { pub(crate) fn apply_new_throughput_counters(
let cycle = self.cycle; &self,
let raw_data = &mut self.raw_data; ) {
let raw_data = &self.raw_data;
let self_cycle = self.cycle.load(std::sync::atomic::Ordering::Relaxed);
throughput_for_each(&mut |xdp_ip, counts| { throughput_for_each(&mut |xdp_ip, counts| {
if let Some(entry) = raw_data.get_mut(xdp_ip) { if let Some(mut entry) = raw_data.get_mut(xdp_ip) {
entry.bytes = (0, 0); entry.bytes = (0, 0);
entry.packets = (0, 0); entry.packets = (0, 0);
for c in counts { for c in counts {
@ -126,14 +123,25 @@ impl ThroughputTracker {
} }
} }
if entry.packets != entry.prev_packets { if entry.packets != entry.prev_packets {
entry.most_recent_cycle = cycle; entry.most_recent_cycle = self_cycle;
if let Some(parents) = &entry.network_json_parents {
let net_json = NETWORK_JSON.read().unwrap();
net_json.add_throughput_cycle(
parents,
(
entry.bytes.0 - entry.prev_bytes.0,
entry.bytes.1 - entry.prev_bytes.1,
),
);
}
} }
} else { } else {
let circuit_id = Self::lookup_circuit_id(xdp_ip); let circuit_id = Self::lookup_circuit_id(xdp_ip);
let mut entry = ThroughputEntry { let mut entry = ThroughputEntry {
circuit_id: circuit_id.clone(), circuit_id: circuit_id.clone(),
network_json_parents: Self::lookup_network_parents(circuit_id), network_json_parents: Self::lookup_network_parents(circuit_id),
first_cycle: self.cycle, first_cycle: self_cycle,
most_recent_cycle: 0, most_recent_cycle: 0,
bytes: (0, 0), bytes: (0, 0),
packets: (0, 0), packets: (0, 0),
@ -160,25 +168,42 @@ impl ThroughputTracker {
}); });
} }
pub(crate) fn apply_rtt_data(&mut self) { pub(crate) fn apply_rtt_data(&self) {
let self_cycle = self.cycle.load(std::sync::atomic::Ordering::Relaxed);
rtt_for_each(&mut |raw_ip, rtt| { rtt_for_each(&mut |raw_ip, rtt| {
if rtt.has_fresh_data != 0 { if rtt.has_fresh_data != 0 {
let ip = XdpIpAddress(*raw_ip); let ip = XdpIpAddress(*raw_ip);
if let Some(tracker) = self.raw_data.get_mut(&ip) { if let Some(mut tracker) = self.raw_data.get_mut(&ip) {
tracker.recent_rtt_data = rtt.rtt; tracker.recent_rtt_data = rtt.rtt;
tracker.last_fresh_rtt_data_cycle = self.cycle; tracker.last_fresh_rtt_data_cycle = self_cycle;
if let Some(parents) = &tracker.network_json_parents {
let net_json = NETWORK_JSON.write().unwrap();
net_json.add_rtt_cycle(parents, tracker.median_latency());
}
} }
} }
}); });
} }
pub(crate) fn update_totals(&mut self, net_json: &mut NetworkJson) { #[inline(always)]
self.bytes_per_second = (0, 0); fn set_atomic_tuple_to_zero(tuple: &(AtomicU64, AtomicU64)) {
self.packets_per_second = (0, 0); tuple.0.store(0, std::sync::atomic::Ordering::Relaxed);
self.shaped_bytes_per_second = (0, 0); tuple.1.store(0, std::sync::atomic::Ordering::Relaxed);
}
#[inline(always)]
fn add_atomic_tuple(tuple: &(AtomicU64, AtomicU64), n: (u64, u64)) {
tuple.0.fetch_add(n.0, std::sync::atomic::Ordering::Relaxed);
tuple.1.fetch_add(n.1, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn update_totals(&self) {
Self::set_atomic_tuple_to_zero(&self.bytes_per_second);
Self::set_atomic_tuple_to_zero(&self.packets_per_second);
Self::set_atomic_tuple_to_zero(&self.shaped_bytes_per_second);
self self
.raw_data .raw_data
.values() .iter()
.map(|v| { .map(|v| {
( (
v.bytes.0.saturating_sub(v.prev_bytes.0), v.bytes.0.saturating_sub(v.prev_bytes.0),
@ -186,181 +211,40 @@ impl ThroughputTracker {
v.packets.0.saturating_sub(v.prev_packets.0), v.packets.0.saturating_sub(v.prev_packets.0),
v.packets.1.saturating_sub(v.prev_packets.1), v.packets.1.saturating_sub(v.prev_packets.1),
v.tc_handle.as_u32() > 0, v.tc_handle.as_u32() > 0,
&v.network_json_parents,
v.median_latency(),
) )
}) })
.for_each( .for_each(|(bytes_down, bytes_up, packets_down, packets_up, shaped)| {
|(bytes_down, bytes_up, packets_down, packets_up, shaped, parents, median_rtt)| { Self::add_atomic_tuple(&self.bytes_per_second, (bytes_down, bytes_up));
self.bytes_per_second.0 = Self::add_atomic_tuple(&self.packets_per_second, (packets_down, packets_up));
self.bytes_per_second.0.checked_add(bytes_down).unwrap_or(0); if shaped {
self.bytes_per_second.1 = Self::add_atomic_tuple(&self.shaped_bytes_per_second, (bytes_down, bytes_up));
self.bytes_per_second.1.checked_add(bytes_up).unwrap_or(0); }
self.packets_per_second.0 = });
self.packets_per_second.0.checked_add(packets_down).unwrap_or(0);
self.packets_per_second.1 =
self.packets_per_second.1.checked_add(packets_up).unwrap_or(0);
if shaped {
self.shaped_bytes_per_second.0 = self
.shaped_bytes_per_second
.0
.checked_add(bytes_down)
.unwrap_or(0);
self.shaped_bytes_per_second.1 = self
.shaped_bytes_per_second
.1
.checked_add(bytes_up)
.unwrap_or(0);
}
// If we have parent node data, we apply it now
if let Some(parents) = parents {
net_json.add_throughput_cycle(
parents,
(self.bytes_per_second.0, self.bytes_per_second.1),
median_rtt,
)
}
},
);
} }
pub(crate) fn next_cycle(&mut self) { pub(crate) fn next_cycle(&self) {
self.cycle += 1; self.cycle.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
} }
// pub(crate) fn tick(
// &mut self,
// value_dump: &[(XdpIpAddress, Vec<HostCounter>)],
// rtt: Result<Vec<([u8; 16], RttTrackingEntry)>>,
// ) -> Result<()> {
// // Copy previous byte/packet numbers and reset RTT data
// self.raw_data.iter_mut().for_each(|(_k, v)| {
// if v.first_cycle < self.cycle {
// v.bytes_per_second.0 = u64::checked_sub(v.bytes.0, v.prev_bytes.0).unwrap_or(0);
// v.bytes_per_second.1 = u64::checked_sub(v.bytes.1, v.prev_bytes.1).unwrap_or(0);
// v.packets_per_second.0 =
// u64::checked_sub(v.packets.0, v.prev_packets.0).unwrap_or(0);
// v.packets_per_second.1 =
// u64::checked_sub(v.packets.1, v.prev_packets.1).unwrap_or(0);
// v.prev_bytes = v.bytes;
// v.prev_packets = v.packets;
// }
// // Roll out stale RTT data
// if self.cycle > RETIRE_AFTER_SECONDS
// && v.last_fresh_rtt_data_cycle < self.cycle - RETIRE_AFTER_SECONDS
// {
// v.recent_rtt_data = [0; 60];
// }
// });
// value_dump.iter().for_each(|(xdp_ip, counts)| {
// if let Some(entry) = self.raw_data.get_mut(xdp_ip) {
// entry.bytes = (0, 0);
// entry.packets = (0, 0);
// for c in counts {
// entry.bytes.0 += c.download_bytes;
// entry.bytes.1 += c.upload_bytes;
// entry.packets.0 += c.download_packets;
// entry.packets.1 += c.upload_packets;
// if c.tc_handle != 0 {
// entry.tc_handle = TcHandle::from_u32(c.tc_handle);
// }
// if c.last_seen != 0 {
// entry.last_seen = c.last_seen;
// }
// }
// if entry.packets != entry.prev_packets {
// entry.most_recent_cycle = self.cycle;
// }
// } else {
// let mut entry = ThroughputEntry {
// first_cycle: self.cycle,
// most_recent_cycle: 0,
// bytes: (0, 0),
// packets: (0, 0),
// prev_bytes: (0, 0),
// prev_packets: (0, 0),
// bytes_per_second: (0, 0),
// packets_per_second: (0, 0),
// tc_handle: TcHandle::zero(),
// recent_rtt_data: [0; 60],
// last_fresh_rtt_data_cycle: 0,
// last_seen: 0,
// };
// for c in counts {
// entry.bytes.0 += c.download_bytes;
// entry.bytes.1 += c.upload_bytes;
// entry.packets.0 += c.download_packets;
// entry.packets.1 += c.upload_packets;
// if c.tc_handle != 0 {
// entry.tc_handle = TcHandle::from_u32(c.tc_handle);
// }
// }
// self.raw_data.insert(*xdp_ip, entry);
// }
// });
// // Apply RTT data
// if let Ok(rtt_dump) = rtt {
// for (raw_ip, rtt) in rtt_dump {
// if rtt.has_fresh_data != 0 {
// let ip = XdpIpAddress(raw_ip);
// if let Some(tracker) = self.raw_data.get_mut(&ip) {
// tracker.recent_rtt_data = rtt.rtt;
// tracker.last_fresh_rtt_data_cycle = self.cycle;
// }
// }
// }
// }
// // Update totals
// self.bytes_per_second = (0, 0);
// self.packets_per_second = (0, 0);
// self.shaped_bytes_per_second = (0, 0);
// self.raw_data
// .iter()
// .map(|(_k, v)| {
// (
// v.bytes.0 - v.prev_bytes.0,
// v.bytes.1 - v.prev_bytes.1,
// v.packets.0 - v.prev_packets.0,
// v.packets.1 - v.prev_packets.1,
// v.tc_handle.as_u32() > 0,
// )
// })
// .for_each(|(bytes_down, bytes_up, packets_down, packets_up, shaped)| {
// self.bytes_per_second.0 += bytes_down;
// self.bytes_per_second.1 += bytes_up;
// self.packets_per_second.0 += packets_down;
// self.packets_per_second.1 += packets_up;
// if shaped {
// self.shaped_bytes_per_second.0 += bytes_down;
// self.shaped_bytes_per_second.1 += bytes_up;
// }
// });
// // Onto the next cycle
// self.cycle += 1;
// Ok(())
// }
pub(crate) fn bits_per_second(&self) -> (u64, u64) { pub(crate) fn bits_per_second(&self) -> (u64, u64) {
(self.bytes_per_second.0 * 8, self.bytes_per_second.1 * 8) (self.bytes_per_second.0.load(std::sync::atomic::Ordering::Relaxed) * 8, self.bytes_per_second.1.load(std::sync::atomic::Ordering::Relaxed) * 8)
} }
pub(crate) fn shaped_bits_per_second(&self) -> (u64, u64) { pub(crate) fn shaped_bits_per_second(&self) -> (u64, u64) {
(self.shaped_bytes_per_second.0 * 8, self.shaped_bytes_per_second.1 * 8) (self.shaped_bytes_per_second.0.load(std::sync::atomic::Ordering::Relaxed) * 8, self.shaped_bytes_per_second.1.load(std::sync::atomic::Ordering::Relaxed) * 8)
} }
pub(crate) fn packets_per_second(&self) -> (u64, u64) { pub(crate) fn packets_per_second(&self) -> (u64, u64) {
self.packets_per_second (
self.packets_per_second.0.load(std::sync::atomic::Ordering::Relaxed),
self.packets_per_second.1.load(std::sync::atomic::Ordering::Relaxed),
)
} }
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn dump(&self) { pub(crate) fn dump(&self) {
for (k, v) in self.raw_data.iter() { for v in self.raw_data.iter() {
let ip = k.as_ip(); let ip = v.key().as_ip();
log::info!("{:<34}{:?}", ip, v.tc_handle); log::info!("{:<34}{:?}", ip, v.tc_handle);
} }
} }