Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/devkit/benches/congestion_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ use stellar_devkit::simulation::congestion_predictor::{
fn bench_predict(c: &mut Criterion) {
let mut group = c.benchmark_group("congestion_predictor");

group.bench_function("predict_1000_calls", |b| {
group.bench_function("predict_1M", |b| {
b.iter(|| {
for i in 0..1_000u64 {
let _ = CongestionPredictor::predict(i % 1000, 100 + i * 10);
for i in 0..1_000_000u64 {
let _ = CongestionPredictor::predict(i % 1000, 100 + (i * 10) % 10_000);
}
})
});

group.bench_function("congestion_score_1000_calls", |b| {
group.bench_function("congestion_score_1M", |b| {
b.iter(|| {
for i in 0..1_000u64 {
for i in 0..1_000_000u64 {
let input = CongestionInput {
recent_avg_fee: 100.0 + i as f64,
capacity_usage: (i % 100) as f64 / 100.0,
Expand Down
29 changes: 29 additions & 0 deletions packages/devkit/src/simulation/fee_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,35 @@ pub fn apply_timestamps(points: &mut [FeePoint], epoch_ms: u64, interval_ms: u64
}
}

/// Serialise a slice of `FeePoint`s to a CSV string.
///
/// The output contains a header row followed by one row per point:
/// `timestamp,fee_amount,ledger_sequence,is_spike`
///
/// # Examples
///
/// ```
/// use stellar_devkit::simulation::fee_model::{FeePoint, export_csv};
///
/// let points = vec![
/// FeePoint { timestamp: 0, fee: 100, ledger: 1, is_spike: false },
/// FeePoint { timestamp: 5, fee: 500, ledger: 2, is_spike: true },
/// ];
/// let csv = export_csv(&points);
/// assert!(csv.starts_with("timestamp,fee_amount,ledger_sequence,is_spike"));
/// assert!(csv.contains("0,100,1,false"));
/// assert!(csv.contains("5,500,2,true"));
/// ```
pub fn export_csv(points: &[FeePoint]) -> String {
let mut buf = String::with_capacity(points.len() * 48);
buf.push_str("timestamp,fee_amount,ledger_sequence,is_spike\n");
for p in points {
use std::fmt::Write;
let _ = writeln!(buf, "{},{},{},{}", p.timestamp, p.fee, p.ledger, p.is_spike);
}
buf
}

/// A fee curve snapshot matching the Horizon `fee_stats` shape.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FeeCurve {
Expand Down
142 changes: 142 additions & 0 deletions packages/devkit/tests/network_load.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use stellar_devkit::simulation::fee_model::FeeCurve;
use stellar_devkit::simulation::network_load::{NetworkLoad, NetworkLoadConfig};

/// Assert that the fee curve's 50th percentile increases monotonically as
/// capacity_usage approaches 1.0.
#[test]
fn fee_percentiles_increase_with_pressure() {
let base_fee = 100;
let max_fee = 10_000;
let pressures = [0.0, 0.25, 0.5, 0.75, 1.0];
let mut prev_p50 = 0u64;
for &pressure in &pressures {
let curve = FeeCurve::from_pressure(base_fee, max_fee, pressure, 1);
let p50: u64 = curve.fee_charged.p50.parse().unwrap();
assert!(
p50 >= prev_p50,
"p50 decreased from {prev_p50} to {p50} at pressure {pressure}"
);
prev_p50 = p50;
}
}

/// Assert p99 also increases monotonically with pressure.
#[test]
fn fee_p99_increases_with_pressure() {
let base_fee = 100;
let max_fee = 10_000;
let pressures = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0];
let mut prev_p99 = 0u64;
for &pressure in &pressures {
let curve = FeeCurve::from_pressure(base_fee, max_fee, pressure, 1);
let p99: u64 = curve.fee_charged.p99.parse().unwrap();
assert!(
p99 >= prev_p99,
"p99 decreased from {prev_p99} to {p99} at pressure {pressure}"
);
prev_p99 = p99;
}
}

/// Assert that every percentile in the fee curve is monotonic w.r.t. pressure.
#[test]
fn all_percentiles_monotonic_with_pressure() {
let base_fee = 100;
let max_fee = 10_000;
let pressures = [0.0, 0.3, 0.6, 1.0];

let mut prev: Vec<u64> = Vec::new();
for &pressure in &pressures {
let curve = FeeCurve::from_pressure(base_fee, max_fee, pressure, 1);
let pct_values: Vec<u64> = [
&curve.fee_charged.min,
&curve.fee_charged.p10,
&curve.fee_charged.p20,
&curve.fee_charged.p30,
&curve.fee_charged.p40,
&curve.fee_charged.p50,
&curve.fee_charged.p60,
&curve.fee_charged.p70,
&curve.fee_charged.p80,
&curve.fee_charged.p90,
&curve.fee_charged.p95,
&curve.fee_charged.p99,
&curve.fee_charged.max,
]
.iter()
.map(|s| s.parse::<u64>().unwrap())
.collect();

if !prev.is_empty() {
for (i, (&cur, &prev_val)) in pct_values.iter().zip(prev.iter()).enumerate() {
assert!(
cur >= prev_val,
"percentile at index {i} decreased from {prev_val} to {cur} when pressure moved to {pressure}"
);
}
}
prev = pct_values;
}
}

/// Assert that avg_pressure increases when max_tx is increased.
#[test]
fn avg_pressure_increases_with_max_tx() {
let low = NetworkLoadConfig {
min_tx: 10,
max_tx: 100,
..Default::default()
};
let high = NetworkLoadConfig {
min_tx: 10,
max_tx: 900,
..Default::default()
};
assert!(
high.avg_pressure() > low.avg_pressure(),
"avg_pressure should increase with max_tx"
);
}

/// Assert that simulated ledger pressure is monotonic in tx_count.
#[test]
fn simulated_ledger_pressure_increases_with_tx_count() {
let config = NetworkLoadConfig {
min_tx: 1,
max_tx: 1000,
ledger_capacity: 1000,
seed: Some(42),
..Default::default()
};
let mut sim = NetworkLoad::new(config);
let ledgers = sim.simulate(100);

for window in ledgers.windows(2) {
// pressure must be monotonic in tx_count (same ledger capacity)
if window[1].tx_count > window[0].tx_count {
assert!(
window[1].pressure >= window[0].pressure,
"pressure decreased when tx_count increased"
);
}
}
}

/// Assert every ledger pressure is within [0.0, 1.0].
#[test]
fn simulated_pressure_in_bounds() {
let config = NetworkLoadConfig {
seed: Some(7),
..Default::default()
};
let mut sim = NetworkLoad::new(config);
let ledgers = sim.simulate(200);
for l in &ledgers {
assert!(
(0.0..=1.0).contains(&l.pressure),
"pressure {} out of [0,1] at ledger {}",
l.pressure,
l.ledger_seq
);
}
}
Loading