mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-12 02:56:21 +00:00
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:
@@ -0,0 +1,213 @@
|
||||
package syntax
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Parse parses the specified query string. It is shorthand for constructing a
|
||||
// parser for s and calling its Parse method.
|
||||
func Parse(s string) (Query, error) {
|
||||
return NewParser(strings.NewReader(s)).Parse()
|
||||
}
|
||||
|
||||
// Query is the root of the parse tree for a query. A query is the conjunction
|
||||
// of one or more conditions.
|
||||
type Query []Condition
|
||||
|
||||
func (q Query) String() string {
|
||||
ss := make([]string, len(q))
|
||||
for i, cond := range q {
|
||||
ss[i] = cond.String()
|
||||
}
|
||||
return strings.Join(ss, " AND ")
|
||||
}
|
||||
|
||||
// A Condition is a single conditional expression, consisting of a tag, a
|
||||
// comparison operator, and an optional argument. The type of the argument
|
||||
// depends on the operator.
|
||||
type Condition struct {
|
||||
Tag string
|
||||
Op Token
|
||||
Arg *Arg
|
||||
|
||||
opText string
|
||||
}
|
||||
|
||||
func (c Condition) String() string {
|
||||
s := c.Tag + " " + c.opText
|
||||
if c.Arg != nil {
|
||||
return s + " " + c.Arg.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// An Arg is the argument of a comparison operator.
|
||||
type Arg struct {
|
||||
Type Token
|
||||
text string
|
||||
}
|
||||
|
||||
func (a *Arg) String() string {
|
||||
if a == nil {
|
||||
return ""
|
||||
}
|
||||
switch a.Type {
|
||||
case TString:
|
||||
return "'" + a.text + "'"
|
||||
case TTime:
|
||||
return "TIME " + a.text
|
||||
case TDate:
|
||||
return "DATE " + a.text
|
||||
default:
|
||||
return a.text
|
||||
}
|
||||
}
|
||||
|
||||
// Number returns the value of the argument text as a number, or a NaN if the
|
||||
// text does not encode a valid number value.
|
||||
func (a *Arg) Number() float64 {
|
||||
if a == nil {
|
||||
return -1
|
||||
}
|
||||
v, err := strconv.ParseFloat(a.text, 64)
|
||||
if err == nil && v >= 0 {
|
||||
return v
|
||||
}
|
||||
return math.NaN()
|
||||
}
|
||||
|
||||
// Time returns the value of the argument text as a time, or the zero value if
|
||||
// the text does not encode a timestamp or datestamp.
|
||||
func (a *Arg) Time() time.Time {
|
||||
var ts time.Time
|
||||
if a == nil {
|
||||
return ts
|
||||
}
|
||||
var err error
|
||||
switch a.Type {
|
||||
case TDate:
|
||||
ts, err = ParseDate(a.text)
|
||||
case TTime:
|
||||
ts, err = ParseTime(a.text)
|
||||
}
|
||||
if err == nil {
|
||||
return ts
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// Value returns the value of the argument text as a string, or "".
|
||||
func (a *Arg) Value() string {
|
||||
if a == nil {
|
||||
return ""
|
||||
}
|
||||
return a.text
|
||||
}
|
||||
|
||||
// Parser is a query expression parser. The grammar for query expressions is
|
||||
// defined in the syntax package documentation.
|
||||
type Parser struct {
|
||||
scanner *Scanner
|
||||
}
|
||||
|
||||
// NewParser constructs a new parser that reads the input from r.
|
||||
func NewParser(r io.Reader) *Parser {
|
||||
return &Parser{scanner: NewScanner(r)}
|
||||
}
|
||||
|
||||
// Parse parses the complete input and returns the resulting query.
|
||||
func (p *Parser) Parse() (Query, error) {
|
||||
cond, err := p.parseCond()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conds := []Condition{cond}
|
||||
for p.scanner.Next() != io.EOF {
|
||||
if tok := p.scanner.Token(); tok != TAnd {
|
||||
return nil, fmt.Errorf("offset %d: got %v, want %v", p.scanner.Pos(), tok, TAnd)
|
||||
}
|
||||
cond, err := p.parseCond()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, nil
|
||||
}
|
||||
|
||||
// parseCond parses a conditional expression: tag OP value.
|
||||
func (p *Parser) parseCond() (Condition, error) {
|
||||
var cond Condition
|
||||
if err := p.require(TTag); err != nil {
|
||||
return cond, err
|
||||
}
|
||||
cond.Tag = p.scanner.Text()
|
||||
if err := p.require(TLeq, TGeq, TLt, TGt, TEq, TContains, TExists); err != nil {
|
||||
return cond, err
|
||||
}
|
||||
cond.Op = p.scanner.Token()
|
||||
cond.opText = p.scanner.Text()
|
||||
|
||||
var err error
|
||||
switch cond.Op {
|
||||
case TLeq, TGeq, TLt, TGt:
|
||||
err = p.require(TNumber, TTime, TDate)
|
||||
case TEq:
|
||||
err = p.require(TNumber, TTime, TDate, TString)
|
||||
case TContains:
|
||||
err = p.require(TString)
|
||||
case TExists:
|
||||
// no argument
|
||||
return cond, nil
|
||||
default:
|
||||
return cond, fmt.Errorf("offset %d: unexpected operator %v", p.scanner.Pos(), cond.Op)
|
||||
}
|
||||
if err != nil {
|
||||
return cond, err
|
||||
}
|
||||
cond.Arg = &Arg{Type: p.scanner.Token(), text: p.scanner.Text()}
|
||||
return cond, nil
|
||||
}
|
||||
|
||||
// require advances the scanner and requires that the resulting token is one of
|
||||
// the specified token types.
|
||||
func (p *Parser) require(tokens ...Token) error {
|
||||
if err := p.scanner.Next(); err != nil {
|
||||
return fmt.Errorf("offset %d: %w", p.scanner.Pos(), err)
|
||||
}
|
||||
got := p.scanner.Token()
|
||||
for _, tok := range tokens {
|
||||
if tok == got {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("offset %d: got %v, wanted %s", p.scanner.Pos(), got, tokLabel(tokens))
|
||||
}
|
||||
|
||||
// tokLabel makes a human-readable summary string for the given token types.
|
||||
func tokLabel(tokens []Token) string {
|
||||
if len(tokens) == 1 {
|
||||
return tokens[0].String()
|
||||
}
|
||||
last := len(tokens) - 1
|
||||
ss := make([]string, len(tokens)-1)
|
||||
for i, tok := range tokens[:last] {
|
||||
ss[i] = tok.String()
|
||||
}
|
||||
return strings.Join(ss, ", ") + " or " + tokens[last].String()
|
||||
}
|
||||
|
||||
// ParseDate parses s as a date string in the format used by DATE values.
|
||||
func ParseDate(s string) (time.Time, error) {
|
||||
return time.Parse("2006-01-02", s)
|
||||
}
|
||||
|
||||
// ParseTime parses s as a timestamp in the format used by TIME values.
|
||||
func ParseTime(s string) (time.Time, error) {
|
||||
return time.Parse(time.RFC3339, s)
|
||||
}
|
||||
Reference in New Issue
Block a user