Skip to content

Commit 7a808a5

Browse files
committed
Add async payment throughput benchmark
Introduces a criterion-based benchmark that sends 1000 concurrent payments between two LDK nodes to measure total duration. Also adds a CI job to automatically run the benchmark.
1 parent 07d359b commit 7a808a5

File tree

3 files changed

+238
-0
lines changed

3 files changed

+238
-0
lines changed

.github/workflows/rust.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,41 @@ jobs:
9090
if: "matrix.platform != 'windows-latest' && matrix.build-uniffi"
9191
run: |
9292
RUSTFLAGS="--cfg no_download" cargo test --features uniffi
93+
94+
benchmark:
95+
runs-on: ubuntu-latest
96+
env:
97+
TOOLCHAIN: stable
98+
steps:
99+
- name: Checkout source code
100+
uses: actions/checkout@v3
101+
- name: Install Rust toolchain
102+
run: |
103+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable
104+
rustup override set stable
105+
- name: Enable caching for bitcoind
106+
id: cache-bitcoind
107+
uses: actions/cache@v4
108+
with:
109+
path: bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
110+
key: bitcoind-${{ runner.os }}-${{ runner.arch }}
111+
- name: Enable caching for electrs
112+
id: cache-electrs
113+
uses: actions/cache@v4
114+
with:
115+
path: bin/electrs-${{ runner.os }}-${{ runner.arch }}
116+
key: electrs-${{ runner.os }}-${{ runner.arch }}
117+
- name: Download bitcoind/electrs
118+
if: "(steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')"
119+
run: |
120+
source ./scripts/download_bitcoind_electrs.sh
121+
mkdir bin
122+
mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
123+
mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }}
124+
- name: Set bitcoind/electrs environment variables
125+
run: |
126+
echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
127+
echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
128+
- name: Run benchmarks
129+
run: |
130+
cargo bench

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ log = { version = "0.4.22", default-features = false, features = ["std"]}
103103

104104
vss-client = "0.3"
105105
prost = { version = "0.11.6", default-features = false}
106+
criterion = { version = "0.7.0", features = ["async_tokio"] }
106107

107108
[target.'cfg(windows)'.dependencies]
108109
winapi = { version = "0.3", features = ["winbase"] }
@@ -148,3 +149,8 @@ check-cfg = [
148149
"cfg(cln_test)",
149150
"cfg(lnd_test)",
150151
]
152+
153+
[[bench]]
154+
name = "payments"
155+
path = "tests/benchmarks.rs"
156+
harness = false

