copybook_codec_memory/scratch.rs
1// SPDX-License-Identifier: AGPL-3.0-or-later
2use smallvec::SmallVec;
3
4/// Small vector for digit buffers (≤32 bytes on stack)
5///
6/// Stack-allocated buffer used for packed/zoned decimal digit processing.
7/// The 32-byte inline capacity handles typical COBOL numeric fields (up to
8/// PIC S9(31)) without heap allocation.
9pub type DigitBuffer = SmallVec<[u8; 32]>;
10
11/// Reusable scratch buffers for worker threads
12///
13/// Pre-allocated buffers that can be reused across multiple record processing
14/// operations to minimize heap allocations in hot codec paths. This is a critical
15/// performance optimization that eliminates allocation overhead during high-throughput
16/// data processing.
17///
18/// # Usage Pattern
19///
20/// 1. Create once per worker thread: [`ScratchBuffers::new()`]
21/// 2. Use buffers during record processing
22/// 3. Clear buffers after each record: [`clear()`](ScratchBuffers::clear)
23/// 4. Reuse for next record (no reallocation)
24///
25/// # Performance Benefits
26///
27/// - **Zero allocations** in steady-state processing (after initial capacity growth)
28/// - **CPU cache-friendly** - buffers stay hot in L1/L2 cache
29/// - **Reduced GC pressure** - minimal heap churn
30/// - **Thread-local** - no synchronization overhead
31///
32/// # Buffer Purposes
33///
34/// - **`digit_buffer`** - Packed/zoned decimal digit extraction (stack-allocated up to 32 bytes)
35/// - **`byte_buffer`** - General-purpose byte storage (EBCDIC conversion, field data)
36/// - **`string_buffer`** - UTF-8 text processing (field values, JSON strings)
37///
38/// # Examples
39///
40/// ## Basic Usage
41///
42/// ```rust
43/// use copybook_codec_memory::ScratchBuffers;
44///
45/// let mut scratch = ScratchBuffers::new();
46///
47/// // Use buffers for processing
48/// scratch.digit_buffer.push(5);
49/// scratch.byte_buffer.extend_from_slice(b"EBCDIC data");
50/// scratch.string_buffer.push_str("converted text");
51///
52/// // Clear for next record (no deallocation)
53/// scratch.clear();
54/// assert_eq!(scratch.digit_buffer.len(), 0);
55/// assert_eq!(scratch.byte_buffer.len(), 0);
56/// assert_eq!(scratch.string_buffer.len(), 0);
57/// ```
58///
59/// ## Integration with Codec Functions
60///
61/// ```ignore
62/// use copybook_codec_memory::ScratchBuffers;
63/// use copybook_codec::{decode_record_with_scratch, DecodeOptions};
64/// use copybook_core::parse_copybook;
65///
66/// let copybook = "01 RECORD.\n 05 FIELD PIC X(10).";
67/// let schema = parse_copybook(copybook).unwrap();
68/// let data = vec![0xF1; 10]; // EBCDIC '1' repeated
69///
70/// let mut scratch = ScratchBuffers::new();
71/// let options = DecodeOptions::new();
72///
73/// // Process records in loop (reusing scratch buffers)
74/// for _ in 0..1000 {
75/// let _value = decode_record_with_scratch(&schema, &data, &options, &mut scratch).unwrap();
76/// scratch.clear(); // Reuse buffers for next record
77/// }
78/// ```
79///
80/// ## Capacity Management
81///
82/// ```rust
83/// use copybook_codec_memory::ScratchBuffers;
84///
85/// let mut scratch = ScratchBuffers::new();
86///
87/// // Pre-allocate for large records
88/// scratch.ensure_byte_capacity(8192); // 8 KB record size
89/// scratch.ensure_string_capacity(4096); // 4 KB string fields
90///
91/// // Buffers now have sufficient capacity for processing
92/// assert!(scratch.byte_buffer.capacity() >= 8192);
93/// assert!(scratch.string_buffer.capacity() >= 4096);
94/// ```
95#[derive(Debug)]
96pub struct ScratchBuffers {
97 /// Buffer for digit processing in packed/zoned decimal codecs
98 ///
99 /// Stack-allocated up to 32 bytes (typical COBOL numerics), then spills to heap.
100 pub digit_buffer: DigitBuffer,
101
102 /// General-purpose byte buffer for record processing
103 ///
104 /// Used for EBCDIC conversion, field extraction, and intermediate data storage.
105 /// Initial capacity: 1 KB.
106 pub byte_buffer: Vec<u8>,
107
108 /// String buffer for text processing
109 ///
110 /// Used for UTF-8 text fields, JSON string construction, and character conversion.
111 /// Initial capacity: 512 characters.
112 pub string_buffer: String,
113}
114
115impl ScratchBuffers {
116 /// Create new scratch buffers with reasonable initial capacity
117 ///
118 /// Allocates buffers with initial capacities optimized for typical COBOL records:
119 /// - `digit_buffer`: Stack-allocated (no heap until >32 bytes)
120 /// - `byte_buffer`: 1 KB initial heap capacity
121 /// - `string_buffer`: 512 characters initial heap capacity
122 ///
123 /// These defaults handle most COBOL record layouts without reallocation.
124 ///
125 /// # Examples
126 ///
127 /// ```rust
128 /// use copybook_codec_memory::ScratchBuffers;
129 ///
130 /// let scratch = ScratchBuffers::new();
131 /// assert_eq!(scratch.digit_buffer.len(), 0);
132 /// assert!(scratch.byte_buffer.capacity() >= 1024);
133 /// assert!(scratch.string_buffer.capacity() >= 512);
134 /// ```
135 #[inline]
136 #[must_use]
137 pub fn new() -> Self {
138 Self {
139 digit_buffer: SmallVec::new(),
140 byte_buffer: Vec::with_capacity(1024), // 1KB initial capacity
141 string_buffer: String::with_capacity(512), // 512 chars initial capacity
142 }
143 }
144
145 /// Clear all buffers for reuse
146 ///
147 /// Resets buffer lengths to zero without deallocating memory. This is the
148 /// key operation for buffer reuse - call after each record to prepare for
149 /// the next processing iteration.
150 ///
151 /// **Performance note**: This is O(1) and does not touch buffer capacity.
152 ///
153 /// # Examples
154 ///
155 /// ```rust
156 /// use copybook_codec_memory::ScratchBuffers;
157 ///
158 /// let mut scratch = ScratchBuffers::new();
159 ///
160 /// // Use buffers
161 /// scratch.digit_buffer.push(5);
162 /// scratch.byte_buffer.extend_from_slice(b"data");
163 /// scratch.string_buffer.push_str("text");
164 ///
165 /// // Clear for reuse (capacity unchanged)
166 /// let byte_capacity = scratch.byte_buffer.capacity();
167 /// scratch.clear();
168 ///
169 /// assert_eq!(scratch.digit_buffer.len(), 0);
170 /// assert_eq!(scratch.byte_buffer.len(), 0);
171 /// assert_eq!(scratch.string_buffer.len(), 0);
172 /// assert_eq!(scratch.byte_buffer.capacity(), byte_capacity);
173 /// ```
174 #[inline]
175 pub fn clear(&mut self) {
176 self.digit_buffer.clear();
177 self.byte_buffer.clear();
178 self.string_buffer.clear();
179 }
180
181 /// Ensure byte buffer has at least the specified capacity
182 ///
183 /// Pre-allocates byte buffer capacity to avoid reallocation during processing.
184 /// Use this when you know the maximum record size in advance.
185 ///
186 /// **Performance optimization**: Fast-path optimized for common case where
187 /// capacity is already sufficient. Growth only occurs when needed.
188 ///
189 /// # Arguments
190 ///
191 /// * `capacity` - Minimum required capacity in bytes
192 ///
193 /// # Examples
194 ///
195 /// ```rust
196 /// use copybook_codec_memory::ScratchBuffers;
197 ///
198 /// let mut scratch = ScratchBuffers::new();
199 ///
200 /// // Pre-allocate for 8 KB records
201 /// scratch.ensure_byte_capacity(8192);
202 /// assert!(scratch.byte_buffer.capacity() >= 8192);
203 ///
204 /// // No reallocation on subsequent calls with same/lower capacity
205 /// scratch.ensure_byte_capacity(4096); // No-op, already sufficient
206 /// ```
207 #[inline]
208 pub fn ensure_byte_capacity(&mut self, capacity: usize) {
209 // Fast path: most allocations don't need to grow the buffer
210 if self.byte_buffer.capacity() < capacity {
211 // Cold path: grow the buffer
212 self.grow_byte_buffer(capacity);
213 }
214 }
215
216 /// Ensure string buffer has at least the specified capacity
217 ///
218 /// Pre-allocates string buffer capacity to avoid reallocation during text processing.
219 /// Use this when you know the maximum string field size in advance.
220 ///
221 /// **Performance optimization**: Fast-path optimized for common case where
222 /// capacity is already sufficient. Growth only occurs when needed.
223 ///
224 /// # Arguments
225 ///
226 /// * `capacity` - Minimum required capacity in characters (not bytes)
227 ///
228 /// # Examples
229 ///
230 /// ```rust
231 /// use copybook_codec_memory::ScratchBuffers;
232 ///
233 /// let mut scratch = ScratchBuffers::new();
234 ///
235 /// // Pre-allocate for 4096 character fields
236 /// scratch.ensure_string_capacity(4096);
237 /// assert!(scratch.string_buffer.capacity() >= 4096);
238 ///
239 /// // No reallocation on subsequent calls with same/lower capacity
240 /// scratch.ensure_string_capacity(2048); // No-op, already sufficient
241 /// ```
242 #[inline]
243 pub fn ensure_string_capacity(&mut self, capacity: usize) {
244 // Fast path: most allocations don't need to grow the buffer
245 if self.string_buffer.capacity() < capacity {
246 // Cold path: grow the buffer
247 self.grow_string_buffer(capacity);
248 }
249 }
250
251 /// Cold path: grow byte buffer (separate function for better optimization)
252 ///
253 /// Uses `reserve(capacity - len)` to ensure final capacity >= requested.
254 /// Note: `reserve(additional)` guarantees `capacity >= len + additional`,
255 /// so we compute based on `len`, not current capacity.
256 #[cold]
257 #[inline(never)]
258 fn grow_byte_buffer(&mut self, capacity: usize) {
259 let len = self.byte_buffer.len();
260 if capacity > len {
261 self.byte_buffer.reserve(capacity - len);
262 }
263 }
264
265 /// Cold path: grow string buffer (separate function for better optimization)
266 ///
267 /// Uses `reserve(capacity - len)` to ensure final capacity >= requested.
268 /// Note: `reserve(additional)` guarantees `capacity >= len + additional`,
269 /// so we compute based on `len`, not current capacity.
270 #[cold]
271 #[inline(never)]
272 fn grow_string_buffer(&mut self, capacity: usize) {
273 let len = self.string_buffer.len();
274 if capacity > len {
275 self.string_buffer.reserve(capacity - len);
276 }
277 }
278}
279
280impl Default for ScratchBuffers {
281 #[inline]
282 fn default() -> Self {
283 Self::new()
284 }
285}