Skip to content

Commit c3cf08a

Browse files
committed
indexer: optimize idx.BlockList performance
turns out, a unified query with `COUNT(*) OVER() AS total_count` is 10x slower than two separate queries `SELECT *` and `SELECT COUNT(*)` also, optimize even further (~1000x) for the most common query: when listing all blocks without filters, don't even count, just return last height the benchmark code used to test is included
1 parent 420aee2 commit c3cf08a

File tree

6 files changed

+186
-17
lines changed

6 files changed

+186
-17
lines changed

vochain/indexer/bench_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package indexer
22

33
import (
44
"bytes"
5+
"context"
56
"fmt"
67
"math/big"
78
"sync"
@@ -14,6 +15,7 @@ import (
1415
"go.vocdoni.io/dvote/test/testcommon/testutil"
1516
"go.vocdoni.io/dvote/util"
1617
"go.vocdoni.io/dvote/vochain"
18+
indexerdb "go.vocdoni.io/dvote/vochain/indexer/db"
1719
"go.vocdoni.io/dvote/vochain/state"
1820
"go.vocdoni.io/dvote/vochain/transaction/vochaintx"
1921
"go.vocdoni.io/proto/build/go/models"
@@ -197,3 +199,84 @@ func BenchmarkNewProcess(b *testing.B) {
197199
log.Infof("indexed %d new processes, took %s",
198200
numProcesses, time.Since(startTime))
199201
}
202+
203+
func BenchmarkBlockList(b *testing.B) {
204+
app := vochain.TestBaseApplication(b)
205+
206+
idx, err := New(app, Options{DataDir: b.TempDir()})
207+
qt.Assert(b, err, qt.IsNil)
208+
209+
count := 100000
210+
211+
createDummyBlocks(b, idx, count)
212+
213+
b.ReportAllocs()
214+
b.ResetTimer()
215+
216+
benchmarkBlockList := func(b *testing.B,
217+
limit int, offset int, chainID string, hash string, proposerAddress string,
218+
) {
219+
b.ReportAllocs()
220+
b.ResetTimer()
221+
222+
for i := 0; i < b.N; i++ {
223+
blocks, total, err := idx.BlockList(limit, offset, chainID, hash, proposerAddress)
224+
qt.Assert(b, err, qt.IsNil)
225+
qt.Assert(b, blocks, qt.HasLen, limit, qt.Commentf("%+v", blocks))
226+
qt.Assert(b, blocks[0].TxCount, qt.Equals, int64(0))
227+
qt.Assert(b, total, qt.Equals, uint64(count))
228+
}
229+
}
230+
231+
// Run sub-benchmarks with different limits and filters
232+
b.Run("BlockListLimit1", func(b *testing.B) {
233+
benchmarkBlockList(b, 1, 0, "", "", "")
234+
})
235+
236+
b.Run("BlockListLimit10", func(b *testing.B) {
237+
benchmarkBlockList(b, 10, 0, "", "", "")
238+
})
239+
240+
b.Run("BlockListLimit100", func(b *testing.B) {
241+
benchmarkBlockList(b, 100, 0, "", "", "")
242+
})
243+
244+
b.Run("BlockListOffset", func(b *testing.B) {
245+
benchmarkBlockList(b, 10, count/2, "", "", "")
246+
})
247+
248+
b.Run("BlockListWithChainID", func(b *testing.B) {
249+
benchmarkBlockList(b, 10, 0, "test", "", "")
250+
})
251+
252+
b.Run("BlockListWithHashSubstr", func(b *testing.B) {
253+
benchmarkBlockList(b, 10, 0, "", "cafe", "")
254+
})
255+
b.Run("BlockListWithHashExact", func(b *testing.B) {
256+
benchmarkBlockList(b, 10, 0, "", "cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe", "")
257+
})
258+
}
259+
260+
func createDummyBlocks(b *testing.B, idx *Indexer, n int) {
261+
idx.blockMu.Lock()
262+
defer idx.blockMu.Unlock()
263+
264+
queries := idx.blockTxQueries()
265+
for h := 1; h <= n; h++ {
266+
_, err := queries.CreateBlock(context.TODO(), indexerdb.CreateBlockParams{
267+
ChainID: "test",
268+
Height: int64(h),
269+
Time: time.Now(),
270+
Hash: nonNullBytes([]byte{
271+
0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe,
272+
0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe, 0xca, 0xfe,
273+
}),
274+
ProposerAddress: nonNullBytes([]byte{0xfe, 0xde}),
275+
LastBlockHash: nonNullBytes([]byte{0xca, 0xfe}),
276+
},
277+
)
278+
qt.Assert(b, err, qt.IsNil)
279+
}
280+
err := idx.blockTx.Commit()
281+
qt.Assert(b, err, qt.IsNil)
282+
}

vochain/indexer/block.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,22 +71,30 @@ func (idx *Indexer) BlockList(limit, offset int, chainID, hash, proposerAddress
7171
for _, row := range results {
7272
list = append(list, indexertypes.BlockFromDBRow(&row))
7373
}
74-
if len(results) == 0 {
75-
return list, 0, nil
74+
count, err := idx.CountBlocks(chainID, hash, proposerAddress)
75+
if err != nil {
76+
return nil, 0, err
7677
}
77-
return list, uint64(results[0].TotalCount), nil
78+
return list, count, nil
7879
}
7980

8081
// CountBlocks returns how many blocks are indexed.
81-
func (idx *Indexer) CountBlocks() (uint64, error) {
82-
results, err := idx.readOnlyQuery.SearchBlocks(context.TODO(), indexerdb.SearchBlocksParams{
83-
Limit: 1,
82+
// If all args passed are empty ("") it will return the last block height, as an optimization.
83+
func (idx *Indexer) CountBlocks(chainID, hash, proposerAddress string) (uint64, error) {
84+
if chainID == "" && hash == "" && proposerAddress == "" {
85+
count, err := idx.readOnlyQuery.LastBlockHeight(context.TODO())
86+
if err != nil {
87+
return 0, err
88+
}
89+
return uint64(count), nil
90+
}
91+
count, err := idx.readOnlyQuery.CountBlocks(context.TODO(), indexerdb.CountBlocksParams{
92+
ChainID: chainID,
93+
HashSubstr: hash,
94+
ProposerAddress: proposerAddress,
8495
})
8596
if err != nil {
8697
return 0, err
8798
}
88-
if len(results) == 0 {
89-
return 0, nil
90-
}
91-
return uint64(results[0].TotalCount), nil
99+
return uint64(count), nil
92100
}

vochain/indexer/db/blocks.sql.go

Lines changed: 43 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vochain/indexer/db/db.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vochain/indexer/indexer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ func (idx *Indexer) ReindexBlocks(inTest bool) {
444444
return
445445
}
446446

447-
idxBlockCount, err := idx.CountBlocks()
447+
idxBlockCount, err := idx.CountBlocks("", "", "")
448448
if err != nil {
449449
log.Warnf("indexer CountBlocks returned error: %s", err)
450450
}

vochain/indexer/queries/blocks.sql

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ SELECT * FROM blocks
2121
WHERE hash = ?
2222
LIMIT 1;
2323

24+
-- name: LastBlockHeight :one
25+
SELECT height FROM blocks
26+
ORDER BY height DESC
27+
LIMIT 1;
28+
2429
-- name: SearchBlocks :many
2530
SELECT
2631
b.*,
27-
COUNT(t.block_index) AS tx_count,
28-
COUNT(*) OVER() AS total_count
32+
COUNT(t.block_index) AS tx_count
2933
FROM blocks AS b
3034
LEFT JOIN transactions AS t
3135
ON b.height = t.block_height
@@ -44,3 +48,18 @@ GROUP BY b.height
4448
ORDER BY b.height DESC
4549
LIMIT sqlc.arg(limit)
4650
OFFSET sqlc.arg(offset);
51+
52+
-- name: CountBlocks :one
53+
SELECT COUNT(*)
54+
FROM blocks AS b
55+
WHERE (
56+
(sqlc.arg(chain_id) = '' OR b.chain_id = sqlc.arg(chain_id))
57+
AND LENGTH(sqlc.arg(hash_substr)) <= 64 -- if passed arg is longer, then just abort the query
58+
AND (
59+
sqlc.arg(hash_substr) = ''
60+
OR (LENGTH(sqlc.arg(hash_substr)) = 64 AND LOWER(HEX(b.hash)) = LOWER(sqlc.arg(hash_substr)))
61+
OR (LENGTH(sqlc.arg(hash_substr)) < 64 AND INSTR(LOWER(HEX(b.hash)), LOWER(sqlc.arg(hash_substr))) > 0)
62+
-- TODO: consider keeping an hash_hex column for faster searches
63+
)
64+
AND (sqlc.arg(proposer_address) = '' OR LOWER(HEX(b.proposer_address)) = LOWER(sqlc.arg(proposer_address)))
65+
);

0 commit comments

Comments
 (0)