@@ -15,20 +15,69 @@ use bitcoin::Amount;
1515use common:: {
1616 expect_channel_pending_event, expect_channel_ready_event, expect_event,
1717 generate_blocks_and_wait, open_channel_no_wait, premine_and_distribute_funds, random_config,
18- setup_bitcoind_and_electrsd, setup_node, setup_two_nodes_with_store, store_bench_configs ,
19- wait_for_payment_success,
18+ random_storage_path , setup_bitcoind_and_electrsd, setup_node, setup_two_nodes_with_store,
19+ store_bench_configs , wait_for_channel_ready_events , wait_for_payment_success,
2020} ;
2121use criterion:: { criterion_group, criterion_main, Criterion } ;
2222use electrsd:: corepc_node:: { Client as BitcoindClient , Node as BitcoinD } ;
23+ use ldk_node:: io:: sqlite_store:: { SqliteStore , KV_TABLE_NAME , SQLITE_DB_FILE_NAME } ;
2324use ldk_node:: { Event , Node } ;
2425use lightning:: ln:: channelmanager:: PaymentId ;
26+ use lightning:: util:: persist:: migrate_kv_store_data_async;
2527use lightning_invoice:: { Bolt11InvoiceDescription , Description } ;
28+ use lightning_persister:: fs_store:: v2:: FilesystemStoreV2 ;
2629
27- use crate :: common:: { open_channel_push_amt, TestChainSource , TestStoreType } ;
30+ use crate :: common:: {
31+ open_channel_push_amt, StoreBenchConfig , TestChainSource , TestConfig , TestStoreType ,
32+ } ;
33+
34+ #[ cfg( feature = "postgres" ) ]
35+ use ldk_node:: io:: postgres_store:: { PostgresStore , POSTGRES_TEST_URL_ENV_VAR } ;
36+
37+ const STARTUP_SEED_SCENARIOS : [ StartupSeedScenario ; 6 ] = [
38+ StartupSeedScenario { channel_count : 1 , payment_count : 2 } ,
39+ StartupSeedScenario { channel_count : 1 , payment_count : 100 } ,
40+ StartupSeedScenario { channel_count : 1 , payment_count : 1_000 } ,
41+ StartupSeedScenario { channel_count : 10 , payment_count : 2 } ,
42+ StartupSeedScenario { channel_count : 100 , payment_count : 2 } ,
43+ StartupSeedScenario { channel_count : 100 , payment_count : 1_000 } ,
44+ ] ;
45+ const STARTUP_SEED_PAYMENT_AMOUNT_MSAT : u64 = 1_000_000 ;
46+ const STARTUP_SEED_MIN_CHANNEL_FUNDING_SAT : u64 = 100_000 ;
47+ const STARTUP_SEED_CHANNEL_BUFFER_SAT : u64 = 1_000_000 ;
48+ const STARTUP_SEED_CHANNEL_BATCH_SIZE : u64 = 2 ;
49+
50+ #[ derive( Clone , Copy ) ]
51+ struct StartupSeedScenario {
52+ channel_count : u64 ,
53+ payment_count : u64 ,
54+ }
55+
56+ impl StartupSeedScenario {
57+ fn bench_name ( self , store_name : & str ) -> String {
58+ format ! ( "{}/channels_{}_payments_{}" , store_name, self . channel_count, self . payment_count)
59+ }
60+
61+ fn runs_in_ci ( self ) -> bool {
62+ self . channel_count == 1 && self . payment_count == 2
63+ }
64+
65+ fn channel_funding_sat ( self ) -> u64 {
66+ let payment_amount_sat = STARTUP_SEED_PAYMENT_AMOUNT_MSAT / 1_000 ;
67+ let payment_funding_sat =
68+ self . payment_count * payment_amount_sat + STARTUP_SEED_CHANNEL_BUFFER_SAT ;
69+ payment_funding_sat. max ( STARTUP_SEED_MIN_CHANNEL_FUNDING_SAT )
70+ }
71+
72+ fn premine_amount_sat ( self ) -> u64 {
73+ self . channel_count * self . channel_funding_sat ( ) + STARTUP_SEED_CHANNEL_BUFFER_SAT
74+ }
75+ }
2876
2977fn operations_benchmark ( c : & mut Criterion ) {
3078 forwarding_benchmark ( c) ;
3179 channel_open_benchmark ( c) ;
80+ startup_benchmark ( c) ;
3281}
3382
3483fn forwarding_benchmark ( c : & mut Criterion ) {
@@ -89,7 +138,7 @@ fn channel_open_benchmark(c: &mut Criterion) {
89138 continue ;
90139 }
91140 let ( node_a, node_b) =
92- setup_two_nodes_with_store ( & chain_source, false , true , false , store_config. store_type ) ;
141+ setup_two_nodes_with_store ( & chain_source, false , false , store_config. store_type ) ;
93142 let node_a = Arc :: new ( node_a) ;
94143 let node_b = Arc :: new ( node_b) ;
95144
@@ -142,6 +191,230 @@ fn channel_open_benchmark(c: &mut Criterion) {
142191 }
143192}
144193
194+ fn startup_benchmark ( c : & mut Criterion ) {
195+ let ( bitcoind, electrsd) = setup_bitcoind_and_electrsd ( ) ;
196+ let chain_source = TestChainSource :: BitcoindRpcSync ( & bitcoind) ;
197+ let runtime = benchmark_runtime ( ) ;
198+
199+ let mut group = c. benchmark_group ( "startup" ) ;
200+ group. sample_size ( 10 ) ;
201+
202+ for startup_seed_scenario in STARTUP_SEED_SCENARIOS {
203+ // Larger seeded startup scenarios are useful locally, but take too long to run in CI.
204+ if is_ci ( ) && !startup_seed_scenario. runs_in_ci ( ) {
205+ continue ;
206+ }
207+
208+ let matching_store_configs: Vec < _ > = store_bench_configs ( )
209+ . into_iter ( )
210+ . filter ( |store_config| {
211+ let bench_name = startup_seed_scenario. bench_name ( store_config. name ) ;
212+ should_register_bench ( "startup" , & bench_name)
213+ } )
214+ . collect ( ) ;
215+ if matching_store_configs. is_empty ( ) {
216+ continue ;
217+ }
218+
219+ // Seed a canonical sqlite node once, then copy its store into each backend under test. This
220+ // keeps the channel/payment history identical across stores while avoiding repeated expensive
221+ // channel and payment setup for every store backend.
222+ let seeded_config = setup_startup_seed_node (
223+ & chain_source,
224+ & bitcoind,
225+ & electrsd,
226+ startup_seed_scenario,
227+ & runtime,
228+ ) ;
229+ let startup_configs = migrate_startup_seed_configs (
230+ & seeded_config,
231+ startup_seed_scenario,
232+ matching_store_configs,
233+ & runtime,
234+ ) ;
235+
236+ for ( bench_name, config) in startup_configs {
237+ group. bench_function ( bench_name, |b| {
238+ b. iter_custom ( |iter| {
239+ let mut total = Duration :: ZERO ;
240+ for _ in 0 ..iter {
241+ let start = Instant :: now ( ) ;
242+ let node = setup_node ( & chain_source, config. clone ( ) ) ;
243+ total += start. elapsed ( ) ;
244+ node. stop ( ) . unwrap ( ) ;
245+ }
246+ total
247+ } ) ;
248+ } ) ;
249+ }
250+ }
251+ }
252+
253+ /// Builds a canonical sqlite node store with the requested channel and payment history.
254+ ///
255+ /// Startup benchmarks use this store as the source fixture for every backend so differences in
256+ /// measured startup time come from loading equivalent persisted state, not from different setup
257+ /// runs.
258+ fn setup_startup_seed_node (
259+ chain_source : & TestChainSource , bitcoind : & BitcoinD , electrsd : & electrsd:: ElectrsD ,
260+ seed_scenario : StartupSeedScenario , runtime : & tokio:: runtime:: Runtime ,
261+ ) -> TestConfig {
262+ let mut config_a = random_config ( ) ;
263+ config_a. store_type = TestStoreType :: Sqlite ;
264+ let node_a = Arc :: new ( setup_node ( chain_source, config_a. clone ( ) ) ) ;
265+
266+ let mut config_b = random_config ( ) ;
267+ config_b. store_type = TestStoreType :: Sqlite ;
268+ let node_b = Arc :: new ( setup_node ( chain_source, config_b) ) ;
269+
270+ runtime. block_on ( async {
271+ let address_a = node_a. onchain_payment ( ) . new_address ( ) . unwrap ( ) ;
272+ premine_and_distribute_funds (
273+ & bitcoind. client ,
274+ & electrsd. client ,
275+ vec ! [ address_a] ,
276+ Amount :: from_sat ( seed_scenario. premine_amount_sat ( ) ) ,
277+ )
278+ . await ;
279+ node_a. sync_wallets ( ) . unwrap ( ) ;
280+ node_b. sync_wallets ( ) . unwrap ( ) ;
281+
282+ let funding_amount_sat = seed_scenario. channel_funding_sat ( ) ;
283+ let mut remaining_channel_count = seed_scenario. channel_count ;
284+ while remaining_channel_count > 0 {
285+ let channel_batch_size = remaining_channel_count. min ( STARTUP_SEED_CHANNEL_BATCH_SIZE ) ;
286+ for _ in 0 ..channel_batch_size {
287+ node_a
288+ . open_channel (
289+ node_b. node_id ( ) ,
290+ node_b. listening_addresses ( ) . unwrap ( ) . first ( ) . unwrap ( ) . clone ( ) ,
291+ funding_amount_sat,
292+ None ,
293+ None ,
294+ )
295+ . unwrap ( ) ;
296+ assert ! ( node_a. list_peers( ) . iter( ) . any( |peer| peer. node_id == node_b. node_id( ) ) ) ;
297+
298+ let funding_txo_a = expect_channel_pending_event ! ( node_a, node_b. node_id( ) ) ;
299+ let funding_txo_b = expect_channel_pending_event ! ( node_b, node_a. node_id( ) ) ;
300+ assert_eq ! ( funding_txo_a, funding_txo_b) ;
301+ node_a. sync_wallets ( ) . unwrap ( ) ;
302+ }
303+ generate_blocks_and_wait ( & bitcoind. client , & electrsd. client , 6 ) . await ;
304+
305+ for _ in 0 ..channel_batch_size {
306+ node_a. sync_wallets ( ) . unwrap ( ) ;
307+ node_b. sync_wallets ( ) . unwrap ( ) ;
308+ wait_for_channel_ready_events ( & node_a, node_b. node_id ( ) , 1 ) . await ;
309+ wait_for_channel_ready_events ( & node_b, node_a. node_id ( ) , 1 ) . await ;
310+ }
311+ remaining_channel_count -= channel_batch_size;
312+ }
313+
314+ for idx in 0 ..seed_scenario. payment_count {
315+ let invoice_description = Bolt11InvoiceDescription :: Direct (
316+ Description :: new ( format ! ( "startup seed {}" , idx + 1 ) ) . unwrap ( ) ,
317+ ) ;
318+ let invoice = node_b
319+ . bolt11_payment ( )
320+ . receive ( STARTUP_SEED_PAYMENT_AMOUNT_MSAT , & invoice_description. into ( ) , 9217 )
321+ . unwrap ( ) ;
322+ let payment_id = node_a. bolt11_payment ( ) . send ( & invoice, None ) . unwrap ( ) ;
323+ wait_for_payment_success ( & node_a, payment_id) . await ;
324+ }
325+
326+ drain_events ( & node_a) ;
327+ drain_events ( & node_b) ;
328+ } ) ;
329+
330+ node_a. stop ( ) . unwrap ( ) ;
331+ node_b. stop ( ) . unwrap ( ) ;
332+
333+ config_a
334+ }
335+
336+ /// Produces benchmark configs backed by copies of the canonical seeded store.
337+ ///
338+ /// Sqlite can reuse the source store directly. Other store backends get a fresh storage path and a
339+ /// migrated copy of the same key-value data.
340+ fn migrate_startup_seed_configs (
341+ source_config : & TestConfig , seed_scenario : StartupSeedScenario ,
342+ store_configs : Vec < StoreBenchConfig > , runtime : & tokio:: runtime:: Runtime ,
343+ ) -> Vec < ( String , TestConfig ) > {
344+ // Open the seeded source store with the same db file and table the node itself uses, otherwise
345+ // we'd read from an empty default-named store and migrate nothing into the other backends.
346+ let source_store = SqliteStore :: new (
347+ source_config. node_config . storage_dir_path . clone ( ) . into ( ) ,
348+ Some ( SQLITE_DB_FILE_NAME . to_string ( ) ) ,
349+ Some ( KV_TABLE_NAME . to_string ( ) ) ,
350+ )
351+ . unwrap ( ) ;
352+
353+ store_configs
354+ . into_iter ( )
355+ . map ( |store_config| {
356+ let mut config = source_config. clone ( ) ;
357+ config. store_type = store_config. store_type ;
358+ if !matches ! ( store_config. store_type, TestStoreType :: Sqlite ) {
359+ config. node_config . storage_dir_path =
360+ random_storage_path ( ) . to_str ( ) . unwrap ( ) . to_owned ( ) ;
361+ migrate_startup_seed_store ( & source_store, & config, runtime) ;
362+ }
363+
364+ ( seed_scenario. bench_name ( store_config. name ) , config)
365+ } )
366+ . collect ( )
367+ }
368+
369+ fn migrate_startup_seed_store (
370+ source_store : & SqliteStore , destination_config : & TestConfig , runtime : & tokio:: runtime:: Runtime ,
371+ ) {
372+ runtime. block_on ( async {
373+ match destination_config. store_type {
374+ TestStoreType :: Sqlite => { } ,
375+ TestStoreType :: FilesystemStore => {
376+ let destination_store = FilesystemStoreV2 :: new (
377+ destination_config. node_config . storage_dir_path . clone ( ) . into ( ) ,
378+ )
379+ . unwrap ( ) ;
380+ migrate_kv_store_data_async ( source_store, & destination_store) . await . unwrap ( ) ;
381+ } ,
382+ #[ cfg( feature = "postgres" ) ]
383+ TestStoreType :: Postgres => {
384+ let connection_string = postgres_connection_string ( ) ;
385+ let table_name = postgres_table_name ( destination_config) ;
386+ let destination_store =
387+ PostgresStore :: new ( connection_string, None , Some ( table_name) , None )
388+ . await
389+ . unwrap ( ) ;
390+ migrate_kv_store_data_async ( source_store, & destination_store) . await . unwrap ( ) ;
391+ } ,
392+ TestStoreType :: TestSyncStore => {
393+ unreachable ! ( "startup benches do not use TestSyncStore" )
394+ } ,
395+ }
396+ } ) ;
397+ }
398+
399+ #[ cfg( feature = "postgres" ) ]
400+ fn postgres_connection_string ( ) -> String {
401+ std:: env:: var ( POSTGRES_TEST_URL_ENV_VAR )
402+ . unwrap_or_else ( |_| "host=localhost user=postgres password=postgres" . to_string ( ) )
403+ }
404+
405+ #[ cfg( feature = "postgres" ) ]
406+ fn postgres_table_name ( config : & TestConfig ) -> String {
407+ format ! (
408+ "test_{}" ,
409+ config
410+ . node_config
411+ . storage_dir_path
412+ . chars( )
413+ . filter( |c| c. is_ascii_alphanumeric( ) )
414+ . collect:: <String >( )
415+ )
416+ }
417+
145418/// Returns whether the benchmark identified by `group/name` matches the CLI filters.
146419///
147420/// Criterion applies its own filters after benchmark registration, but these benches do expensive
@@ -158,13 +431,17 @@ fn should_register_bench(group: &str, name: &str) -> bool {
158431 } )
159432}
160433
434+ fn is_ci ( ) -> bool {
435+ std:: env:: var ( "CI" ) . is_ok_and ( |value| !value. is_empty ( ) && value != "0" && value != "false" )
436+ }
437+
161438fn setup_forwarding_nodes (
162439 chain_source : & TestChainSource , bitcoind : & BitcoinD , electrsd : & electrsd:: ElectrsD ,
163440 store_type : TestStoreType , runtime : & tokio:: runtime:: Runtime ,
164441) -> Vec < Arc < Node > > {
165442 let mut nodes = Vec :: new ( ) ;
166443 for _ in 0 ..3 {
167- let mut config = random_config ( true ) ;
444+ let mut config = random_config ( ) ;
168445 config. store_type = store_type;
169446 nodes. push ( Arc :: new ( setup_node ( chain_source, config) ) ) ;
170447 }
0 commit comments