From de06c29ae982adc183319025e4a3f056a56d5c06 Mon Sep 17 00:00:00 2001 From: ziggie Date: Tue, 3 Mar 2026 19:23:12 +0100 Subject: [PATCH] sqldb: add benchmark for sqlite connection number The results suggests that the default of 2 is a good conservative approach. The higher the connection number and the higher the workload the performance decreases in WAL mode since readers still need coordination (WAL index shm for example). --- sqldb/sqlite_bench_test.go | 204 +++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 sqldb/sqlite_bench_test.go diff --git a/sqldb/sqlite_bench_test.go b/sqldb/sqlite_bench_test.go new file mode 100644 index 000000000..bc5cea3ac --- /dev/null +++ b/sqldb/sqlite_bench_test.go @@ -0,0 +1,204 @@ +//go:build !js && !(windows && (arm || 386)) && !(linux && (ppc64 || mips || mipsle || mips64)) + +package sqldb + +import ( + "context" + "database/sql" + "fmt" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/lightningnetwork/lnd/sqldb/sqlc" + "github.com/stretchr/testify/require" +) + +// BenchmarkSqliteMaxConns benchmarks sequential reads against a SQLite +// database with varying MaxConnections settings. +// +// Run with: +// +// go test -bench=BenchmarkSqliteMaxConns -benchmem -run=^$ ./sqldb/ +func BenchmarkSqliteMaxConns(b *testing.B) { + const numInvoices = 500 + + // connCounts contains the MaxConnections values we want to compare. + // 0 means "use the library default" (currently 2 for SQLite). + connCounts := []int{1, 2, 4, 8, 16, 0} + + // Build a fresh SQLite database that will be shared across all + // sub-benchmarks. We insert a fixed set of invoices once and then + // execute read-only queries from multiple goroutines. + dbFileName := filepath.Join(b.TempDir(), "bench.db") + + // Open the store once with migrations so the schema is in place. + setupStore, err := NewSqliteStore(&SqliteConfig{ + SkipMigrations: false, + }, dbFileName) + require.NoError(b, err) + + require.NoError(b, setupStore.ApplyAllMigrations( + context.Background(), GetMigrations(), + )) + + ctx := context.Background() + + // Insert test invoices. We use a predictable hash per invoice so we + // can look them up deterministically during the benchmark. + hashes := make([][]byte, numInvoices) + for i := range numInvoices { + hash := make([]byte, 32) + hash[0] = byte(i) + hash[1] = byte(i >> 8) + hashes[i] = hash + + _, err := setupStore.InsertInvoice( + ctx, sqlc.InsertInvoiceParams{ + Hash: hash, + PaymentAddr: hash, + PaymentRequestHash: hash, + Expiry: 3600, + CreatedAt: time.Now(), + }, + ) + require.NoError(b, err) + } + + require.NoError(b, setupStore.DB.Close()) + + for _, maxConns := range connCounts { + name := fmt.Sprintf("MaxConns=%d", maxConns) + if maxConns == 0 { + name = fmt.Sprintf("MaxConns=default(%d)", + DefaultSqliteMaxConns) + } + + b.Run(name, func(b *testing.B) { + store, err := NewSqliteStore( + &SqliteConfig{ + SkipMigrations: true, + MaxConnections: maxConns, + }, dbFileName, + ) + require.NoError(b, err) + + b.Cleanup(func() { + require.NoError(b, store.DB.Close()) + }) + + var i int + for b.Loop() { + hash := hashes[i%numInvoices] + i++ + + _, err := store.GetInvoice( + ctx, + sqlc.GetInvoiceParams{ + Hash: hash, + }, + ) + if err != nil { + require.ErrorIs(b, err, sql.ErrNoRows) + } + } + }) + } +} + +// BenchmarkSqliteMaxConnsConcurrentReads measures aggregate read throughput +// for a fixed level of goroutine concurrency to complement the sequential +// benchmark above. Each iteration launches a fixed number of goroutines that +// all issue reads simultaneously, directly stressing the connection pool. +func BenchmarkSqliteMaxConnsConcurrentReads(b *testing.B) { + const ( + numInvoices = 500 + goroutines = 16 + ) + + connCounts := []int{1, 2, 4, 8, 16, 0} + + dbFileName := filepath.Join(b.TempDir(), "bench_conc.db") + + setupStore, err := NewSqliteStore(&SqliteConfig{ + SkipMigrations: false, + }, dbFileName) + require.NoError(b, err) + + require.NoError(b, setupStore.ApplyAllMigrations( + context.Background(), GetMigrations(), + )) + + ctx := context.Background() + + hashes := make([][]byte, numInvoices) + for i := range numInvoices { + hash := make([]byte, 32) + hash[0] = byte(i) + hash[1] = byte(i >> 8) + hashes[i] = hash + + _, err := setupStore.InsertInvoice( + ctx, sqlc.InsertInvoiceParams{ + Hash: hash, + PaymentAddr: hash, + PaymentRequestHash: hash, + Expiry: 3600, + CreatedAt: time.Now(), + }, + ) + require.NoError(b, err) + } + + require.NoError(b, setupStore.DB.Close()) + + for _, maxConns := range connCounts { + name := fmt.Sprintf("MaxConns=%d", maxConns) + if maxConns == 0 { + name = fmt.Sprintf("MaxConns=default(%d)", + DefaultSqliteMaxConns) + } + + b.Run(name, func(b *testing.B) { + store, err := NewSqliteStore( + &SqliteConfig{ + SkipMigrations: true, + MaxConnections: maxConns, + }, dbFileName, + ) + require.NoError(b, err) + + b.Cleanup(func() { + require.NoError(b, store.DB.Close()) + }) + + for b.Loop() { + var wg sync.WaitGroup + wg.Add(goroutines) + + for g := range goroutines { + go func() { + defer wg.Done() + + hash := hashes[g%numInvoices] + _, err := store.GetInvoice( + ctx, + sqlc.GetInvoiceParams{ + Hash: hash, + }, + ) + if err != nil && + err != sql.ErrNoRows { + + b.Errorf("GetInvoice:"+ + " %v", err) + } + }() + } + + wg.Wait() + } + }) + } +}