tests/benchmarks.rs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
mod common;
2+
3+
use std::time::Instant;
4+
use std::{sync::Arc, time::Duration};
5+
6+
use bitcoin::hex::DisplayHex;
7+
use bitcoin::Amount;
8+
use common::{
9+
expect_channel_ready_event, generate_blocks_and_wait, premine_and_distribute_funds,
10+
setup_bitcoind_and_electrsd, setup_two_nodes_with_store, TestChainSource,
11+
};
12+
use criterion::{criterion_group, criterion_main, Criterion};
13+
use ldk_node::{Event, Node};
14+
use lightning_types::payment::{PaymentHash, PaymentPreimage};
15+
use rand::RngCore;
16+
use tokio::task::{self};
17+
18+
use crate::common::open_channel_push_amt;
19+
20+
fn spawn_payment(node_a: Arc<Node>, node_b: Arc<Node>, amount_msat: u64) {
21+
let mut preimage_bytes = [0u8; 32];
22+
rand::thread_rng().fill_bytes(&mut preimage_bytes);
23+
let preimage = PaymentPreimage(preimage_bytes);
24+
let payment_hash: PaymentHash = preimage.into();
25+
26+
// Spawn each payment as a separate async task
27+
task::spawn(async move {
28+
println!("{}: Starting payment", payment_hash.0.as_hex());
29+
30+
loop {
31+
// Pre-check the HTLC slots to try to avoid the performance impact of a failed payment.
32+
while node_a.list_channels()[0].next_outbound_htlc_limit_msat == 0 {
33+
println!("{}: Waiting for HTLC slots to free up", payment_hash.0.as_hex());
34+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
35+
}
36+
37+
let payment_id = node_a.spontaneous_payment().send_with_preimage(
38+
amount_msat,
39+
node_b.node_id(),
40+
preimage,
41+
None,
42+
);
43+
44+
match payment_id {
45+
Ok(payment_id) => {
46+
println!(
47+
"{}: Awaiting payment with id {}",
48+
payment_hash.0.as_hex(),
49+
payment_id
50+
);
51+
break;
52+
},
53+
Err(e) => {
54+
println!("{}: Payment attempt failed: {:?}", payment_hash.0.as_hex(), e);
55+
56+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
57+
},
58+
}
59+
}
60+
});
61+
}
62+
63+
async fn send_payments(node_a: Arc<Node>, node_b: Arc<Node>) -> std::time::Duration {
64+
let start = Instant::now();
65+
66+
let total_payments = 1000;
67+
let amount_msat = 10_000_000;
68+
69+
let mut success_count = 0;
70+
for _ in 0..total_payments {
71+
spawn_payment(node_a.clone(), node_b.clone(), amount_msat);
72+
}
73+
74+
while success_count < total_payments {
75+
match node_a.next_event_async().await {
76+
Event::PaymentSuccessful { payment_id, payment_hash, .. } => {
77+
if let Some(id) = payment_id {
78+
success_count += 1;
79+
println!("{}: Payment with id {:?} completed", payment_hash.0.as_hex(), id);
80+
} else {
81+
println!("Payment completed (no payment_id)");
82+
}
83+
},
84+
Event::PaymentFailed { payment_id, payment_hash, .. } => {
85+
println!("{}: Payment {:?} failed", payment_hash.unwrap().0.as_hex(), payment_id);
86+
87+
// The payment failed, so we need to respawn it.
88+
spawn_payment(node_a.clone(), node_b.clone(), amount_msat);
89+
},
90+
ref e => {
91+
println!("Received non-payment event: {:?}", e);
92+
},
93+
}
94+
95+
node_a.event_handled().unwrap();
96+
}
97+
98+
let duration = start.elapsed();
99+
println!("Time elapsed: {:?}", duration);
100+
101+
// Send back the money for the next iteration.
102+
let mut preimage_bytes = [0u8; 32];
103+
rand::thread_rng().fill_bytes(&mut preimage_bytes);
104+
node_b
105+
.spontaneous_payment()
106+
.send_with_preimage(
107+
amount_msat * total_payments,
108+
node_a.node_id(),
109+
PaymentPreimage(preimage_bytes),
110+
None,
111+
)
112+
.ok()
113+
.unwrap();
114+
115+
duration
116+
}
117+
118+
fn payment_benchmark(c: &mut Criterion) {
119+
// Set up two nodes. Because this is slow, we reuse the same nodes for each sample.
120+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
121+
let chain_source = TestChainSource::Esplora(&electrsd);
122+
123+
let (node_a, node_b) = setup_two_nodes_with_store(
124+
&chain_source,
125+
false,
126+
true,
127+
false,
128+
common::TestStoreType::Sqlite,
129+
);
130+
131+
let runtime =
132+
tokio::runtime::Builder::new_multi_thread().worker_threads(4).enable_all().build().unwrap();
133+
134+
let node_a = Arc::new(node_a);
135+
let node_b = Arc::new(node_b);
136+
137+
// Fund the nodes and setup a channel between them. The criterion function cannot be async, so we need to execute
138+
// the setup using a runtime.
139+
let node_a_cloned = Arc::clone(&node_a);
140+
let node_b_cloned = Arc::clone(&node_b);
141+
runtime.block_on(async move {
142+
let address_a = node_a_cloned.onchain_payment().new_address().unwrap();
143+
let premine_sat = 25_000_000;
144+
premine_and_distribute_funds(
145+
&bitcoind.client,
146+
&electrsd.client,
147+
vec![address_a],
148+
Amount::from_sat(premine_sat),
149+
)
150+
.await;
151+
node_a_cloned.sync_wallets().unwrap();
152+
node_b_cloned.sync_wallets().unwrap();
153+
open_channel_push_amt(
154+
&node_a_cloned,
155+
&node_b_cloned,
156+
16_000_000,
157+
Some(1_000_000_000),
158+
false,
159+
&electrsd,
160+
)
161+
.await;
162+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
163+
node_a_cloned.sync_wallets().unwrap();
164+
node_b_cloned.sync_wallets().unwrap();
165+
expect_channel_ready_event!(node_a_cloned, node_b_cloned.node_id());
166+
expect_channel_ready_event!(node_b_cloned, node_a_cloned.node_id());
167+
});
168+
169+
let mut group = c.benchmark_group("payments");
170+
group.sample_size(10);
171+
172+
group.bench_function("payments", |b| {
173+
// Use custom timing so that sending back the money at the end of each iteration isn't included in the
174+
// measurement.
175+
b.to_async(&runtime).iter_custom(|iter| {
176+
let node_a = Arc::clone(&node_a);
177+
let node_b = Arc::clone(&node_b);
178+
179+
async move {
180+
let mut total = Duration::ZERO;
181+
for _i in 0..iter {
182+
let node_a = Arc::clone(&node_a);
183+
let node_b = Arc::clone(&node_b);
184+
185+
total += send_payments(node_a, node_b).await;
186+
}
187+
total
188+
}
189+
});
190+
});
191+
}
192+
193+
criterion_group!(benches, payment_benchmark);
194+
criterion_main!(benches);

0 commit comments

Comments
 (0)