1#![doc = include_str!("../README.md")]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20#![cfg_attr(docsrs, doc(auto_cfg))]
21#![deny(missing_docs)]
22use std::fmt::Debug;
23use std::sync::Arc;
24use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
25
26use opendal_core::raw::*;
27use opendal_core::*;
28
29#[derive(Clone)]
56pub struct TailCutLayerBuilder {
57 percentile: u8,
58 safety_factor: f64,
59 window: Duration,
60 min_samples: usize,
61 min_deadline: Duration,
62 max_deadline: Duration,
63}
64
65impl Default for TailCutLayerBuilder {
66 fn default() -> Self {
67 Self {
68 percentile: 95,
69 safety_factor: 1.3,
70 window: Duration::from_secs(60),
71 min_samples: 200,
72 min_deadline: Duration::from_millis(500),
73 max_deadline: Duration::from_secs(30),
74 }
75 }
76}
77
78impl TailCutLayerBuilder {
79 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn percentile(mut self, percentile: u8) -> Self {
94 assert!(
95 (50..=99).contains(&percentile),
96 "percentile must be between 50 and 99"
97 );
98 self.percentile = percentile;
99 self
100 }
101
102 pub fn safety_factor(mut self, factor: f64) -> Self {
113 assert!(
114 (1.0..=5.0).contains(&factor),
115 "safety_factor must be between 1.0 and 5.0"
116 );
117 self.safety_factor = factor;
118 self
119 }
120
121 pub fn window(mut self, window: Duration) -> Self {
132 assert!(
133 window <= Duration::from_secs(120),
134 "window must be <= 120 seconds"
135 );
136 self.window = window;
137 self
138 }
139
140 pub fn min_samples(mut self, min_samples: usize) -> Self {
147 self.min_samples = min_samples;
148 self
149 }
150
151 pub fn min_deadline(mut self, deadline: Duration) -> Self {
158 self.min_deadline = deadline;
159 self
160 }
161
162 pub fn max_deadline(mut self, deadline: Duration) -> Self {
169 self.max_deadline = deadline;
170 self
171 }
172
173 pub fn build(self) -> TailCutLayer {
204 TailCutLayer {
205 config: Arc::new(TailCutConfig {
206 percentile: self.percentile,
207 safety_factor: self.safety_factor,
208 window: self.window,
209 min_samples: self.min_samples,
210 min_deadline: self.min_deadline,
211 max_deadline: self.max_deadline,
212 }),
213 stats: Arc::new(TailCutStats::new()),
214 }
215 }
216}
217
218#[derive(Debug)]
220struct TailCutConfig {
221 percentile: u8,
222 safety_factor: f64,
223 window: Duration,
224 min_samples: usize,
225 min_deadline: Duration,
226 max_deadline: Duration,
227}
228
229#[derive(Clone)]
259pub struct TailCutLayer {
260 config: Arc<TailCutConfig>,
261 stats: Arc<TailCutStats>,
262}
263
264impl Debug for TailCutLayer {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 f.debug_struct("TailCutLayer")
267 .field("config", &self.config)
268 .finish_non_exhaustive()
269 }
270}
271
272impl Default for TailCutLayer {
273 fn default() -> Self {
274 Self::builder().build()
275 }
276}
277
278impl TailCutLayer {
279 pub fn builder() -> TailCutLayerBuilder {
281 TailCutLayerBuilder::default()
282 }
283
284 pub fn new() -> Self {
288 Self::default()
289 }
290}
291
292impl Layer for TailCutLayer {
293 fn apply_service(&self, inner: Servicer) -> Servicer {
294 Arc::new(self.layer(inner))
295 }
296}
297
298impl TailCutLayer {
299 fn layer(&self, inner: Servicer) -> TailCutService {
300 TailCutService {
301 inner,
302 config: self.config.clone(),
303 stats: self.stats.clone(),
304 }
305 }
306}
307
308#[doc(hidden)]
309pub struct TailCutService {
311 inner: Servicer,
312 config: Arc<TailCutConfig>,
313 stats: Arc<TailCutStats>,
314}
315
316impl Debug for TailCutService {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.debug_struct("TailCutService")
319 .field("config", &self.config)
320 .finish_non_exhaustive()
321 }
322}
323
324impl TailCutService {
325 fn calculate_deadline(&self, op: Operation, size: Option<u64>) -> Option<Duration> {
327 let op_stats = self.stats.stats_for(op);
328
329 if op_stats.total_samples(size, self.config.window) < self.config.min_samples {
330 return None;
331 }
332
333 let q = self.config.percentile as f64 / 100.0;
334 let pctl = op_stats.quantile(size, q, self.config.window)?;
335
336 let deadline = Duration::from_secs_f64(pctl.as_secs_f64() * self.config.safety_factor);
337 Some(deadline.clamp(self.config.min_deadline, self.config.max_deadline))
338 }
339
340 async fn with_deadline<F, T>(&self, op: Operation, size: Option<u64>, fut: F) -> Result<T>
341 where
342 F: Future<Output = Result<T>>,
343 {
344 let start = Instant::now();
345
346 let result = if let Some(deadline) = self.calculate_deadline(op, size) {
347 match tokio::time::timeout(deadline, fut).await {
348 Ok(res) => res,
349 Err(_) => Err(Error::new(ErrorKind::Unexpected, "cancelled by tail cut")
350 .with_operation(op)
351 .with_context("percentile", format!("P{}", self.config.percentile))
352 .with_context("deadline", format!("{:?}", deadline))
353 .set_temporary()),
354 }
355 } else {
356 fut.await
357 };
358
359 if result.is_ok() {
360 let latency = start.elapsed();
361 self.stats.stats_for(op).record(size, latency);
362 }
363
364 result
365 }
366}
367
368impl Service for TailCutService {
369 type Reader = TailCutWrapper<oio::Reader>;
370 type Writer = TailCutWrapper<oio::Writer>;
371 type Lister = TailCutWrapper<oio::Lister>;
372 type Deleter = TailCutWrapper<oio::Deleter>;
373 type Copier = TailCutWrapper<oio::Copier>;
374
375 fn info(&self) -> ServiceInfo {
376 self.inner.info()
377 }
378
379 fn capability(&self) -> Capability {
380 self.inner.capability()
381 }
382
383 async fn create_dir(
384 &self,
385 ctx: &OperationContext,
386 path: &str,
387 args: OpCreateDir,
388 ) -> Result<RpCreateDir> {
389 self.with_deadline(
390 Operation::CreateDir,
391 None,
392 self.inner.create_dir(ctx, path, args),
393 )
394 .await
395 }
396
397 fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
398 self.inner
399 .read(ctx, path, args)
400 .map(|r| TailCutWrapper::new(r, None, self.config.clone(), self.stats.clone()))
401 }
402
403 fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
404 self.inner
405 .write(ctx, path, args)
406 .map(|w| TailCutWrapper::new(w, None, self.config.clone(), self.stats.clone()))
407 }
408
409 fn copy(
410 &self,
411 ctx: &OperationContext,
412 from: &str,
413 to: &str,
414 args: OpCopy,
415 opts: OpCopier,
416 ) -> Result<Self::Copier> {
417 self.inner
418 .copy(ctx, from, to, args, opts)
419 .map(|c| TailCutWrapper::new(c, None, self.config.clone(), self.stats.clone()))
420 }
421
422 async fn rename(
423 &self,
424 ctx: &OperationContext,
425 from: &str,
426 to: &str,
427 args: OpRename,
428 ) -> Result<RpRename> {
429 self.with_deadline(
430 Operation::Rename,
431 None,
432 self.inner.rename(ctx, from, to, args),
433 )
434 .await
435 }
436
437 async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
438 self.with_deadline(Operation::Stat, None, self.inner.stat(ctx, path, args))
439 .await
440 }
441
442 fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
443 self.inner
444 .delete(ctx)
445 .map(|d| TailCutWrapper::new(d, None, self.config.clone(), self.stats.clone()))
446 }
447
448 fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
449 self.inner
450 .list(ctx, path, args)
451 .map(|l| TailCutWrapper::new(l, None, self.config.clone(), self.stats.clone()))
452 }
453
454 async fn presign(
455 &self,
456 ctx: &OperationContext,
457 path: &str,
458 args: OpPresign,
459 ) -> Result<RpPresign> {
460 self.with_deadline(
461 Operation::Presign,
462 None,
463 self.inner.presign(ctx, path, args),
464 )
465 .await
466 }
467}
468
469#[doc(hidden)]
470pub struct TailCutWrapper<R> {
472 inner: R,
473 size: Option<u64>,
474 config: Arc<TailCutConfig>,
475 stats: Arc<TailCutStats>,
476}
477
478impl<R> TailCutWrapper<R> {
479 fn new(
480 inner: R,
481 size: Option<u64>,
482 config: Arc<TailCutConfig>,
483 stats: Arc<TailCutStats>,
484 ) -> Self {
485 Self {
486 inner,
487 size,
488 config,
489 stats,
490 }
491 }
492
493 fn calculate_deadline(&self, op: Operation) -> Option<Duration> {
494 self.calculate_deadline_for(op, self.size)
495 }
496
497 fn calculate_deadline_for(&self, op: Operation, size: Option<u64>) -> Option<Duration> {
498 let op_stats = self.stats.stats_for(op);
499
500 if op_stats.total_samples(size, self.config.window) < self.config.min_samples {
501 return None;
502 }
503
504 let q = self.config.percentile as f64 / 100.0;
505 let pctl = op_stats.quantile(size, q, self.config.window)?;
506
507 let deadline = Duration::from_secs_f64(pctl.as_secs_f64() * self.config.safety_factor);
508 Some(deadline.clamp(self.config.min_deadline, self.config.max_deadline))
509 }
510
511 #[inline]
512 async fn with_io_deadline<F, T>(
513 deadline: Option<Duration>,
514 percentile: u8,
515 stats: &Arc<TailCutStats>,
516 size: Option<u64>,
517 op: Operation,
518 fut: F,
519 ) -> Result<T>
520 where
521 F: std::future::Future<Output = Result<T>>,
522 {
523 let start = Instant::now();
524
525 let result = if let Some(dl) = deadline {
526 match tokio::time::timeout(dl, fut).await {
527 Ok(res) => res,
528 Err(_) => Err(
529 Error::new(ErrorKind::Unexpected, "io cancelled by tail cut")
530 .with_operation(op)
531 .with_context("percentile", format!("P{}", percentile))
532 .with_context("deadline", format!("{:?}", dl))
533 .set_temporary(),
534 ),
535 }
536 } else {
537 fut.await
538 };
539
540 if result.is_ok() {
541 let latency = start.elapsed();
542 stats.stats_for(op).record(size, latency);
543 }
544
545 result
546 }
547}
548
549impl<R: oio::ReadStream> oio::ReadStream for TailCutWrapper<R> {
550 async fn read(&mut self) -> Result<Buffer> {
551 let deadline = self.calculate_deadline_for(Operation::Read, self.size);
552 Self::with_io_deadline(
553 deadline,
554 self.config.percentile,
555 &self.stats,
556 self.size,
557 Operation::Read,
558 self.inner.read(),
559 )
560 .await
561 }
562}
563
564impl<R: oio::Read> oio::Read for TailCutWrapper<R> {
565 async fn open(&self, range: BytesRange) -> Result<(RpRead, Box<dyn oio::ReadStreamDyn>)> {
566 let size = range.size();
567 let deadline = self.calculate_deadline_for(Operation::Read, size);
568 let (rp, stream) = Self::with_io_deadline(
569 deadline,
570 self.config.percentile,
571 &self.stats,
572 size,
573 Operation::Read,
574 self.inner.open(range),
575 )
576 .await?;
577
578 Ok((
579 rp,
580 Box::new(TailCutWrapper::new(
581 stream,
582 size,
583 self.config.clone(),
584 self.stats.clone(),
585 )) as Box<dyn oio::ReadStreamDyn>,
586 ))
587 }
588
589 async fn read(&self, range: BytesRange) -> Result<(RpRead, Buffer)> {
590 let size = range.size();
591 let deadline = self.calculate_deadline_for(Operation::Read, size);
592 Self::with_io_deadline(
593 deadline,
594 self.config.percentile,
595 &self.stats,
596 size,
597 Operation::Read,
598 self.inner.read(range),
599 )
600 .await
601 }
602}
603
604impl<R: oio::Write> oio::Write for TailCutWrapper<R> {
605 async fn write(&mut self, bs: Buffer) -> Result<()> {
606 let deadline = self.calculate_deadline(Operation::Write);
607 Self::with_io_deadline(
608 deadline,
609 self.config.percentile,
610 &self.stats,
611 self.size,
612 Operation::Write,
613 self.inner.write(bs),
614 )
615 .await
616 }
617
618 async fn close(&mut self) -> Result<Metadata> {
619 let deadline = self.calculate_deadline(Operation::Write);
620 Self::with_io_deadline(
621 deadline,
622 self.config.percentile,
623 &self.stats,
624 self.size,
625 Operation::Write,
626 self.inner.close(),
627 )
628 .await
629 }
630
631 async fn abort(&mut self) -> Result<()> {
632 let deadline = self.calculate_deadline(Operation::Write);
633 Self::with_io_deadline(
634 deadline,
635 self.config.percentile,
636 &self.stats,
637 self.size,
638 Operation::Write,
639 self.inner.abort(),
640 )
641 .await
642 }
643}
644
645impl<R: oio::List> oio::List for TailCutWrapper<R> {
646 async fn next(&mut self) -> Result<Option<oio::Entry>> {
647 let deadline = self.calculate_deadline(Operation::List);
648 Self::with_io_deadline(
649 deadline,
650 self.config.percentile,
651 &self.stats,
652 self.size,
653 Operation::List,
654 self.inner.next(),
655 )
656 .await
657 }
658}
659
660impl<R: oio::Delete> oio::Delete for TailCutWrapper<R> {
661 async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> {
662 self.inner.delete(path, args).await
663 }
664
665 async fn close(&mut self) -> Result<()> {
666 let deadline = self.calculate_deadline(Operation::Delete);
667 Self::with_io_deadline(
668 deadline,
669 self.config.percentile,
670 &self.stats,
671 self.size,
672 Operation::Delete,
673 self.inner.close(),
674 )
675 .await
676 }
677}
678
679impl<C: oio::Copy> oio::Copy for TailCutWrapper<C> {
680 async fn next(&mut self) -> Result<Option<usize>> {
681 let deadline = self.calculate_deadline(Operation::Copy);
682 Self::with_io_deadline(
683 deadline,
684 self.config.percentile,
685 &self.stats,
686 self.size,
687 Operation::Copy,
688 self.inner.next(),
689 )
690 .await
691 }
692
693 async fn close(&mut self) -> Result<Metadata> {
694 let deadline = self.calculate_deadline(Operation::Copy);
695 Self::with_io_deadline(
696 deadline,
697 self.config.percentile,
698 &self.stats,
699 self.size,
700 Operation::Copy,
701 self.inner.close(),
702 )
703 .await
704 }
705
706 async fn abort(&mut self) -> Result<()> {
707 let deadline = self.calculate_deadline(Operation::Copy);
708 Self::with_io_deadline(
709 deadline,
710 self.config.percentile,
711 &self.stats,
712 self.size,
713 Operation::Copy,
714 self.inner.abort(),
715 )
716 .await
717 }
718}
719
720struct TailCutStats {
722 operations: [Arc<OperationStats>; 7],
724}
725
726impl TailCutStats {
727 fn new() -> Self {
728 Self {
729 operations: std::array::from_fn(|_| Arc::new(OperationStats::new())),
730 }
731 }
732
733 fn stats_for(&self, op: Operation) -> &Arc<OperationStats> {
734 let idx = match op {
735 Operation::Read => 0,
736 Operation::Write => 1,
737 Operation::Stat => 2,
738 Operation::List => 3,
739 Operation::Delete => 4,
740 Operation::Copy => 5,
741 Operation::Rename => 6,
742 _ => 2, };
744 &self.operations[idx]
745 }
746}
747
748struct OperationStats {
750 buckets: Vec<SizeBucket>,
751}
752
753impl OperationStats {
754 fn new() -> Self {
755 Self {
756 buckets: vec![
757 SizeBucket::new(0, Some(4 * 1024)), SizeBucket::new(4 * 1024, Some(64 * 1024)), SizeBucket::new(64 * 1024, Some(1024 * 1024)), SizeBucket::new(1024 * 1024, Some(16 * 1024 * 1024)), SizeBucket::new(16 * 1024 * 1024, Some(256 * 1024 * 1024)), SizeBucket::new(256 * 1024 * 1024, None), ],
764 }
765 }
766
767 fn bucket_for(&self, size: Option<u64>) -> &SizeBucket {
768 let size = size.unwrap_or(u64::MAX);
769
770 self.buckets
771 .iter()
772 .find(|b| b.contains(size))
773 .unwrap_or(&self.buckets[self.buckets.len() - 1])
774 }
775
776 fn record(&self, size: Option<u64>, latency: Duration) {
777 self.bucket_for(size).histogram.record(latency);
778 }
779
780 fn quantile(&self, size: Option<u64>, q: f64, window: Duration) -> Option<Duration> {
781 self.bucket_for(size).histogram.quantile(q, window)
782 }
783
784 fn total_samples(&self, size: Option<u64>, window: Duration) -> usize {
785 self.bucket_for(size).histogram.total_samples(window)
786 }
787}
788
789struct SizeBucket {
791 min_size: u64,
792 max_size: Option<u64>,
793 histogram: WindowedHistogram,
794}
795
796impl SizeBucket {
797 fn new(min_size: u64, max_size: Option<u64>) -> Self {
798 Self {
799 min_size,
800 max_size,
801 histogram: WindowedHistogram::new(),
802 }
803 }
804
805 fn contains(&self, size: u64) -> bool {
806 size >= self.min_size && self.max_size.is_none_or(|max| size < max)
807 }
808}
809
810const SLICE_DURATION_MS: u64 = 10_000; const NUM_SLICES: usize = 12; const NUM_BUCKETS: usize = 17; struct WindowedHistogram {
816 slices: Box<[TimeSlice; NUM_SLICES]>,
817 current_idx: AtomicUsize,
818 last_rotate: AtomicU64,
819}
820
821impl WindowedHistogram {
822 fn new() -> Self {
823 Self {
824 slices: Box::new(std::array::from_fn(|_| TimeSlice::new())),
825 current_idx: AtomicUsize::new(0),
826 last_rotate: AtomicU64::new(Self::now_ms()),
827 }
828 }
829
830 fn record(&self, latency: Duration) {
831 self.maybe_rotate();
832
833 let bucket_idx = Self::latency_to_bucket(latency);
834 let slice_idx = self.current_idx.load(Ordering::Relaxed);
835
836 self.slices[slice_idx].buckets[bucket_idx].fetch_add(1, Ordering::Relaxed);
837 }
838
839 fn quantile(&self, q: f64, window: Duration) -> Option<Duration> {
840 debug_assert!((0.0..=1.0).contains(&q), "quantile must be in [0, 1]");
841
842 let snapshot = self.snapshot(window);
843 let total: u64 = snapshot.iter().sum();
844
845 if total == 0 {
846 return None;
847 }
848
849 let target = (total as f64 * q).ceil() as u64;
850 let mut cumsum = 0u64;
851
852 for (bucket_idx, &count) in snapshot.iter().enumerate() {
853 cumsum += count;
854 if cumsum >= target {
855 return Some(Self::bucket_to_latency(bucket_idx));
856 }
857 }
858
859 Some(Self::bucket_to_latency(NUM_BUCKETS - 1))
860 }
861
862 fn total_samples(&self, window: Duration) -> usize {
863 self.snapshot(window).iter().map(|&v| v as usize).sum()
864 }
865
866 fn snapshot(&self, window: Duration) -> [u64; NUM_BUCKETS] {
867 let mut result = [0u64; NUM_BUCKETS];
868 let now_ms = Self::now_ms();
869 let window_ms = window.as_millis() as u64;
870
871 for slice in self.slices.iter() {
872 let start = slice.start_epoch_ms.load(Ordering::Acquire);
873
874 if start > 0 && now_ms.saturating_sub(start) < window_ms + SLICE_DURATION_MS {
875 for (i, bucket) in slice.buckets.iter().enumerate() {
876 result[i] += bucket.load(Ordering::Relaxed);
877 }
878 }
879 }
880
881 result
882 }
883
884 fn maybe_rotate(&self) {
885 let now = Self::now_ms();
886 let last_rotate = self.last_rotate.load(Ordering::Relaxed);
887
888 if now - last_rotate >= SLICE_DURATION_MS
889 && self
890 .last_rotate
891 .compare_exchange(last_rotate, now, Ordering::Release, Ordering::Relaxed)
892 .is_ok()
893 {
894 let old_idx = self.current_idx.load(Ordering::Relaxed);
895 let new_idx = (old_idx + 1) % NUM_SLICES;
896
897 let new_slice = &self.slices[new_idx];
898 new_slice.start_epoch_ms.store(now, Ordering::Release);
899 for bucket in &new_slice.buckets {
900 bucket.store(0, Ordering::Relaxed);
901 }
902
903 self.current_idx.store(new_idx, Ordering::Release);
904 }
905 }
906
907 fn latency_to_bucket(latency: Duration) -> usize {
908 let ms = latency.as_millis() as u64;
909
910 if ms == 0 {
911 return 0;
912 }
913
914 let bucket = 64 - ms.leading_zeros();
915 (bucket as usize).min(NUM_BUCKETS - 1)
916 }
917
918 fn bucket_to_latency(bucket_idx: usize) -> Duration {
919 if bucket_idx == 0 {
920 Duration::from_millis(1)
921 } else if bucket_idx >= NUM_BUCKETS - 1 {
922 Duration::from_secs(64)
923 } else {
924 Duration::from_millis(1u64 << bucket_idx)
925 }
926 }
927
928 fn now_ms() -> u64 {
929 u64::try_from(Timestamp::now().into_inner().as_millisecond()).unwrap()
931 }
932}
933
934struct TimeSlice {
936 buckets: [AtomicU64; NUM_BUCKETS],
938 start_epoch_ms: AtomicU64,
939}
940
941impl TimeSlice {
942 fn new() -> Self {
943 Self {
944 buckets: std::array::from_fn(|_| AtomicU64::new(0)),
945 start_epoch_ms: AtomicU64::new(0),
946 }
947 }
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953
954 #[test]
955 fn test_latency_to_bucket() {
956 assert_eq!(
957 WindowedHistogram::latency_to_bucket(Duration::from_millis(0)),
958 0
959 );
960 assert_eq!(
961 WindowedHistogram::latency_to_bucket(Duration::from_millis(1)),
962 1
963 );
964 assert_eq!(
965 WindowedHistogram::latency_to_bucket(Duration::from_millis(2)),
966 2
967 );
968 assert_eq!(
969 WindowedHistogram::latency_to_bucket(Duration::from_millis(4)),
970 3
971 );
972 assert_eq!(
973 WindowedHistogram::latency_to_bucket(Duration::from_millis(8)),
974 4
975 );
976 assert_eq!(
977 WindowedHistogram::latency_to_bucket(Duration::from_millis(500)),
978 9
979 );
980 assert_eq!(
981 WindowedHistogram::latency_to_bucket(Duration::from_secs(1)),
982 10
983 );
984 assert_eq!(
985 WindowedHistogram::latency_to_bucket(Duration::from_secs(2)),
986 11
987 );
988 assert_eq!(
989 WindowedHistogram::latency_to_bucket(Duration::from_secs(64)),
990 16
991 );
992 assert_eq!(
993 WindowedHistogram::latency_to_bucket(Duration::from_secs(1000)),
994 16
995 );
996 }
997
998 #[test]
999 fn test_size_bucket_contains() {
1000 let bucket = SizeBucket::new(0, Some(4096));
1001 assert!(bucket.contains(0));
1002 assert!(bucket.contains(4095));
1003 assert!(!bucket.contains(4096));
1004
1005 let bucket = SizeBucket::new(4096, None);
1006 assert!(!bucket.contains(4095));
1007 assert!(bucket.contains(4096));
1008 assert!(bucket.contains(u64::MAX));
1009 }
1010
1011 #[tokio::test]
1012 async fn test_histogram_basic() {
1013 let hist = WindowedHistogram::new();
1014 let now = WindowedHistogram::now_ms();
1015 hist.slices[0].start_epoch_ms.store(now, Ordering::Release);
1016
1017 hist.record(Duration::from_millis(10));
1018 hist.record(Duration::from_millis(20));
1019 hist.record(Duration::from_millis(30));
1020
1021 let samples = hist.total_samples(Duration::from_secs(60));
1022 assert_eq!(samples, 3);
1023
1024 let p50 = hist.quantile(0.5, Duration::from_secs(60));
1025 assert!(p50.is_some());
1026 }
1027
1028 #[tokio::test]
1029 async fn test_tail_cut_layer_build() {
1030 let layer = TailCutLayer::builder()
1031 .percentile(95)
1032 .safety_factor(1.5)
1033 .window(Duration::from_secs(60))
1034 .min_samples(100)
1035 .min_deadline(Duration::from_millis(200))
1036 .max_deadline(Duration::from_secs(20))
1037 .build();
1038
1039 assert_eq!(layer.config.percentile, 95);
1040 assert_eq!(layer.config.safety_factor, 1.5);
1041 assert_eq!(layer.config.window, Duration::from_secs(60));
1042 assert_eq!(layer.config.min_samples, 100);
1043 assert_eq!(layer.config.min_deadline, Duration::from_millis(200));
1044 assert_eq!(layer.config.max_deadline, Duration::from_secs(20));
1045 }
1046
1047 #[tokio::test]
1048 async fn test_layer_clone_shares_stats() {
1049 let layer = TailCutLayer::new();
1050 let cloned = layer.clone();
1051
1052 assert!(Arc::ptr_eq(&layer.stats, &cloned.stats));
1053 }
1054}