1use crate::traits::BlockStore;
15use async_trait::async_trait;
16use ipfrs_core::{Block, Cid, Result};
17use lru::LruCache;
18use parking_lot::Mutex;
19use serde::Serialize;
20use std::num::NonZeroUsize;
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::sync::Arc;
23
24#[derive(Debug, Clone)]
30pub struct CacheConfig {
31 pub l1_capacity: NonZeroUsize,
35
36 pub max_block_bytes: usize,
41}
42
43impl Default for CacheConfig {
44 fn default() -> Self {
45 Self {
46 l1_capacity: NonZeroUsize::new(1024).expect("1024 > 0"),
47 max_block_bytes: 256 * 1024,
48 }
49 }
50}
51
52#[derive(Debug, Default, Serialize)]
62pub struct CacheStats {
63 pub hits: AtomicU64,
65 pub misses: AtomicU64,
67 pub evictions: AtomicU64,
69 pub puts: AtomicU64,
71}
72
73impl CacheStats {
74 pub fn hit_rate(&self) -> f64 {
78 let h = self.hits.load(Ordering::Relaxed);
79 let m = self.misses.load(Ordering::Relaxed);
80 let total = h + m;
81 if total == 0 {
82 0.0
83 } else {
84 h as f64 / total as f64
85 }
86 }
87
88 pub fn snapshot(&self) -> CacheStatsSnapshot {
90 let hits = self.hits.load(Ordering::Relaxed);
91 let misses = self.misses.load(Ordering::Relaxed);
92 let evictions = self.evictions.load(Ordering::Relaxed);
93 let puts = self.puts.load(Ordering::Relaxed);
94 let total = hits + misses;
95 let hit_rate = if total == 0 {
96 0.0
97 } else {
98 hits as f64 / total as f64
99 };
100 CacheStatsSnapshot {
101 hits,
102 misses,
103 evictions,
104 puts,
105 hit_rate,
106 }
107 }
108}
109
110#[derive(Clone, Debug, Serialize)]
116pub struct CacheStatsSnapshot {
117 pub hits: u64,
119 pub misses: u64,
121 pub evictions: u64,
123 pub puts: u64,
125 pub hit_rate: f64,
127}
128
129pub struct CachedBlockStore<S: BlockStore> {
146 inner: S,
148 cache: Mutex<LruCache<String, bytes::Bytes>>,
150 max_block_bytes: usize,
152 stats: CacheStats,
154}
155
156impl<S: BlockStore> CachedBlockStore<S> {
157 pub fn new(inner: S, config: CacheConfig) -> Self {
159 Self {
160 inner,
161 cache: Mutex::new(LruCache::new(config.l1_capacity)),
162 max_block_bytes: config.max_block_bytes,
163 stats: CacheStats::default(),
164 }
165 }
166
167 pub fn with_default_config(inner: S) -> Self {
169 Self::new(inner, CacheConfig::default())
170 }
171
172 pub fn stats(&self) -> CacheStatsSnapshot {
174 self.stats.snapshot()
175 }
176
177 pub fn cache_size(&self) -> usize {
179 self.cache.lock().len()
180 }
181
182 pub fn invalidate(&self, cid: &Cid) {
184 self.cache.lock().pop(&cid.to_string());
185 }
186
187 pub fn clear_cache(&self) {
189 self.cache.lock().clear();
190 }
191
192 pub fn inner(&self) -> &S {
194 &self.inner
195 }
196
197 fn l1_insert(&self, cid: &Cid, data: bytes::Bytes) {
203 let key = cid.to_string();
204 let mut guard = self.cache.lock();
205 if guard.push(key, data).is_some() {
207 self.stats.evictions.fetch_add(1, Ordering::Relaxed);
208 }
209 }
210
211 fn l1_peek(&self, cid: &Cid) -> Option<bytes::Bytes> {
213 self.cache.lock().get(&cid.to_string()).cloned()
214 }
215}
216
217#[async_trait]
222impl<S: BlockStore> BlockStore for CachedBlockStore<S> {
223 async fn put(&self, block: &Block) -> Result<()> {
225 self.stats.puts.fetch_add(1, Ordering::Relaxed);
226 self.inner.put(block).await?;
227 if block.data().len() <= self.max_block_bytes {
228 self.l1_insert(block.cid(), block.data().clone());
229 }
230 Ok(())
231 }
232
233 async fn get(&self, cid: &Cid) -> Result<Option<Block>> {
235 if let Some(data) = self.l1_peek(cid) {
237 self.stats.hits.fetch_add(1, Ordering::Relaxed);
238 return Ok(Some(Block::from_parts(*cid, data)));
239 }
240
241 self.stats.misses.fetch_add(1, Ordering::Relaxed);
243 match self.inner.get(cid).await? {
244 Some(block) => {
245 if block.data().len() <= self.max_block_bytes {
247 self.l1_insert(cid, block.data().clone());
248 }
249 Ok(Some(block))
250 }
251 None => Ok(None),
252 }
253 }
254
255 async fn has(&self, cid: &Cid) -> Result<bool> {
257 if self.cache.lock().contains(&cid.to_string()) {
258 return Ok(true);
259 }
260 self.inner.has(cid).await
261 }
262
263 async fn delete(&self, cid: &Cid) -> Result<()> {
265 self.cache.lock().pop(&cid.to_string());
266 self.inner.delete(cid).await
267 }
268
269 fn list_cids(&self) -> Result<Vec<Cid>> {
270 self.inner.list_cids()
271 }
272
273 fn len(&self) -> usize {
274 self.inner.len()
275 }
276
277 fn is_empty(&self) -> bool {
278 self.inner.is_empty()
279 }
280
281 async fn flush(&self) -> Result<()> {
282 self.inner.flush().await
283 }
284
285 async fn close(&self) -> Result<()> {
286 self.clear_cache();
287 self.inner.close().await
288 }
289
290 async fn get_many(&self, cids: &[Cid]) -> Result<Vec<Option<Block>>> {
295 let mut results: Vec<Option<Block>> = Vec::with_capacity(cids.len());
296 let mut miss_cids: Vec<Cid> = Vec::new();
297 let mut miss_indices: Vec<usize> = Vec::new();
298
299 {
301 let mut guard = self.cache.lock();
302 for (i, cid) in cids.iter().enumerate() {
303 if let Some(data) = guard.get(&cid.to_string()).cloned() {
304 self.stats.hits.fetch_add(1, Ordering::Relaxed);
305 results.push(Some(Block::from_parts(*cid, data)));
306 } else {
307 self.stats.misses.fetch_add(1, Ordering::Relaxed);
308 results.push(None);
309 miss_cids.push(*cid);
310 miss_indices.push(i);
311 }
312 }
313 }
314
315 if !miss_cids.is_empty() {
316 let fetched = self.inner.get_many(&miss_cids).await?;
317 let mut guard = self.cache.lock();
318 for (idx, block_opt) in miss_indices.iter().zip(fetched.iter()) {
319 if let Some(block) = block_opt {
320 if block.data().len() <= self.max_block_bytes
321 && guard
322 .push(block.cid().to_string(), block.data().clone())
323 .is_some()
324 {
325 self.stats.evictions.fetch_add(1, Ordering::Relaxed);
326 }
327 results[*idx] = Some(block.clone());
328 }
329 }
330 }
331
332 Ok(results)
333 }
334
335 async fn put_many(&self, blocks: &[Block]) -> Result<()> {
336 self.stats
337 .puts
338 .fetch_add(blocks.len() as u64, Ordering::Relaxed);
339
340 self.inner.put_many(blocks).await?;
342
343 let mut guard = self.cache.lock();
345 for block in blocks {
346 if block.data().len() <= self.max_block_bytes
347 && guard
348 .push(block.cid().to_string(), block.data().clone())
349 .is_some()
350 {
351 self.stats.evictions.fetch_add(1, Ordering::Relaxed);
352 }
353 }
354
355 Ok(())
356 }
357
358 async fn has_many(&self, cids: &[Cid]) -> Result<Vec<bool>> {
359 let mut results: Vec<bool> = Vec::with_capacity(cids.len());
360 let mut miss_cids: Vec<Cid> = Vec::new();
361 let mut miss_indices: Vec<usize> = Vec::new();
362
363 {
364 let guard = self.cache.lock();
365 for (i, cid) in cids.iter().enumerate() {
366 if guard.contains(&cid.to_string()) {
367 results.push(true);
368 } else {
369 results.push(false);
370 miss_cids.push(*cid);
371 miss_indices.push(i);
372 }
373 }
374 }
375
376 if !miss_cids.is_empty() {
377 let store_results = self.inner.has_many(&miss_cids).await?;
378 for (idx, &exists) in miss_indices.iter().zip(store_results.iter()) {
379 results[*idx] = exists;
380 }
381 }
382
383 Ok(results)
384 }
385
386 async fn delete_many(&self, cids: &[Cid]) -> Result<()> {
387 {
388 let mut guard = self.cache.lock();
389 for cid in cids {
390 guard.pop(&cid.to_string());
391 }
392 }
393 self.inner.delete_many(cids).await
394 }
395}
396
397pub struct BlockCache {
405 cache: Arc<Mutex<LruCache<Cid, Block>>>,
406 capacity: usize,
407 hits: Arc<AtomicU64>,
408 misses: Arc<AtomicU64>,
409}
410
411impl BlockCache {
412 pub fn new(capacity: usize) -> Self {
414 let cap =
415 NonZeroUsize::new(capacity).unwrap_or_else(|| NonZeroUsize::new(1000).expect("1000>0"));
416 Self {
417 cache: Arc::new(Mutex::new(LruCache::new(cap))),
418 capacity,
419 hits: Arc::new(AtomicU64::new(0)),
420 misses: Arc::new(AtomicU64::new(0)),
421 }
422 }
423
424 #[inline]
426 pub fn get(&self, cid: &Cid) -> Option<Block> {
427 let result = self.cache.lock().get(cid).cloned();
428 if result.is_some() {
429 self.hits.fetch_add(1, Ordering::Relaxed);
430 } else {
431 self.misses.fetch_add(1, Ordering::Relaxed);
432 }
433 result
434 }
435
436 #[inline]
438 pub fn put(&self, block: Block) {
439 self.cache.lock().put(*block.cid(), block);
440 }
441
442 pub fn remove(&self, cid: &Cid) {
444 self.cache.lock().pop(cid);
445 }
446
447 pub fn clear(&self) {
449 self.cache.lock().clear();
450 self.hits.store(0, Ordering::Relaxed);
451 self.misses.store(0, Ordering::Relaxed);
452 }
453
454 pub fn stats(&self) -> LegacyCacheStats {
456 LegacyCacheStats {
457 hits: self.hits.load(Ordering::Relaxed),
458 misses: self.misses.load(Ordering::Relaxed),
459 size: self.cache.lock().len(),
460 capacity: self.capacity,
461 }
462 }
463
464 pub fn len(&self) -> usize {
466 self.cache.lock().len()
467 }
468
469 pub fn is_empty(&self) -> bool {
471 self.cache.lock().is_empty()
472 }
473}
474
475#[derive(Debug, Clone, Default)]
481pub struct LegacyCacheStats {
482 pub hits: u64,
484 pub misses: u64,
486 pub size: usize,
488 pub capacity: usize,
490}
491
492impl LegacyCacheStats {
493 pub fn hit_rate(&self) -> f64 {
495 let total = self.hits + self.misses;
496 if total == 0 {
497 0.0
498 } else {
499 self.hits as f64 / total as f64
500 }
501 }
502
503 pub fn miss_rate(&self) -> f64 {
505 1.0 - self.hit_rate()
506 }
507}
508
509pub struct TieredBlockCache {
517 l1_cache: Arc<Mutex<LruCache<Cid, Block>>>,
518 l2_cache: Arc<Mutex<LruCache<Cid, Block>>>,
519 l1_capacity: usize,
520 l2_capacity: usize,
521 l1_hits: Arc<AtomicU64>,
522 l2_hits: Arc<AtomicU64>,
523 misses: Arc<AtomicU64>,
524}
525
526impl TieredBlockCache {
527 pub fn new(l1_capacity: usize, l2_capacity: usize) -> Self {
532 let l1_cap = NonZeroUsize::new(l1_capacity)
533 .unwrap_or_else(|| NonZeroUsize::new(100).expect("100>0"));
534 let l2_cap = NonZeroUsize::new(l2_capacity)
535 .unwrap_or_else(|| NonZeroUsize::new(1000).expect("1000>0"));
536
537 Self {
538 l1_cache: Arc::new(Mutex::new(LruCache::new(l1_cap))),
539 l2_cache: Arc::new(Mutex::new(LruCache::new(l2_cap))),
540 l1_capacity,
541 l2_capacity,
542 l1_hits: Arc::new(AtomicU64::new(0)),
543 l2_hits: Arc::new(AtomicU64::new(0)),
544 misses: Arc::new(AtomicU64::new(0)),
545 }
546 }
547
548 #[inline]
550 pub fn get(&self, cid: &Cid) -> Option<Block> {
551 if let Some(block) = self.l1_cache.lock().get(cid) {
552 self.l1_hits.fetch_add(1, Ordering::Relaxed);
553 return Some(block.clone());
554 }
555
556 if let Some(block) = self.l2_cache.lock().get(cid) {
557 self.l2_hits.fetch_add(1, Ordering::Relaxed);
558 let block_clone = block.clone();
559 self.l1_cache.lock().put(*cid, block_clone.clone());
560 return Some(block_clone);
561 }
562
563 self.misses.fetch_add(1, Ordering::Relaxed);
564 None
565 }
566
567 #[inline]
569 pub fn put(&self, block: Block) {
570 let cid = *block.cid();
571 if let Some(evicted) = self.l1_cache.lock().push(cid, block) {
572 self.l2_cache.lock().put(evicted.0, evicted.1);
573 }
574 }
575
576 pub fn remove(&self, cid: &Cid) {
578 self.l1_cache.lock().pop(cid);
579 self.l2_cache.lock().pop(cid);
580 }
581
582 pub fn clear(&self) {
584 self.l1_cache.lock().clear();
585 self.l2_cache.lock().clear();
586 self.l1_hits.store(0, Ordering::Relaxed);
587 self.l2_hits.store(0, Ordering::Relaxed);
588 self.misses.store(0, Ordering::Relaxed);
589 }
590
591 pub fn stats(&self) -> TieredCacheStats {
593 TieredCacheStats {
594 l1_size: self.l1_cache.lock().len(),
595 l1_capacity: self.l1_capacity,
596 l2_size: self.l2_cache.lock().len(),
597 l2_capacity: self.l2_capacity,
598 l1_hits: self.l1_hits.load(Ordering::Relaxed),
599 l2_hits: self.l2_hits.load(Ordering::Relaxed),
600 misses: self.misses.load(Ordering::Relaxed),
601 }
602 }
603}
604
605#[derive(Debug, Clone)]
607pub struct TieredCacheStats {
608 pub l1_size: usize,
610 pub l1_capacity: usize,
612 pub l2_size: usize,
614 pub l2_capacity: usize,
616 pub l1_hits: u64,
618 pub l2_hits: u64,
620 pub misses: u64,
622}
623
624impl TieredCacheStats {
625 pub fn hit_rate(&self) -> f64 {
627 let total_hits = self.l1_hits + self.l2_hits;
628 let total = total_hits + self.misses;
629 if total == 0 {
630 0.0
631 } else {
632 total_hits as f64 / total as f64
633 }
634 }
635
636 pub fn l1_hit_rate(&self) -> f64 {
638 let total = self.l1_hits + self.l2_hits + self.misses;
639 if total == 0 {
640 0.0
641 } else {
642 self.l1_hits as f64 / total as f64
643 }
644 }
645
646 pub fn l2_hit_rate(&self) -> f64 {
648 let total = self.l1_hits + self.l2_hits + self.misses;
649 if total == 0 {
650 0.0
651 } else {
652 self.l2_hits as f64 / total as f64
653 }
654 }
655
656 pub fn miss_rate(&self) -> f64 {
658 1.0 - self.hit_rate()
659 }
660}
661
662pub struct TieredCachedBlockStore<S: BlockStore> {
669 store: S,
670 cache: TieredBlockCache,
671}
672
673impl<S: BlockStore> TieredCachedBlockStore<S> {
674 pub fn new(store: S, l1_capacity: usize, l2_capacity: usize) -> Self {
676 Self {
677 store,
678 cache: TieredBlockCache::new(l1_capacity, l2_capacity),
679 }
680 }
681
682 pub fn store(&self) -> &S {
684 &self.store
685 }
686
687 pub fn cache_stats(&self) -> TieredCacheStats {
689 self.cache.stats()
690 }
691}
692
693#[async_trait]
694impl<S: BlockStore> BlockStore for TieredCachedBlockStore<S> {
695 async fn put(&self, block: &Block) -> Result<()> {
696 self.cache.put(block.clone());
697 self.store.put(block).await
698 }
699
700 async fn get(&self, cid: &Cid) -> Result<Option<Block>> {
701 if let Some(block) = self.cache.get(cid) {
702 return Ok(Some(block));
703 }
704 if let Some(block) = self.store.get(cid).await? {
705 self.cache.put(block.clone());
706 Ok(Some(block))
707 } else {
708 Ok(None)
709 }
710 }
711
712 async fn has(&self, cid: &Cid) -> Result<bool> {
713 if self.cache.get(cid).is_some() {
714 return Ok(true);
715 }
716 self.store.has(cid).await
717 }
718
719 async fn delete(&self, cid: &Cid) -> Result<()> {
720 self.cache.remove(cid);
721 self.store.delete(cid).await
722 }
723
724 fn list_cids(&self) -> Result<Vec<Cid>> {
725 self.store.list_cids()
726 }
727
728 fn len(&self) -> usize {
729 self.store.len()
730 }
731
732 fn is_empty(&self) -> bool {
733 self.store.is_empty()
734 }
735
736 async fn flush(&self) -> Result<()> {
737 self.store.flush().await
738 }
739
740 async fn close(&self) -> Result<()> {
741 self.cache.clear();
742 self.store.close().await
743 }
744}
745
746#[cfg(all(test, feature = "sled-backend"))]
751mod tests {
752 use super::*;
753 use crate::blockstore::BlockStoreConfig;
754 use crate::memory::MemoryBlockStore;
755 use bytes::Bytes;
756 use ipfrs_core::Block;
757
758 fn unique_path(tag: &str) -> std::path::PathBuf {
763 std::env::temp_dir().join(format!(
764 "ipfrs-cache-test-{}-{}-{}",
765 tag,
766 std::process::id(),
767 fastrand::u64(..)
768 ))
769 }
770
771 fn make_block(content: &[u8]) -> Block {
772 Block::new(Bytes::copy_from_slice(content)).expect("block creation")
773 }
774
775 fn make_store_with_config(config: CacheConfig) -> CachedBlockStore<MemoryBlockStore> {
776 CachedBlockStore::new(MemoryBlockStore::new(), config)
777 }
778
779 fn make_store() -> CachedBlockStore<MemoryBlockStore> {
780 CachedBlockStore::with_default_config(MemoryBlockStore::new())
781 }
782
783 #[tokio::test]
789 async fn test_cache_hit() {
790 let store = make_store();
791 let block = make_block(b"hello world");
792
793 store.put(&block).await.expect("put");
794 let result = store.get(block.cid()).await.expect("get");
795 assert!(result.is_some());
796
797 let snap = store.stats();
798 assert_eq!(snap.hits, 1, "expected 1 L1 hit");
799 assert_eq!(snap.misses, 0, "expected 0 misses");
800 }
801
802 #[tokio::test]
809 async fn test_cache_miss_then_populate() {
810 let mem = MemoryBlockStore::new();
812 let block = make_block(b"L2 resident block");
813 mem.put(&block).await.expect("l2 put");
814
815 let store = CachedBlockStore::with_default_config(mem);
817
818 let r1 = store.get(block.cid()).await.expect("get 1");
820 assert!(r1.is_some());
821
822 let snap1 = store.stats();
823 assert_eq!(snap1.misses, 1);
824 assert_eq!(snap1.hits, 0);
825
826 let r2 = store.get(block.cid()).await.expect("get 2");
828 assert!(r2.is_some());
829
830 let snap2 = store.stats();
831 assert_eq!(snap2.hits, 1);
832 assert_eq!(snap2.misses, 1);
833
834 assert_eq!(store.cache_size(), 1);
836 }
837
838 #[tokio::test]
845 async fn test_cache_eviction() {
846 let cap = NonZeroUsize::new(3).expect("3>0");
847 let config = CacheConfig {
848 l1_capacity: cap,
849 max_block_bytes: 64 * 1024,
850 };
851 let store = make_store_with_config(config);
852
853 for i in 0_u8..3 {
855 let block = make_block(&[i; 8]);
856 store.put(&block).await.expect("put");
857 }
858 assert_eq!(store.cache_size(), 3);
859 assert_eq!(store.stats().evictions, 0);
860
861 let extra = make_block(b"extra block!!!");
863 store.put(&extra).await.expect("put extra");
864
865 assert_eq!(store.cache_size(), 3);
866 assert_eq!(store.stats().evictions, 1, "expected exactly 1 eviction");
867 }
868
869 #[tokio::test]
875 async fn test_cache_delete_removes_from_both() {
876 let store = make_store();
877 let block = make_block(b"to be deleted");
878
879 store.put(&block).await.expect("put");
880 store.delete(block.cid()).await.expect("delete");
881
882 assert_eq!(store.cache_size(), 0);
884
885 let result = store.get(block.cid()).await.expect("get after delete");
887 assert!(result.is_none());
888 }
889
890 #[tokio::test]
897 async fn test_cache_large_block_skipped() {
898 let config = CacheConfig {
899 l1_capacity: NonZeroUsize::new(1024).expect("1024>0"),
900 max_block_bytes: 4, };
902 let store = make_store_with_config(config);
903
904 let large_block = make_block(b"larger than threshold");
905 store.put(&large_block).await.expect("put large");
906
907 assert_eq!(store.cache_size(), 0, "large block must not enter L1");
909
910 let result = store.get(large_block.cid()).await.expect("get large");
912 assert!(result.is_some(), "large block must be in L2");
913 }
914
915 #[tokio::test]
921 async fn test_hit_rate_calculation() {
922 let store = make_store();
923 let block = make_block(b"hit rate test");
924
925 store.put(&block).await.expect("put");
926
927 for _ in 0_u8..3 {
929 store.get(block.cid()).await.expect("hit");
930 }
931
932 let absent = make_block(b"absent block");
934 store.get(absent.cid()).await.expect("miss");
935
936 let snap = store.stats();
937 assert_eq!(snap.hits, 3);
938 assert_eq!(snap.misses, 1);
939
940 let expected = 3.0_f64 / 4.0_f64;
941 let diff = (snap.hit_rate - expected).abs();
942 assert!(
943 diff < 1e-10,
944 "hit_rate={} expected={}",
945 snap.hit_rate,
946 expected
947 );
948 }
949
950 #[tokio::test]
957 async fn test_cache_stats_snapshot() {
958 let config = CacheConfig {
959 l1_capacity: NonZeroUsize::new(2).expect("2>0"),
960 max_block_bytes: 64 * 1024,
961 };
962 let store = make_store_with_config(config);
963
964 let b1 = make_block(b"block-one");
965 let b2 = make_block(b"block-two");
966 let b3 = make_block(b"block-three");
967
968 store.put(&b1).await.expect("put b1");
970 store.put(&b2).await.expect("put b2");
971 store.put(&b3).await.expect("put b3"); store.get(b2.cid()).await.expect("get b2");
975 let absent = make_block(b"does-not-exist");
976 store.get(absent.cid()).await.expect("absent get");
977
978 let snap = store.stats();
979 assert_eq!(snap.puts, 3, "puts");
980 assert_eq!(snap.hits, 1, "hits");
981 assert_eq!(snap.misses, 1, "misses");
982 assert!(snap.evictions >= 1, "at least one eviction expected");
983 let expected_rate = 0.5_f64;
984 let diff = (snap.hit_rate - expected_rate).abs();
985 assert!(diff < 1e-10, "hit_rate mismatch: {}", snap.hit_rate);
986 }
987
988 #[tokio::test]
993 async fn test_cached_sled_put_get() {
994 use crate::blockstore::SledBlockStore;
995
996 let path = unique_path("sled");
997 let _ = std::fs::remove_dir_all(&path);
998
999 let config_sled = BlockStoreConfig {
1000 path: path.clone(),
1001 cache_size: 4 * 1024 * 1024,
1002 };
1003 let sled = SledBlockStore::new(config_sled).expect("sled open");
1004 let store = CachedBlockStore::with_default_config(sled);
1005
1006 let block = make_block(b"sled cache integration");
1007 store.put(&block).await.expect("put");
1008
1009 let r = store.get(block.cid()).await.expect("get");
1011 assert!(r.is_some());
1012 assert_eq!(store.stats().hits, 1);
1013
1014 let _ = std::fs::remove_dir_all(&path);
1015 }
1016}