lnd/sqldb/sqlite_bench_test.go
ziggie cb36d0d977
sqldb: fix error comparison and refactor sqlite bench helpers
Replace direct `err != sql.ErrNoRows` comparison with `errors.Is` and
extract the repeated fetch-and-check logic into a helper to reduce
duplication across the sequential and concurrent benchmarks.
2026-03-12 18:24:05 +01:00

196 lines
4.7 KiB
Go

//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"
)
// getInvoiceByHashForBench fetches an invoice by hash and reports any
// error except sql.ErrNoRows (expected when the invoice doesn't exist).
func getInvoiceByHashForBench(b *testing.B, store *SqliteStore,
ctx context.Context, hash []byte) {
_, err := store.GetInvoiceByHash(ctx, hash)
if err != nil {
require.ErrorIs(b, err, sql.ErrNoRows)
}
}
// 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++
getInvoiceByHashForBench(b, store, ctx, hash)
}
})
}
}
// 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 {
hash := hashes[g%numInvoices]
go func(h []byte) {
defer wg.Done()
getInvoiceByHashForBench(
b, store, ctx, h,
)
}(hash)
}
wg.Wait()
}
})
}
}