Skip to content

Commit

Permalink
types/Coin: compile and reuse Regexps to reduce massive RAM+CPU burn (c…
Browse files Browse the repository at this point in the history
…osmos#7989)

* types/Coin: compile and reuse Regexps to reduce massive RAM+CPU burn

Fixes a resource (CPU+RAM) burn from recompiling Regexps which would
consume 1.2+MiB of RAM and 1.1ms per ParseCoin invocation, which means
that 1,000 invocations would consume 1.2GiB of RAM and >1second.

With the change, ParseCoin now takes 3.2us down from 1.1ms, with
dramatic results:

name         old time/op    new time/op    delta
ParseCoin-8    1.04ms ± 4%    0.00ms ± 1%  -99.69%  (p=0.000 n=9+10)

name         old alloc/op   new alloc/op   delta
ParseCoin-8    1.20MB ± 0%    0.00MB ± 0%  -99.94%  (p=0.000 n=10+10)

name         old allocs/op  new allocs/op  delta
ParseCoin-8     17.8k ± 0%      0.0k ± 0%  -99.89%  (p=0.000 n=10+10)

Fixes cosmos#7986.

* replace the CoinDenomRegex variable with SetCoinDenomRegex() (cosmos#7998)

Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
Co-authored-by: Alessio Treglia <alessio@tendermint.com>
  • Loading branch information
3 people authored Nov 20, 2020
1 parent a7296d3 commit 6344d62
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 69 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/golang/mock v1.4.4
github.com/golang/protobuf v1.4.3
github.com/golang/snappy v0.0.2 // indirect
github.com/google/go-cmp v0.5.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/grpc-ecosystem/grpc-gateway v1.16.0
Expand Down
50 changes: 0 additions & 50 deletions go.sum

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions types/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package types_test

import (
"testing"

"github.com/cosmos/cosmos-sdk/types"
)

var coinStrs = []string{
"2000ATM",
"5000AMX",
"192XXX",
"1e9BTC",
}

func BenchmarkParseCoin(b *testing.B) {
var blankCoin types.Coin
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for _, coinStr := range coinStrs {
coin, err := types.ParseCoin(coinStr)
if err != nil {
b.Fatal(err)
}
if coin == blankCoin {
b.Fatal("Unexpectedly returned a blank coin")
}
}
}
}
34 changes: 19 additions & 15 deletions types/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -602,32 +602,36 @@ var (
reAmt = `[[:digit:]]+`
reDecAmt = `[[:digit:]]+(?:\.[[:digit:]]+)?|\.[[:digit:]]+`
reSpc = `[[:space:]]*`
reDnm = returnReDnm
reCoin = returnReCoin
reDecCoin = returnDecCoin
reDnm *regexp.Regexp
reCoin *regexp.Regexp
reDecCoin *regexp.Regexp
)

func returnDecCoin() *regexp.Regexp {
return regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, CoinDenomRegex()))
}
func returnReCoin() *regexp.Regexp {
return regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, CoinDenomRegex()))
}
func returnReDnm() *regexp.Regexp {
return regexp.MustCompile(fmt.Sprintf(`^%s$`, CoinDenomRegex()))
func init() {
SetCoinDenomRegex(DefaultCoinDenomRegex)
}

// DefaultCoinDenomRegex returns the default regex string
func DefaultCoinDenomRegex() string {
return reDnmString
}

// CoinDenomRegex returns the current regex string and can be overwritten for custom validation
var CoinDenomRegex = DefaultCoinDenomRegex
// coinDenomRegex returns the current regex string and can be overwritten for custom validation
var coinDenomRegex = DefaultCoinDenomRegex

// SetCoinDenomRegex allows for coin's custom validation by overriding the regular
// expression string used for denom validation.
func SetCoinDenomRegex(reFn func() string) {
coinDenomRegex = reFn

reDnm = regexp.MustCompile(fmt.Sprintf(`^%s$`, coinDenomRegex()))
reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, coinDenomRegex()))
reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, coinDenomRegex()))
}

// ValidateDenom is the default validation function for Coin.Denom.
func ValidateDenom(denom string) error {
if !reDnm().MatchString(denom) {
if !reDnm.MatchString(denom) {
return fmt.Errorf("invalid denom: %s", denom)
}
return nil
Expand All @@ -645,7 +649,7 @@ func mustValidateDenom(denom string) {
func ParseCoin(coinStr string) (coin Coin, err error) {
coinStr = strings.TrimSpace(coinStr)

matches := reCoin().FindStringSubmatch(coinStr)
matches := reCoin.FindStringSubmatch(coinStr)
if matches == nil {
return Coin{}, fmt.Errorf("invalid coin expression: %s", coinStr)
}
Expand Down
6 changes: 3 additions & 3 deletions types/coin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ func (s *coinTestSuite) TestCoinIsValid() {
func (s *coinTestSuite) TestCustomValidation() {

newDnmRegex := `[\x{1F600}-\x{1F6FF}]`
sdk.CoinDenomRegex = func() string {
sdk.SetCoinDenomRegex(func() string {
return newDnmRegex
}
})

cases := []struct {
coin sdk.Coin
Expand All @@ -119,7 +119,7 @@ func (s *coinTestSuite) TestCustomValidation() {
for i, tc := range cases {
s.Require().Equal(tc.expectPass, tc.coin.IsValid(), "unexpected result for IsValid, tc #%d", i)
}
sdk.CoinDenomRegex = sdk.DefaultCoinDenomRegex
sdk.SetCoinDenomRegex(sdk.DefaultCoinDenomRegex)
}

func (s *coinTestSuite) TestAddCoin() {
Expand Down
2 changes: 1 addition & 1 deletion types/dec_coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,7 @@ func (coins DecCoins) Sort() DecCoins {
func ParseDecCoin(coinStr string) (coin DecCoin, err error) {
coinStr = strings.TrimSpace(coinStr)

matches := reDecCoin().FindStringSubmatch(coinStr)
matches := reDecCoin.FindStringSubmatch(coinStr)
if matches == nil {
return DecCoin{}, fmt.Errorf("invalid decimal coin expression: %s", coinStr)
}
Expand Down

0 comments on commit 6344d62

Please sign in to comment.