1use 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
15pub struct MemoryTracker {
17 snapshots: Arc<RwLock<HashMap<String, MemorySnapshot>>>,
19 completed: Arc<RwLock<Vec<MemoryAllocation>>>,
21 system: Arc<RwLock<System>>,
23 config: MemoryTrackingConfig,
25}
26
27#[derive(Debug, Clone)]
29pub struct MemoryTrackingConfig {
30 pub enable_detailed_tracking: bool,
32 pub track_backtraces: bool,
34 pub max_allocations: usize,
36 pub retention_period: Duration,
38 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), warning_threshold_bytes: 100 * 1024 * 1024, }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct MemorySnapshot {
57 pub query_id: String,
59 pub operation_name: Option<String>,
61 #[serde(skip, default = "Instant::now")]
63 pub start_time: Instant,
64 pub start_timestamp: u64,
66 pub initial_memory: u64,
68 pub peak_memory: u64,
70 pub current_memory: u64,
72 pub allocation_count: u64,
74 pub deallocation_count: u64,
76 pub total_allocated: u64,
78 pub total_deallocated: u64,
80}
81
82impl MemorySnapshot {
83 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 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 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 pub fn net_memory_change(&self) -> i64 {
122 self.total_allocated as i64 - self.total_deallocated as i64
123 }
124
125 pub fn elapsed(&self) -> Duration {
127 self.start_time.elapsed()
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct MemoryAllocation {
134 pub query_id: String,
136 pub operation_name: Option<String>,
138 pub start_time: u64,
140 pub end_time: u64,
142 pub duration_ms: u64,
144 pub initial_memory: u64,
146 pub peak_memory: u64,
148 pub final_memory: u64,
150 pub total_allocated: u64,
152 pub total_deallocated: u64,
154 pub net_change: i64,
156 pub allocation_count: u64,
158 pub deallocation_count: u64,
160 pub leaked_bytes: u64,
162 pub metadata: HashMap<String, String>,
164}
165
166impl MemoryAllocation {
167 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 pub fn has_leak(&self) -> bool {
200 self.leaked_bytes > 0
201 }
202
203 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 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 pub async fn start_tracking(
226 &self,
227 query_id: String,
228 operation_name: Option<String>,
229 ) -> Result<()> {
230 let mut system = self.system.write().await;
232 system.refresh_memory();
233
234 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 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 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 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 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 let mut completed = self.completed.write().await;
291 completed.push(allocation.clone());
292
293 self.cleanup_old_allocations(&mut completed).await;
295
296 Ok(allocation)
297 }
298
299 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 pub async fn get_completed_allocations(&self) -> Vec<MemoryAllocation> {
307 let completed = self.completed.read().await;
308 completed.clone()
309 }
310
311 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 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 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 sorted.sort_by_key(|s| Reverse(s.total_allocated));
362 sorted.truncate(limit);
363 sorted
364 }
365
366 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 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
419pub struct MemoryLeakStatistics {
420 pub total_queries: u64,
422 pub queries_with_leaks: u64,
424 pub total_allocated: u64,
426 pub total_deallocated: u64,
428 pub total_leaked: u64,
430 pub avg_allocation_per_query: u64,
432 pub leak_rate: f64,
434}
435
436impl MemoryLeakStatistics {
437 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#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct MemoryStatistics {
450 pub active_queries: usize,
452 pub completed_queries: usize,
454 pub total_allocated: u64,
456 pub total_deallocated: u64,
458 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 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 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 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 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 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); 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); 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}