Merge pull request #2543 from nspcc-dev/perf-new

Minor allocation improvements
This commit is contained in:
Roman Khimov 2022-06-07 11:04:59 +03:00 committed by GitHub
commit 8673d2a79c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 63 deletions

View file

@ -202,7 +202,7 @@ func TestAppCall(t *testing.T) {
fc := fakechain.NewFakeChain()
ic := interop.NewContext(trigger.Application, fc, dao.NewSimple(storage.NewMemoryStore(), false, false),
interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, zaptest.NewLogger(t))
interop.DefaultBaseExecFee, native.DefaultStoragePrice, contractGetter, nil, nil, nil, nil, zaptest.NewLogger(t))
t.Run("valid script", func(t *testing.T) {
src := getAppCallScript(fmt.Sprintf("%#v", ih.BytesBE()))

View file

@ -1095,7 +1095,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
close(aerdone)
}()
_ = cache.GetItemCtx() // Prime serialization context cache (it'll be reused by upper layer DAOs).
aer, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache, trigger.OnPersist)
aer, v, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache, trigger.OnPersist, nil)
if err != nil {
// Release goroutines, don't care about errors, we already have one.
close(aerchan)
@ -1107,10 +1107,8 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
for _, tx := range block.Transactions {
systemInterop := bc.newInteropContext(trigger.Application, cache, block, tx)
v := systemInterop.SpawnVM()
systemInterop.ReuseVM(v)
v.LoadScriptWithFlags(tx.Script, callflag.All)
v.SetPriceGetter(systemInterop.GetPrice)
v.LoadToken = contract.LoadToken(systemInterop)
v.GasLimit = tx.SystemFee
err := systemInterop.Exec()
@ -1145,7 +1143,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
aerchan <- aer
}
aer, err = bc.runPersist(bc.contracts.GetPostPersistScript(), block, cache, trigger.PostPersist)
aer, _, err = bc.runPersist(bc.contracts.GetPostPersistScript(), block, cache, trigger.PostPersist, v)
if err != nil {
// Release goroutines, don't care about errors, we already have one.
close(aerchan)
@ -1280,15 +1278,18 @@ func (bc *Blockchain) IsExtensibleAllowed(u util.Uint160) bool {
return n < len(us)
}
func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.Simple, trig trigger.Type) (*state.AppExecResult, error) {
func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.Simple, trig trigger.Type, v *vm.VM) (*state.AppExecResult, *vm.VM, error) {
systemInterop := bc.newInteropContext(trig, cache, block, nil)
v := systemInterop.SpawnVM()
if v == nil {
v = systemInterop.SpawnVM()
} else {
systemInterop.ReuseVM(v)
}
v.LoadScriptWithFlags(script, callflag.All)
v.SetPriceGetter(systemInterop.GetPrice)
if err := systemInterop.Exec(); err != nil {
return nil, fmt.Errorf("VM has failed: %w", err)
return nil, v, fmt.Errorf("VM has failed: %w", err)
} else if _, err := systemInterop.DAO.Persist(); err != nil {
return nil, fmt.Errorf("can't save changes: %w", err)
return nil, v, fmt.Errorf("can't save changes: %w", err)
}
return &state.AppExecResult{
Container: block.Hash(), // application logs can be retrieved by block hash
@ -1299,7 +1300,7 @@ func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.S
Stack: v.Estack().ToArray(),
Events: systemInterop.Notifications,
},
}, nil
}, v, nil
}
func (bc *Blockchain) handleNotification(note *state.NotificationEvent, d *dao.Simple,
@ -1365,6 +1366,7 @@ func (bc *Blockchain) processTokenTransfer(cache *dao.Simple, transCache map[uti
if !isNEP11 {
nep17xfer = &state.NEP17Transfer{
Asset: id,
Amount: *amount,
From: from,
To: to,
Block: b.Index,
@ -1376,6 +1378,7 @@ func (bc *Blockchain) processTokenTransfer(cache *dao.Simple, transCache map[uti
nep11xfer := &state.NEP11Transfer{
NEP17Transfer: state.NEP17Transfer{
Asset: id,
Amount: *amount,
From: from,
To: to,
Block: b.Index,
@ -1388,13 +1391,14 @@ func (bc *Blockchain) processTokenTransfer(cache *dao.Simple, transCache map[uti
nep17xfer = &nep11xfer.NEP17Transfer
}
if !from.Equals(util.Uint160{}) {
_ = nep17xfer.Amount.Neg(amount) // We already have the Int.
if appendTokenTransfer(cache, transCache, from, transfer, id, b.Index, b.Timestamp, isNEP11) != nil {
_ = nep17xfer.Amount.Neg(&nep17xfer.Amount)
err := appendTokenTransfer(cache, transCache, from, transfer, id, b.Index, b.Timestamp, isNEP11)
_ = nep17xfer.Amount.Neg(&nep17xfer.Amount)
if err != nil {
return
}
}
if !to.Equals(util.Uint160{}) {
_ = nep17xfer.Amount.Set(amount) // We already have the Int.
_ = appendTokenTransfer(cache, transCache, to, transfer, id, b.Index, b.Timestamp, isNEP11) // Nothing useful we can do.
}
}
@ -1451,7 +1455,7 @@ func appendTokenTransfer(cache *dao.Simple, transCache map[util.Uint160]transfer
*nextBatch++
*currTimestamp = bTimestamp
// Put makes a copy of it anyway.
log.Raw = log.Raw[:0]
log.Reset()
}
transCache[addr] = transferData
return nil
@ -2164,9 +2168,7 @@ func (bc *Blockchain) GetEnrollments() ([]state.Validator, error) {
// GetTestVM returns an interop context with VM set up for a test run.
func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) *interop.Context {
systemInterop := bc.newInteropContext(t, bc.dao, b, tx)
vm := systemInterop.SpawnVM()
vm.SetPriceGetter(systemInterop.GetPrice)
vm.LoadToken = contract.LoadToken(systemInterop)
_ = systemInterop.SpawnVM() // All the other code suppose that the VM is ready.
return systemInterop
}
@ -2199,9 +2201,7 @@ func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transact
return nil, fmt.Errorf("failed to initialize native cache backed by historic DAO: %w", err)
}
systemInterop := bc.newInteropContext(t, dTrie, b, tx)
vm := systemInterop.SpawnVM()
vm.SetPriceGetter(systemInterop.GetPrice)
vm.LoadToken = contract.LoadToken(systemInterop)
_ = systemInterop.SpawnVM() // All the other code suppose that the VM is ready.
return systemInterop, nil
}
@ -2276,8 +2276,6 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
}
vm := interopCtx.SpawnVM()
vm.SetPriceGetter(interopCtx.GetPrice)
vm.LoadToken = contract.LoadToken(interopCtx)
vm.GasLimit = gas
if err := bc.InitVerificationContext(interopCtx, hash, witness); err != nil {
return 0, err
@ -2373,7 +2371,7 @@ func (bc *Blockchain) newInteropContext(trigger trigger.Type, d *dao.Simple, blo
// changes that were not yet persisted to Blockchain's dao.
baseStorageFee = bc.contracts.Policy.GetStoragePriceInternal(d)
}
ic := interop.NewContext(trigger, bc, d, baseExecFee, baseStorageFee, bc.contracts.Management.GetContract, bc.contracts.Contracts, block, tx, bc.log)
ic := interop.NewContext(trigger, bc, d, baseExecFee, baseStorageFee, bc.contracts.Management.GetContract, bc.contracts.Contracts, contract.LoadToken, block, tx, bc.log)
ic.Functions = systemInterops
switch {
case tx != nil:

View file

@ -64,6 +64,7 @@ type Context struct {
getContract func(*dao.Simple, util.Uint160) (*state.Contract, error)
baseExecFee int64
baseStorageFee int64
loadToken func(ic *Context, id int32) error
GetRandomCounter uint32
signers []transaction.Signer
}
@ -71,6 +72,7 @@ type Context struct {
// NewContext returns new interop context.
func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, baseStorageFee int64,
getContract func(*dao.Simple, util.Uint160) (*state.Contract, error), natives []Contract,
loadTokenFunc func(ic *Context, id int32) error,
block *block.Block, tx *transaction.Transaction, log *zap.Logger) *Context {
dao := d.GetPrivate()
cfg := bc.GetConfig()
@ -88,6 +90,7 @@ func NewContext(trigger trigger.Type, bc Ledger, d *dao.Simple, baseExecFee, bas
getContract: getContract,
baseExecFee: baseExecFee,
baseStorageFee: baseStorageFee,
loadToken: loadTokenFunc,
}
}
@ -298,6 +301,12 @@ func (ic *Context) BaseStorageFee() int64 {
return ic.baseStorageFee
}
// LoadToken wraps externally provided load-token loading function providing it with context,
// this function can then be easily used by VM.
func (ic *Context) LoadToken(id int32) error {
return ic.loadToken(ic, id)
}
// SyscallHandler handles syscall with id.
func (ic *Context) SyscallHandler(_ *vm.VM, id uint32) error {
f := ic.GetFunction(id)
@ -317,10 +326,22 @@ func (ic *Context) SyscallHandler(_ *vm.VM, id uint32) error {
// SpawnVM spawns a new VM with the specified gas limit and set context.VM field.
func (ic *Context) SpawnVM() *vm.VM {
v := vm.NewWithTrigger(ic.Trigger)
ic.initVM(v)
return v
}
func (ic *Context) initVM(v *vm.VM) {
v.LoadToken = ic.LoadToken
v.GasLimit = -1
v.SyscallHandler = ic.SyscallHandler
v.SetPriceGetter(ic.GetPrice)
ic.VM = v
return v
}
// ReuseVM resets given VM and allows to reuse it in the current context.
func (ic *Context) ReuseVM(v *vm.VM) {
v.Reset(ic.Trigger)
ic.initVM(v)
}
// RegisterCancelFunc adds the given function to the list of functions to be called after the VM

View file

@ -22,26 +22,24 @@ type policyChecker interface {
}
// LoadToken calls method specified by the token id.
func LoadToken(ic *interop.Context) func(id int32) error {
return func(id int32) error {
ctx := ic.VM.Context()
if !ctx.GetCallFlags().Has(callflag.ReadStates | callflag.AllowCall) {
return errors.New("invalid call flags")
}
tok := ctx.NEF.Tokens[id]
if int(tok.ParamCount) > ctx.Estack().Len() {
return errors.New("stack is too small")
}
args := make([]stackitem.Item, tok.ParamCount)
for i := range args {
args[i] = ic.VM.Estack().Pop().Item()
}
cs, err := ic.GetContract(tok.Hash)
if err != nil {
return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err)
}
return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args, false)
func LoadToken(ic *interop.Context, id int32) error {
ctx := ic.VM.Context()
if !ctx.GetCallFlags().Has(callflag.ReadStates | callflag.AllowCall) {
return errors.New("invalid call flags")
}
tok := ctx.NEF.Tokens[id]
if int(tok.ParamCount) > ctx.Estack().Len() {
return errors.New("stack is too small")
}
args := make([]stackitem.Item, tok.ParamCount)
for i := range args {
args[i] = ic.VM.Estack().Pop().Item()
}
cs, err := ic.GetContract(tok.Hash)
if err != nil {
return fmt.Errorf("token contract %s not found: %w", tok.Hash.StringLE(), err)
}
return callInternal(ic, cs, tok.Method, tok.CallFlag, tok.HasReturn, args, false)
}
// Call calls a contract with flags.

View file

@ -73,7 +73,7 @@ func initCheckMultisigVMNoArgs(container *transaction.Transaction) *vm.VM {
trigger.Verification,
fakechain.NewFakeChain(),
dao.NewSimple(storage.NewMemoryStore(), false, false),
interop.DefaultBaseExecFee, native.DefaultStoragePrice, nil, nil, nil,
interop.DefaultBaseExecFee, native.DefaultStoragePrice, nil, nil, nil, nil,
container,
nil)
ic.Container = container

View file

@ -16,6 +16,8 @@ const TokenTransferBatchSize = 128
// TokenTransferLog is a serialized log of token transfers.
type TokenTransferLog struct {
Raw []byte
buf *bytes.Buffer
iow *io.BinWriter
}
// NEP17Transfer represents a single NEP-17 Transfer event.
@ -111,18 +113,30 @@ func (lg *TokenTransferLog) Append(tr io.Serializable) error {
lg.Raw = append(lg.Raw, 0)
}
b := bytes.NewBuffer(lg.Raw)
w := io.NewBinWriterFromIO(b)
tr.EncodeBinary(w)
if w.Err != nil {
return w.Err
if lg.buf == nil {
lg.buf = bytes.NewBuffer(lg.Raw)
}
lg.Raw = b.Bytes()
if lg.iow == nil {
lg.iow = io.NewBinWriterFromIO(lg.buf)
}
tr.EncodeBinary(lg.iow)
if lg.iow.Err != nil {
return lg.iow.Err
}
lg.Raw = lg.buf.Bytes()
lg.Raw[0]++
return nil
}
// Reset resets the state of the log, clearing all entries, but keeping existing
// buffer for future writes.
func (lg *TokenTransferLog) Reset() {
lg.Raw = lg.Raw[:0]
lg.buf = nil
lg.iow = nil
}
// ForEachNEP11 iterates over a transfer log returning on the first error.
func (lg *TokenTransferLog) ForEachNEP11(f func(*NEP11Transfer) (bool, error)) (bool, error) {
if lg == nil || len(lg.Raw) == 0 {

View file

@ -48,3 +48,12 @@ func BenchmarkScriptPushPop(t *testing.B) {
})
}
}
func BenchmarkIsSignatureContract(t *testing.B) {
b64script := "DCED2eixa9myLTNF1tTN4xvhw+HRYVMuPQzOy5Xs4utYM25BVuezJw=="
script, err := base64.StdEncoding.DecodeString(b64script)
require.NoError(t, err)
for n := 0; n < t.N; n++ {
_ = IsSignatureContract(script)
}
}

View file

@ -118,17 +118,14 @@ func ParseSignatureContract(script []byte) ([]byte, bool) {
return nil, false
}
ctx := NewContext(script)
instr, param, err := ctx.Next()
if err != nil || instr != opcode.PUSHDATA1 || len(param) != 33 {
return nil, false
// We don't use Context for this simple case, it's more efficient this way.
if script[0] == byte(opcode.PUSHDATA1) && // PUSHDATA1
script[1] == 33 && // with a public key parameter
script[35] == byte(opcode.SYSCALL) && // and a CheckSig SYSCALL.
binary.LittleEndian.Uint32(script[36:]) == verifyInteropID {
return script[2:35], true
}
pub := param
instr, param, err = ctx.Next()
if err != nil || instr != opcode.SYSCALL || binary.LittleEndian.Uint32(param) != verifyInteropID {
return nil, false
}
return pub, true
return nil, false
}
// IsStandardContract checks whether the passed script is a signature or

View file

@ -122,6 +122,24 @@ func (v *VM) SetPriceGetter(f func(opcode.Opcode, []byte) int64) {
v.getPrice = f
}
// Reset allows to reuse existing VM for subsequent executions making them somewhat
// more efficient. It reuses invocation and evaluation stacks as well as VM structure
// itself.
func (v *VM) Reset(t trigger.Type) {
v.state = NoneState
v.getPrice = nil
v.istack.elems = v.istack.elems[:0]
v.estack.elems = v.estack.elems[:0]
v.uncaughtException = nil
v.refs = 0
v.gasConsumed = 0
v.GasLimit = 0
v.SyscallHandler = nil
v.LoadToken = nil
v.trigger = t
v.invTree = nil
}
// GasConsumed returns the amount of GAS consumed during execution.
func (v *VM) GasConsumed() int64 {
return v.gasConsumed