Skip to main content

oxirs_gql/observability/
memory_tracking.rs

1//! Memory Allocation Tracking
2//!
3//! Tracks memory allocations per GraphQL query to identify memory leaks,
4//! optimize memory usage, and provide detailed memory profiling.
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::cmp::Reverse;
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12use sysinfo::System;
13use tokio::sync::RwLock;
14
15/// Memory allocation tracker
16pub struct MemoryTracker {
17    /// Query memory snapshots
18    snapshots: Arc<RwLock<HashMap<String, MemorySnapshot>>>,
19    /// Completed allocations
20    completed: Arc<RwLock<Vec<MemoryAllocation>>>,
21    /// System info for memory stats
22    system: Arc<RwLock<System>>,
23    /// Configuration
24    config: MemoryTrackingConfig,
25}
26
27/// Memory tracking configuration
28#[derive(Debug, Clone)]
29pub struct MemoryTrackingConfig {
30    /// Enable detailed tracking
31    pub enable_detailed_tracking: bool,
32    /// Track allocation backtraces
33    pub track_backtraces: bool,
34    /// Maximum allocations to store
35    pub max_allocations: usize,
36    /// Retention period for completed allocations
37    pub retention_period: Duration,
38    /// Memory threshold for warnings (bytes)
39    pub warning_threshold_bytes: u64,
40}
41
42impl Default for MemoryTrackingConfig {
43    fn default() -> Self {
44        Self {
45            enable_detailed_tracking: true,
46            track_backtraces: false,
47            max_allocations: 1000,
48            retention_period: Duration::from_secs(3600), // 1 hour
49            warning_threshold_bytes: 100 * 1024 * 1024,  // 100 MB
50        }
51    }
52}
53
54/// Memory snapshot at a point in time
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct MemorySnapshot {
57    /// Query ID
58    pub query_id: String,
59    /// Operation name
60    pub operation_name: Option<String>,
61    /// Start time (not serialized)
62    #[serde(skip, default = "Instant::now")]
63    pub start_time: Instant,
64    /// Start timestamp (Unix epoch)
65    pub start_timestamp: u64,
66    /// Initial memory usage (bytes)
67    pub initial_memory: u64,
68    /// Peak memory usage (bytes)
69    pub peak_memory: u64,
70    /// Current memory usage (bytes)
71    pub current_memory: u64,
72    /// Number of allocations
73    pub allocation_count: u64,
74    /// Number of deallocations
75    pub deallocation_count: u64,
76    /// Total bytes allocated
77    pub total_allocated: u64,
78    /// Total bytes deallocated
79    pub total_deallocated: u64,
80}
81
82impl MemorySnapshot {
83    /// Create a new memory snapshot
84    pub fn new(query_id: String, operation_name: Option<String>, initial_memory: u64) -> Self {
85        let now = SystemTime::now()
86            .duration_since(UNIX_EPOCH)
87            .expect("SystemTime should be after UNIX_EPOCH")
88            .as_secs();
89
90        Self {
91            query_id,
92            operation_name,
93            start_time: Instant::now(),
94            start_timestamp: now,
95            initial_memory,
96            peak_memory: initial_memory,
97            current_memory: initial_memory,
98            allocation_count: 0,
99            deallocation_count: 0,
100            total_allocated: 0,
101            total_deallocated: 0,
102        }
103    }
104
105    /// Record an allocation
106    pub fn record_allocation(&mut self, bytes: u64) {
107        self.allocation_count += 1;
108        self.total_allocated += bytes;
109        self.current_memory += bytes;
110        self.peak_memory = self.peak_memory.max(self.current_memory);
111    }
112
113    /// Record a deallocation
114    pub fn record_deallocation(&mut self, bytes: u64) {
115        self.deallocation_count += 1;
116        self.total_deallocated += bytes;
117        self.current_memory = self.current_memory.saturating_sub(bytes);
118    }
119
120    /// Get net memory change
121    pub fn net_memory_change(&self) -> i64 {
122        self.total_allocated as i64 - self.total_deallocated as i64
123    }
124
125    /// Get elapsed time
126    pub fn elapsed(&self) -> Duration {
127        self.start_time.elapsed()
128    }
129}
130
131/// Completed memory allocation record
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct MemoryAllocation {
134    /// Query ID
135    pub query_id: String,
136    /// Operation name
137    pub operation_name: Option<String>,
138    /// Start time (Unix epoch)
139    pub start_time: u64,
140    /// End time (Unix epoch)
141    pub end_time: u64,
142    /// Duration in milliseconds
143    pub duration_ms: u64,
144    /// Initial memory (bytes)
145    pub initial_memory: u64,
146    /// Peak memory (bytes)
147    pub peak_memory: u64,
148    /// Final memory (bytes)
149    pub final_memory: u64,
150    /// Total allocated (bytes)
151    pub total_allocated: u64,
152    /// Total deallocated (bytes)
153    pub total_deallocated: u64,
154    /// Net memory change (bytes)
155    pub net_change: i64,
156    /// Allocation count
157    pub allocation_count: u64,
158    /// Deallocation count
159    pub deallocation_count: u64,
160    /// Memory leaked (bytes)
161    pub leaked_bytes: u64,
162    /// Metadata
163    pub metadata: HashMap<String, String>,
164}
165
166impl MemoryAllocation {
167    /// Create from snapshot
168    pub fn from_snapshot(snapshot: MemorySnapshot) -> Self {
169        let now = SystemTime::now()
170            .duration_since(UNIX_EPOCH)
171            .expect("SystemTime should be after UNIX_EPOCH")
172            .as_secs();
173
174        let net_change = snapshot.net_memory_change();
175        let leaked_bytes = if net_change > 0 { net_change as u64 } else { 0 };
176
177        let duration_ms = snapshot.elapsed().as_millis() as u64;
178
179        Self {
180            query_id: snapshot.query_id,
181            operation_name: snapshot.operation_name.clone(),
182            start_time: snapshot.start_timestamp,
183            end_time: now,
184            duration_ms,
185            initial_memory: snapshot.initial_memory,
186            peak_memory: snapshot.peak_memory,
187            final_memory: snapshot.current_memory,
188            total_allocated: snapshot.total_allocated,
189            total_deallocated: snapshot.total_deallocated,
190            net_change,
191            allocation_count: snapshot.allocation_count,
192            deallocation_count: snapshot.deallocation_count,
193            leaked_bytes,
194            metadata: HashMap::new(),
195        }
196    }
197
198    /// Check if memory was leaked
199    pub fn has_leak(&self) -> bool {
200        self.leaked_bytes > 0
201    }
202
203    /// Get memory efficiency (deallocated / allocated)
204    pub fn efficiency(&self) -> f64 {
205        if self.total_allocated == 0 {
206            1.0
207        } else {
208            self.total_deallocated as f64 / self.total_allocated as f64
209        }
210    }
211}
212
213impl MemoryTracker {
214    /// Create a new memory tracker
215    pub fn new(config: MemoryTrackingConfig) -> Self {
216        Self {
217            snapshots: Arc::new(RwLock::new(HashMap::new())),
218            completed: Arc::new(RwLock::new(Vec::new())),
219            system: Arc::new(RwLock::new(System::new_all())),
220            config,
221        }
222    }
223
224    /// Start tracking a query
225    pub async fn start_tracking(
226        &self,
227        query_id: String,
228        operation_name: Option<String>,
229    ) -> Result<()> {
230        // Refresh system info
231        let mut system = self.system.write().await;
232        system.refresh_memory();
233
234        // Get current process memory (estimate)
235        let current_memory = system.used_memory();
236
237        drop(system);
238
239        let snapshot = MemorySnapshot::new(query_id.clone(), operation_name, current_memory);
240
241        let mut snapshots = self.snapshots.write().await;
242        snapshots.insert(query_id, snapshot);
243
244        Ok(())
245    }
246
247    /// Record an allocation
248    pub async fn record_allocation(&self, query_id: &str, bytes: u64) -> Result<()> {
249        let mut snapshots = self.snapshots.write().await;
250
251        if let Some(snapshot) = snapshots.get_mut(query_id) {
252            snapshot.record_allocation(bytes);
253
254            // Check warning threshold
255            if snapshot.current_memory > self.config.warning_threshold_bytes {
256                tracing::warn!(
257                    query_id = %query_id,
258                    current_memory = snapshot.current_memory,
259                    threshold = self.config.warning_threshold_bytes,
260                    "Query exceeded memory warning threshold"
261                );
262            }
263        }
264
265        Ok(())
266    }
267
268    /// Record a deallocation
269    pub async fn record_deallocation(&self, query_id: &str, bytes: u64) -> Result<()> {
270        let mut snapshots = self.snapshots.write().await;
271
272        if let Some(snapshot) = snapshots.get_mut(query_id) {
273            snapshot.record_deallocation(bytes);
274        }
275
276        Ok(())
277    }
278
279    /// Stop tracking and get allocation record
280    pub async fn stop_tracking(&self, query_id: &str) -> Result<MemoryAllocation> {
281        let mut snapshots = self.snapshots.write().await;
282
283        let snapshot = snapshots
284            .remove(query_id)
285            .ok_or_else(|| anyhow::anyhow!("Query not found: {}", query_id))?;
286
287        let allocation = MemoryAllocation::from_snapshot(snapshot);
288
289        // Store in completed
290        let mut completed = self.completed.write().await;
291        completed.push(allocation.clone());
292
293        // Cleanup old allocations
294        self.cleanup_old_allocations(&mut completed).await;
295
296        Ok(allocation)
297    }
298
299    /// Get current snapshot
300    pub async fn get_snapshot(&self, query_id: &str) -> Option<MemorySnapshot> {
301        let snapshots = self.snapshots.read().await;
302        snapshots.get(query_id).cloned()
303    }
304
305    /// Get completed allocations
306    pub async fn get_completed_allocations(&self) -> Vec<MemoryAllocation> {
307        let completed = self.completed.read().await;
308        completed.clone()
309    }
310
311    /// Get allocations by operation
312    pub async fn get_allocations_by_operation(
313        &self,
314        operation_name: &str,
315    ) -> Vec<MemoryAllocation> {
316        let completed = self.completed.read().await;
317        completed
318            .iter()
319            .filter(|a| {
320                a.operation_name
321                    .as_ref()
322                    .map(|n| n == operation_name)
323                    .unwrap_or(false)
324            })
325            .cloned()
326            .collect()
327    }
328
329    /// Get memory leak statistics
330    pub async fn get_leak_statistics(&self) -> MemoryLeakStatistics {
331        let completed = self.completed.read().await;
332
333        let mut stats = MemoryLeakStatistics::default();
334
335        for allocation in completed.iter() {
336            stats.total_queries += 1;
337            stats.total_allocated += allocation.total_allocated;
338            stats.total_deallocated += allocation.total_deallocated;
339
340            if allocation.has_leak() {
341                stats.queries_with_leaks += 1;
342                stats.total_leaked += allocation.leaked_bytes;
343            }
344        }
345
346        if let Some(avg) = stats.total_allocated.checked_div(stats.total_queries) {
347            stats.avg_allocation_per_query = avg;
348            stats.leak_rate = stats.queries_with_leaks as f64 / stats.total_queries as f64;
349        }
350
351        stats
352    }
353
354    /// Get top memory consumers sorted by total_allocated (the query's own allocations)
355    pub async fn get_top_consumers(&self, limit: usize) -> Vec<MemoryAllocation> {
356        let completed = self.completed.read().await;
357
358        let mut sorted = completed.clone();
359        // Sort by total_allocated (query's own allocations) rather than peak_memory
360        // which includes varying system memory baseline
361        sorted.sort_by_key(|s| Reverse(s.total_allocated));
362        sorted.truncate(limit);
363        sorted
364    }
365
366    /// Get memory statistics
367    pub async fn get_statistics(&self) -> MemoryStatistics {
368        let snapshots = self.snapshots.read().await;
369        let completed = self.completed.read().await;
370
371        let active_queries = snapshots.len();
372        let completed_queries = completed.len();
373
374        let mut total_allocated = 0;
375        let mut total_deallocated = 0;
376        let mut total_leaked = 0;
377
378        for allocation in completed.iter() {
379            total_allocated += allocation.total_allocated;
380            total_deallocated += allocation.total_deallocated;
381            total_leaked += allocation.leaked_bytes;
382        }
383
384        MemoryStatistics {
385            active_queries,
386            completed_queries,
387            total_allocated,
388            total_deallocated,
389            total_leaked,
390        }
391    }
392
393    /// Cleanup old allocations
394    async fn cleanup_old_allocations(&self, completed: &mut Vec<MemoryAllocation>) {
395        let retention_secs = self.config.retention_period.as_secs();
396        let now = SystemTime::now()
397            .duration_since(UNIX_EPOCH)
398            .expect("SystemTime should be after UNIX_EPOCH")
399            .as_secs();
400
401        completed.retain(|a| now - a.end_time < retention_secs);
402
403        // Also limit by count
404        if completed.len() > self.config.max_allocations {
405            let excess = completed.len() - self.config.max_allocations;
406            completed.drain(0..excess);
407        }
408    }
409}
410
411impl Default for MemoryTracker {
412    fn default() -> Self {
413        Self::new(MemoryTrackingConfig::default())
414    }
415}
416
417/// Memory leak statistics
418#[derive(Debug, Clone, Default, Serialize, Deserialize)]
419pub struct MemoryLeakStatistics {
420    /// Total queries tracked
421    pub total_queries: u64,
422    /// Queries with memory leaks
423    pub queries_with_leaks: u64,
424    /// Total bytes allocated
425    pub total_allocated: u64,
426    /// Total bytes deallocated
427    pub total_deallocated: u64,
428    /// Total bytes leaked
429    pub total_leaked: u64,
430    /// Average allocation per query
431    pub avg_allocation_per_query: u64,
432    /// Leak rate (0.0 to 1.0)
433    pub leak_rate: f64,
434}
435
436impl MemoryLeakStatistics {
437    /// Get efficiency (deallocated / allocated)
438    pub fn efficiency(&self) -> f64 {
439        if self.total_allocated == 0 {
440            1.0
441        } else {
442            self.total_deallocated as f64 / self.total_allocated as f64
443        }
444    }
445}
446
447/// Memory statistics
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct MemoryStatistics {
450    /// Active queries being tracked
451    pub active_queries: usize,
452    /// Completed queries
453    pub completed_queries: usize,
454    /// Total bytes allocated
455    pub total_allocated: u64,
456    /// Total bytes deallocated
457    pub total_deallocated: u64,
458    /// Total bytes leaked
459    pub total_leaked: u64,
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_memory_snapshot_creation() {
468        let snapshot =
469            MemorySnapshot::new("query-1".to_string(), Some("GetUser".to_string()), 1000);
470
471        assert_eq!(snapshot.query_id, "query-1");
472        assert_eq!(snapshot.operation_name, Some("GetUser".to_string()));
473        assert_eq!(snapshot.initial_memory, 1000);
474        assert_eq!(snapshot.peak_memory, 1000);
475        assert_eq!(snapshot.current_memory, 1000);
476        assert_eq!(snapshot.allocation_count, 0);
477    }
478
479    #[test]
480    fn test_snapshot_record_allocation() {
481        let mut snapshot = MemorySnapshot::new("query-1".to_string(), None, 1000);
482
483        snapshot.record_allocation(500);
484
485        assert_eq!(snapshot.allocation_count, 1);
486        assert_eq!(snapshot.total_allocated, 500);
487        assert_eq!(snapshot.current_memory, 1500);
488        assert_eq!(snapshot.peak_memory, 1500);
489    }
490
491    #[test]
492    fn test_snapshot_record_deallocation() {
493        let mut snapshot = MemorySnapshot::new("query-1".to_string(), None, 1000);
494
495        snapshot.record_allocation(500);
496        snapshot.record_deallocation(300);
497
498        assert_eq!(snapshot.deallocation_count, 1);
499        assert_eq!(snapshot.total_deallocated, 300);
500        assert_eq!(snapshot.current_memory, 1200);
501        assert_eq!(snapshot.peak_memory, 1500);
502    }
503
504    #[test]
505    fn test_snapshot_net_memory_change() {
506        let mut snapshot = MemorySnapshot::new("query-1".to_string(), None, 1000);
507
508        snapshot.record_allocation(500);
509        snapshot.record_deallocation(300);
510
511        assert_eq!(snapshot.net_memory_change(), 200);
512    }
513
514    #[test]
515    fn test_memory_allocation_from_snapshot() {
516        let mut snapshot = MemorySnapshot::new("query-1".to_string(), None, 1000);
517
518        snapshot.record_allocation(500);
519        snapshot.record_deallocation(300);
520
521        let allocation = MemoryAllocation::from_snapshot(snapshot);
522
523        assert_eq!(allocation.query_id, "query-1");
524        assert_eq!(allocation.total_allocated, 500);
525        assert_eq!(allocation.total_deallocated, 300);
526        assert_eq!(allocation.net_change, 200);
527        assert_eq!(allocation.leaked_bytes, 200);
528        assert!(allocation.has_leak());
529    }
530
531    #[test]
532    fn test_memory_allocation_no_leak() {
533        let mut snapshot = MemorySnapshot::new("query-1".to_string(), None, 1000);
534
535        snapshot.record_allocation(500);
536        snapshot.record_deallocation(500);
537
538        let allocation = MemoryAllocation::from_snapshot(snapshot);
539
540        assert_eq!(allocation.net_change, 0);
541        assert_eq!(allocation.leaked_bytes, 0);
542        assert!(!allocation.has_leak());
543    }
544
545    #[test]
546    fn test_memory_allocation_efficiency() {
547        let mut snapshot = MemorySnapshot::new("query-1".to_string(), None, 1000);
548
549        snapshot.record_allocation(1000);
550        snapshot.record_deallocation(800);
551
552        let allocation = MemoryAllocation::from_snapshot(snapshot);
553
554        assert_eq!(allocation.efficiency(), 0.8);
555    }
556
557    #[tokio::test]
558    async fn test_memory_tracker_start_stop() {
559        let tracker = MemoryTracker::default();
560
561        tracker
562            .start_tracking("query-1".to_string(), Some("GetUser".to_string()))
563            .await
564            .expect("should succeed");
565
566        let snapshot = tracker.get_snapshot("query-1").await;
567        assert!(snapshot.is_some());
568
569        let allocation = tracker
570            .stop_tracking("query-1")
571            .await
572            .expect("should succeed");
573        assert_eq!(allocation.query_id, "query-1");
574
575        let snapshot = tracker.get_snapshot("query-1").await;
576        assert!(snapshot.is_none());
577    }
578
579    #[tokio::test]
580    async fn test_memory_tracker_record_allocations() {
581        let tracker = MemoryTracker::default();
582
583        tracker
584            .start_tracking("query-1".to_string(), None)
585            .await
586            .expect("should succeed");
587
588        tracker
589            .record_allocation("query-1", 500)
590            .await
591            .expect("should succeed");
592        tracker
593            .record_allocation("query-1", 300)
594            .await
595            .expect("should succeed");
596
597        let snapshot = tracker
598            .get_snapshot("query-1")
599            .await
600            .expect("should succeed");
601        assert_eq!(snapshot.allocation_count, 2);
602        assert_eq!(snapshot.total_allocated, 800);
603    }
604
605    #[tokio::test]
606    async fn test_memory_tracker_record_deallocations() {
607        let tracker = MemoryTracker::default();
608
609        tracker
610            .start_tracking("query-1".to_string(), None)
611            .await
612            .expect("should succeed");
613
614        tracker
615            .record_allocation("query-1", 500)
616            .await
617            .expect("should succeed");
618        tracker
619            .record_deallocation("query-1", 200)
620            .await
621            .expect("should succeed");
622
623        let snapshot = tracker
624            .get_snapshot("query-1")
625            .await
626            .expect("should succeed");
627        assert_eq!(snapshot.deallocation_count, 1);
628        assert_eq!(snapshot.total_deallocated, 200);
629    }
630
631    #[tokio::test]
632    async fn test_memory_tracker_completed_allocations() {
633        let tracker = MemoryTracker::default();
634
635        tracker
636            .start_tracking("query-1".to_string(), None)
637            .await
638            .expect("should succeed");
639        tracker
640            .record_allocation("query-1", 500)
641            .await
642            .expect("should succeed");
643        tracker
644            .stop_tracking("query-1")
645            .await
646            .expect("should succeed");
647
648        let completed = tracker.get_completed_allocations().await;
649        assert_eq!(completed.len(), 1);
650        assert_eq!(completed[0].query_id, "query-1");
651    }
652
653    #[tokio::test]
654    async fn test_memory_tracker_allocations_by_operation() {
655        let tracker = MemoryTracker::default();
656
657        tracker
658            .start_tracking("query-1".to_string(), Some("GetUser".to_string()))
659            .await
660            .expect("should succeed");
661        tracker
662            .record_allocation("query-1", 500)
663            .await
664            .expect("should succeed");
665        tracker
666            .stop_tracking("query-1")
667            .await
668            .expect("should succeed");
669
670        tracker
671            .start_tracking("query-2".to_string(), Some("GetPosts".to_string()))
672            .await
673            .expect("should succeed");
674        tracker
675            .record_allocation("query-2", 300)
676            .await
677            .expect("should succeed");
678        tracker
679            .stop_tracking("query-2")
680            .await
681            .expect("should succeed");
682
683        let allocations = tracker.get_allocations_by_operation("GetUser").await;
684        assert_eq!(allocations.len(), 1);
685        assert_eq!(allocations[0].operation_name, Some("GetUser".to_string()));
686    }
687
688    #[tokio::test]
689    async fn test_memory_tracker_leak_statistics() {
690        let tracker = MemoryTracker::default();
691
692        // Query with leak
693        tracker
694            .start_tracking("query-1".to_string(), None)
695            .await
696            .expect("should succeed");
697        tracker
698            .record_allocation("query-1", 1000)
699            .await
700            .expect("should succeed");
701        tracker
702            .record_deallocation("query-1", 800)
703            .await
704            .expect("should succeed");
705        tracker
706            .stop_tracking("query-1")
707            .await
708            .expect("should succeed");
709
710        // Query without leak
711        tracker
712            .start_tracking("query-2".to_string(), None)
713            .await
714            .expect("should succeed");
715        tracker
716            .record_allocation("query-2", 500)
717            .await
718            .expect("should succeed");
719        tracker
720            .record_deallocation("query-2", 500)
721            .await
722            .expect("should succeed");
723        tracker
724            .stop_tracking("query-2")
725            .await
726            .expect("should succeed");
727
728        let stats = tracker.get_leak_statistics().await;
729        assert_eq!(stats.total_queries, 2);
730        assert_eq!(stats.queries_with_leaks, 1);
731        assert_eq!(stats.total_leaked, 200);
732        assert_eq!(stats.leak_rate, 0.5);
733    }
734
735    #[tokio::test]
736    async fn test_memory_tracker_top_consumers() {
737        let tracker = MemoryTracker::default();
738
739        tracker
740            .start_tracking("query-1".to_string(), None)
741            .await
742            .expect("should succeed");
743        tracker
744            .record_allocation("query-1", 1000)
745            .await
746            .expect("should succeed");
747        tracker
748            .stop_tracking("query-1")
749            .await
750            .expect("should succeed");
751
752        tracker
753            .start_tracking("query-2".to_string(), None)
754            .await
755            .expect("should succeed");
756        tracker
757            .record_allocation("query-2", 2000)
758            .await
759            .expect("should succeed");
760        tracker
761            .stop_tracking("query-2")
762            .await
763            .expect("should succeed");
764
765        tracker
766            .start_tracking("query-3".to_string(), None)
767            .await
768            .expect("should succeed");
769        tracker
770            .record_allocation("query-3", 500)
771            .await
772            .expect("should succeed");
773        tracker
774            .stop_tracking("query-3")
775            .await
776            .expect("should succeed");
777
778        let top = tracker.get_top_consumers(2).await;
779        assert_eq!(top.len(), 2);
780        // Verify ordering by total_allocated (highest first)
781        assert!(
782            top[0].total_allocated >= top[1].total_allocated,
783            "Top consumers should be sorted by total_allocated descending: {:?}",
784            top.iter()
785                .map(|t| (&t.query_id, t.total_allocated))
786                .collect::<Vec<_>>()
787        );
788        // Verify the highest consumer has the expected total_allocated (query-2 with 2000)
789        assert_eq!(
790            top[0].total_allocated, 2000,
791            "Highest consumer should have 2000 bytes allocated"
792        );
793        assert_eq!(
794            top[0].query_id, "query-2",
795            "Highest consumer should be query-2"
796        );
797        // Second highest is query-1 with 1000
798        assert_eq!(
799            top[1].total_allocated, 1000,
800            "Second highest consumer should have 1000 bytes allocated"
801        );
802        assert_eq!(
803            top[1].query_id, "query-1",
804            "Second highest consumer should be query-1"
805        );
806    }
807
808    #[tokio::test]
809    async fn test_memory_tracker_statistics() {
810        let tracker = MemoryTracker::default();
811
812        tracker
813            .start_tracking("query-1".to_string(), None)
814            .await
815            .expect("should succeed");
816        tracker
817            .start_tracking("query-2".to_string(), None)
818            .await
819            .expect("should succeed");
820
821        tracker
822            .record_allocation("query-1", 500)
823            .await
824            .expect("should succeed");
825        tracker
826            .stop_tracking("query-1")
827            .await
828            .expect("should succeed");
829
830        let stats = tracker.get_statistics().await;
831        assert_eq!(stats.active_queries, 1); // query-2 still active
832        assert_eq!(stats.completed_queries, 1);
833    }
834
835    #[test]
836    fn test_memory_tracking_config() {
837        let config = MemoryTrackingConfig::default();
838
839        assert!(config.enable_detailed_tracking);
840        assert_eq!(config.max_allocations, 1000);
841        assert_eq!(config.warning_threshold_bytes, 100 * 1024 * 1024);
842    }
843
844    #[test]
845    fn test_memory_leak_statistics_efficiency() {
846        let stats = MemoryLeakStatistics {
847            total_allocated: 1000,
848            total_deallocated: 800,
849            ..Default::default()
850        };
851
852        assert_eq!(stats.efficiency(), 0.8);
853    }
854
855    #[tokio::test]
856    async fn test_memory_tracker_not_found() {
857        let tracker = MemoryTracker::default();
858
859        let result = tracker.stop_tracking("nonexistent").await;
860        assert!(result.is_err());
861
862        let snapshot = tracker.get_snapshot("nonexistent").await;
863        assert!(snapshot.is_none());
864    }
865
866    #[test]
867    fn test_snapshot_peak_memory() {
868        let mut snapshot = MemorySnapshot::new("query-1".to_string(), None, 1000);
869
870        snapshot.record_allocation(500);
871        assert_eq!(snapshot.peak_memory, 1500);
872
873        snapshot.record_allocation(300);
874        assert_eq!(snapshot.peak_memory, 1800);
875
876        snapshot.record_deallocation(500);
877        assert_eq!(snapshot.peak_memory, 1800); // Peak doesn't decrease
878        assert_eq!(snapshot.current_memory, 1300);
879    }
880
881    #[test]
882    fn test_memory_allocation_metadata() {
883        let snapshot = MemorySnapshot::new("query-1".to_string(), None, 1000);
884        let mut allocation = MemoryAllocation::from_snapshot(snapshot);
885
886        allocation
887            .metadata
888            .insert("user_id".to_string(), "123".to_string());
889        allocation
890            .metadata
891            .insert("query_type".to_string(), "mutation".to_string());
892
893        assert_eq!(allocation.metadata.get("user_id"), Some(&"123".to_string()));
894        assert_eq!(
895            allocation.metadata.get("query_type"),
896            Some(&"mutation".to_string())
897        );
898    }
899}