What changed, and why it matters
This commit only adds a new benchmark test file. It does not change any production code, database schema, or user-facing behavior. Benchmarks are performance measurement tools used by developers and are not part of the running software that handles Lightning Network payments.
No security action required. This is a test-only addition. Reviewers may optionally verify the benchmark does not accidentally leave indexes in an inconsistent state when run locally, but the default skip and b.Cleanup patterns mitigate this.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces BenchmarkNodeHorizonIndex in graph/db/benchmark_test.go. It measures NodeUpdatesInHorizon query performance under two index configurations (old single-column vs new composite indexes) across SQLite and optional Postgres backends. The benchmark is skipped by default (b.Skipf), requires a local SQL database, and performs DDL (DROP/CREATE INDEX) only within the test context. No runtime logic, migrations, or schema defaults are modified.
Changed components
graph/db/benchmark_test.goInspect captured patch +167 / −0
diff --git a/graph/db/benchmark_test.go b/graph/db/benchmark_test.go
index 9a3f76d..d076444 100644
--- a/graph/db/benchmark_test.go
+++ b/graph/db/benchmark_test.go
@@ -750,6 +750,173 @@ func BenchmarkGraphReadMethods(b *testing.B) {
}
}
+// 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
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.