diff --git a/Cargo.lock b/Cargo.lock index 40eb240..f9eb1bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.21" @@ -278,6 +284,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.54" @@ -308,6 +320,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.5.54" @@ -409,6 +448,61 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -424,6 +518,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -804,6 +904,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1202,12 +1313,32 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -1452,6 +1583,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl" version = "0.10.75" @@ -1585,6 +1722,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1782,6 +1947,26 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1941,6 +2126,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -2379,6 +2573,7 @@ name = "stellar-devkit" version = "0.1.0" dependencies = [ "axum", + "criterion", "serde_json", "thiserror", "tokio", @@ -2553,6 +2748,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -2868,6 +3073,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2983,6 +3198,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-core" version = "0.62.2" diff --git a/packages/devkit/Cargo.toml b/packages/devkit/Cargo.toml index 15e8fc1..1326ef9 100644 --- a/packages/devkit/Cargo.toml +++ b/packages/devkit/Cargo.toml @@ -12,3 +12,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } [dev-dependencies] serde_json = "1" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "fee_model_bench" +harness = false diff --git a/packages/devkit/benches/fee_model_bench.rs b/packages/devkit/benches/fee_model_bench.rs new file mode 100644 index 0000000..7d0f787 --- /dev/null +++ b/packages/devkit/benches/fee_model_bench.rs @@ -0,0 +1,15 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use stellar_devkit::simulation::fee_model::{FeeModel, FeeModelConfig}; + +fn bench_generate_100k(c: &mut Criterion) { + let config = FeeModelConfig { + ledger_count: 100_000, + ..Default::default() + }; + c.bench_function("fee_model_generate_100k", |b| { + b.iter(|| FeeModel::generate(&config)) + }); +} + +criterion_group!(benches, bench_generate_100k); +criterion_main!(benches); diff --git a/packages/devkit/src/cli/export.rs b/packages/devkit/src/cli/export.rs index 16350ae..4454a41 100644 --- a/packages/devkit/src/cli/export.rs +++ b/packages/devkit/src/cli/export.rs @@ -1,2 +1,21 @@ +use crate::simulation::fee_model::FeePoint; +use std::fmt::Write as FmtWrite; + /// Exports devkit results to external formats. pub struct Export; + +impl Export { + /// Serialize fee points to CSV string with columns: timestamp,fee,ledger,is_spike. + pub fn to_csv(points: &[FeePoint]) -> String { + let mut out = String::from("timestamp,fee,ledger,is_spike\n"); + for p in points { + writeln!(out, "{},{},{},{}", p.timestamp, p.fee, p.ledger, p.is_spike).unwrap(); + } + out + } + + /// Write fee points to a CSV file. + pub fn write_csv(points: &[FeePoint], path: &std::path::Path) -> std::io::Result<()> { + std::fs::write(path, Self::to_csv(points)) + } +} diff --git a/packages/devkit/src/harness/scenarios/mod.rs b/packages/devkit/src/harness/scenarios/mod.rs index 3e0f82a..ce3551c 100644 --- a/packages/devkit/src/harness/scenarios/mod.rs +++ b/packages/devkit/src/harness/scenarios/mod.rs @@ -27,6 +27,8 @@ impl ScenarioRotator { } } +use std::path::Path; + /// Loads a scenario JSON file from the given path and returns its contents. pub fn load_from_file(path: &Path) -> std::io::Result { std::fs::read_to_string(path) diff --git a/packages/devkit/src/simulation/fee_model.rs b/packages/devkit/src/simulation/fee_model.rs index ba26726..59277a1 100644 --- a/packages/devkit/src/simulation/fee_model.rs +++ b/packages/devkit/src/simulation/fee_model.rs @@ -1,7 +1,71 @@ +/// A single simulated fee data point. +#[derive(Debug, Clone)] +pub struct FeePoint { + pub timestamp: u64, + pub fee: u64, + pub ledger: u64, + pub is_spike: bool, +} + +/// Configuration for a single simulation scenario. +#[derive(Debug, Clone)] +pub struct FeeModelConfig { + pub base_fee: u64, + pub ledger_count: u64, + pub spike_probability: f64, + pub spike_multiplier: u64, +} + +impl Default for FeeModelConfig { + fn default() -> Self { + Self { + base_fee: 100, + ledger_count: 100, + spike_probability: 0.05, + spike_multiplier: 10, + } + } +} + /// Models for simulating Stellar transaction fee behaviour. pub struct FeeModel; impl FeeModel { + /// Generate fee points for a single config. + pub fn generate(config: &FeeModelConfig) -> Vec { + let mut points = Vec::with_capacity(config.ledger_count as usize); + let mut pseudo = 6364136223846793005u64; + for i in 0..config.ledger_count { + pseudo = pseudo.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + let rand_f = (pseudo >> 33) as f64 / u32::MAX as f64; + let is_spike = rand_f < config.spike_probability; + let fee = if is_spike { + config.base_fee * config.spike_multiplier + } else { + config.base_fee + }; + points.push(FeePoint { timestamp: i * 5, fee, ledger: i + 1, is_spike }); + } + points + } + + /// Run multiple scenarios sequentially and return combined output. + pub fn run_scenarios(configs: &[FeeModelConfig]) -> Vec { + let mut all = Vec::new(); + let mut ledger_offset = 0u64; + let mut time_offset = 0u64; + for config in configs { + let mut points = Self::generate(config); + for p in &mut points { + p.ledger += ledger_offset; + p.timestamp += time_offset; + } + let last = points.last().map(|p| (p.ledger, p.timestamp)).unwrap_or((0, 0)); + ledger_offset = last.0; + time_offset = last.1 + 5; + all.extend(points); + } + all /// Generates `count` baseline fee values (in stroops) at the Stellar minimum (100). pub fn baseline(count: usize) -> Vec { vec![100.0; count] diff --git a/packages/devkit/src/simulation/network_load.rs b/packages/devkit/src/simulation/network_load.rs index 13c02d6..9ca24ef 100644 --- a/packages/devkit/src/simulation/network_load.rs +++ b/packages/devkit/src/simulation/network_load.rs @@ -1,2 +1,17 @@ /// Generates synthetic network load profiles for simulation. pub struct NetworkLoad; + +impl NetworkLoad { + /// Returns a fee multiplier (1.0–3.0) based on hour of day (0–23). + /// Peak hours (8–20) have higher fees simulating daytime congestion. + pub fn diurnal_multiplier(hour: u8) -> f64 { + // Simple sinusoidal: peak at hour 14 (2pm UTC), trough at hour 2 (2am UTC) + let angle = std::f64::consts::PI * (hour as f64 - 2.0) / 12.0; + 1.0 + angle.sin().max(0.0) * 2.0 + } + + /// Apply diurnal multiplier to a base fee given the hour of day. + pub fn diurnal_fee(base_fee: u64, hour: u8) -> u64 { + (base_fee as f64 * Self::diurnal_multiplier(hour)).round() as u64 + } +}