Skip to content

Commit b78ee25

Browse files
committed
pool State.MarshalBinary and use it in UpdateState — saves 2 allocs per block
State.ToProto allocated pb.State + pb.Version + timestamppb.Timestamp per block. MarshalBinary now pools those structs and returns the marshaled bytes directly. pkg/store/batch.UpdateState switched from ToProto+proto.Marshal to MarshalBinary.
1 parent 805672e commit b78ee25

3 files changed

Lines changed: 37 additions & 9 deletions

File tree

autoresearch.jsonl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
{"run":5,"commit":"0720b44","metric":74,"metrics":{"bytes_per_op":12192,"ns_per_op":31624},"status":"keep","description":"unsafe.Slice in Data.ToProto() — eliminates txsToByteSlices [][]byte allocation","timestamp":1775293190538,"segment":0,"confidence":7,"iterationTokens":9987,"asi":{"hypothesis":"Use unsafe.Slice in Data.ToProto() to avoid txsToByteSlices allocation","next_target":"Data.Hash() marshal + sha256 allocation; Header.ToProto() allocates pb.Header; Data.Size() marshals for metrics","notes":"Biggest single win yet — per-TX allocation eliminated for every protobuf encoding","result":"-3 allocs, ~8KB, ~0.5µs faster"}}
77
{"run":6,"commit":"$(git r","metric":74,"metrics":{"bytes_per_op":12187,"ns_per_op":31217},"status":"discard","description":"Reverted hand-written HashSlim wire encoder — produced different hashes than MarshalBinary","timestamp":1775294347584,"segment":0,"confidence":2.8,"iterationTokens":25063,"asi":{"hypothesis":"Direct protobuf wire encoding in HashSlim to avoid pb.Header/pb.Version allocations","result":"Hash mismatch — wire encoding differences in version field (0a02 vs 0a04)","rollback_reason":"Hand-written encoder produces different byte output than protobuf MarshalBinary; would break hash verification","next_action_hint":"Focus on safe optimizations: avoid creating pb.SignedHeader/pb.Signer structs when possible, reduce allocations in store path, look at SignedHeader.ToProto hot path"}}
88
{"run":7,"commit":"ccbc2e4","metric":64,"metrics":{"bytes_per_op":11130,"ns_per_op":31570},"status":"keep","description":"sync.Pool for protobuf message structs in MarshalBinary — eliminates 10 allocs per block","timestamp":1775296105928,"segment":0,"confidence":5.666666666666667,"iterationTokens":11490,"asi":{"hypothesis":"Pool pb.Header, pb.Version, pb.Data, pb.Metadata, pb.SignedHeader, pb.Signer, pb.State to avoid struct allocs in marshal hot path","result":"saved 10 allocs, ~1KB — from 74 to 64 allocs/op","key_files":"types/serialization.go","notes":"Only MarshalBinary uses pools (consumes result immediately). ToProto() API unchanged for external callers.","next_target":"Store path: NewBasicBatch, Put, GenerateKey, getIndexKey allocate per-store-op. Also Datastore.Put allocates."}}
9+
{"run":8,"commit":"805672e","metric":56,"metrics":{"bytes_per_op":10217,"ns_per_op":31037},"status":"keep","description":"Pooled SignedHeader.MarshalBinary — reuse pb.SignedHeader/pb.Header/pb.Signer/pb.Version structs","timestamp":1775296668832,"segment":0,"confidence":8.333333333333334,"iterationTokens":22478,"asi":{"hypothesis":"Pool protobuf structs in SignedHeader.MarshalBinary to eliminate 4 struct allocs per marshal","result":"saved 8 allocs, ~1KB — from 64 to 56 allocs/op","key_files":"types/serialization.go","next_target":"State.ToProto (11.5MB), Hash.String() for metrics (10.5MB), datastore batch/put allocs"}}

pkg/store/batch.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77

88
ds "github.com/ipfs/go-datastore"
9-
"google.golang.org/protobuf/proto"
109

1110
"github.com/evstack/ev-node/types"
1211
)
@@ -84,18 +83,14 @@ func (b *DefaultBatch) SaveBlockDataFromBytes(header *types.SignedHeader, header
8483
return nil
8584
}
8685

87-
// UpdateState updates the state in the batch
86+
// UpdateState updates the state in the batch.
87+
// Uses pooled State.MarshalBinary to reduce per-block allocations.
8888
func (b *DefaultBatch) UpdateState(state types.State) error {
89-
// Save the state at the height specified in the state itself
9089
height := state.LastBlockHeight
9190

92-
pbState, err := state.ToProto()
91+
data, err := state.MarshalBinary()
9392
if err != nil {
94-
return fmt.Errorf("failed to convert type state to protobuf type: %w", err)
95-
}
96-
data, err := proto.Marshal(pbState)
97-
if err != nil {
98-
return fmt.Errorf("failed to marshal state to protobuf: %w", err)
93+
return fmt.Errorf("failed to marshal state: %w", err)
9994
}
10095

10196
return b.batch.Put(b.ctx, ds.RawKey(getStateAtHeightKey(height)), data)

types/serialization.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,38 @@ func (d *Data) FromProto(other *pb.Data) error {
528528
return nil
529529
}
530530

531+
// MarshalBinary encodes State into binary form using pooled protobuf messages
532+
// to reduce per-block allocations in the UpdateState hot path.
533+
func (s *State) MarshalBinary() ([]byte, error) {
534+
ps := pbStatePool.Get().(*pb.State)
535+
ps.Reset()
536+
537+
pv := pbVersionPool.Get().(*pb.Version)
538+
pv.Block, pv.App = s.Version.Block, s.Version.App
539+
540+
pts := &timestamppb.Timestamp{
541+
Seconds: s.LastBlockTime.Unix(),
542+
Nanos: int32(s.LastBlockTime.Nanosecond()),
543+
}
544+
545+
ps.Version = pv
546+
ps.ChainId = s.ChainID
547+
ps.InitialHeight = s.InitialHeight
548+
ps.LastBlockHeight = s.LastBlockHeight
549+
ps.LastBlockTime = pts
550+
ps.DaHeight = s.DAHeight
551+
ps.AppHash = s.AppHash
552+
ps.LastHeaderHash = s.LastHeaderHash
553+
554+
bz, err := proto.Marshal(ps)
555+
556+
ps.Reset()
557+
pbStatePool.Put(ps)
558+
pv.Reset()
559+
pbVersionPool.Put(pv)
560+
return bz, err
561+
}
562+
531563
// ToProto converts State into protobuf representation and returns it.
532564
func (s *State) ToProto() (*pb.State, error) {
533565
// Avoid timestamppb.New allocation by constructing inline.

0 commit comments

Comments
 (0)