Skip to content

perf(saevm): Implement block.BatchedChainVM - #5788

Draft
StephenButtolph wants to merge 17 commits into
masterfrom
StephenButtolph/get-ancestors-perf
Draft

perf(saevm): Implement block.BatchedChainVM#5788
StephenButtolph wants to merge 17 commits into
masterfrom
StephenButtolph/get-ancestors-perf

Conversation

@StephenButtolph

@StephenButtolph StephenButtolph commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Why this should be merged

This reduces the time for GetAncestors to be served by the C-Chain on Fuji by ~x% (TODO: Fill me in).

For the cchainvm specifically, this reduces the time for GetAncestors to be served by the saevm by ~97%. It also reduces the time for BatchedParseBlock to be served by the saevm by ~80%.

goos: darwin
goarch: arm64
pkg: github.com/ava-labs/avalanchego/vms/saevm/sae
cpu: Apple M2 Max
BenchmarkGetAncestors/batched-12 	     730	   1691156 ns/op	 4118715 B/op	   11618 allocs/op
BenchmarkGetAncestors/serial-12  	      20	  58872404 ns/op	38040896 B/op	  539283 allocs/op
BenchmarkBatchedParseBlock/batched-12         	     100	  10989066 ns/op	31993217 B/op	  625718 allocs/op
BenchmarkBatchedParseBlock/serial-12          	      24	  50062951 ns/op	31736692 B/op	  622091 allocs/op
PASS
ok  	github.com/ava-labs/avalanchego/vms/saevm/sae	55.403s

For BatchedParseBlock, this implementation is only faster (on my machine) when at least 4 blocks are provided.

For GetAncestors, this implementation always significantly outperforms the current behavior.

How this works

VMs can optionally implement block.BatchedChainVM which is preferred by the consensus engine over repeatedly calling VM.GetBlock to serve GetAncestors requests.

Because of VM wrapping, it isn't enough just for the base VM to implement this optional interface - each wrapping VM must implement it. This VM wrapping was originally added to avoid multiple gRPC calls, so the tracedvm, metervm, proposervm, and rpcchainvm implemented this interface. However, the avm, platformvm, coreth, and subnet-evm didn't implement it. When adding cchainvm and transitionvm, we didn't implement this interface either.

By implementing this interface in transitionvm and cchainvm, the propservm will serve its optimized path, improving the performance of both pre-proposervm and post-proposervm block serving. A TODO is added to use this optimized path for post-proposervm block serving without needing the underlying VM to implement block.BatchedChainVM.

For pre-proposervm_ blocks, this delegates the implementation to the cchainvm directly. In the cchainvm, for BatchedParseBlock, we just throw the operation into multiple goroutines. For GetAncestors:

We avoid calling VM.GetBlock. This function currently falls back to vm.settledBlockFromDB for blocks on disk - which calls rawdb.ReadBlock and blocks.RestoreSettledBlock.

blocks.RestoreSettledBlock is slow because it restores the execution artifacts. Which includes reading the execution results DB, reading the receipts from disk, and deriving the receipts (which can even perform signature verification) - in addition to various unnecessary memory allocations. This buys us an ~66% performance improvement.

rawdb.ReadBlock is slow because it 2x rlp.DecodeBytes + does a bunch of useless copies to get a block, only for us to immediately rlp.EncodeToBytes. We instead use types.BlockBytes to stitch the header and body bytes together to get the block bytes with only a single allocation. This buys us an other ~90% performance improvement.

These combine to the ~97% performance improvement.

A TODO is left to investigate a further ~66% performance improvement around using DB iterators, but I worry that we are overly focusing on this micro-benchmark rather than testing in a real system before we make this implementation (significantly) more complex. Additionally, this code-path is only hit with pre-proposervm blocks.

How this was tested

  • Added unit tests
  • Added benchmarks
  • Canary on Fuji
  • Deploy to full Fuji validator set

Need to be documented in RELEASES.md?

No

@StephenButtolph StephenButtolph changed the title Stephen buttolph/get ancestors perf perf(saevm): Implement block.BatchedChainVM Aug 10, 2026
Comment thread vms/saevm/adaptor/vm.go
Comment on lines +114 to +133
// BatchedParseBlock parses each block in its own goroutine, returning an error
// if any of the blocks fail to parse.
func (vm adaptor[BP]) BatchedParseBlock(ctx context.Context, blocksBytes [][]byte) ([]snowman.Block, error) {
var (
eg errgroup.Group
parsed = make([]snowman.Block, len(blocksBytes))
)
eg.SetLimit(runtime.GOMAXPROCS(0))
for i, buf := range blocksBytes {
eg.Go(func() error {
b, err := vm.ParseBlock(ctx, buf)
parsed[i] = b
return err
})
}
if err := eg.Wait(); err != nil {
return nil, err
}
return parsed, nil
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is implemented here rather than as a pass-through because we want the C-chain's version to call it's overridden ParseBlock function. We could alternatively have re-implemented this in the C-Chain code... But that's silly.

Comment thread vms/saevm/adaptor/vm.go
Comment on lines +29 to +35
GetAncestors(
ctx context.Context,
blkID ids.ID,
maxBlocksNum int,
maxBlocksSize int,
timeout time.Duration,
) ([][]byte, error)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetAncestors really needs named arguments. For consistently I named all the arguments on this interface... Although could go back to just the types for the other methods if people care.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants