Skip to main content

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}