Skip to content

Commit 14c1cff

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 222321d commit 14c1cff

File tree

4 files changed

+248
-3
lines changed

4 files changed

+248
-3
lines changed

.github/workflows/benchmarks.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: CI Checks - Benchmarks
2+
3+
on: [push, pull_request]
4+
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
benchmark:
11+
runs-on: ubuntu-latest
12+
env:
13+
TOOLCHAIN: stable
14+
steps:
15+
- name: Checkout source code
16+
uses: actions/checkout@v3
17+
- name: Install Rust toolchain
18+
run: |
19+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable
20+
rustup override set stable
21+
- name: Enable caching for bitcoind
22+
id: cache-bitcoind
23+
uses: actions/cache@v4
24+
with:
25+
path: bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
26+
key: bitcoind-${{ runner.os }}-${{ runner.arch }}
27+
- name: Enable caching for electrs
28+
id: cache-electrs
29+
uses: actions/cache@v4
30+
with:
31+
path: bin/electrs-${{ runner.os }}-${{ runner.arch }}
32+
key: electrs-${{ runner.os }}-${{ runner.arch }}
33+
- name: Download bitcoind/electrs
34+
if: "(steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')"
35+
run: |
36+
source ./scripts/download_bitcoind_electrs.sh
37+
mkdir bin
38+
mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
39+
mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }}
40+
- name: Set bitcoind/electrs environment variables
41+
run: |
42+
echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
43+
echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
44+
- name: Run benchmarks
45+
run: |
46+
cargo bench

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ lightning = { version = "0.2.0-rc1", features = ["std", "_test_utils"] }
114114
#lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] }
115115
proptest = "1.0.0"
116116
regex = "1.5.6"
117+
criterion = { version = "0.7.0", features = ["async_tokio"] }
117118

118119
[target.'cfg(not(no_download))'.dev-dependencies]
119120
electrsd = { version = "0.36.1", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] }
@@ -148,3 +149,7 @@ check-cfg = [
148149
"cfg(cln_test)",
149150
"cfg(lnd_test)",
150151
]
152+
153+
[[bench]]
154+
name = "payments"
155+
harness = false

benches/payments.rs

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

tests/common/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,10 @@ use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2929
use electrsd::{corepc_node, ElectrsD};
3030
use electrum_client::ElectrumApi;
3131
use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig};
32-
use ldk_node::io::sqlite_store::{SqliteStore, KV_TABLE_NAME, SQLITE_DB_FILE_NAME};
32+
use ldk_node::io::sqlite_store::SqliteStore;
3333
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
3434
use ldk_node::{
35-
Builder, CustomTlvRecord, DynStore, Event, LightningBalance, Node, NodeError,
36-
PendingSweepBalance,
35+
Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance,
3736
};
3837
use lightning::io;
3938
use lightning::ln::msgs::SocketAddress;

0 commit comments

Comments
 (0)