-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf: aminocompat: package to allow optimizations iff amino-compatible
Implements a function "AllClear" which checks that an object is amino-json compatible and iff all clear, can permit us to directly: * use encoding/json.Marshal which produces sorted JSON instead of 3 expensive steps to produce sorted JSON: * amino.MarshalJSON * encoding/json.Unmarshal * encoding/json.Marshal and the results are stark ```shell $ benchstat before.txt after.txt name old time/op new time/op delta MsgDeposit-8 10.8µs ± 1% 1.3µs ± 1% -87.55% (p=0.000 n=10+8) name old alloc/op new alloc/op delta MsgDeposit-8 3.75kB ± 0% 0.26kB ± 0% -92.96% (p=0.000 n=10+10) name old allocs/op new allocs/op delta MsgDeposit-8 99.0 ± 0% 13.0 ± 0% -86.87% (p=0.000 n=10+10) ``` Updates #2350
- Loading branch information
Showing
4 changed files
with
303 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
## aminocompat | ||
|
||
The purpose of this code is to help with checking the logic of compatibility with go-amino before | ||
apply any optimizations. | ||
|
||
### Uses | ||
For example as per [issue #2350](https://github.com/cosmos/cosmos-sdk/issues/2350) in which it was deemed | ||
that the cosmos-sdk invoked amino.MarshalJSON then encoding/json.Unmarshal then encoding/json.Marshal so as | ||
to just get sorted JSON. An idea was floated to perhaps head directly to using encoding/json.Marshal | ||
|
||
The tricky thing is that skipping amino checks would mean that previously unsupported types like floating points, | ||
complex numbers, enums, maps would now be blindly supported, yet the module requires an explicit `.Unsafe=true` to | ||
be set. | ||
|
||
To solve that problem, we've implemented a pass `AllClear` which checks that a value's contents are amino-compatible | ||
and if all clear, permits us to use encoding/json.Marshal directly with stark improvements when used with MsgDeposit: | ||
|
||
```shell | ||
$ benchstat before.txt after.txt | ||
name old time/op new time/op delta | ||
MsgDeposit-8 10.8µs ± 1% 1.3µs ± 1% -87.55% (p=0.000 n=10+8) | ||
|
||
name old alloc/op new alloc/op delta | ||
MsgDeposit-8 3.75kB ± 0% 0.26kB ± 0% -92.96% (p=0.000 n=10+10) | ||
|
||
name old allocs/op new allocs/op delta | ||
MsgDeposit-8 99.0 ± 0% 13.0 ± 0% -86.87% (p=0.000 n=10+10) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package aminocompat | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"reflect" | ||
) | ||
|
||
var jsonMarshaler = reflect.TypeOf(new(json.Marshaler)) | ||
|
||
func AllClear(v any) error { | ||
rv := reflect.ValueOf(v) | ||
return allClear(rv) | ||
} | ||
|
||
func allClear(v reflect.Value) error { | ||
if !v.IsValid() { | ||
return errors.New("not valid") | ||
} | ||
|
||
// Derefence the pointer. | ||
for v.Kind() == reflect.Ptr { | ||
if v.IsNil() { | ||
return nil | ||
} | ||
v = reflect.Indirect(v) | ||
} | ||
|
||
// Now we can walk the value as we've dereferenced it. | ||
// 1. If it a json.Marshaler, then skip it. | ||
if vt := v.Type(); vt.Kind() == reflect.Interface && vt.Implements(jsonMarshaler) { | ||
return nil | ||
} | ||
|
||
switch vkind := v.Kind(); vkind { | ||
case reflect.Int, reflect.Uint, reflect.Int8, reflect.Uint8, reflect.Int16, reflect.Uint16, reflect.Int32, reflect.Uint32, reflect.Int64, reflect.Uint64: | ||
return nil | ||
|
||
case reflect.String: | ||
return nil | ||
|
||
case reflect.Map, reflect.Complex64, reflect.Complex128, reflect.Float32, reflect.Float64: | ||
return fmt.Errorf("not supported: %v", vkind) | ||
|
||
case reflect.Struct: | ||
// Walk the struct fields. | ||
typ := v.Type() | ||
for i, n := 0, v.NumField(); i < n; i++ { | ||
fi := v.Field(i) | ||
|
||
for fi.Kind() == reflect.Ptr { | ||
fi = reflect.Indirect(fi) | ||
} | ||
if !fi.IsValid() { | ||
return fmt.Errorf("field #%d is invalid", i) | ||
} | ||
|
||
ti := typ.Field(i) | ||
if unexportedFieldName(ti.Name) { | ||
continue | ||
} | ||
|
||
// Now check this field as well. | ||
if err := allClear(fi); err != nil { | ||
return fmt.Errorf("field #%d: %w", i, err) | ||
} | ||
} | ||
return nil | ||
|
||
case reflect.Slice, reflect.Array: | ||
// To ensure thoroughness, let's just traverse every single element. | ||
// If we've got a slice of a slice or that composition we need to iterate on that. | ||
for i, n := 0, v.Len(); i < n; i++ { | ||
evi := v.Index(i) | ||
if err := allClear(evi); err != nil { | ||
return fmt.Errorf("field #%d: %w", i, err) | ||
} | ||
} | ||
return nil | ||
|
||
case reflect.Interface: | ||
// Walk through the concrete type. | ||
return allClear(v.Elem()) | ||
} | ||
|
||
return fmt.Errorf("not supported: %v", v.Kind()) | ||
} | ||
|
||
func unexportedFieldName(name string) bool { | ||
return len(name) > 0 && name[0] >= 'a' && name[0] <= 'z' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package aminocompat | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestAllClear(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
in any | ||
wantErr string | ||
}{ | ||
{"nil", nil, "not valid"}, | ||
{"int", 12, ""}, | ||
{"string", "aminocompat", ""}, | ||
|
||
// Unsupported basics. | ||
{"map", map[int]int{}, "not supported"}, | ||
{"complex64", complex64(10 + 1i), "not supported"}, | ||
{"complex128", complex128(10 + 1i), "not supported"}, | ||
{"float32", float32(10), "not supported"}, | ||
{"float64", float64(10), "not supported"}, | ||
|
||
// Supported composites | ||
{ | ||
"struct with 8th level value", | ||
&s{ | ||
A: &s2nd{B: &s3rd{A: &s4th{A: s5th{A: s6th{A: s7th{A: &s8th{A: "8th", B: 10}}}}}}}, | ||
}, | ||
"", | ||
}, | ||
{ | ||
"struct with an unexported field but that's unsupported by amino-json", | ||
&sWithunexportedunsupported{ | ||
a: map[string]int{"a": 10}, | ||
B: 10, | ||
C: "st1", | ||
}, | ||
"", | ||
}, | ||
|
||
// Unsupported composites | ||
{ | ||
"struct with map", | ||
&sWithMap{ | ||
A: 10, | ||
B: "ab", | ||
C: map[string]int{"a": 10}, | ||
}, | ||
"not supported: map", | ||
}, | ||
|
||
{ | ||
"slice of maps", | ||
&sWithMapSlice{ | ||
A: [][]map[int]int{ | ||
{map[int]int{1: 10}}, | ||
}, | ||
}, | ||
"not supported: map", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.name, func(t *testing.T) { | ||
err := AllClear(tt.in) | ||
if tt.wantErr != "" { | ||
if err == nil { | ||
t.Fatal("expected an error") | ||
} | ||
if !strings.Contains(err.Error(), tt.wantErr) { | ||
t.Fatalf("could not find\n\t%q\nin\n\t%q", err, tt.wantErr) | ||
} | ||
return | ||
} | ||
|
||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
type s8th struct { | ||
A string | ||
B int | ||
} | ||
|
||
type s7th struct { | ||
A *s8th | ||
} | ||
|
||
type s6th struct { | ||
A s7th | ||
} | ||
|
||
type s5th struct { | ||
A s6th | ||
} | ||
|
||
type s4th struct { | ||
A s5th | ||
} | ||
type s3rd struct { | ||
A *s4th | ||
B string | ||
} | ||
type s2nd struct { | ||
B *s3rd | ||
C int | ||
} | ||
type s struct { | ||
A *s2nd | ||
B []string | ||
} | ||
|
||
type sWithMap struct { | ||
A int | ||
B string | ||
C map[string]int | ||
} | ||
|
||
type sWithunexportedunsupported struct { | ||
a map[string]int | ||
B int | ||
C string | ||
} | ||
|
||
type sWithMapSlice struct { | ||
A [][]map[int]int | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package aminocompat | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
|
||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types/v1" | ||
) | ||
|
||
var sink any | ||
|
||
var coinsPos = sdk.NewCoins(sdk.NewInt64Coin(sdk.DefaultBondDenom, 1000)) | ||
var addr = sdk.AccAddress("addr1") | ||
|
||
func BenchmarkMsgDepositGetSignBytes(b *testing.B) { | ||
b.ReportAllocs() | ||
|
||
for i := 0; i < b.N; i++ { | ||
msg := govtypes.NewMsgDeposit(addr, 0, coinsPos) | ||
sink = msg.GetSignBytes() | ||
} | ||
|
||
if sink == nil { | ||
b.Fatal("Benchmark did not run") | ||
} | ||
sink = nil | ||
} | ||
|
||
func BenchmarkMsgDepositAllClearThenJSONMarshal(b *testing.B) { | ||
b.ReportAllocs() | ||
|
||
for i := 0; i < b.N; i++ { | ||
msg := govtypes.NewMsgDeposit(addr, 0, coinsPos) | ||
if err := AllClear(msg); err != nil { | ||
b.Fatal(err) | ||
} | ||
// Straight away go to JSON marshalling. | ||
blob, err := json.Marshal(msg) | ||
if err != nil { | ||
b.Fatal(err) | ||
} | ||
sink = blob | ||
} | ||
|
||
if sink == nil { | ||
b.Fatal("Benchmark did not run") | ||
} | ||
sink = nil | ||
} |