forked from TrueCloudLab/neoneo-go
stackitem: cache visited items while marshaling json
``` name old time/op new time/op delta ToJSON-8 4.52ms ± 4% 0.05ms ±34% -98.89% (p=0.000 n=8+10) name old alloc/op new alloc/op delta ToJSON-8 2.13MB ± 0% 0.40MB ± 0% -81.44% (p=0.000 n=9+6) name old allocs/op new allocs/op delta ToJSON-8 65.6k ± 0% 0.0k ± 0% -99.95% (p=0.000 n=10+10) ``` Signed-off-by: Evgeniy Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
parent
69cdd5252a
commit
dc0a17dd0e
1 changed files with 62 additions and 45 deletions
|
@ -9,8 +9,6 @@ import (
|
|||
gio "io"
|
||||
"math"
|
||||
"math/big"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
)
|
||||
|
||||
// decoder is a wrapper around json.Decoder helping to mimic C# json decoder behaviour.
|
||||
|
@ -43,87 +41,106 @@ var ErrTooDeep = errors.New("too deep")
|
|||
// Array, Struct -> array
|
||||
// Map -> map with keys as UTF-8 bytes
|
||||
func ToJSON(item Item) ([]byte, error) {
|
||||
buf := io.NewBufBinWriter()
|
||||
toJSON(buf, item)
|
||||
if buf.Err != nil {
|
||||
return nil, buf.Err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
seen := make(map[Item]sliceNoPointer)
|
||||
return toJSON(nil, seen, item)
|
||||
}
|
||||
|
||||
func toJSON(buf *io.BufBinWriter, item Item) {
|
||||
w := buf.BinWriter
|
||||
if w.Err != nil {
|
||||
return
|
||||
} else if buf.Len() > MaxSize {
|
||||
w.Err = errTooBigSize
|
||||
// sliceNoPointer represents sub-slice of a known slice.
|
||||
// It doesn't contain pointer and uses less memory than `[]byte`.
|
||||
type sliceNoPointer struct {
|
||||
start, end int
|
||||
}
|
||||
|
||||
func toJSON(data []byte, seen map[Item]sliceNoPointer, item Item) ([]byte, error) {
|
||||
if len(data) > MaxSize {
|
||||
return nil, errTooBigSize
|
||||
}
|
||||
|
||||
if old, ok := seen[item]; ok {
|
||||
if len(data)+old.end-old.start > MaxSize {
|
||||
return nil, errTooBigSize
|
||||
}
|
||||
return append(data, data[old.start:old.end]...), nil
|
||||
}
|
||||
|
||||
start := len(data)
|
||||
var err error
|
||||
|
||||
switch it := item.(type) {
|
||||
case *Array, *Struct:
|
||||
w.WriteB('[')
|
||||
data = append(data, '[')
|
||||
items := it.Value().([]Item)
|
||||
for i, v := range items {
|
||||
toJSON(buf, v)
|
||||
data, err = toJSON(data, seen, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i < len(items)-1 {
|
||||
w.WriteB(',')
|
||||
data = append(data, ',')
|
||||
}
|
||||
}
|
||||
w.WriteB(']')
|
||||
data = append(data, ']')
|
||||
seen[item] = sliceNoPointer{start, len(data)}
|
||||
case *Map:
|
||||
w.WriteB('{')
|
||||
data = append(data, '{')
|
||||
for i := range it.value {
|
||||
// map key can always be converted to []byte
|
||||
// but are not always a valid UTF-8.
|
||||
writeJSONString(buf.BinWriter, it.value[i].Key)
|
||||
w.WriteBytes([]byte(`:`))
|
||||
toJSON(buf, it.value[i].Value)
|
||||
raw, err := itemToJSONString(it.value[i].Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = append(data, raw...)
|
||||
data = append(data, ':')
|
||||
data, err = toJSON(data, seen, it.value[i].Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if i < len(it.value)-1 {
|
||||
w.WriteB(',')
|
||||
data = append(data, ',')
|
||||
}
|
||||
}
|
||||
w.WriteB('}')
|
||||
data = append(data, '}')
|
||||
seen[item] = sliceNoPointer{start, len(data)}
|
||||
case *BigInteger:
|
||||
if it.value.CmpAbs(big.NewInt(MaxAllowedInteger)) == 1 {
|
||||
w.Err = fmt.Errorf("%w (MaxAllowedInteger)", ErrInvalidValue)
|
||||
return
|
||||
return nil, fmt.Errorf("%w (MaxAllowedInteger)", ErrInvalidValue)
|
||||
}
|
||||
w.WriteBytes([]byte(it.value.String()))
|
||||
data = append(data, it.value.String()...)
|
||||
case *ByteArray, *Buffer:
|
||||
writeJSONString(w, it)
|
||||
raw, err := itemToJSONString(it)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = append(data, raw...)
|
||||
case *Bool:
|
||||
if it.value {
|
||||
w.WriteBytes([]byte("true"))
|
||||
data = append(data, "true"...)
|
||||
} else {
|
||||
w.WriteBytes([]byte("false"))
|
||||
data = append(data, "false"...)
|
||||
}
|
||||
case Null:
|
||||
w.WriteBytes([]byte("null"))
|
||||
data = append(data, "null"...)
|
||||
default:
|
||||
w.Err = fmt.Errorf("%w: %s", ErrUnserializable, it.String())
|
||||
return
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnserializable, it.String())
|
||||
}
|
||||
if w.Err == nil && buf.Len() > MaxSize {
|
||||
w.Err = errTooBigSize
|
||||
if len(data) > MaxSize {
|
||||
return nil, errTooBigSize
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// writeJSONString converts it to string and writes it to w as JSON value
|
||||
// itemToJSONString converts it to string
|
||||
// surrounded in quotes with control characters escaped.
|
||||
func writeJSONString(w *io.BinWriter, it Item) {
|
||||
if w.Err != nil {
|
||||
return
|
||||
}
|
||||
func itemToJSONString(it Item) ([]byte, error) {
|
||||
s, err := ToString(it)
|
||||
if err != nil {
|
||||
w.Err = err
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
data, _ := json.Marshal(s) // error never occurs because `ToString` checks for validity
|
||||
|
||||
// ref https://github.com/neo-project/neo-modules/issues/375 and https://github.com/dotnet/runtime/issues/35281
|
||||
data = bytes.Replace(data, []byte{'+'}, []byte("\\u002B"), -1)
|
||||
|
||||
w.WriteBytes(data)
|
||||
return bytes.Replace(data, []byte{'+'}, []byte("\\u002B"), -1), nil
|
||||
}
|
||||
|
||||
// FromJSON decodes Item from JSON.
|
||||
|
|
Loading…
Reference in a new issue