smol_bytes/bytes/strategy/compact.rs
1//! The **Compact** strategy for `Bytes`.
2//!
3//! This module provides the [`Bytes`](crate::compact::Bytes) type alias configured with the
4//! [`Compact`](crate::compact::Compact) strategy,
5//! which prioritizes **memory efficiency** by aggressively converting heap allocations back
6//! to inline storage whenever possible.
7//!
8//! # Key Characteristics
9//!
10//! - **Aggressive inlining**: Automatically converts heap→inline when data fits (≤62 bytes)
11//! - **Memory-efficient**: Minimizes heap allocations and memory overhead
12//! - **Smart optimization**: Operations like `advance()`, `truncate()`, and `split_to/off()` trigger conversions
13//! - **Best for constrained environments**: Ideal for embedded systems and memory-critical applications
14//!
15//! # When to Use
16//!
17//! Choose this strategy when:
18//!
19//! - **Memory is limited**: Embedded systems, microcontrollers, or memory-constrained environments
20//! - **Many small buffers**: You work with numerous buffers that frequently shrink over time
21//! - **Rare `Bytes` conversions**: You don't often convert to/from `bytes::Bytes`
22//! - **Allocation minimization**: You want to minimize heap allocations at the cost of occasional copies
23//!
24//! # Basic Usage
25//!
26//! ```rust
27//! use smol_bytes::{compact::Bytes, Buf};
28//!
29//! // Small data (≤62 bytes) is stored inline
30//! let small = Bytes::from_static(b"hello world");
31//! assert!(!small.is_heap());
32//!
33//! // Large data starts on heap
34//! let mut large = Bytes::from(vec![1u8; 100]);
35//! assert!(large.is_heap());
36//!
37//! // After shrinking, automatically converts to inline!
38//! large.advance(70); // 30 bytes remain
39//! assert!(!large.is_heap()); // ✓ Now inline!
40//! ```
41//!
42//! # Behavior Details
43//!
44//! ## Memory Layout (Same as Shared)
45//!
46//! ```text
47//! ┌─────────────────────────────────────────┐
48//! │ Bytes (64 bytes on stack) │
49//! ├─────────────────────────────────────────┤
50//! │ Variant: Inline (≤62 bytes) │
51//! │ ┌────────────────────────────────────┐ │
52//! │ │ [u8; 62] data │ │
53//! │ │ u8 length │ │
54//! │ │ u8 current_offset │ │
55//! │ └────────────────────────────────────┘ │
56//! │ │
57//! │ Variant: Heap (>62 bytes only) │
58//! │ ┌────────────────────────────────────┐ │
59//! │ │ bytes::Bytes (Arc<[u8]>) │ │
60//! │ └────────────────────────────────────┘ │
61//! └─────────────────────────────────────────┘
62//! ```
63//!
64//! ## Operations and Allocation Behavior
65//!
66//! ```rust
67//! use smol_bytes::{compact::Bytes, Buf};
68//!
69//! // Start with large heap allocation
70//! let mut data = Bytes::from(vec![1u8; 100]);
71//! assert!(data.is_heap());
72//!
73//! // After advance, automatically inlined (Compact strategy)
74//! data.advance(70); // 30 bytes remain
75//! assert!(!data.is_heap()); // ✓ Converted to inline!
76//! ```
77//!
78//! ## Comparison: Heap→Inline Conversion Triggers
79//!
80//! | Operation | Before | After | Conversion? |
81//! |-----------|--------|-------|-------------|
82//! | `advance(n)` | Heap (100 bytes) | Inline (30 bytes) | ✅ Yes (if ≤62 bytes remain) |
83//! | `truncate(n)` | Heap (100 bytes) | Inline (30 bytes) | ✅ Yes (if n ≤62) |
84//! | `split_to(n)` | Heap (100 bytes) | Inline (remaining) | ✅ Yes (if remaining ≤62) |
85//! | `split_off(n)` | Heap (100 bytes) | Inline (first part) | ✅ Yes (if first ≤62) |
86//! | `slice(range)` | Heap | Inline | ✅ Yes (if result ≤62) |
87//!
88//! # Performance Characteristics
89//!
90//! ## Fast Operations (O(1))
91//!
92//! - `clone()` when inline - Simple memcpy
93//! - `advance()` when staying inline or heap
94//! - `truncate()` when staying inline or heap
95//! - Operations that don't trigger conversion
96//!
97//! ## Linear Operations (O(62) - copies up to 62 bytes)
98//!
99//! - **Heap→Inline conversion** - Copies data to stack (up to 62 bytes)
100//! - `advance()` when triggering conversion
101//! - `truncate()` when triggering conversion
102//! - `split_to()` / `split_off()` when triggering conversion
103//! - `into::<Bytes>()` when inline (must copy to heap)
104//!
105//! **Note**: Since the maximum copy size is fixed at 62 bytes, these operations are very fast in practice!
106//!
107//! # Examples
108//!
109//! ## Stream Processing with Automatic Inlining
110//!
111//! ```rust
112//! use smol_bytes::{compact::Bytes, Buf};
113//!
114//! // Process incoming stream
115//! let mut buffer = Bytes::from(vec![0u8; 1024]);
116//! assert!(buffer.is_heap());
117//!
118//! // As we consume data, it automatically inlines
119//! buffer.advance(1000); // 24 bytes remain
120//! assert!(!buffer.is_heap()); // Saved memory!
121//! ```
122//!
123//! ## Memory-Efficient Buffer Pool
124//!
125//! ```rust
126//! use smol_bytes::compact::Bytes;
127//!
128//! struct BufferPool {
129//! buffers: Vec<Bytes>,
130//! }
131//!
132//! impl BufferPool {
133//! fn new() -> Self {
134//! Self { buffers: Vec::new() }
135//! }
136//!
137//! fn add(&mut self, data: Vec<u8>) {
138//! // Automatically inlines if small enough
139//! self.buffers.push(Bytes::from(data));
140//! }
141//!
142//! fn total_heap_allocations(&self) -> usize {
143//! self.buffers.iter()
144//! .filter(|b| b.is_heap())
145//! .count()
146//! }
147//! }
148//!
149//! let mut pool = BufferPool::new();
150//!
151//! // Add mix of small and large buffers
152//! pool.add(vec![1; 10]); // Inline
153//! pool.add(vec![2; 30]); // Inline
154//! pool.add(vec![3; 100]); // Heap
155//!
156//! // Only one heap allocation!
157//! assert_eq!(pool.total_heap_allocations(), 1);
158//! ```
159//!
160//! ## Truncate for Memory Savings
161//!
162//! ```rust
163//! use smol_bytes::compact::Bytes;
164//!
165//! let mut data = Bytes::from(vec![1u8; 100]);
166//! assert!(data.is_heap());
167//!
168//! // Truncate to small size - automatically inlines
169//! data.truncate(20);
170//! assert!(!data.is_heap());
171//! assert_eq!(data.len(), 20);
172//! ```
173//!
174//! ## Smart Split Operations
175//!
176//! ```rust
177//! use smol_bytes::compact::Bytes;
178//!
179//! let mut data = Bytes::from(vec![1u8; 100]);
180//!
181//! // Split off small portion - both parts optimize
182//! let first = data.split_to(30); // first: 30 bytes (inline)
183//! // data: 70 bytes (heap)
184//!
185//! assert!(!first.is_heap()); // Automatically inlined!
186//! assert!(data.is_heap()); // Still too large for inline
187//! ```
188//!
189//! # Trade-offs vs Shared ImmutableStorage
190//!
191//! ## Advantages
192//!
193//! - ✅ **Lower memory usage**: Fewer heap allocations
194//! - ✅ **Better cache locality**: More data on stack
195//! - ✅ **Fewer allocations**: Automatic heap→inline conversion
196//! - ✅ **Simpler deallocation**: Inline data needs no cleanup
197//!
198//! ## Disadvantages
199//!
200//! - ❌ **Conversion overhead**: O(62) copy when heap→inline (up to 62 bytes)
201//! - ❌ **Bytes conversion cost**: Must copy when inline
202//! - ❌ **More copies**: Cloning inline data copies all bytes
203//! - ❌ **No zero-copy for small data**: Inline can't share with `Bytes`
204//!
205//! # Benchmarks
206//!
207//! Typical performance characteristics (on x86_64):
208//!
209//! - **Heap→Inline conversion**: ~10-20ns for 62 bytes
210//! - **Inline clone**: ~5-10ns for 62 bytes
211//! - **Memory saved**: 32 bytes per buffer (no heap overhead)
212//!
213//! # Migration Guide
214//!
215//! If you're currently using `shared::Bytes` and considering switching:
216//!
217//! ```rust
218//! // Before (Shared strategy)
219//! use smol_bytes::{shared, compact, Buf};
220//!
221//! let mut data = shared::Bytes::from(vec![1u8; 100]);
222//! data.advance(70); // Still heap-allocated
223//! let bytes: bytes::Bytes = data.into(); // Zero-copy ✓
224//!
225//! // After (Compact strategy)
226//!
227//! let mut data = compact::Bytes::from(vec![1u8; 100]);
228//! data.advance(70); // Now inline! Saved memory ✓
229//! let bytes: bytes::Bytes = data.into(); // Copies 30 bytes (still fast!)
230//! ```
231//!
232//! **Rule of thumb**: If you convert to `Bytes` more than once per buffer lifetime,
233//! use `Shared`. If memory is more important than conversion speed, use `Compact`.
234
235use super::ImmutableStorage;
236use crate::{
237 buffer::{Buffer, INLINE_CAP},
238 bytes::raw::{RawBytes, Repr},
239 error::*,
240};
241use bytes::Buf;
242use core::mem;
243use core::ops::RangeBounds;
244
245#[cfg(feature = "pyo3")]
246mod python;
247#[cfg(feature = "pyo3")]
248pub use python::PyCompactBytes;
249
250#[cfg(feature = "wasm")]
251mod wasm;
252
253/// A strategy that aggressively inlines data to minimize heap allocations and memory usage.
254///
255/// # Overview
256///
257/// The `Compact` strategy prioritizes **memory efficiency** over conversion speed. It
258/// automatically converts heap-allocated data back to inline storage whenever possible
259/// (when data size ≤62 bytes) after operations like `advance()`, `truncate()`, or
260/// `split_to/off()`.
261///
262/// This makes `Compact` ideal when:
263/// - Memory footprint is critical
264/// - You want to minimize heap allocations
265/// - You're working with small, frequently-modified buffers
266/// - Conversions to `Bytes` are rare
267///
268/// # Behavior
269///
270/// - **Inline → Inline**: Small data stays inline (≤62 bytes)
271/// - **Heap → Inline**: Automatically inlines when data shrinks ≤62 bytes
272/// - **Smart optimization**: Operations like `advance()`, `truncate()`, and `split_to/off()`
273/// can trigger heap→inline conversion
274///
275/// ## Example
276///
277/// ```rust
278/// use smol_bytes::compact::Bytes;
279/// use bytes::Buf;
280///
281/// // Create heap-allocated bytes (>62 bytes)
282/// let mut data = Bytes::from(vec![1u8; 100]);
283/// assert!(data.is_heap());
284///
285/// // Advance past most data
286/// data.advance(70); // Only 30 bytes remain
287///
288/// // Automatically converted to inline! (Compact strategy saves memory)
289/// assert!(!data.is_heap());
290/// ```
291///
292/// # Comparison with Shared
293///
294/// | Operation | Compact | Shared |
295/// |-----------|---------|--------|
296/// | Heap→Inline on shrink | ✅ Yes | ❌ No |
297/// | Bytes conversion | 📋 May copy | ⚡ Zero-copy |
298/// | Memory usage | 💾 Lower | 💾 Higher |
299/// | Best for | Memory efficiency | Speed, Bytes interop |
300#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
301pub struct Compact(());
302
303impl From<bytes::Bytes> for RawBytes<Compact> {
304 fn from(bytes: bytes::Bytes) -> Self {
305 if bytes.len() <= INLINE_CAP {
306 // SAFETY: length checked against INLINE_CAP above.
307 Self::inline(unsafe { Buffer::copy_from_slice(&bytes) })
308 } else {
309 Self::heap(bytes)
310 }
311 }
312}
313
314impl ImmutableStorage for RawBytes<Compact> {
315 fn slice(&self, range: impl RangeBounds<usize>) -> Self {
316 self.try_slice(range).unwrap_or_else(|e| panic!("{e}"))
317 }
318
319 fn try_slice(&self, range: impl RangeBounds<usize>) -> Result<Self, crate::RangeOutOfBounds>
320 where
321 Self: Sized,
322 {
323 match &self.repr {
324 Repr::Inline(storage) => storage.try_slice(range).map(Self::inline),
325 Repr::Heap(bytes) => {
326 let len = bytes.len();
327 let (begin, end) = normalize_range(range, len)?;
328
329 let slen = end - begin;
330 if slen == 0 {
331 return Ok(Self::new());
332 }
333
334 if slen <= INLINE_CAP {
335 // SAFETY: `normalize_range` proves the source range is initialized,
336 // and `slen <= INLINE_CAP` proves it fits in `Buffer`.
337 return Ok(Self::inline(unsafe {
338 Buffer::copy_from_slice(&self.as_slice()[begin..end])
339 }));
340 }
341
342 Ok(Self::heap(bytes.slice(begin..end)))
343 }
344 }
345 }
346
347 fn split_to(&mut self, at: usize) -> Self {
348 let len = self.len();
349 if at == len {
350 return mem::take(self);
351 }
352 if at == 0 {
353 return Self::new();
354 }
355 assert!(at <= len, "split_to out of bounds: {:?} <= {:?}", at, len);
356
357 let remaining = len - at;
358
359 match &mut self.repr {
360 Repr::Inline(storage) => {
361 Self::inline(storage.try_split_to(at).expect("already checked bounds"))
362 }
363 Repr::Heap(bytes) => {
364 // Build the returned prefix.
365 let ret = if at <= INLINE_CAP {
366 // SAFETY: at <= INLINE_CAP, checked above.
367 Self::inline(unsafe { Buffer::copy_from_slice(&bytes[..at]) })
368 } else {
369 // Both halves are large — use native split (avoids clone+truncate).
370 // bytes::Bytes::split_to advances self for us, so we only need
371 // to check for compact-inline conversion below.
372 let prefix = bytes.split_to(at);
373 // If self's remainder fits inline, compact-convert it.
374 if remaining <= INLINE_CAP {
375 // SAFETY: remaining <= INLINE_CAP, checked above.
376 let repr = Repr::inline(unsafe { Buffer::copy_from_slice(&bytes[..]) });
377 let _ = mem::replace(&mut self.repr, repr);
378 }
379 return Self::heap(prefix);
380 };
381
382 // Prefix was copied into inline; now advance or compact self.
383 if remaining <= INLINE_CAP {
384 // SAFETY: remaining <= INLINE_CAP, checked above.
385 let repr = Repr::inline(unsafe { Buffer::copy_from_slice(&bytes[at..]) });
386 let _ = mem::replace(&mut self.repr, repr);
387 } else {
388 bytes.advance(at);
389 }
390 ret
391 }
392 }
393 }
394
395 fn split_off(&mut self, at: usize) -> Self {
396 let len = self.len();
397 if at == len {
398 return Self::new();
399 }
400 if at == 0 {
401 return mem::take(self);
402 }
403 assert!(at <= len, "split_off out of bounds: {:?} <= {:?}", at, len);
404
405 let output_size = len - at;
406
407 match &mut self.repr {
408 Repr::Inline(storage) => {
409 Self::inline(storage.try_split_off(at).expect("already checked bounds"))
410 }
411 Repr::Heap(bytes) => {
412 // Build the returned tail.
413 let ret = if output_size <= INLINE_CAP {
414 // SAFETY: output_size <= INLINE_CAP, checked above.
415 Self::inline(unsafe { Buffer::copy_from_slice(&bytes[at..]) })
416 } else {
417 // Both halves are large — use native split_off (avoids clone+advance).
418 // bytes::Bytes::split_off truncates self for us.
419 let tail = bytes.split_off(at);
420 // If self fits inline after truncation, compact-convert it.
421 if at <= INLINE_CAP {
422 // SAFETY: at <= INLINE_CAP, checked above.
423 let repr = Repr::inline(unsafe { Buffer::copy_from_slice(&bytes[..]) });
424 let _ = mem::replace(&mut self.repr, repr);
425 }
426 return Self::heap(tail);
427 };
428
429 // Tail was copied into inline; now truncate or compact self.
430 if at <= INLINE_CAP {
431 // SAFETY: at <= INLINE_CAP, checked above.
432 let repr = Repr::inline(unsafe { Buffer::copy_from_slice(&bytes[..at]) });
433 let _ = mem::replace(&mut self.repr, repr);
434 } else {
435 bytes.truncate(at);
436 }
437 ret
438 }
439 }
440 }
441
442 fn truncate(&mut self, new_len: usize) {
443 if new_len >= self.len() {
444 return;
445 }
446
447 match &mut self.repr {
448 Repr::Inline(storage) => {
449 storage.truncate(new_len);
450 }
451 Repr::Heap(bytes) => {
452 if new_len <= INLINE_CAP {
453 // SAFETY: the early return proves `new_len < bytes.len()`, and
454 // `new_len <= INLINE_CAP` proves the initialized prefix fits inline.
455 let repr = Repr::inline(unsafe { Buffer::copy_from_slice(&bytes[..new_len]) });
456 let _ = mem::replace(&mut self.repr, repr);
457 } else {
458 bytes.truncate(new_len);
459 }
460 }
461 }
462 }
463
464 fn advance(&mut self, cnt: usize) {
465 match &mut self.repr {
466 Repr::Inline(storage) => {
467 storage.advance(cnt);
468 }
469 Repr::Heap(bytes) => {
470 if cnt == 0 {
471 return;
472 }
473
474 // check if we can make inline after advance
475 let len = bytes.len();
476 assert!(
477 cnt <= len,
478 "cannot advance past `remaining`: {:?} <= {:?}",
479 cnt,
480 len,
481 );
482
483 let remaining = len - cnt;
484 if remaining <= INLINE_CAP {
485 // SAFETY: bounds checked above, and we are slicing within inline capacity.
486 let repr = Repr::inline(unsafe { Buffer::copy_from_slice(&bytes[cnt..]) });
487 let _ = mem::replace(&mut self.repr, repr);
488 } else {
489 bytes.advance(cnt);
490 }
491 }
492 }
493 }
494
495 fn copy_to_bytes(&mut self, len: usize) -> bytes::Bytes {
496 self.split_to(len).into()
497 }
498
499 fn clear(&mut self) {
500 // Compact keeps heap storage only for payloads that cannot fit inline;
501 // clearing drops the allocation instead of retaining an empty heap handle.
502 *self = Self::new();
503 }
504}
505
506/// A memory-efficient byte buffer that aggressively inlines data to minimize heap usage.
507///
508/// This type alias uses the [`Compact`] strategy.
509///
510/// # When to use
511///
512/// Use `Bytes` (with `Compact` strategy) when:
513/// - Memory footprint is critical
514/// - You're working with small, frequently-modified buffers
515/// - Heap allocations should be minimized
516/// - Conversions to `bytes::Bytes` are infrequent
517///
518/// For applications that frequently convert to/from `Bytes`, consider [`shared::Bytes`](super::shared::Bytes) instead.
519///
520/// ## Example
521///
522/// ```rust
523/// use smol_bytes::compact::Bytes;
524/// use bytes::Buf;
525///
526/// let mut data = Bytes::from(vec![1u8; 100]);
527/// assert!(data.is_heap());
528///
529/// // After advancing, automatically converts to inline
530/// data.advance(70);
531/// assert!(!data.is_heap()); // Saved memory!
532/// ```
533pub type Bytes = RawBytes<Compact>;
534
535/// A compact, memory-efficient UTF-8 string type alias using the [`Compact`] strategy.
536///
537/// This is the compact-strategy immutable UTF-8 wrapper.
538pub type Utf8Bytes = crate::utf8_bytes::Utf8Bytes<Compact>;