Performance improvements for the event query API (#7319)

Rework the implementation of event query parsing and execution to
improve performance and reduce memory usage.

Previous memory and CPU profiles of the pubsub service showed query
processing as a significant hotspot. While we don't have evidence that
this is visibly hurting users, fixing it is fairly easy and self-contained.

Updates #6439.

Typical benchmark results comparing the original implementation (PEG) with the reworked implementation (Custom):
```
TEST                        TIME/OP  BYTES/OP  ALLOCS/OP  SPEEDUP   MEM SAVING
BenchmarkParsePEG-12       51716 ns  526832    27
BenchmarkParseCustom-12     2167 ns    4616    17         23.8x     99.1%
BenchmarkMatchPEG-12        3086 ns    1097    22
BenchmarkMatchCustom-12    294.2 ns      64     3         10.5x     94.1%
```

Components:
* Add a basic parsing benchmark.
* Move the original query implementation to a subdirectory.
* Add lexical scanner for Query expressions.
* Add a parser for Query expressions.
* Implement query compiler.
* Add test cases based on OpenAPI examples.
* Add MustCompile to replace the original MustParse, and update usage.
This commit is contained in:
M. J. Fromberger
2021-11-29 13:08:48 -08:00
committed by GitHub
parent d1f07047ec
commit 1dca1a8f97
34 changed files with 2218 additions and 830 deletions
+6 -6
View File
@@ -38,7 +38,7 @@ func TestEventBusPublishEventTx(t *testing.T) {
query := fmt.Sprintf("tm.event='Tx' AND tx.height=1 AND tx.hash='%X' AND testType.baz=1", tx.Hash())
txsSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.MustParse(query),
Query: tmquery.MustCompile(query),
})
require.NoError(t, err)
@@ -96,7 +96,7 @@ func TestEventBusPublishEventNewBlock(t *testing.T) {
query := "tm.event='NewBlock' AND testType.baz=1 AND testType.foz=2"
blocksSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.MustParse(query),
Query: tmquery.MustCompile(query),
})
require.NoError(t, err)
@@ -205,7 +205,7 @@ func TestEventBusPublishEventTxDuplicateKeys(t *testing.T) {
sub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: fmt.Sprintf("client-%d", i),
Query: tmquery.MustParse(tc.query),
Query: tmquery.MustCompile(tc.query),
})
require.NoError(t, err)
@@ -269,7 +269,7 @@ func TestEventBusPublishEventNewBlockHeader(t *testing.T) {
query := "tm.event='NewBlockHeader' AND testType.baz=1 AND testType.foz=2"
headersSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.MustParse(query),
Query: tmquery.MustCompile(query),
})
require.NoError(t, err)
@@ -312,7 +312,7 @@ func TestEventBusPublishEventNewEvidence(t *testing.T) {
const query = `tm.event='NewEvidence'`
evSub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.MustParse(query),
Query: tmquery.MustCompile(query),
})
require.NoError(t, err)
@@ -352,7 +352,7 @@ func TestEventBusPublish(t *testing.T) {
sub, err := eventBus.SubscribeWithArgs(ctx, tmpubsub.SubscribeArgs{
ClientID: "test",
Query: tmquery.Empty{},
Query: tmquery.All,
Limit: numEventsExpected,
})
require.NoError(t, err)
+1 -1
View File
@@ -115,7 +115,7 @@ func TestBlock(t *testing.T) {
func TestTxSearch(t *testing.T) {
testHash := []byte("test")
testTx := []byte("tx")
testQuery := fmt.Sprintf("tx.hash='%s'", string(testHash))
testQuery := fmt.Sprintf("tx.hash = '%s'", string(testHash))
testTxResult := &abcitypes.TxResult{
Height: 1,
Index: 100,
+10 -12
View File
@@ -14,6 +14,7 @@ import (
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/internal/state/indexer"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
"github.com/tendermint/tendermint/types"
)
@@ -91,10 +92,7 @@ func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64,
default:
}
conditions, err := q.Conditions()
if err != nil {
return nil, fmt.Errorf("failed to parse query conditions: %w", err)
}
conditions := q.Syntax()
// If there is an exact height query, return the result immediately
// (if it exists).
@@ -158,7 +156,7 @@ func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64,
continue
}
startKey, err := orderedcode.Append(nil, c.CompositeKey, fmt.Sprintf("%v", c.Operand))
startKey, err := orderedcode.Append(nil, c.Tag, c.Arg.Value())
if err != nil {
return nil, err
}
@@ -327,7 +325,7 @@ iter:
// matched.
func (idx *BlockerIndexer) match(
ctx context.Context,
c query.Condition,
c syntax.Condition,
startKeyBz []byte,
filteredHeights map[string][]byte,
firstRun bool,
@@ -342,7 +340,7 @@ func (idx *BlockerIndexer) match(
tmpHeights := make(map[string][]byte)
switch {
case c.Op == query.OpEqual:
case c.Op == syntax.TEq:
it, err := dbm.IteratePrefix(idx.store, startKeyBz)
if err != nil {
return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
@@ -361,8 +359,8 @@ func (idx *BlockerIndexer) match(
return nil, err
}
case c.Op == query.OpExists:
prefix, err := orderedcode.Append(nil, c.CompositeKey)
case c.Op == syntax.TExists:
prefix, err := orderedcode.Append(nil, c.Tag)
if err != nil {
return nil, err
}
@@ -389,8 +387,8 @@ func (idx *BlockerIndexer) match(
return nil, err
}
case c.Op == query.OpContains:
prefix, err := orderedcode.Append(nil, c.CompositeKey)
case c.Op == syntax.TContains:
prefix, err := orderedcode.Append(nil, c.Tag)
if err != nil {
return nil, err
}
@@ -408,7 +406,7 @@ func (idx *BlockerIndexer) match(
continue
}
if strings.Contains(eventValue, c.Operand.(string)) {
if strings.Contains(eventValue, c.Arg.Value()) {
tmpHeights[string(it.Value())] = it.Value()
}
+9 -9
View File
@@ -94,39 +94,39 @@ func TestBlockIndexer(t *testing.T) {
results []int64
}{
"block.height = 100": {
q: query.MustParse("block.height = 100"),
q: query.MustCompile(`block.height = 100`),
results: []int64{},
},
"block.height = 5": {
q: query.MustParse("block.height = 5"),
q: query.MustCompile(`block.height = 5`),
results: []int64{5},
},
"begin_event.key1 = 'value1'": {
q: query.MustParse("begin_event.key1 = 'value1'"),
q: query.MustCompile(`begin_event.key1 = 'value1'`),
results: []int64{},
},
"begin_event.proposer = 'FCAA001'": {
q: query.MustParse("begin_event.proposer = 'FCAA001'"),
q: query.MustCompile(`begin_event.proposer = 'FCAA001'`),
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
},
"end_event.foo <= 5": {
q: query.MustParse("end_event.foo <= 5"),
q: query.MustCompile(`end_event.foo <= 5`),
results: []int64{2, 4},
},
"end_event.foo >= 100": {
q: query.MustParse("end_event.foo >= 100"),
q: query.MustCompile(`end_event.foo >= 100`),
results: []int64{1},
},
"block.height > 2 AND end_event.foo <= 8": {
q: query.MustParse("block.height > 2 AND end_event.foo <= 8"),
q: query.MustCompile(`block.height > 2 AND end_event.foo <= 8`),
results: []int64{4, 6, 8},
},
"begin_event.proposer CONTAINS 'FFFFFFF'": {
q: query.MustParse("begin_event.proposer CONTAINS 'FFFFFFF'"),
q: query.MustCompile(`begin_event.proposer CONTAINS 'FFFFFFF'`),
results: []int64{},
},
"begin_event.proposer CONTAINS 'FCAA001'": {
q: query.MustParse("begin_event.proposer CONTAINS 'FCAA001'"),
q: query.MustCompile(`begin_event.proposer CONTAINS 'FCAA001'`),
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
},
}
+4 -4
View File
@@ -6,7 +6,7 @@ import (
"strconv"
"github.com/google/orderedcode"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
"github.com/tendermint/tendermint/types"
)
@@ -85,10 +85,10 @@ func parseValueFromEventKey(key []byte) (string, error) {
return eventValue, nil
}
func lookForHeight(conditions []query.Condition) (int64, bool) {
func lookForHeight(conditions []syntax.Condition) (int64, bool) {
for _, c := range conditions {
if c.CompositeKey == types.BlockHeightKey && c.Op == query.OpEqual {
return c.Operand.(int64), true
if c.Tag == types.BlockHeightKey && c.Op == syntax.TEq {
return int64(c.Arg.Number()), true
}
}
+29 -15
View File
@@ -3,7 +3,7 @@ package indexer
import (
"time"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
)
// QueryRanges defines a mapping between a composite event key and a QueryRange.
@@ -77,32 +77,32 @@ func (qr QueryRange) UpperBoundValue() interface{} {
// LookForRanges returns a mapping of QueryRanges and the matching indexes in
// the provided query conditions.
func LookForRanges(conditions []query.Condition) (ranges QueryRanges, indexes []int) {
func LookForRanges(conditions []syntax.Condition) (ranges QueryRanges, indexes []int) {
ranges = make(QueryRanges)
for i, c := range conditions {
if IsRangeOperation(c.Op) {
r, ok := ranges[c.CompositeKey]
r, ok := ranges[c.Tag]
if !ok {
r = QueryRange{Key: c.CompositeKey}
r = QueryRange{Key: c.Tag}
}
switch c.Op {
case query.OpGreater:
r.LowerBound = c.Operand
case syntax.TGt:
r.LowerBound = conditionArg(c)
case query.OpGreaterEqual:
case syntax.TGeq:
r.IncludeLowerBound = true
r.LowerBound = c.Operand
r.LowerBound = conditionArg(c)
case query.OpLess:
r.UpperBound = c.Operand
case syntax.TLt:
r.UpperBound = conditionArg(c)
case query.OpLessEqual:
case syntax.TLeq:
r.IncludeUpperBound = true
r.UpperBound = c.Operand
r.UpperBound = conditionArg(c)
}
ranges[c.CompositeKey] = r
ranges[c.Tag] = r
indexes = append(indexes, i)
}
}
@@ -112,12 +112,26 @@ func LookForRanges(conditions []query.Condition) (ranges QueryRanges, indexes []
// IsRangeOperation returns a boolean signifying if a query Operator is a range
// operation or not.
func IsRangeOperation(op query.Operator) bool {
func IsRangeOperation(op syntax.Token) bool {
switch op {
case query.OpGreater, query.OpGreaterEqual, query.OpLess, query.OpLessEqual:
case syntax.TGt, syntax.TGeq, syntax.TLt, syntax.TLeq:
return true
default:
return false
}
}
func conditionArg(c syntax.Condition) interface{} {
if c.Arg == nil {
return nil
}
switch c.Arg.Type {
case syntax.TNumber:
return int64(c.Arg.Number())
case syntax.TTime, syntax.TDate:
return c.Arg.Time()
default:
return c.Arg.Value() // string
}
}
+13 -13
View File
@@ -111,39 +111,39 @@ func TestBlockFuncs(t *testing.T) {
results []int64
}{
"block.height = 100": {
q: query.MustParse("block.height = 100"),
q: query.MustCompile(`block.height = 100`),
results: []int64{},
},
"block.height = 5": {
q: query.MustParse("block.height = 5"),
q: query.MustCompile(`block.height = 5`),
results: []int64{5},
},
"begin_event.key1 = 'value1'": {
q: query.MustParse("begin_event.key1 = 'value1'"),
q: query.MustCompile(`begin_event.key1 = 'value1'`),
results: []int64{},
},
"begin_event.proposer = 'FCAA001'": {
q: query.MustParse("begin_event.proposer = 'FCAA001'"),
q: query.MustCompile(`begin_event.proposer = 'FCAA001'`),
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
},
"end_event.foo <= 5": {
q: query.MustParse("end_event.foo <= 5"),
q: query.MustCompile(`end_event.foo <= 5`),
results: []int64{2, 4},
},
"end_event.foo >= 100": {
q: query.MustParse("end_event.foo >= 100"),
q: query.MustCompile(`end_event.foo >= 100`),
results: []int64{1},
},
"block.height > 2 AND end_event.foo <= 8": {
q: query.MustParse("block.height > 2 AND end_event.foo <= 8"),
q: query.MustCompile(`block.height > 2 AND end_event.foo <= 8`),
results: []int64{4, 6, 8},
},
"begin_event.proposer CONTAINS 'FFFFFFF'": {
q: query.MustParse("begin_event.proposer CONTAINS 'FFFFFFF'"),
q: query.MustCompile(`begin_event.proposer CONTAINS 'FFFFFFF'`),
results: []int64{},
},
"begin_event.proposer CONTAINS 'FCAA001'": {
q: query.MustParse("begin_event.proposer CONTAINS 'FCAA001'"),
q: query.MustCompile(`begin_event.proposer CONTAINS 'FCAA001'`),
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
},
}
@@ -175,7 +175,7 @@ func TestTxSearchWithCancelation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number = 1"))
results, err := indexer.SearchTxEvents(ctx, query.MustCompile(`account.number = 1`))
assert.NoError(t, err)
assert.Empty(t, results)
}
@@ -249,7 +249,7 @@ func TestTxSearchDeprecatedIndexing(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.q, func(t *testing.T) {
results, err := indexer.SearchTxEvents(ctx, query.MustParse(tc.q))
results, err := indexer.SearchTxEvents(ctx, query.MustCompile(tc.q))
require.NoError(t, err)
for _, txr := range results {
for _, tr := range tc.results {
@@ -273,7 +273,7 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) {
ctx := context.Background()
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number >= 1"))
results, err := indexer.SearchTxEvents(ctx, query.MustCompile(`account.number >= 1`))
assert.NoError(t, err)
assert.Len(t, results, 1)
@@ -330,7 +330,7 @@ func TestTxSearchMultipleTxs(t *testing.T) {
ctx := context.Background()
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number >= 1"))
results, err := indexer.SearchTxEvents(ctx, query.MustCompile(`account.number >= 1`))
assert.NoError(t, err)
require.Len(t, results, 3)
+17 -19
View File
@@ -14,6 +14,7 @@ import (
abci "github.com/tendermint/tendermint/abci/types"
indexer "github.com/tendermint/tendermint/internal/state/indexer"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
"github.com/tendermint/tendermint/types"
)
@@ -148,10 +149,7 @@ func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResul
filteredHashes := make(map[string][]byte)
// get a list of conditions (like "tx.height > 5")
conditions, err := q.Conditions()
if err != nil {
return nil, fmt.Errorf("error during parsing conditions from query: %w", err)
}
conditions := q.Syntax()
// if there is a hash condition, return the result immediately
hash, ok, err := lookForHash(conditions)
@@ -238,10 +236,10 @@ hashes:
return results, nil
}
func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error) {
func lookForHash(conditions []syntax.Condition) (hash []byte, ok bool, err error) {
for _, c := range conditions {
if c.CompositeKey == types.TxHashKey {
decoded, err := hex.DecodeString(c.Operand.(string))
if c.Tag == types.TxHashKey {
decoded, err := hex.DecodeString(c.Arg.Value())
return decoded, true, err
}
}
@@ -249,10 +247,10 @@ func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error)
}
// lookForHeight returns a height if there is an "height=X" condition.
func lookForHeight(conditions []query.Condition) (height int64) {
func lookForHeight(conditions []syntax.Condition) (height int64) {
for _, c := range conditions {
if c.CompositeKey == types.TxHeightKey && c.Op == query.OpEqual {
return c.Operand.(int64)
if c.Tag == types.TxHeightKey && c.Op == syntax.TEq {
return int64(c.Arg.Number())
}
}
return 0
@@ -265,7 +263,7 @@ func lookForHeight(conditions []query.Condition) (height int64) {
// NOTE: filteredHashes may be empty if no previous condition has matched.
func (txi *TxIndex) match(
ctx context.Context,
c query.Condition,
c syntax.Condition,
startKeyBz []byte,
filteredHashes map[string][]byte,
firstRun bool,
@@ -279,7 +277,7 @@ func (txi *TxIndex) match(
tmpHashes := make(map[string][]byte)
switch {
case c.Op == query.OpEqual:
case c.Op == syntax.TEq:
it, err := dbm.IteratePrefix(txi.store, startKeyBz)
if err != nil {
panic(err)
@@ -301,10 +299,10 @@ func (txi *TxIndex) match(
panic(err)
}
case c.Op == query.OpExists:
case c.Op == syntax.TExists:
// XXX: can't use startKeyBz here because c.Operand is nil
// (e.g. "account.owner/<nil>/" won't match w/ a single row)
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.CompositeKey))
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.Tag))
if err != nil {
panic(err)
}
@@ -325,11 +323,11 @@ func (txi *TxIndex) match(
panic(err)
}
case c.Op == query.OpContains:
case c.Op == syntax.TContains:
// XXX: startKey does not apply here.
// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.CompositeKey))
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.Tag))
if err != nil {
panic(err)
}
@@ -341,7 +339,7 @@ func (txi *TxIndex) match(
if err != nil {
continue
}
if strings.Contains(value, c.Operand.(string)) {
if strings.Contains(value, c.Arg.Value()) {
tmpHashes[string(it.Value())] = it.Value()
}
@@ -577,8 +575,8 @@ func prefixFromCompositeKeyAndValue(compositeKey, value string) []byte {
}
// a small utility function for getting a keys prefix based on a condition and a height
func prefixForCondition(c query.Condition, height int64) []byte {
key := prefixFromCompositeKeyAndValue(c.CompositeKey, fmt.Sprintf("%v", c.Operand))
func prefixForCondition(c syntax.Condition, height int64) []byte {
key := prefixFromCompositeKeyAndValue(c.Tag, c.Arg.Value())
if height > 0 {
var err error
key, err = orderedcode.Append(key, height)
@@ -60,7 +60,7 @@ func BenchmarkTxSearch(b *testing.B) {
}
}
txQuery := query.MustParse("transfer.address = 'address_43' AND transfer.amount = 50")
txQuery := query.MustCompile(`transfer.address = 'address_43' AND transfer.amount = 50`)
b.ResetTimer()
+5 -5
View File
@@ -131,7 +131,7 @@ func TestTxSearch(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.q, func(t *testing.T) {
results, err := indexer.Search(ctx, query.MustParse(tc.q))
results, err := indexer.Search(ctx, query.MustCompile(tc.q))
assert.NoError(t, err)
assert.Len(t, results, tc.resultsLength)
@@ -157,7 +157,7 @@ func TestTxSearchWithCancelation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
results, err := indexer.Search(ctx, query.MustParse("account.number = 1"))
results, err := indexer.Search(ctx, query.MustCompile(`account.number = 1`))
assert.NoError(t, err)
assert.Empty(t, results)
}
@@ -230,7 +230,7 @@ func TestTxSearchDeprecatedIndexing(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.q, func(t *testing.T) {
results, err := indexer.Search(ctx, query.MustParse(tc.q))
results, err := indexer.Search(ctx, query.MustCompile(tc.q))
require.NoError(t, err)
for _, txr := range results {
for _, tr := range tc.results {
@@ -254,7 +254,7 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) {
ctx := context.Background()
results, err := indexer.Search(ctx, query.MustParse("account.number >= 1"))
results, err := indexer.Search(ctx, query.MustCompile(`account.number >= 1`))
assert.NoError(t, err)
assert.Len(t, results, 1)
@@ -311,7 +311,7 @@ func TestTxSearchMultipleTxs(t *testing.T) {
ctx := context.Background()
results, err := indexer.Search(ctx, query.MustParse("account.number >= 1"))
results, err := indexer.Search(ctx, query.MustCompile(`account.number >= 1`))
assert.NoError(t, err)
require.Len(t, results, 3)