Skip to content

Commit 448a61f

Browse files
tamirmsclaude
andcommitted
xdr: comprehensive XDR decoding optimization
Regenerate XDR bindings and update handwritten code with multiple performance optimizations for XDR decoding. Key changes: 1. Buffer-based decoding - xdr.Unmarshal now takes []byte instead of io.Reader - Direct buffer access eliminates io.Reader dispatch overhead - Use xdr.Decoder directly for primitive decoding in strkey package - New Decoder.Decode() method simplifies decode calls 2. Union value-type optimization - Primitive union arms (Bool, Int32, Uint32, Int64, Uint64, etc.) stored as values instead of pointers, reducing allocations - Complex union arms (ScVec, ScMap, structs) remain as pointers - Direct field access for union arms instead of accessor methods 3. Generated accessor improvements - GetX() accessors return (value, bool) for optional handling - MustX() accessors for cases where arm is known to be set 4. Object pooling support - XDR types support pooling via Pool[T] generic type - Enables reuse of decoded objects to reduce GC pressure Performance improvement (LedgerCloseMeta decode, 100 varied ledgers): Without pooling: - Latency: 5.31ms → 3.45ms (-35%) - Throughput: 277 MB/s → 426 MB/s (+54%) - Memory: 8.06 MB → 7.46 MB (-7.5%) - Allocations: 107.7k → 90.7k (-16%) With pooling: - Latency: 5.31ms → 2.58ms (-51%) - Throughput: 277 MB/s → 570 MB/s (+106%) - Memory: 8.06 MB → 2.64 MB (-67%) - Allocations: 107.7k → 33.6k (-69%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ba09a6a commit 448a61f

43 files changed

Lines changed: 6166 additions & 7518 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ debug
1919
*.index
2020
*.xdr
2121
*.db
22+
xdr/testdata/benchmark-ledgers.xdr.zst
2223
*.conf
2324
*.lock
2425
.proto_checksums

benchmarks/xdr_test.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,10 @@ var gxdrInput = func() gxdr.TransactionEnvelope {
3737
}()
3838

3939
func BenchmarkXDRUnmarshalWithReflection(b *testing.B) {
40-
var (
41-
r bytes.Reader
42-
te xdr.TransactionEnvelope
43-
)
40+
var te xdr.TransactionEnvelope
4441
b.ReportAllocs()
4542
for i := 0; i < b.N; i++ {
46-
r.Reset(input)
47-
_, _ = xdr3.Unmarshal(&r, &te)
43+
_, _ = xdr3.Unmarshal(input, &te)
4844
}
4945
}
5046

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ module github.com/stellar/go-stellar-sdk
22

33
go 1.24.0
44

5+
toolchain go1.24.9
6+
57
require (
68
cloud.google.com/go/storage v1.42.0
79
github.com/BurntSushi/toml v1.3.2
@@ -35,7 +37,7 @@ require (
3537
github.com/spf13/cobra v1.7.0
3638
github.com/spf13/pflag v1.0.5
3739
github.com/spf13/viper v1.17.0
38-
github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2
40+
github.com/stellar/go-xdr v0.0.0-20260106211653-82e681bfd3f3
3941
github.com/stretchr/testify v1.10.0
4042
github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8
4143
github.com/xdrpp/goxdr v0.1.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
453453
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
454454
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
455455
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
456-
github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE=
457-
github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps=
456+
github.com/stellar/go-xdr v0.0.0-20260106211653-82e681bfd3f3 h1:uPJuFJUZl57C0I9/3ZPKXl5q6G8SskeVt9H5mJkaXXg=
457+
github.com/stellar/go-xdr v0.0.0-20260106211653-82e681bfd3f3/go.mod h1:ZSIhPj0Ya41YoY0K4msNqwEQyJW/kCzjNAcZJH+uQO0=
458458
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
459459
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
460460
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

historyarchive/archive_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ func TestXdrDecode(t *testing.T) {
517517
assert.Equal(t, len(xdrbytes), 152)
518518

519519
var tmp xdr.BucketEntry
520-
n, err := xdr.Unmarshal(bytes.NewReader(xdrbytes[:]), &tmp)
520+
n, err := xdr.Unmarshal(xdrbytes[:], &tmp)
521521
fmt.Printf("Decoded %d bytes\n", n)
522522
if err != nil {
523523
panic(err)

ingest/change_compactor_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,9 +428,9 @@ func (s *TestChangeCompactorExistingRestoredSuite) SetupTest() {
428428
Type: xdr.ScAddressTypeScAddressTypeContract,
429429
ContractId: &xdr.ContractId{0xca, 0xfe},
430430
},
431-
Key: xdr.ScVal{Type: xdr.ScValTypeScvBool, B: &val},
431+
Key: xdr.ScVal{Type: xdr.ScValTypeScvBool, B: val},
432432
Durability: xdr.ContractDataDurabilityPersistent,
433-
Val: xdr.ScVal{Type: xdr.ScValTypeScvBool, B: &val},
433+
Val: xdr.ScVal{Type: xdr.ScValTypeScvBool, B: val},
434434
},
435435
},
436436
}

ingest/change_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,14 @@ func TestSortChanges(t *testing.T) {
216216
}
217217

218218
func createContractDataEntry() *xdr.ContractDataEntry {
219-
scVal := true
220219
return &xdr.ContractDataEntry{
221220
Contract: xdr.ScAddress{
222221
Type: xdr.ScAddressTypeScAddressTypeContract,
223222
ContractId: &xdr.ContractId{0xca},
224223
},
225224
Key: xdr.ScVal{
226225
Type: xdr.ScValTypeScvBool,
227-
B: &scVal,
226+
B: true,
228227
},
229228
}
230229
}

ingest/checkpoint_change_reader_test.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,9 @@ func (s *CheckpointChangeReaderTestSuite) TearDownTest() {
7979
// TestSimple test reading buckets with a single live entry.
8080
func (s *CheckpointChangeReaderTestSuite) TestSimple() {
8181
meta := metaEntry(23)
82-
liveType := xdr.BucketListTypeLive
8382
meta.MetaEntry.Ext = xdr.BucketMetadataExt{
8483
V: 1,
85-
BucketListType: &liveType,
84+
BucketListType: xdr.BucketListTypeLive,
8685
}
8786
curr1 := createXdrStream(
8887
meta,
@@ -117,10 +116,9 @@ func (s *CheckpointChangeReaderTestSuite) TestSimple() {
117116

118117
func (s *CheckpointChangeReaderTestSuite) TestReadAfterClose() {
119118
meta := metaEntry(23)
120-
liveType := xdr.BucketListTypeLive
121119
meta.MetaEntry.Ext = xdr.BucketMetadataExt{
122120
V: 1,
123-
BucketListType: &liveType,
121+
BucketListType: xdr.BucketListTypeLive,
124122
}
125123
curr1 := createXdrStream(
126124
meta,
@@ -169,10 +167,9 @@ func (s *CheckpointChangeReaderTestSuite) TestReadAfterClose() {
169167

170168
func (s *CheckpointChangeReaderTestSuite) TestContextCanceled() {
171169
meta := metaEntry(23)
172-
liveType := xdr.BucketListTypeLive
173170
meta.MetaEntry.Ext = xdr.BucketMetadataExt{
174171
V: 1,
175-
BucketListType: &liveType,
172+
BucketListType: xdr.BucketListTypeLive,
176173
}
177174
curr1 := createXdrStream(
178175
meta,
@@ -513,10 +510,9 @@ func (s *CheckpointChangeReaderTestSuite) TestMalformedProtocol11BucketNoMeta()
513510
// TestMalformedBucketListType ensures the checkpoint change reader asserts its reading from the live bucketlist
514511
func (s *CheckpointChangeReaderTestSuite) TestMalformedBucketListType() {
515512
meta := metaEntry(23)
516-
hotArchiveType := xdr.BucketListTypeHotArchive
517513
meta.MetaEntry.Ext = xdr.BucketMetadataExt{
518514
V: 1,
519-
BucketListType: &hotArchiveType,
515+
BucketListType: xdr.BucketListTypeHotArchive,
520516
}
521517
curr1 := createXdrStream(
522518
meta,

ingest/hot_archive_iterator_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,13 @@ var hasWithHotArchiveExample = `{
109109
}`
110110

111111
func hotArchiveMetaEntry(version uint32) xdr.HotArchiveBucketEntry {
112-
listType := xdr.BucketListTypeHotArchive
113112
return xdr.HotArchiveBucketEntry{
114113
Type: xdr.HotArchiveBucketEntryTypeHotArchiveMetaentry,
115114
MetaEntry: &xdr.BucketMetadata{
116115
LedgerVersion: xdr.Uint32(version),
117116
Ext: xdr.BucketMetadataExt{
118117
V: 1,
119-
BucketListType: &listType,
118+
BucketListType: xdr.BucketListTypeHotArchive,
120119
},
121120
},
122121
}
@@ -383,15 +382,14 @@ func (h *HotArchiveIteratorTestSuite) TestMissingBucketListType() {
383382
}
384383

385384
func (h *HotArchiveIteratorTestSuite) TestInvalidBucketListType() {
386-
listType := xdr.BucketListTypeLive
387385
curr1 := createXdrStream(
388386
xdr.HotArchiveBucketEntry{
389387
Type: xdr.HotArchiveBucketEntryTypeHotArchiveMetaentry,
390388
MetaEntry: &xdr.BucketMetadata{
391389
LedgerVersion: xdr.Uint32(24),
392390
Ext: xdr.BucketMetadataExt{
393391
V: 1,
394-
BucketListType: &listType,
392+
BucketListType: xdr.BucketListTypeLive,
395393
},
396394
},
397395
},

ingest/ledgerbackend/buffered_meta_pipe_reader.go

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package ledgerbackend
22

33
import (
4-
"bufio"
4+
"bytes"
55
"io"
66
"time"
77

@@ -54,29 +54,28 @@ type metaResult struct {
5454
// while previous ledger are being processed.
5555
// - Limits memory usage in case of large ledgers are closed by the network.
5656
//
57-
// Internally, it keeps two buffers: bufio.Reader with binary ledger data and
58-
// buffered channel with unmarshaled xdr.LedgerCloseMeta objects ready for
59-
// processing. The first buffer removes overhead time connected to reading from
60-
// a file. The second buffer allows unmarshaling binary data into XDR objects
61-
// (which can be a bottleneck) while clients are processing previous ledgers.
57+
// Internally, it reads framed XDR data directly into a reusable buffer and uses
58+
// a reusable Decoder for optimized decoding. The buffered channel stores unmarshaled
59+
// xdr.LedgerCloseMeta objects ready for processing, allowing unmarshaling to
60+
// proceed while clients process previous ledgers.
6261
//
6362
// Finally, when a large ledger (larger than binary buffer) is closed it waits
6463
// until xdr.LedgerCloseMeta objects channel is empty. This prevents memory
6564
// exhaustion when network closes a series a large ledgers.
6665
type bufferedLedgerMetaReader struct {
67-
r *bufio.Reader
68-
c chan metaResult
69-
decoder *xdr3.Decoder
66+
r io.Reader
67+
c chan metaResult
68+
decoder *xdr3.Decoder
69+
frameBuffer bytes.Buffer
7070
}
7171

7272
// newBufferedLedgerMetaReader creates a new meta reader that will shutdown
7373
// when stellar-core terminates.
7474
func newBufferedLedgerMetaReader(reader io.Reader) *bufferedLedgerMetaReader {
75-
r := bufio.NewReaderSize(reader, metaPipeBufferSize)
7675
return &bufferedLedgerMetaReader{
7776
c: make(chan metaResult, ledgerReadAheadBufferSize),
78-
r: r,
79-
decoder: xdr3.NewDecoder(r),
77+
r: reader,
78+
decoder: xdr3.NewDecoder(nil),
8079
}
8180
}
8281

@@ -86,21 +85,41 @@ func newBufferedLedgerMetaReader(reader io.Reader) *bufferedLedgerMetaReader {
8685
// - The next ledger available in the buffer exceeds the meta pipe buffer size.
8786
// In such case the method will block until LedgerCloseMeta buffer is empty.
8887
func (b *bufferedLedgerMetaReader) readLedgerMetaFromPipe() (*xdr.LedgerCloseMeta, error) {
89-
frameLength, err := xdr.ReadFrameLength(b.decoder)
88+
frameLength, err := xdr.ReadFrameLength(b.r)
9089
if err != nil {
91-
return nil, errors.Wrap(err, "error reading frame length")
90+
if err == io.EOF {
91+
return nil, err
92+
}
93+
return nil, errors.Wrap(err, "reading frame length")
9294
}
9395

9496
for frameLength > metaPipeBufferSize && len(b.c) > 0 {
9597
// Wait for LedgerCloseMeta buffer to be cleared to minimize memory usage.
9698
<-time.After(time.Second)
9799
}
98100

101+
// Read frame data directly into reusable buffer
102+
b.frameBuffer.Reset()
103+
b.frameBuffer.Grow(int(frameLength))
104+
n, err := b.frameBuffer.ReadFrom(io.LimitReader(b.r, int64(frameLength)))
105+
if err != nil {
106+
return nil, errors.Wrap(err, "reading frame data")
107+
}
108+
if n != int64(frameLength) {
109+
return nil, errors.Errorf("read %d bytes, expected %d", n, frameLength)
110+
}
111+
112+
// Decode using reusable Decoder for optimized performance
99113
var xlcm xdr.LedgerCloseMeta
100-
_, err = xlcm.DecodeFrom(b.decoder, xdr3.DecodeDefaultMaxDepth)
114+
b.decoder.Reset(b.frameBuffer.Bytes())
115+
bytesRead, err := b.decoder.Decode(&xlcm)
101116
if err != nil {
102117
return nil, errors.Wrap(err, "unmarshaling framed LedgerCloseMeta")
103118
}
119+
if bytesRead != int(frameLength) {
120+
return nil, errors.Errorf("unmarshaled %d bytes, expected %d", bytesRead, frameLength)
121+
}
122+
104123
return &xlcm, nil
105124
}
106125

0 commit comments

Comments
 (0)