mix throughput tester (#5661)
* wip: sending with single client * tag packets to measure latency * constantly logging rates * concurrency * adjusting some values * write results to files upon completion
This commit is contained in:
committed by
GitHub
parent
89eea3100e
commit
d062524d32
Generated
+56
-5
@@ -3731,6 +3731,7 @@ dependencies = [
|
||||
"number_prefix",
|
||||
"portable-atomic",
|
||||
"unicode-width 0.2.0",
|
||||
"vt100",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
@@ -4911,6 +4912,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
@@ -6108,22 +6110,25 @@ version = "1.7.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"arrayref",
|
||||
"async-trait",
|
||||
"axum 0.7.9",
|
||||
"axum-extra",
|
||||
"bip39",
|
||||
"blake2 0.8.1",
|
||||
"bs58",
|
||||
"cargo_metadata 0.18.1",
|
||||
"celes",
|
||||
"chacha",
|
||||
"clap",
|
||||
"colored",
|
||||
"csv",
|
||||
"cupid",
|
||||
"dashmap",
|
||||
"futures",
|
||||
"headers",
|
||||
"human-repr",
|
||||
"humantime-serde",
|
||||
"indicatif",
|
||||
"ipnetwork",
|
||||
"lioness",
|
||||
"nym-authenticator",
|
||||
"nym-bin-common",
|
||||
"nym-client-core-config-types",
|
||||
@@ -6145,6 +6150,8 @@ dependencies = [
|
||||
"nym-sphinx-addressing",
|
||||
"nym-sphinx-forwarding",
|
||||
"nym-sphinx-framing",
|
||||
"nym-sphinx-params",
|
||||
"nym-sphinx-routing",
|
||||
"nym-sphinx-types",
|
||||
"nym-task",
|
||||
"nym-topology",
|
||||
@@ -6154,10 +6161,8 @@ dependencies = [
|
||||
"nym-wireguard",
|
||||
"nym-wireguard-types",
|
||||
"rand 0.8.5",
|
||||
"semver 1.0.26",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"si-scale",
|
||||
"sysinfo",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -6166,6 +6171,7 @@ dependencies = [
|
||||
"toml 0.8.20",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-indicatif",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"utoipa",
|
||||
@@ -10419,6 +10425,18 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-indicatif"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8201ca430e0cd893ef978226fd3516c06d9c494181c8bf4e5b32e30ed4b40aa1"
|
||||
dependencies = [
|
||||
"indicatif",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.4"
|
||||
@@ -11013,6 +11031,39 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vt100"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"log",
|
||||
"unicode-width 0.1.14",
|
||||
"vte",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"utf8parse",
|
||||
"vte_generate_state_changes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vte_generate_state_changes"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "waker-fn"
|
||||
version = "1.2.0"
|
||||
|
||||
@@ -347,6 +347,7 @@ tracing-log = "0.2"
|
||||
tracing-opentelemetry = "0.19.0"
|
||||
tracing-subscriber = "0.3.19"
|
||||
tracing-tree = "0.2.2"
|
||||
tracing-indicatif = "0.3.9"
|
||||
ts-rs = "10.1.0"
|
||||
tungstenite = { version = "0.20.1", default-features = false }
|
||||
uniffi = "0.29.1"
|
||||
|
||||
@@ -21,6 +21,7 @@ serde_json = { workspace = true, optional = true }
|
||||
## tracing
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
|
||||
tracing-tree = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
opentelemetry-jaeger = { workspace = true, features = ["rt-tokio", "collector_client", "isahc_collector_client"], optional = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
@@ -35,7 +36,7 @@ default = []
|
||||
openapi = ["utoipa"]
|
||||
output_format = ["serde_json", "dep:clap"]
|
||||
bin_info_schema = ["schemars"]
|
||||
basic_tracing = ["tracing-subscriber"]
|
||||
basic_tracing = ["dep:tracing", "tracing-subscriber"]
|
||||
tracing = [
|
||||
"basic_tracing",
|
||||
"tracing-tree",
|
||||
|
||||
@@ -44,10 +44,38 @@ pub fn setup_logging() {
|
||||
.init();
|
||||
}
|
||||
|
||||
// don't call init so that we could attach additional layers
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub fn setup_tracing_logger() {
|
||||
let log_builder = tracing_subscriber::fmt()
|
||||
.with_writer(std::io::stderr)
|
||||
pub fn build_tracing_logger() -> impl tracing_subscriber::layer::SubscriberExt {
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(default_tracing_fmt_layer(std::io::stderr))
|
||||
.with(default_tracing_env_filter())
|
||||
}
|
||||
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub fn default_tracing_env_filter() -> tracing_subscriber::filter::EnvFilter {
|
||||
if ::std::env::var("RUST_LOG").is_ok() {
|
||||
tracing_subscriber::filter::EnvFilter::from_default_env()
|
||||
} else {
|
||||
// if the env value was not found, default to `INFO` level rather than `ERROR`
|
||||
tracing_subscriber::filter::EnvFilter::builder()
|
||||
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
|
||||
.parse_lossy("")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub fn default_tracing_fmt_layer<S, W>(
|
||||
writer: W,
|
||||
) -> impl tracing_subscriber::Layer<S> + Sync + Send + 'static
|
||||
where
|
||||
S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
|
||||
W: for<'writer> tracing_subscriber::fmt::MakeWriter<'writer> + Sync + Send + 'static,
|
||||
{
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_writer(writer)
|
||||
// Use a more compact, abbreviated log format
|
||||
.compact()
|
||||
// Display source code file paths
|
||||
@@ -55,18 +83,13 @@ pub fn setup_tracing_logger() {
|
||||
// Display source code line numbers
|
||||
.with_line_number(true)
|
||||
// Don't display the event's target (module path)
|
||||
.with_target(false);
|
||||
.with_target(false)
|
||||
}
|
||||
|
||||
if ::std::env::var("RUST_LOG").is_ok() {
|
||||
log_builder
|
||||
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
|
||||
.init()
|
||||
} else {
|
||||
// default to 'Info
|
||||
log_builder
|
||||
.with_max_level(tracing_subscriber::filter::LevelFilter::INFO)
|
||||
.init()
|
||||
}
|
||||
#[cfg(feature = "basic_tracing")]
|
||||
pub fn setup_tracing_logger() {
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
build_tracing_logger().init()
|
||||
}
|
||||
|
||||
// TODO: This has to be a macro, running it as a function does not work for the file_appender for some reason
|
||||
|
||||
@@ -24,10 +24,10 @@ use tracing::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Config {
|
||||
initial_reconnection_backoff: Duration,
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
pub initial_reconnection_backoff: Duration,
|
||||
pub maximum_reconnection_backoff: Duration,
|
||||
pub initial_connection_timeout: Duration,
|
||||
pub maximum_connection_buffer_size: usize,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -50,7 +50,7 @@ pub trait SendWithoutResponse {
|
||||
// Without response in this context means we will not listen for anything we might get back (not
|
||||
// that we should get anything), including any possible io errors
|
||||
fn send_without_response(
|
||||
&mut self,
|
||||
&self,
|
||||
address: NymNodeRoutingAddress,
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
@@ -196,7 +196,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_connection(&mut self, address: NymNodeRoutingAddress, pending_packet: FramedNymPacket) {
|
||||
fn make_connection(&self, address: NymNodeRoutingAddress, pending_packet: FramedNymPacket) {
|
||||
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
|
||||
|
||||
// this CAN'T fail because we just created the channel which has a non-zero capacity
|
||||
@@ -247,7 +247,7 @@ impl Client {
|
||||
|
||||
impl SendWithoutResponse for Client {
|
||||
fn send_without_response(
|
||||
&mut self,
|
||||
&self,
|
||||
address: NymNodeRoutingAddress,
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
|
||||
@@ -8,7 +8,7 @@ use thiserror::Error;
|
||||
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
|
||||
pub use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
|
||||
|
||||
#[cfg(feature = "outfox")]
|
||||
pub use nym_outfox::{
|
||||
@@ -166,4 +166,20 @@ impl NymPacket {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub fn sphinx_packet_ref(&self) -> Option<&SphinxPacket> {
|
||||
match self {
|
||||
NymPacket::Sphinx(packet) => Some(packet),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub fn as_sphinx_packet(self) -> Option<SphinxPacket> {
|
||||
match self {
|
||||
NymPacket::Sphinx(packet) => Some(packet),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-7
@@ -21,18 +21,19 @@ bip39 = { workspace = true, features = ["zeroize"] }
|
||||
bs58.workspace = true
|
||||
celes = { workspace = true } # country codes
|
||||
colored = { workspace = true }
|
||||
csv = { workspace = true }
|
||||
clap = { workspace = true, features = ["cargo", "env"] }
|
||||
dashmap = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
humantime-serde = { workspace = true }
|
||||
human-repr = { workspace = true }
|
||||
ipnetwork = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
si-scale = { workspace = true }
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-indicatif = { workspace = true }
|
||||
tracing-subscriber.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "sync", "rt-multi-thread"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
@@ -40,9 +41,6 @@ toml = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
# temporary bonding information v1 (to grab and parse nym-mixnode and nym-gateway package versions)
|
||||
semver = { workspace = true }
|
||||
|
||||
# system info:
|
||||
cupid = { workspace = true }
|
||||
sysinfo = { workspace = true }
|
||||
@@ -62,6 +60,8 @@ nym-sphinx-addressing = { path = "../common/nymsphinx/addressing" }
|
||||
nym-sphinx-framing = { path = "../common/nymsphinx/framing" }
|
||||
nym-sphinx-types = { path = "../common/nymsphinx/types" }
|
||||
nym-sphinx-forwarding = { path = "../common/nymsphinx/forwarding" }
|
||||
nym-sphinx-routing = { path = "../common/nymsphinx/routing" }
|
||||
nym-sphinx-params = { path = "../common/nymsphinx/params" }
|
||||
nym-task = { path = "../common/task" }
|
||||
nym-types = { path = "../common/types" }
|
||||
nym-validator-client = { path = "../common/client-libs/validator-client" }
|
||||
@@ -77,8 +77,6 @@ nym-http-api-client = { path = "../common/http-api-client" }
|
||||
# useful for `#[axum_macros::debug_handler]`
|
||||
#axum-macros = "0.3.8"
|
||||
axum.workspace = true
|
||||
axum-extra = { workspace = true, features = ["typed-header"] }
|
||||
headers.workspace = true
|
||||
time = { workspace = true, features = ["serde"] }
|
||||
tower-http = { workspace = true, features = ["fs"] }
|
||||
utoipa = { workspace = true, features = ["axum_extras", "time"] }
|
||||
@@ -95,6 +93,22 @@ nym-network-requester = { path = "../service-providers/network-requester" }
|
||||
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
|
||||
|
||||
|
||||
# throughput tester to recreate lioness
|
||||
# we don't care about particular versions - just pull whatever is used by sphinx
|
||||
[dependencies.lioness]
|
||||
version = "*"
|
||||
|
||||
[dependencies.chacha]
|
||||
version = "*"
|
||||
|
||||
[dependencies.arrayref]
|
||||
version = "*"
|
||||
|
||||
[dependencies.blake2]
|
||||
version = "*"
|
||||
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
# temporary bonding information v1 (to grab and parse nym-mixnode and nym-gateway package versions)
|
||||
cargo_metadata = { workspace = true }
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use crate::cli::helpers::ConfigArgs;
|
||||
use crate::config::upgrade_helpers::try_load_current_config;
|
||||
use crate::error::NymNodeError;
|
||||
use crate::node::bonding_information::BondingInformation;
|
||||
use nym_bin_common::output_format::OutputFormat;
|
||||
|
||||
@@ -21,7 +20,7 @@ pub struct Args {
|
||||
pub(crate) output: OutputFormat,
|
||||
}
|
||||
|
||||
pub async fn execute(args: Args) -> Result<(), NymNodeError> {
|
||||
pub async fn execute(args: Args) -> anyhow::Result<()> {
|
||||
let config = try_load_current_config(args.config.config_path()).await?;
|
||||
let info = BondingInformation::try_load(&config)?;
|
||||
args.output.to_stdout(&info);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::error::NymNodeError;
|
||||
use nym_bin_common::bin_info_owned;
|
||||
use nym_bin_common::output_format::OutputFormat;
|
||||
|
||||
@@ -11,7 +10,7 @@ pub(crate) struct Args {
|
||||
output: OutputFormat,
|
||||
}
|
||||
|
||||
pub(crate) fn execute(args: Args) -> Result<(), NymNodeError> {
|
||||
pub(crate) fn execute(args: Args) -> anyhow::Result<()> {
|
||||
println!("{}", args.output.format(&bin_info_owned!()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub(crate) struct Args {
|
||||
_args: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(_args: Args) -> Result<(), NymNodeError> {
|
||||
pub(crate) fn execute(_args: Args) -> Result<(), NymNodeError> {
|
||||
let orange = TrueColor {
|
||||
r: 251,
|
||||
g: 110,
|
||||
|
||||
@@ -7,3 +7,4 @@ pub(super) mod migrate;
|
||||
pub(crate) mod node_details;
|
||||
pub(super) mod run;
|
||||
pub(super) mod sign;
|
||||
pub(crate) mod test_throughput;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use crate::cli::helpers::ConfigArgs;
|
||||
use crate::config::upgrade_helpers::try_load_current_config;
|
||||
use crate::error::NymNodeError;
|
||||
use crate::node::NymNode;
|
||||
use nym_bin_common::output_format::OutputFormat;
|
||||
|
||||
@@ -21,7 +20,7 @@ pub(crate) struct Args {
|
||||
pub(crate) output: OutputFormat,
|
||||
}
|
||||
|
||||
pub async fn execute(args: Args) -> Result<(), NymNodeError> {
|
||||
pub async fn execute(args: Args) -> anyhow::Result<()> {
|
||||
let config = try_load_current_config(args.config.config_path()).await?;
|
||||
let details = NymNode::new(config).await?.display_details()?;
|
||||
args.output.to_stdout(&details);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use crate::cli::helpers::ConfigArgs;
|
||||
use crate::config::upgrade_helpers::try_load_current_config;
|
||||
use crate::error::NymNodeError;
|
||||
use crate::node::helpers::load_ed25519_identity_keypair;
|
||||
use nym_bin_common::output_format::OutputFormat;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
@@ -70,7 +69,7 @@ fn print_signed_contract_msg(
|
||||
|
||||
// SAFETY: clippy ArgGroup ensures only a single branch is actually called
|
||||
#[allow(clippy::unreachable)]
|
||||
pub async fn execute(args: Args) -> Result<(), NymNodeError> {
|
||||
pub async fn execute(args: Args) -> anyhow::Result<()> {
|
||||
let config = try_load_current_config(args.config.config_path()).await?;
|
||||
let identity_keypair =
|
||||
load_ed25519_identity_keypair(config.storage_paths.keys.ed25519_identity_storage_paths())?;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::cli::helpers::ConfigArgs;
|
||||
use crate::logging::granual_filtered_env;
|
||||
use crate::throughput_tester::test_mixing_throughput;
|
||||
use anyhow::bail;
|
||||
use humantime_serde::re::humantime;
|
||||
use indicatif::ProgressStyle;
|
||||
use nym_bin_common::logging::default_tracing_fmt_layer;
|
||||
use std::env::temp_dir;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing_indicatif::IndicatifLayer;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct Args {
|
||||
#[clap(flatten)]
|
||||
config: ConfigArgs,
|
||||
|
||||
#[clap(long, default_value_t = 10)]
|
||||
senders: usize,
|
||||
|
||||
/// target packet latency, if current value is below threshold, clients will increase their sending rates
|
||||
/// and similarly if it's above it, they will decrease it
|
||||
#[clap(long, default_value = "15ms", value_parser = humantime::parse_duration)]
|
||||
packet_latency_threshold: Duration,
|
||||
|
||||
#[clap(long, default_value_t = 50)]
|
||||
starting_sending_batch_size: usize,
|
||||
|
||||
#[clap(long, default_value = "50ms", value_parser = humantime::parse_duration)]
|
||||
starting_sending_delay: Duration,
|
||||
|
||||
#[clap(long, short)]
|
||||
output_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn init_test_logger() -> anyhow::Result<()> {
|
||||
let indicatif_layer = IndicatifLayer::new()
|
||||
.with_progress_style(ProgressStyle::with_template(
|
||||
"{span_child_prefix}{spinner} {span_fields} -- {span_name} {wide_msg}",
|
||||
)?)
|
||||
.with_span_child_prefix_symbol("↳ ")
|
||||
.with_span_child_prefix_indent(" ");
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(default_tracing_fmt_layer(
|
||||
indicatif_layer.get_stderr_writer(),
|
||||
))
|
||||
.with(indicatif_layer)
|
||||
.with(granual_filtered_env()?)
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn execute(args: Args) -> anyhow::Result<()> {
|
||||
init_test_logger()?;
|
||||
|
||||
let output_dir = match args.output_directory {
|
||||
Some(output_dir) => {
|
||||
if !output_dir.is_dir() {
|
||||
bail!("'{}' is not a directory", output_dir.display());
|
||||
}
|
||||
|
||||
output_dir
|
||||
}
|
||||
None => {
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
temp_dir()
|
||||
.join("nym-node-throughput-testing")
|
||||
.join(now.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
test_mixing_throughput(
|
||||
args.config.config_path(),
|
||||
args.senders,
|
||||
args.packet_latency_threshold,
|
||||
args.starting_sending_batch_size,
|
||||
args.starting_sending_delay,
|
||||
output_dir,
|
||||
)
|
||||
}
|
||||
+35
-11
@@ -1,14 +1,17 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::cli::commands::{bonding_information, build_info, migrate, node_details, run, sign};
|
||||
use crate::cli::commands::{
|
||||
bonding_information, build_info, migrate, node_details, run, sign, test_throughput,
|
||||
};
|
||||
use crate::env::vars::{NYMNODE_CONFIG_ENV_FILE_ARG, NYMNODE_NO_BANNER_ARG};
|
||||
use crate::error::NymNodeError;
|
||||
use crate::logging::setup_tracing_logger;
|
||||
use clap::{Parser, Subcommand};
|
||||
use nym_bin_common::bin_info;
|
||||
use std::future::Future;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
mod commands;
|
||||
pub(crate) mod commands;
|
||||
mod helpers;
|
||||
|
||||
pub const DEFAULT_NYMNODE_ID: &str = "default-nym-node";
|
||||
@@ -42,15 +45,31 @@ pub(crate) struct Cli {
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
pub(crate) async fn execute(self) -> Result<(), NymNodeError> {
|
||||
match self.command {
|
||||
Commands::BuildInfo(args) => build_info::execute(args),
|
||||
Commands::BondingInformation(args) => bonding_information::execute(args).await,
|
||||
Commands::NodeDetails(args) => node_details::execute(args).await,
|
||||
Commands::Run(args) => run::execute(*args).await,
|
||||
Commands::Migrate(args) => migrate::execute(*args).await,
|
||||
Commands::Sign(args) => sign::execute(args).await,
|
||||
fn execute_async<F: Future>(fut: F) -> anyhow::Result<F::Output> {
|
||||
Ok(tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(fut))
|
||||
}
|
||||
|
||||
pub(crate) fn execute(self) -> anyhow::Result<()> {
|
||||
// NOTE: `test_throughput` sets up its own logger as it has to include additional layers
|
||||
if !matches!(self.command, Commands::TestThroughput(..)) {
|
||||
setup_tracing_logger()?;
|
||||
}
|
||||
|
||||
match self.command {
|
||||
Commands::BuildInfo(args) => build_info::execute(args)?,
|
||||
Commands::BondingInformation(args) => {
|
||||
{ Self::execute_async(bonding_information::execute(args))? }?
|
||||
}
|
||||
Commands::NodeDetails(args) => { Self::execute_async(node_details::execute(args))? }?,
|
||||
Commands::Run(args) => { Self::execute_async(run::execute(*args))? }?,
|
||||
Commands::Migrate(args) => migrate::execute(*args)?,
|
||||
Commands::Sign(args) => { Self::execute_async(sign::execute(args))? }?,
|
||||
Commands::TestThroughput(args) => test_throughput::execute(args)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +92,11 @@ pub(crate) enum Commands {
|
||||
|
||||
/// Use identity key of this node to sign provided message.
|
||||
Sign(sign::Args),
|
||||
|
||||
/// Attempt to approximate the maximum mixnet throughput if nym-node
|
||||
/// was running on this machine in mixnet mode
|
||||
#[clap(hide = true)]
|
||||
TestThroughput(test_throughput::Args),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+16
-17
@@ -1,35 +1,34 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{filter::Directive, EnvFilter};
|
||||
use nym_bin_common::logging::{default_tracing_env_filter, default_tracing_fmt_layer};
|
||||
use tracing_subscriber::filter::Directive;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> {
|
||||
pub(crate) fn granual_filtered_env() -> anyhow::Result<tracing_subscriber::filter::EnvFilter> {
|
||||
fn directive_checked(directive: impl Into<String>) -> anyhow::Result<Directive> {
|
||||
directive.into().parse().map_err(From::from)
|
||||
}
|
||||
|
||||
let log_builder = tracing_subscriber::fmt()
|
||||
// Use a more compact, abbreviated log format
|
||||
.compact()
|
||||
// Display source code file paths
|
||||
.with_file(true)
|
||||
// Display source code line numbers
|
||||
.with_line_number(true)
|
||||
// Don't display the event's target (module path)
|
||||
.with_target(false);
|
||||
let mut filter = default_tracing_env_filter();
|
||||
|
||||
let mut filter = EnvFilter::builder()
|
||||
// if RUST_LOG isn't set, set default level
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
// these crates are more granularly filtered
|
||||
let filter_crates = ["defguard_wireguard_rs"];
|
||||
for crate_name in filter_crates {
|
||||
filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?);
|
||||
}
|
||||
Ok(filter)
|
||||
}
|
||||
|
||||
log_builder.with_env_filter(filter).init();
|
||||
pub(crate) fn build_tracing_logger() -> anyhow::Result<impl SubscriberExt> {
|
||||
Ok(tracing_subscriber::registry()
|
||||
.with(default_tracing_fmt_layer(std::io::stderr))
|
||||
.with(granual_filtered_env()?))
|
||||
}
|
||||
|
||||
pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> {
|
||||
build_tracing_logger()?.init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use crate::{cli::Cli, logging::setup_tracing_logger};
|
||||
use crate::cli::Cli;
|
||||
use clap::{crate_name, crate_version, Parser};
|
||||
use nym_bin_common::logging::maybe_print_banner;
|
||||
use nym_config::defaults::setup_env;
|
||||
@@ -15,10 +15,10 @@ mod env;
|
||||
pub(crate) mod error;
|
||||
mod logging;
|
||||
pub(crate) mod node;
|
||||
pub(crate) mod throughput_tester;
|
||||
pub(crate) mod wireguard;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
fn main() -> anyhow::Result<()> {
|
||||
// std::env::set_var(
|
||||
// "RUST_LOG",
|
||||
// "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn",
|
||||
@@ -26,13 +26,12 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
let cli = Cli::parse();
|
||||
setup_env(cli.config_env_file.as_ref());
|
||||
setup_tracing_logger()?;
|
||||
|
||||
if !cli.no_banner {
|
||||
maybe_print_banner(crate_name!(), crate_version!());
|
||||
}
|
||||
|
||||
cli.execute().await?;
|
||||
cli.execute()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::mixnet::packet_forwarding::global::is_global_ip;
|
||||
use crate::node::shared_network::RoutingFilter;
|
||||
use futures::StreamExt;
|
||||
use nym_mixnet_client::forwarder::{
|
||||
@@ -13,38 +12,33 @@ use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_task::ShutdownToken;
|
||||
use std::io;
|
||||
use std::net::IpAddr;
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
pub(crate) mod global;
|
||||
|
||||
pub struct PacketForwarder<C> {
|
||||
testnet: bool,
|
||||
|
||||
pub struct PacketForwarder<C, F> {
|
||||
delay_queue: NonExhaustiveDelayQueue<MixPacket>,
|
||||
mixnet_client: C,
|
||||
|
||||
metrics: NymNodeMetrics,
|
||||
routing_filter: RoutingFilter,
|
||||
routing_filter: F,
|
||||
|
||||
packet_sender: MixForwardingSender,
|
||||
packet_receiver: MixForwardingReceiver,
|
||||
shutdown: ShutdownToken,
|
||||
}
|
||||
|
||||
impl<C> PacketForwarder<C> {
|
||||
impl<C, F> PacketForwarder<C, F> {
|
||||
pub fn new(
|
||||
client: C,
|
||||
testnet: bool,
|
||||
routing_filter: RoutingFilter,
|
||||
routing_filter: F,
|
||||
metrics: NymNodeMetrics,
|
||||
shutdown: ShutdownToken,
|
||||
) -> Self {
|
||||
let (packet_sender, packet_receiver) = mix_forwarding_channels();
|
||||
|
||||
PacketForwarder {
|
||||
testnet,
|
||||
delay_queue: NonExhaustiveDelayQueue::new(),
|
||||
mixnet_client: client,
|
||||
metrics,
|
||||
@@ -59,22 +53,14 @@ impl<C> PacketForwarder<C> {
|
||||
self.packet_sender.clone()
|
||||
}
|
||||
|
||||
fn should_route(&mut self, ip_addr: IpAddr) -> bool {
|
||||
// only allow non-global ips on testnets
|
||||
if self.testnet && !is_global_ip(&ip_addr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.routing_filter.attempt_resolve(ip_addr).should_route()
|
||||
}
|
||||
|
||||
fn forward_packet(&mut self, packet: MixPacket)
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
let next_hop = packet.next_hop();
|
||||
|
||||
if !self.should_route(next_hop.as_ref().ip()) {
|
||||
if !self.routing_filter.should_route(next_hop.as_ref().ip()) {
|
||||
debug!("dropping packet as the egress address does not belong to any known node");
|
||||
self.metrics
|
||||
.mixnet
|
||||
@@ -113,6 +99,7 @@ impl<C> PacketForwarder<C> {
|
||||
fn handle_done_delaying(&mut self, packet: Expired<MixPacket>)
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
let delayed_packet = packet.into_inner();
|
||||
self.forward_packet(delayed_packet);
|
||||
@@ -121,6 +108,7 @@ impl<C> PacketForwarder<C> {
|
||||
fn handle_new_packet(&mut self, new_packet: PacketToForward)
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
// in case of a zero delay packet, don't bother putting it in the delay queue,
|
||||
// just forward it immediately
|
||||
@@ -152,6 +140,7 @@ impl<C> PacketForwarder<C> {
|
||||
pub async fn run(&mut self)
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
let mut processed = 0;
|
||||
trace!("starting PacketForwarder");
|
||||
|
||||
@@ -29,7 +29,7 @@ use crate::node::mixnet::packet_forwarding::PacketForwarder;
|
||||
use crate::node::mixnet::shared::ProcessingConfig;
|
||||
use crate::node::mixnet::SharedFinalHopData;
|
||||
use crate::node::shared_network::{
|
||||
CachedNetwork, CachedTopologyProvider, NetworkRefresher, RoutingFilter,
|
||||
CachedNetwork, CachedTopologyProvider, NetworkRefresher, OpenFilter, RoutingFilter,
|
||||
};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
@@ -461,6 +461,10 @@ impl NymNode {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown_token<S: Into<String>>(&self, child_suffix: S) -> ShutdownToken {
|
||||
self.shutdown_manager.clone_token(child_suffix)
|
||||
}
|
||||
|
||||
pub(crate) fn with_accepted_operator_terms_and_conditions(
|
||||
mut self,
|
||||
accepted_operator_terms_and_conditions: bool,
|
||||
@@ -528,12 +532,17 @@ impl NymNode {
|
||||
self.x25519_sphinx_keys.public_key()
|
||||
}
|
||||
|
||||
pub(crate) fn x25519_sphinx_keys(&self) -> Arc<x25519::KeyPair> {
|
||||
self.x25519_sphinx_keys.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn x25519_noise_key(&self) -> &x25519::PublicKey {
|
||||
self.x25519_noise_keys.public_key()
|
||||
}
|
||||
|
||||
async fn build_network_refresher(&self) -> Result<NetworkRefresher, NymNodeError> {
|
||||
NetworkRefresher::initialise_new(
|
||||
self.config.debug.testnet,
|
||||
self.user_agent(),
|
||||
self.config.mixnet.nym_api_urls.clone(),
|
||||
self.config.debug.topology_cache_ttl,
|
||||
@@ -970,12 +979,15 @@ impl NymNode {
|
||||
events_sender
|
||||
}
|
||||
|
||||
pub(crate) fn start_mixnet_listener(
|
||||
pub(crate) fn start_mixnet_listener<F>(
|
||||
&self,
|
||||
active_clients_store: &ActiveClientsStore,
|
||||
routing_filter: RoutingFilter,
|
||||
routing_filter: F,
|
||||
shutdown: ShutdownToken,
|
||||
) -> (MixForwardingSender, ActiveConnections) {
|
||||
) -> (MixForwardingSender, ActiveConnections)
|
||||
where
|
||||
F: RoutingFilter + Send + Sync + 'static,
|
||||
{
|
||||
let processing_config = ProcessingConfig::new(&self.config);
|
||||
|
||||
// we're ALWAYS listening for mixnet packets, either for forward or final hops (or both)
|
||||
@@ -1002,7 +1014,6 @@ impl NymNode {
|
||||
|
||||
let mut packet_forwarder = PacketForwarder::new(
|
||||
mixnet_client,
|
||||
self.config.debug.testnet,
|
||||
routing_filter,
|
||||
self.metrics.clone(),
|
||||
shutdown.clone_with_suffix("mix-packet-forwarder"),
|
||||
@@ -1028,6 +1039,19 @@ impl NymNode {
|
||||
(mix_packet_sender, active_connections)
|
||||
}
|
||||
|
||||
pub(crate) async fn run_minimal_mixnet_processing(self) -> Result<(), NymNodeError> {
|
||||
self.start_mixnet_listener(
|
||||
&ActiveClientsStore::new(),
|
||||
OpenFilter,
|
||||
self.shutdown_manager.clone_token("mixnet-traffic"),
|
||||
);
|
||||
|
||||
self.shutdown_manager.close();
|
||||
self.shutdown_manager.wait_for_shutdown_signal().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(mut self) -> Result<(), NymNodeError> {
|
||||
info!("starting Nym Node {} with the following modes: mixnode: {}, entry: {}, exit: {}, wireguard: {}",
|
||||
self.ed25519_identity_key(),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::error::NymNodeError;
|
||||
use crate::node::mixnet::packet_forwarding::global::is_global_ip;
|
||||
use arc_swap::ArcSwap;
|
||||
use async_trait::async_trait;
|
||||
use nym_gateway::node::UserAgent;
|
||||
@@ -22,8 +23,34 @@ use tracing::log::error;
|
||||
use tracing::{debug, trace, warn};
|
||||
use url::Url;
|
||||
|
||||
pub(crate) trait RoutingFilter {
|
||||
fn should_route(&self, ip: IpAddr) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub(crate) struct OpenFilter;
|
||||
|
||||
impl RoutingFilter for OpenFilter {
|
||||
fn should_route(&self, _: IpAddr) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl RoutingFilter for NetworkRoutingFilter {
|
||||
fn should_route(&self, ip: IpAddr) -> bool {
|
||||
// only allow non-global ips on testnets
|
||||
if self.testnet_mode && !is_global_ip(&ip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.attempt_resolve(ip).should_route()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RoutingFilter {
|
||||
pub(crate) struct NetworkRoutingFilter {
|
||||
testnet_mode: bool,
|
||||
|
||||
resolved: KnownNodes,
|
||||
|
||||
// while this is technically behind a lock, it should not be called too often as once resolved it will
|
||||
@@ -31,9 +58,10 @@ pub(crate) struct RoutingFilter {
|
||||
pending: UnknownNodes,
|
||||
}
|
||||
|
||||
impl RoutingFilter {
|
||||
fn new_empty() -> Self {
|
||||
RoutingFilter {
|
||||
impl NetworkRoutingFilter {
|
||||
fn new_empty(testnet_mode: bool) -> Self {
|
||||
NetworkRoutingFilter {
|
||||
testnet_mode,
|
||||
resolved: Default::default(),
|
||||
pending: Default::default(),
|
||||
}
|
||||
@@ -254,11 +282,12 @@ pub struct NetworkRefresher {
|
||||
shutdown_token: ShutdownToken,
|
||||
|
||||
network: CachedNetwork,
|
||||
routing_filter: RoutingFilter,
|
||||
routing_filter: NetworkRoutingFilter,
|
||||
}
|
||||
|
||||
impl NetworkRefresher {
|
||||
pub(crate) async fn initialise_new(
|
||||
testnet: bool,
|
||||
user_agent: UserAgent,
|
||||
nym_api_urls: Vec<Url>,
|
||||
full_refresh_interval: Duration,
|
||||
@@ -280,7 +309,7 @@ impl NetworkRefresher {
|
||||
pending_check_interval,
|
||||
shutdown_token,
|
||||
network: CachedNetwork::new_empty(),
|
||||
routing_filter: RoutingFilter::new_empty(),
|
||||
routing_filter: NetworkRoutingFilter::new_empty(testnet),
|
||||
};
|
||||
|
||||
this.obtain_initial_network().await?;
|
||||
@@ -403,7 +432,7 @@ impl NetworkRefresher {
|
||||
.map_err(|source| NymNodeError::InitialTopologyQueryFailure { source })
|
||||
}
|
||||
|
||||
pub(crate) fn routing_filter(&self) -> RoutingFilter {
|
||||
pub(crate) fn routing_filter(&self) -> NetworkRoutingFilter {
|
||||
self.routing_filter.clone()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::throughput_tester::stats::ClientStats;
|
||||
use anyhow::bail;
|
||||
use arrayref::array_ref;
|
||||
use blake2::VarBlake2b;
|
||||
use chacha::ChaCha;
|
||||
use futures::{stream, SinkExt, Stream, StreamExt};
|
||||
use human_repr::{HumanCount, HumanDuration, HumanThroughput};
|
||||
use lioness::Lioness;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx_framing::codec::{NymCodec, NymCodecError};
|
||||
use nym_sphinx_framing::packet::FramedNymPacket;
|
||||
use nym_sphinx_params::PacketSize;
|
||||
use nym_sphinx_routing::generate_hop_delays;
|
||||
use nym_sphinx_types::header::keys::PayloadKey;
|
||||
use nym_sphinx_types::{
|
||||
Destination, DestinationAddressBytes, Node, NymPacket, SphinxHeader,
|
||||
DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH,
|
||||
};
|
||||
use nym_task::ShutdownToken;
|
||||
use rand::rngs::OsRng;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::select;
|
||||
use tokio::time::{interval, sleep, Instant};
|
||||
use tokio_util::codec::Framed;
|
||||
use tracing::{debug, error, info, Span};
|
||||
use tracing_indicatif::span_ext::IndicatifSpanExt;
|
||||
|
||||
struct PacketTag {
|
||||
sending_timestamp: OffsetDateTime,
|
||||
batch_id: u64,
|
||||
index: u64,
|
||||
}
|
||||
|
||||
impl PacketTag {
|
||||
const SIZE: usize = 32;
|
||||
|
||||
fn elapsed(&self) -> time::Duration {
|
||||
OffsetDateTime::now_utc() - self.sending_timestamp
|
||||
}
|
||||
|
||||
fn elapsed_nanos(&self) -> u64 {
|
||||
// here we're making few assumptions: the latency is lower than u64::MAX
|
||||
// and it's strictly positive (which are rather valid...)
|
||||
self.elapsed().whole_nanoseconds() as u64
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
self.sending_timestamp
|
||||
.unix_timestamp_nanos()
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(self.batch_id.to_be_bytes())
|
||||
.chain(self.index.to_be_bytes())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn from_bytes(bytes: &[u8]) -> PacketTag {
|
||||
let sending_timestamp = i128::from_be_bytes(bytes[0..16].try_into().unwrap());
|
||||
let sending_timestamp =
|
||||
OffsetDateTime::from_unix_timestamp_nanos(sending_timestamp).unwrap();
|
||||
|
||||
let batch_id = u64::from_be_bytes(bytes[8..16].try_into().unwrap());
|
||||
let index = u64::from_be_bytes(bytes[16..24].try_into().unwrap());
|
||||
PacketTag {
|
||||
sending_timestamp,
|
||||
batch_id,
|
||||
index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ThroughputTestingClient {
|
||||
stats: ClientStats,
|
||||
last_received_update: Instant,
|
||||
last_received_at_update: usize,
|
||||
current_batch: u64,
|
||||
sending_delay: Duration,
|
||||
latency_threshold: Duration,
|
||||
current_batch_size: usize,
|
||||
forward_header_bytes: Vec<u8>,
|
||||
unwrapped_forward_payload_bytes: Vec<u8>,
|
||||
shutdown_token: ShutdownToken,
|
||||
local_address: SocketAddr,
|
||||
listener: TcpListener,
|
||||
forward_connection: Framed<TcpStream, NymCodec>,
|
||||
payload_key: PayloadKey,
|
||||
}
|
||||
|
||||
impl ThroughputTestingClient {
|
||||
pub(crate) async fn try_create(
|
||||
initial_sending_delay: Duration,
|
||||
initial_batch_size: usize,
|
||||
latency_threshold: Duration,
|
||||
node_keys: &x25519::KeyPair,
|
||||
node_listener: SocketAddr,
|
||||
stats: ClientStats,
|
||||
cancellation_token: ShutdownToken,
|
||||
) -> anyhow::Result<Self> {
|
||||
// attempt to bind to some port to receive processed packets
|
||||
let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], 0))).await?;
|
||||
let local_address = listener.local_addr()?;
|
||||
|
||||
info!("listening on {local_address}");
|
||||
|
||||
// create the sphinx packet we're going to be repeatedly sending
|
||||
// (next hop has to be our mixnode, then this client, and then it doesn't matter since the packet won't
|
||||
// get further processed)
|
||||
let mut rng = OsRng;
|
||||
// keys of this client
|
||||
let ephemeral_keys = x25519::KeyPair::new(&mut rng);
|
||||
|
||||
let route = [
|
||||
Node::new(
|
||||
NymNodeRoutingAddress::from(node_listener).try_into()?,
|
||||
(*node_keys.public_key()).into(),
|
||||
),
|
||||
Node::new(
|
||||
NymNodeRoutingAddress::from(local_address).try_into()?,
|
||||
(*ephemeral_keys.public_key()).into(),
|
||||
),
|
||||
Node::new(
|
||||
NymNodeRoutingAddress::from(local_address).try_into()?,
|
||||
(*ephemeral_keys.public_key()).into(),
|
||||
),
|
||||
];
|
||||
let destination = Destination::new(
|
||||
DestinationAddressBytes::from_bytes([0u8; DESTINATION_ADDRESS_LENGTH]),
|
||||
[0u8; IDENTIFIER_LENGTH],
|
||||
);
|
||||
let delays = generate_hop_delays(Duration::default(), 3);
|
||||
let payload = PacketSize::RegularPacket.payload_size();
|
||||
|
||||
let forward_packet =
|
||||
NymPacket::sphinx_build(payload, b"foomp", &route, &destination, &delays)?;
|
||||
|
||||
// SAFETY: we constructed a sphinx packet...
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let sphinx_packet = forward_packet.as_sphinx_packet().unwrap();
|
||||
let header = &sphinx_packet.header;
|
||||
|
||||
// derive the routing keys of our node so we could tag the payload to figure out latency
|
||||
// by tagging the packet
|
||||
let routing_keys = SphinxHeader::compute_routing_keys(
|
||||
&header.shared_secret,
|
||||
(&node_keys.private_key()).as_ref(),
|
||||
);
|
||||
let payload_key = routing_keys.payload_key;
|
||||
let unwrapped_payload = sphinx_packet.payload.unwrap(&payload_key)?;
|
||||
let unwrapped_forward_payload_bytes = unwrapped_payload.into_bytes();
|
||||
|
||||
let start = Instant::now();
|
||||
let forward_connection = loop {
|
||||
if let Ok(connection) = TcpStream::connect(node_listener).await {
|
||||
break connection;
|
||||
}
|
||||
// fallback
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
if start.elapsed() > Duration::from_secs(10) {
|
||||
bail!("failed to connect to local nym-node")
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ThroughputTestingClient {
|
||||
stats,
|
||||
last_received_update: Instant::now(),
|
||||
last_received_at_update: 0,
|
||||
current_batch: 0,
|
||||
sending_delay: initial_sending_delay,
|
||||
latency_threshold,
|
||||
current_batch_size: initial_batch_size,
|
||||
forward_header_bytes: sphinx_packet.header.to_bytes(),
|
||||
unwrapped_forward_payload_bytes,
|
||||
shutdown_token: cancellation_token,
|
||||
local_address,
|
||||
listener,
|
||||
forward_connection: Framed::new(forward_connection, NymCodec),
|
||||
payload_key,
|
||||
})
|
||||
}
|
||||
|
||||
fn update_progress_bar(&mut self) {
|
||||
let received = self.stats.received();
|
||||
let sent = self.stats.sent();
|
||||
let latency = self.stats.average_latency_duration();
|
||||
let received_since_update = received - self.last_received_at_update;
|
||||
|
||||
let time_delta_secs = self.last_received_update.elapsed().as_secs_f64();
|
||||
let receive_rate = received_since_update as f64 / time_delta_secs;
|
||||
|
||||
self.last_received_at_update = received;
|
||||
self.last_received_update = Instant::now();
|
||||
// I couldn't figure out how to directly pull it from span fields without duplication,
|
||||
// so that's a second best
|
||||
Span::current().pb_set_message(&format!(
|
||||
"{}: CURRENT SENDING DELAY/BATCH: {} / {} | received: {} sent: {} (avg packet latency: {}, avg receive rate: {})",
|
||||
self.local_address,
|
||||
self.sending_delay.human_duration(),
|
||||
self.current_batch_size,
|
||||
received.human_count_bare(),
|
||||
sent.human_count_bare(),
|
||||
latency.human_duration(),
|
||||
receive_rate.human_throughput("packets")
|
||||
));
|
||||
}
|
||||
|
||||
fn lioness_encrypt(&self, block: &mut [u8]) -> anyhow::Result<()> {
|
||||
let lioness_cipher = Lioness::<VarBlake2b, ChaCha>::new_raw(array_ref!(
|
||||
self.payload_key,
|
||||
0,
|
||||
lioness::RAW_KEY_SIZE
|
||||
));
|
||||
lioness_cipher.encrypt(block)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tag_framed_packet(&self, tag: PacketTag) -> anyhow::Result<FramedNymPacket> {
|
||||
let tag_bytes = tag.to_bytes();
|
||||
|
||||
let mut payload_bytes = self.unwrapped_forward_payload_bytes.clone();
|
||||
payload_bytes[..PacketTag::SIZE].copy_from_slice(&tag_bytes);
|
||||
|
||||
self.lioness_encrypt(&mut payload_bytes)?;
|
||||
|
||||
let mut packet_bytes = self.forward_header_bytes.clone();
|
||||
packet_bytes.append(&mut payload_bytes);
|
||||
|
||||
let forward_packet = NymPacket::sphinx_from_bytes(&packet_bytes)?;
|
||||
Ok(FramedNymPacket::new(forward_packet, Default::default()))
|
||||
}
|
||||
|
||||
async fn send_packets(&mut self) -> anyhow::Result<()> {
|
||||
// mess with our payload in such a way that upon unwrapping by the first hop,
|
||||
// we'll get our tag
|
||||
let mut batch = Vec::with_capacity(self.current_batch_size);
|
||||
let now = OffsetDateTime::now_utc();
|
||||
for i in 0..self.current_batch_size {
|
||||
let tag = PacketTag {
|
||||
sending_timestamp: now,
|
||||
batch_id: self.current_batch,
|
||||
index: i as u64,
|
||||
};
|
||||
let framed_packet = self.tag_framed_packet(tag)?;
|
||||
batch.push(Ok(framed_packet));
|
||||
}
|
||||
|
||||
self.current_batch += 1;
|
||||
self.forward_connection
|
||||
.send_all(&mut stream::iter(batch))
|
||||
.await?;
|
||||
self.stats.new_sent_batch(self.current_batch_size);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// don't bother processing packets, just increment the count because that's the only thing that matters
|
||||
fn handle_received(&mut self, maybe_packet: Result<FramedNymPacket, NymCodecError>) {
|
||||
let Ok(received) = maybe_packet else {
|
||||
error!("FAILED TO RECEIVE PACKET");
|
||||
return;
|
||||
};
|
||||
let inner = received.into_inner();
|
||||
// safety: we sent a sphinx packet...
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let sphinx = inner.as_sphinx_packet().unwrap();
|
||||
let tag = PacketTag::from_bytes(sphinx.payload.as_bytes());
|
||||
|
||||
self.stats.new_received(tag.elapsed_nanos());
|
||||
}
|
||||
|
||||
fn update_sending_rates(&mut self) {
|
||||
let current = self.stats.average_latency_nanos() as f64;
|
||||
let threshold = self.latency_threshold.as_nanos() as f64;
|
||||
|
||||
let saturation = current / threshold;
|
||||
|
||||
let sending_delay_nanos = self.sending_delay.as_nanos();
|
||||
let batch_size = self.current_batch_size;
|
||||
|
||||
let diff = 1. - saturation;
|
||||
|
||||
if saturation > 1. {
|
||||
debug!("saturation {saturation:.2}, packet latency over threshold: need to decrease sending rate");
|
||||
} else {
|
||||
debug!("saturation {saturation:.2}, packet latency under threshold: can increase sending rate");
|
||||
}
|
||||
|
||||
// be conservative and only apply 50% of the diff
|
||||
// (and split it equally between sending delay and batch size)
|
||||
// but also make sure the current values don't increase by more than 5%
|
||||
let mut new_batch_size = (batch_size as f64 * (1. + 0.25 * diff)).floor() as u64;
|
||||
let mut new_sending_delay_nanos =
|
||||
(sending_delay_nanos as f64 * (1. - 0.25 * diff)).floor() as u64;
|
||||
|
||||
if (new_batch_size as f64) > (batch_size as f64 * 1.05) {
|
||||
new_batch_size = ((batch_size as f64) * 1.05) as u64;
|
||||
}
|
||||
if (new_batch_size as f64) < (batch_size as f64 * 0.95) {
|
||||
new_batch_size = ((batch_size as f64) * 0.95) as u64;
|
||||
}
|
||||
|
||||
if (new_sending_delay_nanos as f64) > (sending_delay_nanos as f64 * 1.05) {
|
||||
new_sending_delay_nanos = ((sending_delay_nanos as f64) * 1.05) as u64;
|
||||
}
|
||||
if (new_sending_delay_nanos as f64) < (sending_delay_nanos as f64 * 0.95) {
|
||||
new_sending_delay_nanos = ((sending_delay_nanos as f64) * 0.95) as u64;
|
||||
}
|
||||
|
||||
// normalize values
|
||||
if new_batch_size < 20 {
|
||||
new_batch_size = 20;
|
||||
}
|
||||
let mut new_sending_delay = Duration::from_nanos(new_sending_delay_nanos);
|
||||
|
||||
if new_sending_delay.is_zero() {
|
||||
new_sending_delay = Duration::from_micros(500);
|
||||
}
|
||||
if new_sending_delay.as_millis() > 100 {
|
||||
new_sending_delay = Duration::from_millis(100);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"changing sending delay from {} to {}",
|
||||
self.sending_delay.human_duration(),
|
||||
new_sending_delay.human_duration()
|
||||
);
|
||||
debug!("changing sending batch from {batch_size} to {new_batch_size}");
|
||||
|
||||
self.sending_delay = new_sending_delay;
|
||||
self.current_batch_size = new_batch_size as usize;
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
pub(crate) async fn run(mut self) -> anyhow::Result<()> {
|
||||
let mut ingress_connection = StreamWrapper::default();
|
||||
|
||||
let mut sending_interval = interval(self.sending_delay);
|
||||
sending_interval.reset();
|
||||
|
||||
// quite arbitrary
|
||||
let mut update_interval = interval(Duration::from_millis(500));
|
||||
update_interval.reset();
|
||||
|
||||
let mut last_rate_update = Instant::now();
|
||||
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = self.shutdown_token.cancelled() => {
|
||||
info!("cancelled");
|
||||
return Ok(());
|
||||
}
|
||||
_ = update_interval.tick() => {
|
||||
self.update_progress_bar();
|
||||
|
||||
// every 500ms attempt to adjust sending rates
|
||||
if last_rate_update.elapsed() > Duration::from_millis(500) {
|
||||
last_rate_update = Instant::now();
|
||||
self.update_sending_rates();
|
||||
sending_interval = interval(self.sending_delay);
|
||||
sending_interval.reset();
|
||||
}
|
||||
|
||||
}
|
||||
accepted = self.listener.accept() => {
|
||||
info!("accepted connection");
|
||||
if ingress_connection.inner.is_some() {
|
||||
// this should never happen under local settings
|
||||
// (and since it's not exposed to 'proper' traffic, it's fine to panic and shutdown)
|
||||
panic!("attempted to overwrite existing connection")
|
||||
}
|
||||
let (stream, _) = accepted?;
|
||||
let framed = Framed::new(stream, NymCodec);
|
||||
ingress_connection.set(framed);
|
||||
}
|
||||
received = ingress_connection.next() => {
|
||||
let Some(received) = received else {
|
||||
// if the stream has terminated, we return
|
||||
if ingress_connection.inner.is_some() {
|
||||
return Ok(())
|
||||
}
|
||||
continue;
|
||||
};
|
||||
self.handle_received(received)
|
||||
}
|
||||
_ = sending_interval.tick() => {
|
||||
self.send_packets().await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// I must be blind, because I couldn't find something to do equivalent of `OptionStream`...
|
||||
#[derive(Default)]
|
||||
struct StreamWrapper {
|
||||
inner: Option<Framed<TcpStream, NymCodec>>,
|
||||
maybe_initial_waker: Option<Waker>,
|
||||
}
|
||||
|
||||
impl StreamWrapper {
|
||||
fn set(&mut self, inner: Framed<TcpStream, NymCodec>) {
|
||||
self.inner = Some(inner);
|
||||
if let Some(waker) = self.maybe_initial_waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for StreamWrapper {
|
||||
type Item = <Framed<TcpStream, NymCodec> as Stream>::Item;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match self.inner.as_mut() {
|
||||
None => {
|
||||
self.maybe_initial_waker = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
Some(inner) => Pin::new(inner).poll_next(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match &self.inner {
|
||||
None => (0, None),
|
||||
Some(inner) => inner.size_hint(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::throughput_tester::stats::ClientStats;
|
||||
use colored::Colorize;
|
||||
use human_repr::{HumanCount, HumanDuration, HumanThroughput};
|
||||
use nym_task::ShutdownToken;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::create_dir_all;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use sysinfo::System;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::select;
|
||||
use tokio::time::{interval, Instant};
|
||||
use tracing::{error, info, Span};
|
||||
use tracing_indicatif::span_ext::IndicatifSpanExt;
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub(crate) struct StatsRecord {
|
||||
timestamp: i64,
|
||||
receive_rate: f64,
|
||||
latency: u64,
|
||||
sent: usize,
|
||||
received: usize,
|
||||
}
|
||||
|
||||
pub(crate) struct GlobalStatsUpdater {
|
||||
system: System,
|
||||
last_update: Instant,
|
||||
last_total_received: usize,
|
||||
header_span: Span,
|
||||
client_stats: Vec<ClientStats>,
|
||||
|
||||
global_records: Vec<StatsRecord>,
|
||||
records: HashMap<usize, Vec<StatsRecord>>,
|
||||
|
||||
output_directory: PathBuf,
|
||||
shutdown: ShutdownToken,
|
||||
}
|
||||
|
||||
impl GlobalStatsUpdater {
|
||||
pub(crate) fn new(
|
||||
header_span: Span,
|
||||
client_stats: Vec<ClientStats>,
|
||||
output_directory: PathBuf,
|
||||
shutdown: ShutdownToken,
|
||||
) -> Self {
|
||||
let mut system_info = System::new_all();
|
||||
system_info.refresh_cpu_usage();
|
||||
|
||||
// pre-allocate vecs
|
||||
let mut records = HashMap::new();
|
||||
for (i, _) in client_stats.iter().enumerate() {
|
||||
records.insert(i, vec![]);
|
||||
}
|
||||
|
||||
GlobalStatsUpdater {
|
||||
system: system_info,
|
||||
last_update: Instant::now(),
|
||||
last_total_received: 0,
|
||||
header_span,
|
||||
client_stats,
|
||||
global_records: vec![],
|
||||
records,
|
||||
output_directory,
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_stats_span(&mut self) {
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
let time_delta_secs = self.last_update.elapsed().as_secs_f64();
|
||||
|
||||
let mut all_received = 0;
|
||||
let mut all_sent = 0;
|
||||
let mut all_latencies = 0;
|
||||
for (i, stat) in self.client_stats.iter().enumerate() {
|
||||
// SAFETY: we create all entries during initialisation
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let records = self.records.get_mut(&i).unwrap();
|
||||
|
||||
let mut client_record = StatsRecord::default();
|
||||
|
||||
let sent = stat.sent();
|
||||
let received = stat.received();
|
||||
let latency = stat.average_latency_nanos();
|
||||
|
||||
client_record.timestamp = now;
|
||||
client_record.received = received;
|
||||
client_record.sent = sent;
|
||||
client_record.latency = latency;
|
||||
if let Some(last) = records.last() {
|
||||
let receive_rate = (received - last.received) as f64 / time_delta_secs;
|
||||
client_record.receive_rate = receive_rate;
|
||||
}
|
||||
records.push(client_record);
|
||||
|
||||
all_sent += sent;
|
||||
all_received += received;
|
||||
all_latencies += latency;
|
||||
}
|
||||
|
||||
let receive_rate = (all_received - self.last_total_received) as f64 / time_delta_secs;
|
||||
let avg_rate = receive_rate.human_throughput("packets");
|
||||
let avg_latency = all_latencies as f64 / self.client_stats.len() as f64;
|
||||
|
||||
self.global_records.push(StatsRecord {
|
||||
timestamp: now,
|
||||
receive_rate,
|
||||
latency: avg_latency as u64,
|
||||
sent: all_sent,
|
||||
received: all_received,
|
||||
});
|
||||
|
||||
self.system.refresh_cpu_usage();
|
||||
let cpu_usage = self.system.global_cpu_usage();
|
||||
let cpu_count = self.system.cpus().len();
|
||||
let usage_per_cpu = cpu_usage / cpu_count as f32;
|
||||
|
||||
let formatted_usage = if usage_per_cpu < 0.3 {
|
||||
format!("{:.2}%", usage_per_cpu * 100.).green().bold()
|
||||
} else if usage_per_cpu < 0.7 {
|
||||
format!("{:.2}%", usage_per_cpu * 100.).yellow().bold()
|
||||
} else {
|
||||
format!("{:.2}%", usage_per_cpu * 100.).red().bold()
|
||||
};
|
||||
|
||||
self.header_span.pb_set_message(&format!(
|
||||
"active_clients: {} | total received: {} total sent {} (avg packet latency: {}, total receive rate: {avg_rate}), avg core load: {formatted_usage}",
|
||||
self.client_stats.len(),
|
||||
all_received.human_count_bare(),
|
||||
all_sent.human_count_bare(),
|
||||
Duration::from_nanos(avg_latency as u64).human_duration()
|
||||
));
|
||||
self.last_total_received = all_received;
|
||||
self.last_update = Instant::now();
|
||||
}
|
||||
|
||||
fn save_results_to_files(&self) -> anyhow::Result<()> {
|
||||
create_dir_all(self.output_directory.as_path())?;
|
||||
|
||||
let global = self.output_directory.join("global_stats.csv");
|
||||
let mut writer = csv::Writer::from_path(&global)?;
|
||||
for record in &self.global_records {
|
||||
writer.serialize(record)?;
|
||||
}
|
||||
|
||||
info!("wrote global stats to {}", global.display());
|
||||
|
||||
for (sender_id, records) in self.records.iter() {
|
||||
let output = self
|
||||
.output_directory
|
||||
.join(format!("sender{}.csv", sender_id));
|
||||
let mut writer = csv::Writer::from_path(&output)?;
|
||||
for record in records {
|
||||
writer.serialize(record)?;
|
||||
}
|
||||
info!("wrote client records to {}", output.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&mut self) {
|
||||
let mut update_interval = interval(Duration::from_millis(500));
|
||||
|
||||
loop {
|
||||
select! {
|
||||
biased;
|
||||
_ = self.shutdown.cancelled() => {
|
||||
if let Err(err) = self.save_results_to_files() {
|
||||
error!("failed to save measurement results to files: {err}")
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ = update_interval.tick() => {
|
||||
self.update_stats_span();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::config::upgrade_helpers::try_load_current_config;
|
||||
use crate::node::NymNode;
|
||||
use crate::throughput_tester::client::ThroughputTestingClient;
|
||||
use crate::throughput_tester::global_stats::GlobalStatsUpdater;
|
||||
use crate::throughput_tester::stats::ClientStats;
|
||||
use futures::future::join_all;
|
||||
use human_repr::HumanDuration;
|
||||
use indicatif::{ProgressState, ProgressStyle};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_task::ShutdownToken;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::runtime;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{info, info_span, instrument};
|
||||
use tracing_indicatif::span_ext::IndicatifSpanExt;
|
||||
|
||||
pub(crate) mod client;
|
||||
pub(crate) mod global_stats;
|
||||
mod stats;
|
||||
|
||||
pub struct ThroughputTest {
|
||||
node_runtime: Runtime,
|
||||
clients_runtime: Runtime,
|
||||
}
|
||||
|
||||
impl ThroughputTest {
|
||||
fn new(senders: usize) -> anyhow::Result<Self> {
|
||||
Ok(ThroughputTest {
|
||||
node_runtime: runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.thread_name("nym-node-pool")
|
||||
.build()?,
|
||||
clients_runtime: runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.worker_threads(senders)
|
||||
.thread_name("testing-clients-pool")
|
||||
.build()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_nymnode(&self, config_path: PathBuf) -> anyhow::Result<NymNode> {
|
||||
self.node_runtime.block_on(async {
|
||||
let mut config = try_load_current_config(config_path).await?;
|
||||
|
||||
// make sure to change bind address to localhost!
|
||||
config
|
||||
.mixnet
|
||||
.bind_address
|
||||
.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||
|
||||
let nym_node = NymNode::new(config).await?;
|
||||
Ok(nym_node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
skip_all,
|
||||
fields(
|
||||
sender_id = %sender_id
|
||||
)
|
||||
|
||||
)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_testing_client(
|
||||
sender_id: usize,
|
||||
node_keys: Arc<x25519::KeyPair>,
|
||||
node_listener: SocketAddr,
|
||||
packet_latency_threshold: Duration,
|
||||
starting_sending_batch_size: usize,
|
||||
starting_sending_delay: Duration,
|
||||
stats: ClientStats,
|
||||
shutdown_token: ShutdownToken,
|
||||
) -> anyhow::Result<()> {
|
||||
let _ = sender_id;
|
||||
let client = ThroughputTestingClient::try_create(
|
||||
starting_sending_delay,
|
||||
starting_sending_batch_size,
|
||||
packet_latency_threshold,
|
||||
&node_keys,
|
||||
node_listener,
|
||||
stats,
|
||||
shutdown_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// wait a random amount of time before actually starting to desync the clients a bit
|
||||
// (so they wouldn't update their rates at the same time)
|
||||
let delay = Duration::from_millis(thread_rng().gen_range(10..200));
|
||||
info!(
|
||||
"waiting for {} before attempting to start the processing loop",
|
||||
delay.human_duration()
|
||||
);
|
||||
sleep(delay).await;
|
||||
|
||||
client.run().await
|
||||
}
|
||||
|
||||
pub(crate) fn test_mixing_throughput(
|
||||
config_path: PathBuf,
|
||||
senders: usize,
|
||||
packet_latency_threshold: Duration,
|
||||
starting_sending_batch_size: usize,
|
||||
starting_sending_delay: Duration,
|
||||
output_directory: PathBuf,
|
||||
) -> anyhow::Result<()> {
|
||||
let tester = ThroughputTest::new(senders)?;
|
||||
|
||||
let nym_node = tester.prepare_nymnode(config_path)?;
|
||||
let listener = nym_node.config().mixnet.bind_address;
|
||||
|
||||
let sphinx_keys = nym_node.x25519_sphinx_keys();
|
||||
|
||||
let mut stats = Vec::with_capacity(senders);
|
||||
for _ in 0..senders {
|
||||
stats.push(ClientStats::default())
|
||||
}
|
||||
|
||||
let header_span = info_span!("header");
|
||||
header_span.pb_set_style(
|
||||
&ProgressStyle::with_template(
|
||||
"testing mixing throughput of this machine... {wide_msg} {elapsed}\n{wide_bar}",
|
||||
)?
|
||||
.with_key(
|
||||
"elapsed",
|
||||
|state: &ProgressState, writer: &mut dyn std::fmt::Write| {
|
||||
let _ = writer.write_str(&format!("{}", state.elapsed().human_duration()));
|
||||
},
|
||||
)
|
||||
.progress_chars("---"),
|
||||
);
|
||||
header_span.pb_start();
|
||||
|
||||
// Bit of a hack to show a full "-----" line underneath the header.
|
||||
header_span.pb_set_length(1);
|
||||
header_span.pb_set_position(1);
|
||||
|
||||
let mut tasks_handles = Vec::new();
|
||||
|
||||
for (sender_id, stats) in stats.iter().enumerate() {
|
||||
let token = nym_node.shutdown_token(format!("dummy-load-client-{sender_id}"));
|
||||
|
||||
let client_future = run_testing_client(
|
||||
sender_id,
|
||||
sphinx_keys.clone(),
|
||||
listener,
|
||||
packet_latency_threshold,
|
||||
starting_sending_batch_size,
|
||||
starting_sending_delay,
|
||||
stats.clone(),
|
||||
token,
|
||||
);
|
||||
let handle = tester.clients_runtime.spawn(client_future);
|
||||
tasks_handles.push(handle);
|
||||
}
|
||||
|
||||
let mut global_stats = GlobalStatsUpdater::new(
|
||||
header_span,
|
||||
stats,
|
||||
output_directory,
|
||||
nym_node.shutdown_token("global-stats"),
|
||||
);
|
||||
|
||||
let stats_handle = tester.clients_runtime.spawn(async move {
|
||||
global_stats.run().await;
|
||||
Ok(())
|
||||
});
|
||||
tasks_handles.push(stats_handle);
|
||||
|
||||
tester
|
||||
.node_runtime
|
||||
.block_on(async move { nym_node.run_minimal_mixnet_processing().await })?;
|
||||
|
||||
tester.clients_runtime.block_on(join_all(tasks_handles));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct ClientStats {
|
||||
inner: Arc<ClientStatsInner>,
|
||||
}
|
||||
|
||||
impl ClientStats {
|
||||
pub(crate) fn new_received(&self, new_measurement: u64) {
|
||||
const ALPHA: f64 = 0.0005;
|
||||
const ONE_SUB_ALPHA: f64 = 1.0 - ALPHA;
|
||||
|
||||
let old_average_latency = self.inner.average_latency_nanos.load(Ordering::SeqCst);
|
||||
|
||||
let new_average = if old_average_latency == 0 {
|
||||
new_measurement
|
||||
} else {
|
||||
((ALPHA * new_measurement as f64) + ONE_SUB_ALPHA * old_average_latency as f64) as u64
|
||||
};
|
||||
|
||||
self.inner.received.fetch_add(1, Ordering::SeqCst);
|
||||
self.inner
|
||||
.average_latency_nanos
|
||||
.store(new_average, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn new_sent_batch(&self, batch_size: usize) {
|
||||
self.inner.sent.fetch_add(batch_size, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn received(&self) -> usize {
|
||||
self.inner.received.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub(crate) fn sent(&self) -> usize {
|
||||
self.inner.sent.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub(crate) fn average_latency_nanos(&self) -> u64 {
|
||||
self.inner.average_latency_nanos.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub(crate) fn average_latency_duration(&self) -> Duration {
|
||||
Duration::from_nanos(self.average_latency_nanos())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientStatsInner {
|
||||
sent: AtomicUsize,
|
||||
received: AtomicUsize,
|
||||
average_latency_nanos: AtomicU64,
|
||||
}
|
||||
Reference in New Issue
Block a user