prax_query/
memory.rs

1//! Memory optimization utilities for prax-query.
2//!
3//! This module provides memory-efficient alternatives and pooling mechanisms
4//! to reduce allocation counts and peak memory usage.
5//!
6//! # Optimization Strategies
7//!
8//! 1. **Object Pooling**: Reuse allocated objects instead of allocating new ones
9//! 2. **Compact Types**: Smaller type representations for common cases
10//! 3. **Inline Storage**: Store small data inline to avoid heap allocation
11//! 4. **String Deduplication**: Share identical strings via interning
12//!
13//! # Example
14//!
15//! ```rust
16//! use prax_query::memory::{StringPool, CompactFilter};
17//! use std::sync::Arc;
18//!
19//! // Reuse strings from a pool
20//! let pool = StringPool::new();
21//! let s1 = pool.intern("id");
22//! let s2 = pool.intern("id");
23//! // s1 and s2 point to the same allocation
24//!
25//! // Use compact filters for simple cases
26//! let filter = CompactFilter::eq_int(Arc::from("id"), 42);
27//! ```
28
29use parking_lot::Mutex;
30use std::collections::HashSet;
31use std::sync::Arc;
32use tracing::info;
33
34// ============================================================================
35// String Pool
36// ============================================================================
37
38/// A thread-safe pool for interning strings.
39///
40/// Reduces memory usage by ensuring only one copy of each unique string exists.
41#[derive(Debug, Default)]
42pub struct StringPool {
43    strings: Mutex<HashSet<Arc<str>>>,
44}
45
46impl StringPool {
47    /// Create a new empty string pool.
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Create a pool with pre-allocated capacity.
53    pub fn with_capacity(capacity: usize) -> Self {
54        info!(capacity, "StringPool initialized");
55        Self {
56            strings: Mutex::new(HashSet::with_capacity(capacity)),
57        }
58    }
59
60    /// Intern a string, returning a shared reference.
61    pub fn intern(&self, s: &str) -> Arc<str> {
62        let mut strings = self.strings.lock();
63
64        // Check if already interned
65        if let Some(existing) = strings.get(s) {
66            return Arc::clone(existing);
67        }
68
69        // Intern new string
70        let arc: Arc<str> = Arc::from(s);
71        strings.insert(Arc::clone(&arc));
72        arc
73    }
74
75    /// Get the number of interned strings.
76    pub fn len(&self) -> usize {
77        self.strings.lock().len()
78    }
79
80    /// Check if the pool is empty.
81    pub fn is_empty(&self) -> bool {
82        self.strings.lock().is_empty()
83    }
84
85    /// Clear all interned strings.
86    pub fn clear(&self) {
87        self.strings.lock().clear();
88    }
89
90    /// Get memory statistics.
91    pub fn stats(&self) -> PoolStats {
92        let strings = self.strings.lock();
93        let count = strings.len();
94        let total_bytes: usize = strings.iter().map(|s| s.len()).sum();
95        PoolStats {
96            count,
97            total_bytes,
98            avg_bytes: if count > 0 { total_bytes / count } else { 0 },
99        }
100    }
101}
102
103/// Statistics for a pool.
104#[derive(Debug, Clone, Default)]
105pub struct PoolStats {
106    /// Number of items in the pool.
107    pub count: usize,
108    /// Total bytes used.
109    pub total_bytes: usize,
110    /// Average bytes per item.
111    pub avg_bytes: usize,
112}
113
114// ============================================================================
115// Compact Filter Types
116// ============================================================================
117
118/// A compact filter representation for simple equality filters.
119///
120/// Uses less memory than the full `Filter` enum for common cases:
121/// - Equality on integer fields: 16 bytes vs ~40 bytes
122/// - Equality on string fields with short values: ~32 bytes vs ~56 bytes
123#[derive(Debug, Clone, PartialEq)]
124pub enum CompactFilter {
125    /// Equality on integer field.
126    EqInt {
127        /// Field name (interned).
128        field: Arc<str>,
129        /// Integer value.
130        value: i64,
131    },
132    /// Equality on boolean field.
133    EqBool {
134        /// Field name (interned).
135        field: Arc<str>,
136        /// Boolean value.
137        value: bool,
138    },
139    /// Equality on string field.
140    EqStr {
141        /// Field name (interned).
142        field: Arc<str>,
143        /// String value.
144        value: Arc<str>,
145    },
146    /// IS NULL check.
147    IsNull {
148        /// Field name (interned).
149        field: Arc<str>,
150    },
151    /// IS NOT NULL check.
152    IsNotNull {
153        /// Field name (interned).
154        field: Arc<str>,
155    },
156    /// Greater than on integer field.
157    GtInt {
158        /// Field name (interned).
159        field: Arc<str>,
160        /// Integer value.
161        value: i64,
162    },
163    /// Less than on integer field.
164    LtInt {
165        /// Field name (interned).
166        field: Arc<str>,
167        /// Integer value.
168    value: i64,
169    },
170    /// AND of two compact filters.
171    And(Box<CompactFilter>, Box<CompactFilter>),
172    /// OR of two compact filters.
173    Or(Box<CompactFilter>, Box<CompactFilter>),
174}
175
176impl CompactFilter {
177    /// Create an equality filter on an integer field.
178    #[inline]
179    pub fn eq_int(field: impl Into<Arc<str>>, value: i64) -> Self {
180        Self::EqInt {
181            field: field.into(),
182            value,
183        }
184    }
185
186    /// Create an equality filter on a boolean field.
187    #[inline]
188    pub fn eq_bool(field: impl Into<Arc<str>>, value: bool) -> Self {
189        Self::EqBool {
190            field: field.into(),
191            value,
192        }
193    }
194
195    /// Create an equality filter on a string field.
196    #[inline]
197    pub fn eq_str(field: impl Into<Arc<str>>, value: impl Into<Arc<str>>) -> Self {
198        Self::EqStr {
199            field: field.into(),
200            value: value.into(),
201        }
202    }
203
204    /// Create an IS NULL filter.
205    #[inline]
206    pub fn is_null(field: impl Into<Arc<str>>) -> Self {
207        Self::IsNull {
208            field: field.into(),
209        }
210    }
211
212    /// Create an IS NOT NULL filter.
213    #[inline]
214    pub fn is_not_null(field: impl Into<Arc<str>>) -> Self {
215        Self::IsNotNull {
216            field: field.into(),
217        }
218    }
219
220    /// Create a greater-than filter on an integer field.
221    #[inline]
222    pub fn gt_int(field: impl Into<Arc<str>>, value: i64) -> Self {
223        Self::GtInt {
224            field: field.into(),
225            value,
226        }
227    }
228
229    /// Create a less-than filter on an integer field.
230    #[inline]
231    pub fn lt_int(field: impl Into<Arc<str>>, value: i64) -> Self {
232        Self::LtInt {
233            field: field.into(),
234            value,
235        }
236    }
237
238    /// Combine with another filter using AND.
239    #[inline]
240    pub fn and(self, other: Self) -> Self {
241        Self::And(Box::new(self), Box::new(other))
242    }
243
244    /// Combine with another filter using OR.
245    #[inline]
246    pub fn or(self, other: Self) -> Self {
247        Self::Or(Box::new(self), Box::new(other))
248    }
249
250    /// Convert to SQL condition string for PostgreSQL.
251    pub fn to_sql_postgres(&self, param_offset: &mut usize) -> String {
252        match self {
253            Self::EqInt { field, .. } => {
254                *param_offset += 1;
255                format!("{} = ${}", field, *param_offset)
256            }
257            Self::EqBool { field, .. } => {
258                *param_offset += 1;
259                format!("{} = ${}", field, *param_offset)
260            }
261            Self::EqStr { field, .. } => {
262                *param_offset += 1;
263                format!("{} = ${}", field, *param_offset)
264            }
265            Self::IsNull { field } => format!("{} IS NULL", field),
266            Self::IsNotNull { field } => format!("{} IS NOT NULL", field),
267            Self::GtInt { field, .. } => {
268                *param_offset += 1;
269                format!("{} > ${}", field, *param_offset)
270            }
271            Self::LtInt { field, .. } => {
272                *param_offset += 1;
273                format!("{} < ${}", field, *param_offset)
274            }
275            Self::And(left, right) => {
276                let left_sql = left.to_sql_postgres(param_offset);
277                let right_sql = right.to_sql_postgres(param_offset);
278                format!("({} AND {})", left_sql, right_sql)
279            }
280            Self::Or(left, right) => {
281                let left_sql = left.to_sql_postgres(param_offset);
282                let right_sql = right.to_sql_postgres(param_offset);
283                format!("({} OR {})", left_sql, right_sql)
284            }
285        }
286    }
287
288    /// Get the approximate size in bytes.
289    pub fn size_bytes(&self) -> usize {
290        match self {
291            Self::EqInt { .. } | Self::GtInt { .. } | Self::LtInt { .. } => 24, // Arc<str> + i64
292            Self::EqBool { .. } => 17, // Arc<str> + bool
293            Self::EqStr { field, value } => 16 + field.len() + value.len(),
294            Self::IsNull { .. } | Self::IsNotNull { .. } => 16, // Arc<str>
295            Self::And(l, r) | Self::Or(l, r) => 16 + l.size_bytes() + r.size_bytes(),
296        }
297    }
298}
299
300// ============================================================================
301// Reusable Buffer Pool
302// ============================================================================
303
304/// A pool of reusable String buffers.
305///
306/// Reduces allocation by reusing String buffers for SQL generation.
307#[derive(Debug, Default)]
308pub struct BufferPool {
309    buffers: Mutex<Vec<String>>,
310    default_capacity: usize,
311}
312
313impl BufferPool {
314    /// Create a new buffer pool.
315    pub fn new() -> Self {
316        Self {
317            buffers: Mutex::new(Vec::new()),
318            default_capacity: 256,
319        }
320    }
321
322    /// Create a pool with custom default buffer capacity.
323    pub fn with_capacity(default_capacity: usize) -> Self {
324        info!(default_capacity, "BufferPool initialized");
325        Self {
326            buffers: Mutex::new(Vec::new()),
327            default_capacity,
328        }
329    }
330
331    /// Get a buffer from the pool or create a new one.
332    pub fn get(&self) -> PooledBuffer {
333        let buffer = self.buffers.lock().pop().unwrap_or_else(|| {
334            String::with_capacity(self.default_capacity)
335        });
336        PooledBuffer {
337            buffer,
338            pool: self,
339        }
340    }
341
342    /// Return a buffer to the pool.
343    fn return_buffer(&self, mut buffer: String) {
344        buffer.clear();
345        // Only keep reasonably sized buffers to avoid memory bloat
346        if buffer.capacity() <= 4096 {
347            self.buffers.lock().push(buffer);
348        }
349    }
350
351    /// Get the number of available buffers.
352    pub fn available(&self) -> usize {
353        self.buffers.lock().len()
354    }
355
356    /// Clear all pooled buffers.
357    pub fn clear(&self) {
358        self.buffers.lock().clear();
359    }
360}
361
362/// A buffer borrowed from a pool.
363///
364/// Automatically returns to the pool when dropped.
365pub struct PooledBuffer<'a> {
366    buffer: String,
367    pool: &'a BufferPool,
368}
369
370impl<'a> PooledBuffer<'a> {
371    /// Get mutable access to the buffer.
372    pub fn as_mut(&mut self) -> &mut String {
373        &mut self.buffer
374    }
375
376    /// Take ownership of the buffer (does not return to pool).
377    pub fn take(mut self) -> String {
378        std::mem::take(&mut self.buffer)
379    }
380}
381
382impl<'a> std::ops::Deref for PooledBuffer<'a> {
383    type Target = String;
384
385    fn deref(&self) -> &Self::Target {
386        &self.buffer
387    }
388}
389
390impl<'a> std::ops::DerefMut for PooledBuffer<'a> {
391    fn deref_mut(&mut self) -> &mut Self::Target {
392        &mut self.buffer
393    }
394}
395
396impl<'a> Drop for PooledBuffer<'a> {
397    fn drop(&mut self) {
398        if !self.buffer.is_empty() || self.buffer.capacity() > 0 {
399            let buffer = std::mem::take(&mut self.buffer);
400            self.pool.return_buffer(buffer);
401        }
402    }
403}
404
405// ============================================================================
406// Memory Usage Tracking
407// ============================================================================
408
409/// Track memory usage for debugging and optimization.
410#[derive(Debug, Clone, Default)]
411pub struct MemoryStats {
412    /// Number of allocations.
413    pub allocations: u64,
414    /// Number of deallocations.
415    pub deallocations: u64,
416    /// Current bytes allocated.
417    pub current_bytes: usize,
418    /// Peak bytes allocated.
419    pub peak_bytes: usize,
420    /// Total bytes allocated (lifetime).
421    pub total_bytes: usize,
422}
423
424impl MemoryStats {
425    /// Create new empty stats.
426    pub fn new() -> Self {
427        Self::default()
428    }
429
430    /// Record an allocation.
431    pub fn record_alloc(&mut self, bytes: usize) {
432        self.allocations += 1;
433        self.current_bytes += bytes;
434        self.total_bytes += bytes;
435        if self.current_bytes > self.peak_bytes {
436            self.peak_bytes = self.current_bytes;
437        }
438    }
439
440    /// Record a deallocation.
441    pub fn record_dealloc(&mut self, bytes: usize) {
442        self.deallocations += 1;
443        self.current_bytes = self.current_bytes.saturating_sub(bytes);
444    }
445
446    /// Get the net allocation count.
447    pub fn net_allocations(&self) -> i64 {
448        self.allocations as i64 - self.deallocations as i64
449    }
450}
451
452// ============================================================================
453// Global Pools (Lazy Initialized)
454// ============================================================================
455
456/// Global string pool for common field names.
457pub static GLOBAL_STRING_POOL: std::sync::LazyLock<StringPool> =
458    std::sync::LazyLock::new(|| {
459        let pool = StringPool::with_capacity(128);
460        // Pre-populate with common field names
461        for name in COMMON_FIELD_NAMES {
462            pool.intern(name);
463        }
464        pool
465    });
466
467/// Global buffer pool for SQL generation.
468pub static GLOBAL_BUFFER_POOL: std::sync::LazyLock<BufferPool> =
469    std::sync::LazyLock::new(|| BufferPool::with_capacity(256));
470
471/// Common field names to pre-populate the string pool.
472const COMMON_FIELD_NAMES: &[&str] = &[
473    "id", "uuid", "name", "email", "username", "password",
474    "title", "description", "content", "body", "status", "type",
475    "role", "active", "enabled", "deleted", "verified", "published",
476    "count", "score", "priority", "order", "position", "age",
477    "amount", "price", "quantity", "user_id", "post_id", "comment_id",
478    "category_id", "parent_id", "author_id", "owner_id",
479    "created_at", "updated_at", "deleted_at", "published_at",
480    "expires_at", "starts_at", "ends_at", "last_login_at", "verified_at",
481    "slug", "url", "path", "key", "value", "token", "code", "version",
482];
483
484/// Intern a string using the global pool.
485#[inline]
486pub fn intern(s: &str) -> Arc<str> {
487    GLOBAL_STRING_POOL.intern(s)
488}
489
490/// Get a buffer from the global pool.
491#[inline]
492pub fn get_buffer() -> PooledBuffer<'static> {
493    GLOBAL_BUFFER_POOL.get()
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn test_string_pool_interning() {
502        let pool = StringPool::new();
503
504        let s1 = pool.intern("hello");
505        let s2 = pool.intern("hello");
506
507        // Should be the same Arc
508        assert!(Arc::ptr_eq(&s1, &s2));
509        assert_eq!(pool.len(), 1);
510    }
511
512    #[test]
513    fn test_string_pool_different_strings() {
514        let pool = StringPool::new();
515
516        let s1 = pool.intern("hello");
517        let s2 = pool.intern("world");
518
519        assert!(!Arc::ptr_eq(&s1, &s2));
520        assert_eq!(pool.len(), 2);
521    }
522
523    #[test]
524    fn test_compact_filter_eq_int() {
525        let filter = CompactFilter::eq_int(Arc::from("id"), 42);
526        let mut offset = 0;
527        let sql = filter.to_sql_postgres(&mut offset);
528        assert_eq!(sql, "id = $1");
529        assert_eq!(offset, 1);
530    }
531
532    #[test]
533    fn test_compact_filter_and() {
534        let filter = CompactFilter::eq_int(Arc::from("id"), 42)
535            .and(CompactFilter::eq_bool(Arc::from("active"), true));
536        let mut offset = 0;
537        let sql = filter.to_sql_postgres(&mut offset);
538        assert_eq!(sql, "(id = $1 AND active = $2)");
539        assert_eq!(offset, 2);
540    }
541
542    #[test]
543    fn test_compact_filter_is_null() {
544        let filter = CompactFilter::is_null(Arc::from("deleted_at"));
545        let mut offset = 0;
546        let sql = filter.to_sql_postgres(&mut offset);
547        assert_eq!(sql, "deleted_at IS NULL");
548        assert_eq!(offset, 0); // No params for IS NULL
549    }
550
551    #[test]
552    fn test_buffer_pool() {
553        let pool = BufferPool::new();
554
555        {
556            let mut buffer = pool.get();
557            buffer.push_str("hello");
558            assert_eq!(&*buffer, "hello");
559        } // Buffer returned to pool here
560
561        assert_eq!(pool.available(), 1);
562
563        {
564            let buffer = pool.get();
565            assert!(buffer.is_empty()); // Buffer was cleared
566        }
567    }
568
569    #[test]
570    fn test_global_intern() {
571        let s1 = intern("id");
572        let s2 = intern("id");
573        assert!(Arc::ptr_eq(&s1, &s2));
574    }
575
576    #[test]
577    fn test_memory_stats() {
578        let mut stats = MemoryStats::new();
579
580        stats.record_alloc(100);
581        stats.record_alloc(200);
582        assert_eq!(stats.current_bytes, 300);
583        assert_eq!(stats.peak_bytes, 300);
584
585        stats.record_dealloc(100);
586        assert_eq!(stats.current_bytes, 200);
587        assert_eq!(stats.peak_bytes, 300); // Peak unchanged
588    }
589}
590