lnd/graph/db/benchmark_test.go
Oli 8047149c6a
multi: upgrade to btcd v2 modules
Migrate all btcd dependencies to the new per-package v2 modules (wire/v2,
txscript/v2, chaincfg/v2, chainhash/v2, btcutil/v2, psbt/v2, btcec/v2)
introduced by btcd v0.26.0, and pin the tagged ecosystem versions:
btcwallet v0.17.0, neutrino v0.18.0 and lightning-onion v1.4.0.

The bulk of the import rewrite was produced by the scripted diff from
https://github.com/btcsuite/btcd/pull/2547 (followed by 'make rpc'). The
address symbols that moved out of btcutil into the new address package
are imported as btcaddr where a local "address" variable would otherwise
shadow them. The go.mod/go.sum updates and the remaining manual
compilation fixes are folded into this single commit so it builds on its
own (the migration was previously split into a reproducible scripted-diff
plus follow-ups, intended to be squashed on merge).
2026-06-24 10:58:36 -07:00

1023 lines
26 KiB
Go

package graphdb
import (
"context"
"database/sql"
"errors"
"fmt"
"path"
"sync"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg/v2"
"github.com/lightningnetwork/lnd/batch"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/kvdb/postgres"
"github.com/lightningnetwork/lnd/kvdb/sqlbase"
"github.com/lightningnetwork/lnd/kvdb/sqlite"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/stretchr/testify/require"
"golang.org/x/time/rate"
)
// Here we define various database paths, connection strings and file names that
// we will use to open the database connections. These should be changed to
// point to your actual local test databases.
const (
bboltDBPath = "testdata/kvdb"
kvdbSqlitePath = "testdata/kvdb"
nativeSQLSqlitePath = "testdata"
kvdbPostgresDNS = "postgres://test@localhost/graphbenchmark_kvdb"
nativeSQLPostgresDNS = "postgres://test@localhost/graphbenchmark"
kvdbSqliteFile = "channel.sqlite"
kvdbBBoltFile = "channel.db"
nativeSQLSqliteFile = "lnd.sqlite"
testMaxSQLiteConnections = 2
testMaxPostgresConnections = 50
testSQLBusyTimeout = 5 * time.Second
)
// Here we define some variables that will be used to configure the graph stores
// we open for testing. These can be modified to suit your testing needs.
var (
// dbTestChain is the chain hash used for initialising the test
// databases. This should be changed to match the chain hash of the
// database you are testing against.
dbTestChain = *chaincfg.MainNetParams.GenesisHash
// testStoreOptions is used to configure the graph stores we open for
// testing.
testStoreOptions = []StoreOptionModifier{
WithBatchCommitInterval(500 * time.Millisecond),
}
// testSqlitePragmaOpts is a set of SQLite pragma options that we apply
// to the SQLite databases we open for testing.
testSqlitePragmaOpts = []string{
"synchronous=full",
"auto_vacuum=incremental",
"fullfsync=true",
}
)
// dbConnection is a struct that holds the name of the database connection
// and a function to open the connection.
type dbConnection struct {
name string
open func(testing.TB) Store
}
// This var block defines the various database connections that we will use
// for testing. Each connection is defined as a dbConnection struct that
// contains a name and an open function. The open function is used to create
// a new Store instance for the given database type.
var (
// kvdbBBoltConn is a connection to a kvdb-bbolt database called
// channel.db.
kvdbBBoltConn = dbConnection{
name: "kvdb-bbolt",
open: func(b testing.TB) Store {
return connectBBoltDB(b, bboltDBPath, kvdbBBoltFile)
},
}
// kvdbSqliteConn is a connection to a kvdb-sqlite database called
// channel.sqlite.
kvdbSqliteConn = dbConnection{
name: "kvdb-sqlite",
open: func(b testing.TB) Store {
return connectKVDBSqlite(
b, kvdbSqlitePath, kvdbSqliteFile,
)
},
}
// nativeSQLSqliteConn is a connection to a native SQL sqlite database
// called lnd.sqlite.
nativeSQLSqliteConn = dbConnection{
name: "native-sqlite",
open: func(b testing.TB) Store {
return connectNativeSQLite(
b, sqldb.DefaultSQLiteConfig(),
nativeSQLSqlitePath, nativeSQLSqliteFile,
)
},
}
// kvdbPostgresConn is a connection to a kvdb-postgres database
// using a postgres connection string.
kvdbPostgresConn = dbConnection{
name: "kvdb-postgres",
open: func(b testing.TB) Store {
return connectKVDBPostgres(b, kvdbPostgresDNS)
},
}
// nativeSQLPostgresConn is a connection to a native SQL postgres
// database using a postgres connection string.
nativeSQLPostgresConn = dbConnection{
name: "native-postgres",
open: func(b testing.TB) Store {
return connectNativePostgres(
b, sqldb.DefaultPostgresConfig(),
nativeSQLPostgresDNS,
)
},
}
)
// connectNativePostgres creates a Store instance backed by a native Postgres
// database for testing purposes.
func connectNativePostgres(t testing.TB, cfg *sqldb.QueryConfig,
dsn string) Store {
return newSQLStore(t, cfg, sqlPostgres(t, dsn))
}
// sqlPostgres creates a sqldb.DB instance backed by a native Postgres database
// for testing purposes.
func sqlPostgres(t testing.TB, dsn string) BatchedSQLQueries {
store, err := sqldb.NewPostgresStore(&sqldb.PostgresConfig{
Dsn: dsn,
MaxConnections: testMaxPostgresConnections,
})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, store.Close())
})
return newSQLExecutor(t, store)
}
// connectNativeSQLite creates a Store instance backed by a native SQLite
// database for testing purposes.
func connectNativeSQLite(t testing.TB, cfg *sqldb.QueryConfig, dbPath,
file string) Store {
return newSQLStore(t, cfg, sqlSQLite(t, dbPath, file))
}
// sqlSQLite creates a sqldb.DB instance backed by a native SQLite database for
// testing purposes.
func sqlSQLite(t testing.TB, dbPath, file string) BatchedSQLQueries {
store, err := sqldb.NewSqliteStore(
&sqldb.SqliteConfig{
MaxConnections: testMaxSQLiteConnections,
BusyTimeout: testSQLBusyTimeout,
PragmaOptions: testSqlitePragmaOpts,
},
path.Join(dbPath, file),
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, store.Close())
})
return newSQLExecutor(t, store)
}
// kvdbPostgres creates a kvdb.Backend instance backed by a kvdb-postgres
// database for testing purposes.
func kvdbPostgres(t testing.TB, dsn string) kvdb.Backend {
kvStore, err := kvdb.Open(
kvdb.PostgresBackendName, t.Context(),
&postgres.Config{
Dsn: dsn,
MaxConnections: testMaxPostgresConnections,
},
// NOTE: we use the raw string here else we get an
// import cycle if we try to import lncfg.NSChannelDB.
"channeldb",
)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, kvStore.Close())
})
return kvStore
}
// connectKVDBPostgres creates a Store instance backed by a kvdb-postgres
// database for testing purposes.
func connectKVDBPostgres(t testing.TB, dsn string) Store {
return newKVStore(t, kvdbPostgres(t, dsn))
}
// kvdbSqlite creates a kvdb.Backend instance backed by a kvdb-sqlite
// database for testing purposes.
func kvdbSqlite(t testing.TB, dbPath, fileName string) kvdb.Backend {
sqlbase.Init(testMaxSQLiteConnections)
kvStore, err := kvdb.Open(
kvdb.SqliteBackendName, t.Context(),
&sqlite.Config{
BusyTimeout: testSQLBusyTimeout,
MaxConnections: testMaxSQLiteConnections,
PragmaOptions: testSqlitePragmaOpts,
}, dbPath, fileName,
// NOTE: we use the raw string here else we get an
// import cycle if we try to import lncfg.NSChannelDB.
"channeldb",
)
require.NoError(t, err)
return kvStore
}
// connectKVDBSqlite creates a Store instance backed by a kvdb-sqlite
// database for testing purposes.
func connectKVDBSqlite(t testing.TB, dbPath, fileName string) Store {
return newKVStore(t, kvdbSqlite(t, dbPath, fileName))
}
// connectBBoltDB creates a new BBolt database connection for testing.
func connectBBoltDB(t testing.TB, dbPath, fileName string) Store {
return newKVStore(t, kvdbBBolt(t, dbPath, fileName))
}
// kvdbBBolt creates a new bbolt backend for testing.
func kvdbBBolt(t testing.TB, dbPath, fileName string) kvdb.Backend {
cfg := &kvdb.BoltBackendConfig{
DBPath: dbPath,
DBFileName: fileName,
NoFreelistSync: true,
AutoCompact: false,
AutoCompactMinAge: kvdb.DefaultBoltAutoCompactMinAge,
DBTimeout: kvdb.DefaultDBTimeout,
}
kvStore, err := kvdb.GetBoltBackend(cfg)
require.NoError(t, err)
return kvStore
}
// newKVStore creates a new KVStore instance for testing using a provided
// kvdb.Backend instance.
func newKVStore(t testing.TB, backend kvdb.Backend) Store {
store, err := NewKVStore(backend, testStoreOptions...)
require.NoError(t, err)
return store
}
// newSQLExecutor creates a new BatchedSQLQueries instance for testing using a
// provided sqldb.DB instance.
func newSQLExecutor(t testing.TB, db sqldb.DB) BatchedSQLQueries {
err := db.ApplyAllMigrations(
t.Context(), sqldb.GetMigrations(),
)
require.NoError(t, err)
return sqldb.NewTransactionExecutor(
db.GetBaseDB(), func(tx *sql.Tx) SQLQueries {
return db.GetBaseDB().WithTx(tx)
},
)
}
// newSQLStore creates a new SQLStore instance for testing using a provided
// sqldb.DB instance.
func newSQLStore(t testing.TB, cfg *sqldb.QueryConfig,
db BatchedSQLQueries) Store {
store, err := NewSQLStore(
&SQLStoreConfig{
ChainHash: dbTestChain,
QueryCfg: cfg,
},
db, testStoreOptions...,
)
require.NoError(t, err)
return store
}
// TestPopulateDBs is a helper test that can be used to populate various local
// graph DBs from some source graph DB. This can then be used to run the
// various benchmark tests against the same graph data.
//
// TODO(elle): this test reveals that the batching logic we use might be
// problematic for postgres backends. This needs some investigation & it might
// make sense to only use LazyAdd for sqlite backends & it may make sense to
// also add a maximum batch size to avoid grouping too many updates at once.
// Observations:
// - the LazyAdd options need to be turned off for channel/policy update calls
// for both the native SQL postgres & kvdb postgres backend for this test to
// succeed.
// - The LazyAdd option must be added for the sqlite backends, else it takes
// very long to sync the graph.
func TestPopulateDBs(t *testing.T) {
t.Parallel()
ctx := t.Context()
// NOTE: uncomment the line below to run this test locally, then provide
// the desired source database (and make sure the destination Postgres
// databases exist and are running).
t.Skipf("Skipping local helper test")
// Set your desired source database here. For kvdbSqliteConn, a file
// called testdata/kvdb/channel.sqlite must exist and be populated with
// a KVDB based channel graph.
sourceDB := kvdbSqliteConn
// Populate this list with the desired destination databases.
destinations := []dbConnection{
kvdbBBoltConn,
nativeSQLSqliteConn,
kvdbPostgresConn,
nativeSQLPostgresConn,
}
// Open and start the source graph.
src, err := NewChannelGraph(sourceDB.open(t))
require.NoError(t, err)
require.NoError(t, src.Start())
t.Cleanup(func() {
require.NoError(t, src.Stop())
})
// countNodes is a helper function to count the number of nodes in the
// graph.
countNodes := func(graph *ChannelGraph) int {
numNodes := 0
v1Graph := NewVersionedGraph(graph, lnwire.GossipVersion1)
err := v1Graph.ForEachNode(
ctx,
func(node *models.Node) error {
numNodes++
return nil
}, func() {
numNodes = 0
},
)
require.NoError(t, err)
return numNodes
}
// countChannels is a helper function to count the number of channels
// in the graph.
countChannels := func(graph *ChannelGraph) (int, int) {
var (
numChans = 0
numPolicies = 0
)
err := graph.ForEachChannel(
ctx, lnwire.GossipVersion1,
func(info *models.ChannelEdgeInfo, policy,
policy2 *models.ChannelEdgePolicy) error {
numChans++
if policy != nil {
numPolicies++
}
if policy2 != nil {
numPolicies++
}
return nil
}, func() {
numChans = 0
numPolicies = 0
})
require.NoError(t, err)
return numChans, numPolicies
}
t.Logf("Number of nodes in source graph (%s): %d", sourceDB.name,
countNodes(src))
numChan, numPol := countChannels(src)
t.Logf("Number of channels & policies in source graph (%s): %d "+
"channels, %d policies", sourceDB.name, numChan, numPol)
for _, destDB := range destinations {
t.Run(destDB.name, func(t *testing.T) {
t.Parallel()
// Open and start the destination graph.
dest, err := NewChannelGraph(destDB.open(t))
require.NoError(t, err)
require.NoError(t, dest.Start())
t.Cleanup(func() {
require.NoError(t, dest.Stop())
})
t.Logf("Number of nodes in %s graph: %d", destDB.name,
countNodes(dest))
numChan, numPol := countChannels(dest)
t.Logf("Number of channels in %s graph: %d, %d",
destDB.name, numChan, numPol)
// Sync the source graph to the destination graph.
syncGraph(t, src, dest)
t.Logf("Number of nodes in %s graph after sync: %d",
destDB.name, countNodes(dest))
numChan, numPol = countChannels(dest)
t.Logf("Number of channels in %s graph after sync: "+
"%d, %d", destDB.name, numChan, numPol)
})
}
}
// syncGraph synchronizes the source graph with the destination graph by
// copying all nodes and channels from the source to the destination.
func syncGraph(t *testing.T, src, dest *ChannelGraph) {
ctx := t.Context()
var (
s = rate.Sometimes{
Interval: 10 * time.Second,
}
t0 = time.Now()
chunk = 0
total = 0
mu sync.Mutex
)
reportNodeStats := func() {
elapsed := time.Since(t0).Seconds()
ratePerSec := float64(chunk) / elapsed
t.Logf("Synced %d nodes (last chunk: %d) "+
"(%.2f nodes/second)",
total, chunk, ratePerSec)
t0 = time.Now()
}
var wgNodes sync.WaitGroup
v1Src := NewVersionedGraph(src, lnwire.GossipVersion1)
err := v1Src.ForEachNode(ctx, func(node *models.Node) error {
wgNodes.Add(1)
go func() {
defer wgNodes.Done()
err := dest.AddNode(ctx, node, batch.LazyAdd())
require.NoError(t, err)
mu.Lock()
total++
chunk++
s.Do(func() {
reportNodeStats()
chunk = 0
})
mu.Unlock()
}()
return nil
}, func() {})
require.NoError(t, err)
wgNodes.Wait()
reportNodeStats()
t.Logf("Done syncing %d nodes", total)
total = 0
chunk = 0
t0 = time.Now()
reportChanStats := func() {
elapsed := time.Since(t0).Seconds()
ratePerSec := float64(chunk) / elapsed
t.Logf("Synced %d channels (and its "+
"policies) (last chunk: %d) "+
"(%.2f channels/second)",
total, chunk, ratePerSec)
t0 = time.Now()
}
var wgChans sync.WaitGroup
err = src.ForEachChannel(ctx, lnwire.GossipVersion1,
func(info *models.ChannelEdgeInfo,
policy1, policy2 *models.ChannelEdgePolicy) error {
// Add each channel & policy. We do this in a goroutine
// to take advantage of batch processing.
wgChans.Add(1)
go func() {
defer wgChans.Done()
err := dest.AddChannelEdge(
ctx, info, batch.LazyAdd(),
)
if !errors.Is(err, ErrEdgeAlreadyExist) {
require.NoError(t, err)
}
if policy1 != nil {
err = dest.UpdateEdgePolicy(
ctx, policy1, batch.LazyAdd(),
)
require.NoError(t, err)
}
if policy2 != nil {
err = dest.UpdateEdgePolicy(
ctx, policy2, batch.LazyAdd(),
)
require.NoError(t, err)
}
mu.Lock()
total++
chunk++
s.Do(func() {
reportChanStats()
chunk = 0
})
mu.Unlock()
}()
return nil
}, func() {})
require.NoError(t, err)
wgChans.Wait()
reportChanStats()
t.Logf("Done syncing %d channels", total)
}
// BenchmarkCacheLoading benchmarks how long it takes to load the in-memory
// graph cache from a populated database.
//
// NOTE: this is to be run against a local graph database. It can be run
// either against a kvdb-bbolt channel.db file, or a kvdb-sqlite channel.sqlite
// file or a postgres connection containing the channel graph in kvdb format and
// finally, it can be run against a native SQL sqlite or postgres database.
//
// NOTE: the TestPopulateDBs test helper can be used to populate a set of test
// DBs from a single source db.
func BenchmarkCacheLoading(b *testing.B) {
ctx := b.Context()
tests := []dbConnection{
kvdbBBoltConn,
kvdbSqliteConn,
nativeSQLSqliteConn,
kvdbPostgresConn,
nativeSQLPostgresConn,
}
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
store := test.open(b)
// Reset timer to exclude setup time.
b.ResetTimer()
for i := 0; i < b.N; i++ {
b.StopTimer()
graph, err := NewChannelGraph(store)
require.NoError(b, err)
b.StartTimer()
require.NoError(b, graph.populateCache(ctx))
}
})
}
}
// BenchmarkGraphReadMethods benchmarks various read calls of various Store
// implementations.
//
// NOTE: this is to be run against a local graph database. It can be run
// either against a kvdb-bbolt channel.db file, or a kvdb-sqlite channel.sqlite
// file or a postgres connection containing the channel graph in kvdb format and
// finally, it can be run against a native SQL sqlite or postgres database.
//
// NOTE: the TestPopulateDBs test helper can be used to populate a set of test
// DBs from a single source db.
func BenchmarkGraphReadMethods(b *testing.B) {
ctx := b.Context()
backends := []dbConnection{
kvdbBBoltConn,
kvdbSqliteConn,
nativeSQLSqliteConn,
kvdbPostgresConn,
nativeSQLPostgresConn,
}
// We use a counter to make sure that any call-back is doing something
// useful, otherwise the compiler may optimize it away in the future.
var counter int64
tests := []struct {
name string
fn func(b testing.TB, store Store)
}{
{
name: "ForEachNode",
fn: func(b testing.TB, store Store) {
err := store.ForEachNode(
ctx, lnwire.GossipVersion1,
func(_ *models.Node) error {
// Increment the counter to
// ensure the callback is doing
// something.
counter++
return nil
}, func() {},
)
require.NoError(b, err)
},
},
{
name: "ForEachChannel",
fn: func(b testing.TB, store Store) {
//nolint:ll
err := store.ForEachChannel(
ctx, lnwire.GossipVersion1,
func(_ *models.ChannelEdgeInfo,
_ *models.ChannelEdgePolicy,
_ *models.ChannelEdgePolicy) error {
// Increment the counter to
// ensure the callback is doing
// something.
counter++
return nil
}, func() {},
)
require.NoError(b, err)
},
},
{
name: "NodeUpdatesInHorizon",
fn: func(b testing.TB, store Store) {
iter := store.NodeUpdatesInHorizon(
ctx, lnwire.GossipVersion1,
NodeUpdateRange{
StartTime: fn.Some(
time.Unix(0, 0),
),
EndTime: fn.Some(time.Now()),
},
)
_, err := fn.CollectErr(iter)
require.NoError(b, err)
},
},
{
name: "ForEachNodeCacheable",
fn: func(b testing.TB, store Store) {
err := store.ForEachNodeCacheable(
ctx, lnwire.GossipVersion1,
func(_ route.Vertex,
_ *lnwire.FeatureVector) error {
// Increment the counter to
// ensure the callback is doing
// something.
counter++
return nil
}, func() {},
)
require.NoError(b, err)
},
},
{
name: "ForEachNodeCached",
fn: func(b testing.TB, store Store) {
//nolint:ll
err := store.ForEachNodeCached(
ctx, lnwire.GossipVersion1,
func(context.Context,
route.Vertex,
map[uint64]*DirectedChannel) error {
// Increment the counter to
// ensure the callback is doing
// something.
counter++
return nil
}, func() {},
)
require.NoError(b, err)
},
},
{
name: "ChanUpdatesInHorizon",
fn: func(b testing.TB, store Store) {
iter := store.ChanUpdatesInHorizon(
ctx, lnwire.GossipVersion1,
ChanUpdateRange{
StartTime: fn.Some(
time.Unix(0, 0),
),
EndTime: fn.Some(time.Now()),
},
)
_, err := fn.CollectErr(iter)
require.NoError(b, err)
},
},
}
for _, test := range tests {
for _, db := range backends {
name := fmt.Sprintf("%s-%s", test.name, db.name)
b.Run(name, func(b *testing.B) {
store := db.open(b)
// Reset timer to exclude setup time.
b.ResetTimer()
for i := 0; i < b.N; i++ {
test.fn(b, store)
}
})
}
}
}
// BenchmarkNodeHorizonIndex benchmarks the NodeUpdatesInHorizon query under
// different index configurations to measure the performance impact of the
// composite (version, last_update, pub_key) index vs the old single-column
// (last_update) index.
//
// NOTE: this is to be run against a local native SQL database. The
// TestPopulateDBs test helper can be used to populate the test DB.
func BenchmarkNodeHorizonIndex(b *testing.B) {
ctx := b.Context()
// NOTE: uncomment the line below to run this benchmark locally.
b.Skipf("Skipping local benchmark test")
// NOTE: Set this to true to also benchmark against postgres.
testPostgres := false
// sqlBackend holds a Store for queries and a raw *sql.DB handle for
// index DDL manipulation between benchmark runs.
type sqlBackend struct {
name string
rawDB *sql.DB
store Store
}
// openRawDB opens a raw *sql.DB connection to the same database that
// the given dbConnection targets. This is used for DDL operations
// (DROP/CREATE INDEX) that are not exposed through the Store interface.
openSQLiteRawDB := func(b testing.TB) *sql.DB {
sqliteStore, err := sqldb.NewSqliteStore(
&sqldb.SqliteConfig{
MaxConnections: testMaxSQLiteConnections,
BusyTimeout: testSQLBusyTimeout,
PragmaOptions: testSqlitePragmaOpts,
},
path.Join(nativeSQLSqlitePath, nativeSQLSqliteFile),
)
require.NoError(b, err)
b.Cleanup(func() {
require.NoError(b, sqliteStore.Close())
})
return sqliteStore.GetBaseDB().DB
}
openPostgresRawDB := func(b testing.TB) *sql.DB {
pgStore, err := sqldb.NewPostgresStore(
&sqldb.PostgresConfig{
Dsn: nativeSQLPostgresDNS,
MaxConnections: testMaxPostgresConnections,
},
)
require.NoError(b, err)
b.Cleanup(func() {
require.NoError(b, pgStore.Close())
})
return pgStore.GetBaseDB().DB
}
backends := []sqlBackend{
{
name: nativeSQLSqliteConn.name,
rawDB: openSQLiteRawDB(b),
store: nativeSQLSqliteConn.open(b),
},
}
if testPostgres {
backends = append(backends, sqlBackend{
name: nativeSQLPostgresConn.name,
rawDB: openPostgresRawDB(b),
store: nativeSQLPostgresConn.open(b),
})
}
// Index configurations to compare.
type indexConfig struct {
name string
setup string
}
configs := []indexConfig{
{
name: "old-indexes",
setup: `
DROP INDEX IF EXISTS graph_node_last_update_idx;
CREATE INDEX IF NOT EXISTS graph_node_last_update_idx
ON graph_nodes(last_update);
DROP INDEX IF EXISTS graph_channels_node_id_1_idx;
DROP INDEX IF EXISTS graph_channels_node_id_2_idx;
CREATE INDEX IF NOT EXISTS graph_channels_node_id_1_idx
ON graph_channels(node_id_1);
CREATE INDEX IF NOT EXISTS graph_channels_node_id_2_idx
ON graph_channels(node_id_2);
`,
},
{
name: "new-indexes",
setup: `
DROP INDEX IF EXISTS graph_node_last_update_idx;
CREATE INDEX IF NOT EXISTS graph_node_last_update_idx
ON graph_nodes(version, last_update, pub_key);
DROP INDEX IF EXISTS graph_channels_node_id_1_idx;
DROP INDEX IF EXISTS graph_channels_node_id_2_idx;
CREATE INDEX IF NOT EXISTS graph_channels_node_id_1_idx
ON graph_channels(node_id_1, version);
CREATE INDEX IF NOT EXISTS graph_channels_node_id_2_idx
ON graph_channels(node_id_2, version);
`,
},
}
// Query variants to benchmark.
type queryVariant struct {
name string
opts []IteratorOption
}
variants := []queryVariant{
{
name: "all-nodes",
},
{
name: "public-only",
opts: []IteratorOption{WithIterPublicNodesOnly()},
},
}
for _, backend := range backends {
for _, cfg := range configs {
for _, variant := range variants {
name := fmt.Sprintf("%s/%s/%s",
backend.name, cfg.name,
variant.name,
)
b.Run(name, func(b *testing.B) {
// Apply the index configuration.
_, err := backend.rawDB.ExecContext(
ctx, cfg.setup,
)
require.NoError(b, err)
b.ResetTimer()
//nolint:ll
for i := 0; i < b.N; i++ {
iter := backend.store.NodeUpdatesInHorizon(
ctx,
lnwire.GossipVersion1,
NodeUpdateRange{
StartTime: fn.Some(time.Unix(0, 0)),
EndTime: fn.Some(time.Now()),
},
variant.opts...,
)
nodes, err := fn.CollectErr(iter)
require.NoError(b, err)
// Prevent the compiler from
// optimizing away the result.
_ = len(nodes)
}
})
}
}
}
}
// BenchmarkFindOptimalSQLQueryConfig uses the ForEachNode and ForEachChannel
// methods to find the optimal maximum sqldb QueryConfig values for a given
// database backend. This is useful for determining the best default values for
// each backend. The ForEachNode and ForEachChannel methods are used since
// they make use of both batching and pagination.
func BenchmarkFindOptimalSQLQueryConfig(b *testing.B) {
// NOTE: Set this to true if you want to test with a postgres backend.
testPostgres := false
// NOTE: Set this to true if you want to test with various batch sizes.
// By default, page sizes will be tested.
testBatching := false
// Set the various page sizes we want to test.
//
// NOTE: these are the sqlite paging testing values.
testSizes := []uint32{20, 50, 100, 150, 500}
configOption := "MaxPageSize"
if testBatching {
configOption = "MaxBatchSize"
testSizes = []uint32{
50, 100, 150, 200, 250, 300, 350,
}
}
dbName := "sqlite"
if testPostgres {
dbName = "postgres"
// Set the various page sizes we want to test.
//
// NOTE: these are the postgres paging values.
testSizes = []uint32{5000, 7000, 10000, 12000}
if testBatching {
testSizes = []uint32{
1000, 2000, 5000, 7000, 10000,
}
}
}
for _, size := range testSizes {
b.Run(fmt.Sprintf("%s-%s-%d", configOption, dbName, size),
func(b *testing.B) {
ctx := b.Context()
cfg := sqldb.DefaultSQLiteConfig()
if testPostgres {
cfg = sqldb.DefaultPostgresConfig()
}
if testBatching {
cfg.MaxBatchSize = size
} else {
cfg.MaxPageSize = size
}
store := connectNativeSQLite(
b, cfg, nativeSQLSqlitePath,
nativeSQLSqliteFile,
)
if testPostgres {
store = connectNativePostgres(
b, cfg, nativeSQLPostgresDNS,
)
}
// Reset timer to exclude setup time.
b.ResetTimer()
for i := 0; i < b.N; i++ {
var (
numNodes = 0
numChannels = 0
)
err := store.ForEachNode(
ctx, lnwire.GossipVersion1,
func(_ *models.Node) error {
numNodes++
return nil
}, func() {},
)
require.NoError(b, err)
//nolint:ll
err = store.ForEachChannel(
ctx, lnwire.GossipVersion1,
func(_ *models.ChannelEdgeInfo,
_,
_ *models.ChannelEdgePolicy) error {
numChannels++
return nil
}, func() {},
)
require.NoError(b, err)
}
},
)
}
}