Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions token/services/storage/db/sql/common/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,12 @@ func (db *TokenStore) UnspentTokensIterator(ctx context.Context) (tdriver.Unspen
// append. UNION ALL is used (not UNION) to skip the per-row sort/hash
// dedup pass; duplicates between the two branches (and within branch 1 when
// a token has multiple ownership rows) are filtered at the iterator layer.
func (db *TokenStore) UnspentTokensIteratorBy(ctx context.Context, walletID string, tokenType token.Type) (tdriver.UnspentTokensIterator, error) {
// buildUnspentTokensIteratorByQuery builds the SQL query and args for
// UnspentTokensIteratorBy without executing it. Extracted so benchmarks can
// compare executing this exact query dynamically (query built and run fresh
// each call, the production path) against running it via a statement
// prepared once ahead of time, using identical SQL in both cases.
func buildUnspentTokensIteratorByQuery(db *TokenStore, walletID string, tokenType token.Type) (string, []any) {
tokenTable := q.Table(db.table.Tokens)
ownershipTable := q.Table(db.table.Ownership)
joinCond := cond.And(
Expand Down Expand Up @@ -258,7 +263,12 @@ func (db *TokenStore) UnspentTokensIteratorBy(ctx context.Context, walletID stri
branch1.FormatTo(db.ci, sb)
sb.WriteString(" UNION ALL ")
branch2.FormatTo(db.ci, sb)
query, args := sb.Build()

return sb.Build()
}

func (db *TokenStore) UnspentTokensIteratorBy(ctx context.Context, walletID string, tokenType token.Type) (tdriver.UnspentTokensIterator, error) {
query, args := buildUnspentTokensIteratorByQuery(db, walletID, tokenType)

logging.Debug(logger, query, args)
rows, err := db.readDB.QueryContext(ctx, query, args...)
Expand Down
99 changes: 99 additions & 0 deletions token/services/storage/db/sql/common/tokens_prepared_bench_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package common

import (
"context"
"database/sql"
"testing"
"time"

"github.com/LFDT-Panurus/panurus/token/services/benchmark"
tokentype "github.com/LFDT-Panurus/panurus/token/token"
)

// RunUnspentTokensIteratorByPreparedComparison benchmarks UnspentTokensIteratorBy
// against a version of the same query executed via a statement prepared once
// outside the timed loop, using the same seed data, worker count, and duration
// as RunTokenStoreBenchmarks so the numbers are directly comparable.
//
// This exists to answer: "how much would moving to prepared statements help
// for our primary token-selection query?" (see #1183). The query built by
// UnspentTokensIteratorBy is dynamic (its shape depends on whether walletID /
// tokenType are empty), so this comparison fixes both parameters to non-empty
// values matching the seeded data, which is the common case in production.
func RunUnspentTokensIteratorByPreparedComparison(b *testing.B, store *TokenStore) {
b.Helper()

const walletID = "wallet0"
tokenType := tokentype.Type("GOLD")

b.Run("UnspentTokensIteratorBy_Dynamic", func(b *testing.B) {
SeedBenchTokens(b, store, 1000)
cfg := benchmark.NewConfig(4, 5*time.Second, 500*time.Millisecond)
result := benchmark.RunBenchmark(
cfg,
func() *TokenStore { return store },
func(s *TokenStore) error {
it, err := s.UnspentTokensIteratorBy(context.Background(), walletID, tokenType)
if err != nil {
return err
}
defer it.Close()
for {
tok, err := it.Next()
if err != nil {
return err
}
if tok == nil {
break
}
}
return nil
},
)
result.Print()
})

b.Run("UnspentTokensIteratorBy_Prepared", func(b *testing.B) {
SeedBenchTokens(b, store, 1000)

query, args := buildUnspentTokensIteratorByQuery(store, walletID, tokenType)

stmt, err := store.readDB.PrepareContext(context.Background(), query)
if err != nil {
b.Fatalf("failed preparing statement: %v", err)
}
defer func() { _ = stmt.Close() }()

cfg := benchmark.NewConfig(4, 5*time.Second, 500*time.Millisecond)
result := benchmark.RunBenchmark(
cfg,
func() *sql.Stmt { return stmt },
func(s *sql.Stmt) error {
rows, err := s.QueryContext(context.Background(), args...)
if err != nil {
return err
}
defer func() { _ = rows.Close() }()

it := &dedupedTokenRowsIterator{rows: rows, seen: make(map[string]struct{})}
for {
tok, err := it.Next()
if err != nil {
return err
}
if tok == nil {
break
}
}
return nil
},
)
result.Print()
})
}
6 changes: 6 additions & 0 deletions token/services/storage/db/sql/postgres/tokens_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,9 @@ func BenchmarkTokenStore(b *testing.B) {
defer cleanup()
sqlcommon.RunTokenStoreBenchmarks(b, store)
}

func BenchmarkTokenStorePreparedComparison(b *testing.B) {
store, cleanup := openBenchTokenStore(b)
defer cleanup()
sqlcommon.RunUnspentTokensIteratorByPreparedComparison(b, store)
}
Loading