Skip to main content

procwire_client/protocol/
header_pool.rs

1//! Header buffer pool for zero-allocation header encoding.
2//!
3//! Provides a pool of pre-allocated 11-byte header buffers that can be
4//! reused across frame sends to avoid per-frame allocations.
5//!
6//! # Design
7//!
8//! The pool uses thread-local storage with round-robin allocation:
9//! - Each thread gets its own pool of 16 buffers
10//! - No locking or atomic operations on the hot path
11//! - Headers are overwritten in a circular fashion
12//!
13//! # Usage
14//!
15//! ```ignore
16//! use procwire_client::protocol::header_pool::HeaderPool;
17//!
18//! let mut pool = HeaderPool::new();
19//! let buf = pool.acquire();
20//! // Use buf for header encoding...
21//! ```
22//!
23//! # Note
24//!
25//! Since `Header::encode()` already returns a stack-allocated `[u8; 11]`,
26//! the main benefit of this pool is for scenarios where you need to hold
27//! multiple headers simultaneously or pass them across async boundaries.
28
29use super::wire_format::{Header, HEADER_POOL_SIZE, HEADER_SIZE};
30
31/// A pool of pre-allocated header buffers.
32///
33/// Uses round-robin allocation with no synchronization overhead.
34/// Thread-safe when used from a single thread (typical async executor usage).
35pub struct HeaderPool {
36    /// Pre-allocated header buffers.
37    buffers: [[u8; HEADER_SIZE]; HEADER_POOL_SIZE],
38    /// Current index (round-robin).
39    index: usize,
40}
41
42impl HeaderPool {
43    /// Create a new header pool with zeroed buffers.
44    #[inline]
45    pub const fn new() -> Self {
46        Self {
47            buffers: [[0u8; HEADER_SIZE]; HEADER_POOL_SIZE],
48            index: 0,
49        }
50    }
51
52    /// Acquire the next buffer from the pool (round-robin).
53    ///
54    /// Returns a mutable reference to a 11-byte buffer.
55    /// The buffer content is NOT cleared - it may contain old data.
56    #[inline]
57    pub fn acquire(&mut self) -> &mut [u8; HEADER_SIZE] {
58        let buf = &mut self.buffers[self.index];
59        self.index = (self.index + 1) % HEADER_POOL_SIZE;
60        buf
61    }
62
63    /// Acquire and encode a header into a pooled buffer.
64    ///
65    /// This is a convenience method that combines `acquire()` and encoding.
66    #[inline]
67    pub fn encode(&mut self, header: &Header) -> &[u8; HEADER_SIZE] {
68        let buf = self.acquire();
69        header.encode_into(buf);
70        buf
71    }
72
73    /// Get the current index (for debugging).
74    #[inline]
75    pub fn current_index(&self) -> usize {
76        self.index
77    }
78
79    /// Reset the pool index to 0.
80    #[inline]
81    pub fn reset(&mut self) {
82        self.index = 0;
83    }
84}
85
86impl Default for HeaderPool {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92// Thread-local pool for truly zero-contention access
93thread_local! {
94    static THREAD_LOCAL_POOL: std::cell::RefCell<HeaderPool> =
95        const { std::cell::RefCell::new(HeaderPool::new()) };
96}
97
98/// Encode a header using the thread-local pool.
99///
100/// This returns a copy of the encoded header since we can't safely
101/// return a reference to thread-local data across await points.
102///
103/// For most use cases, prefer `Header::encode()` directly since it
104/// also returns a stack-allocated array with no heap allocation.
105#[inline]
106pub fn encode_header_pooled(header: &Header) -> [u8; HEADER_SIZE] {
107    THREAD_LOCAL_POOL.with(|pool| {
108        let mut pool = pool.borrow_mut();
109        let buf = pool.acquire();
110        header.encode_into(buf);
111        *buf
112    })
113}
114
115/// Access the thread-local header pool directly.
116///
117/// Use this when you need to encode multiple headers in sequence
118/// without copying.
119///
120/// # Example
121///
122/// ```ignore
123/// with_header_pool(|pool| {
124///     let buf1 = pool.encode(&header1);
125///     let buf2 = pool.encode(&header2);
126///     // Use buf1 and buf2...
127/// });
128/// ```
129pub fn with_header_pool<F, R>(f: F) -> R
130where
131    F: FnOnce(&mut HeaderPool) -> R,
132{
133    THREAD_LOCAL_POOL.with(|pool| {
134        let mut pool = pool.borrow_mut();
135        f(&mut pool)
136    })
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_pool_creation() {
145        let pool = HeaderPool::new();
146        assert_eq!(pool.current_index(), 0);
147    }
148
149    #[test]
150    fn test_pool_acquire_round_robin() {
151        let mut pool = HeaderPool::new();
152
153        for i in 0..HEADER_POOL_SIZE {
154            let _buf = pool.acquire();
155            assert_eq!(pool.current_index(), (i + 1) % HEADER_POOL_SIZE);
156        }
157
158        // Should wrap around
159        assert_eq!(pool.current_index(), 0);
160    }
161
162    #[test]
163    fn test_pool_acquire_returns_different_buffers() {
164        let mut pool = HeaderPool::new();
165
166        let buf1_ptr = pool.acquire().as_ptr();
167        let buf2_ptr = pool.acquire().as_ptr();
168        let buf3_ptr = pool.acquire().as_ptr();
169
170        assert_ne!(buf1_ptr, buf2_ptr);
171        assert_ne!(buf2_ptr, buf3_ptr);
172        assert_ne!(buf1_ptr, buf3_ptr);
173    }
174
175    #[test]
176    fn test_pool_encode() {
177        let mut pool = HeaderPool::new();
178        let header = Header::new(1, 0x03, 42, 100);
179
180        let encoded = pool.encode(&header);
181
182        // Verify encoding
183        assert_eq!(&encoded[0..2], &[0x00, 0x01]); // method_id = 1 (BE)
184        assert_eq!(encoded[2], 0x03); // flags
185        assert_eq!(&encoded[3..7], &[0x00, 0x00, 0x00, 0x2A]); // request_id = 42 (BE)
186        assert_eq!(&encoded[7..11], &[0x00, 0x00, 0x00, 0x64]); // payload_length = 100 (BE)
187    }
188
189    #[test]
190    fn test_pool_reset() {
191        let mut pool = HeaderPool::new();
192
193        pool.acquire();
194        pool.acquire();
195        pool.acquire();
196        assert_eq!(pool.current_index(), 3);
197
198        pool.reset();
199        assert_eq!(pool.current_index(), 0);
200    }
201
202    #[test]
203    fn test_encode_header_pooled() {
204        let header = Header::new(5, 0x07, 1000, 50);
205
206        let encoded = encode_header_pooled(&header);
207
208        assert_eq!(&encoded[0..2], &[0x00, 0x05]); // method_id = 5 (BE)
209        assert_eq!(encoded[2], 0x07); // flags
210        assert_eq!(&encoded[3..7], &[0x00, 0x00, 0x03, 0xE8]); // request_id = 1000 (BE)
211        assert_eq!(&encoded[7..11], &[0x00, 0x00, 0x00, 0x32]); // payload_length = 50 (BE)
212    }
213
214    #[test]
215    fn test_with_header_pool() {
216        let header1 = Header::new(1, 0x00, 1, 10);
217        let header2 = Header::new(2, 0x00, 2, 20);
218
219        let (enc1, enc2) = with_header_pool(|pool| {
220            let e1 = *pool.encode(&header1);
221            let e2 = *pool.encode(&header2);
222            (e1, e2)
223        });
224
225        // Verify both headers encoded correctly
226        assert_eq!(&enc1[0..2], &[0x00, 0x01]);
227        assert_eq!(&enc2[0..2], &[0x00, 0x02]);
228    }
229
230    #[test]
231    fn test_pool_default() {
232        let pool = HeaderPool::default();
233        assert_eq!(pool.current_index(), 0);
234    }
235
236    #[test]
237    fn test_pool_buffers_are_mutable() {
238        let mut pool = HeaderPool::new();
239
240        let buf = pool.acquire();
241        buf[0] = 0xFF;
242        buf[10] = 0xAA;
243
244        // Verify we can write to the buffer
245        assert_eq!(buf[0], 0xFF);
246        assert_eq!(buf[10], 0xAA);
247    }
248
249    #[test]
250    fn test_pool_wrap_around_overwrites() {
251        let mut pool = HeaderPool::new();
252
253        // Fill first buffer with a pattern
254        {
255            let buf = pool.acquire();
256            for b in buf.iter_mut() {
257                *b = 0xAB;
258            }
259        }
260
261        // Advance to wrap around
262        for _ in 0..(HEADER_POOL_SIZE - 1) {
263            pool.acquire();
264        }
265
266        // Now we should get the first buffer again
267        let buf = pool.acquire();
268        // It should still have our pattern (pool doesn't clear)
269        assert_eq!(buf[0], 0xAB);
270    }
271}