@@ -14,18 +14,44 @@ import (
1414// the decompressor to know exactly how many bytes to read.
1515const compressedDataLenSize = 4
1616
17+ // maxPooledCompressionBytes is the maximum scratch size for which we use
18+ // pooled buffers in CompressInodes. Larger serializations allocate as needed.
19+ // maxPooledCompressionBytes is set as a multiple of the default bbolt page size (4096),
20+ // so pooled buffers efficiently match or exceed typical page-based serialization.
21+ var maxPooledCompressionBytes = 16 * DefaultPageSize
22+
1723var (
1824 zstdEncoderOnce sync.Once
1925 zstdDecoderOnce sync.Once
2026 zstdEncoder * zstd.Encoder
2127 zstdDecoder * zstd.Decoder
28+ // compressionBufPool holds buffers for serialization + EncodeAll destination.
29+ // Stored as *[]byte so Put is pointer-like and avoids allocs (SA6002).
30+ // Buffers have cap 2*maxPooledCompressionBytes so we use [0:scratchSize] for
31+ // serialization and [scratchSize:scratchSize] for EncodeAll(dst) (nil cap so
32+ // the encoder allocates its own output; sharing the backing array with src
33+ // as append target regressed benchmarks on typical page sizes).
34+ compressionBufPool = sync.Pool {
35+ New : func () interface {} {
36+ b := make ([]byte , 0 , 2 * maxPooledCompressionBytes )
37+ return & b
38+ },
39+ }
40+ // decompressBufPool holds buffers passed to DecodeAll. Stored as *[]byte (SA6002).
41+ decompressBufPool = sync.Pool {
42+ New : func () interface {} {
43+ b := make ([]byte , 0 , maxPooledCompressionBytes )
44+ return & b
45+ },
46+ }
2247)
2348
2449func getZstdEncoder () * zstd.Encoder {
2550 zstdEncoderOnce .Do (func () {
2651 var err error
2752 zstdEncoder , err = zstd .NewWriter (nil ,
2853 zstd .WithEncoderLevel (zstd .SpeedDefault ),
54+ zstd .WithEncoderCRC (true ),
2955 zstd .WithEncoderConcurrency (1 ),
3056 )
3157 if err != nil {
@@ -60,37 +86,44 @@ func getZstdDecoder() *zstd.Decoder {
6086// On success, the caller should allocate ceil(len(result)/pageSize) pages
6187// and copy the result into the allocated page buffer.
6288func CompressInodes (inodes Inodes , isLeaf bool , pageSize int ) []byte {
63- // First, figure out how large the uncompressed page would be.
64- uncompressedSize := int (PageHeaderSize )
65- var elemSize uintptr
89+ var ph Page
6690 if isLeaf {
67- elemSize = LeafPageElementSize
91+ ph . SetFlags ( LeafPageFlag )
6892 } else {
69- elemSize = BranchPageElementSize
70- }
71- for i := 0 ; i < len (inodes ); i ++ {
72- uncompressedSize += int (elemSize ) + len (inodes [i ].Key ()) + len (inodes [i ].Value ())
93+ ph .SetFlags (BranchPageFlag )
7394 }
95+ ph .SetCount (uint16 (len (inodes )))
96+ uncompressedSize := int (UsedSpaceInPage (inodes , & ph ))
7497 uncompressedPages := (uncompressedSize + pageSize - 1 ) / pageSize
7598
7699 // Serialize the inodes into a scratch buffer large enough for the
77100 // uncompressed page(s). We need a real page layout so that
78101 // WriteInodeToPage can use unsafe pointer arithmetic.
79102 scratchSize := uncompressedPages * pageSize
80- scratch := make ([]byte , scratchSize )
103+ var scratch []byte
104+ var encodeDst []byte
105+ if scratchSize <= maxPooledCompressionBytes {
106+ if ptr := compressionBufPool .Get ().(* []byte ); cap (* ptr ) >= 2 * scratchSize {
107+ pooled := * ptr
108+ scratch = pooled [:scratchSize ]
109+ encodeDst = pooled [scratchSize :scratchSize ]
110+ defer func () { compressionBufPool .Put (ptr ) }()
111+ }
112+ }
113+ if scratch == nil {
114+ scratch = make ([]byte , scratchSize )
115+ encodeDst = nil
116+ }
81117 p := (* Page )(unsafe .Pointer (& scratch [0 ]))
82118 if isLeaf {
83119 p .SetFlags (LeafPageFlag )
84120 } else {
85121 p .SetFlags (BranchPageFlag )
86122 }
87123 p .SetCount (uint16 (len (inodes )))
88- WriteInodeToPage (inodes , p )
89-
90- // Compress the data portion (everything after the page header).
91- dataSize := scratchSize - int (PageHeaderSize )
92- data := scratch [PageHeaderSize :]
93- compressed := getZstdEncoder ().EncodeAll (data [:dataSize ], nil )
124+ used := int (WriteInodeToPage (inodes , p ))
125+ data := scratch [PageHeaderSize :used ]
126+ compressed := getZstdEncoder ().EncodeAll (data , encodeDst )
94127
95128 // Total compressed page size: header + 4-byte length + compressed data.
96129 compressedTotalSize := int (PageHeaderSize ) + compressedDataLenSize + len (compressed )
@@ -139,9 +172,11 @@ func DecompressPage(p *Page, pageSize int) (*Page, []byte, error) {
139172 // Get the compressed data.
140173 compressedData := UnsafeByteSlice (unsafe .Pointer (p ), PageHeaderSize + uintptr (compressedDataLenSize ), 0 , compressedLen )
141174
142- // Decompress the data.
143- decompressed , err := getZstdDecoder ().DecodeAll (compressedData , nil )
175+ // Decompress the data, reusing a pooled buffer when possible to avoid allocs.
176+ ptr := decompressBufPool .Get ().(* []byte )
177+ decompressed , err := getZstdDecoder ().DecodeAll (compressedData , (* ptr )[:0 ])
144178 if err != nil {
179+ decompressBufPool .Put (ptr )
145180 return nil , nil , fmt .Errorf ("zstd DecodeAll on page %d: %w" , p .Id (), err )
146181 }
147182
@@ -158,6 +193,7 @@ func DecompressPage(p *Page, pageSize int) (*Page, []byte, error) {
158193
159194 // Copy the decompressed data after the header.
160195 copy (buf [PageHeaderSize :], decompressed )
196+ decompressBufPool .Put (ptr )
161197
162198 // Clear the compressed flag. Overflow is preserved from the original
163199 // page header — it reflects the on-disk allocation, not the
0 commit comments