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 type Composer = oio::Composer;
375
376 fn info(&self) -> ServiceInfo {
377 self.inner.info()
378 }
379
380 fn capability(&self) -> Capability {
381 self.inner.capability()
382 }
383
384 fn compose(&self, ctx: &OperationContext, to: &str, args: OpCompose) -> Result<Self::Composer> {
385 self.inner.compose(ctx, to, args)
386 }
387
388 async fn create_dir(
389 &self,
390 ctx: &OperationContext,
391 path: &str,
392 args: OpCreateDir,
393 ) -> Result<RpCreateDir> {
394 self.with_deadline(
395 Operation::CreateDir,
396 None,
397 self.inner.create_dir(ctx, path, args),
398 )
399 .await
400 }
401
402 fn read(&self, ctx: &OperationContext, path: &str, args: OpRead) -> Result<Self::Reader> {
403 self.inner
404 .read(ctx, path, args)
405 .map(|r| TailCutWrapper::new(r, None, self.config.clone(), self.stats.clone()))
406 }
407
408 fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result<Self::Writer> {
409 self.inner
410 .write(ctx, path, args)
411 .map(|w| TailCutWrapper::new(w, None, self.config.clone(), self.stats.clone()))
412 }
413
414 fn copy(
415 &self,
416 ctx: &OperationContext,
417 from: &str,
418 to: &str,
419 args: OpCopy,
420 ) -> Result<Self::Copier> {
421 self.inner
422 .copy(ctx, from, to, args)
423 .map(|c| TailCutWrapper::new(c, None, self.config.clone(), self.stats.clone()))
424 }
425
426 async fn rename(
427 &self,
428 ctx: &OperationContext,
429 from: &str,
430 to: &str,
431 args: OpRename,
432 ) -> Result<RpRename> {
433 self.with_deadline(
434 Operation::Rename,
435 None,
436 self.inner.rename(ctx, from, to, args),
437 )
438 .await
439 }
440
441 async fn restore(
442 &self,
443 ctx: &OperationContext,
444 path: &str,
445 args: OpRestore,
446 ) -> Result<RpRestore> {
447 self.inner.restore(ctx, path, args).await
448 }
449
450 async fn stat(&self, ctx: &OperationContext, path: &str, args: OpStat) -> Result<RpStat> {
451 self.with_deadline(Operation::Stat, None, self.inner.stat(ctx, path, args))
452 .await
453 }
454
455 fn delete(&self, ctx: &OperationContext) -> Result<Self::Deleter> {
456 self.inner
457 .delete(ctx)
458 .map(|d| TailCutWrapper::new(d, None, self.config.clone(), self.stats.clone()))
459 }
460
461 fn list(&self, ctx: &OperationContext, path: &str, args: OpList) -> Result<Self::Lister> {
462 self.inner
463 .list(ctx, path, args)
464 .map(|l| TailCutWrapper::new(l, None, self.config.clone(), self.stats.clone()))
465 }
466
467 async fn presign(
468 &self,
469 ctx: &OperationContext,
470 path: &str,
471 args: OpPresign,
472 ) -> Result<RpPresign> {
473 self.with_deadline(
474 Operation::Presign,
475 None,
476 self.inner.presign(ctx, path, args),
477 )
478 .await
479 }
480}
481
482#[doc(hidden)]
483pub struct TailCutWrapper<R> {
485 inner: R,
486 size: Option<u64>,
487 config: Arc<TailCutConfig>,
488 stats: Arc<TailCutStats>,
489}
490
491impl<R> TailCutWrapper<R> {
492 fn new(
493 inner: R,
494 size: Option<u64>,
495 config: Arc<TailCutConfig>,
496 stats: Arc<TailCutStats>,
497 ) -> Self {
498 Self {
499 inner,
500 size,
501 config,
502 stats,
503 }
504 }
505
506 fn calculate_deadline(&self, op: Operation) -> Option<Duration> {
507 self.calculate_deadline_for(op, self.size)
508 }
509
510 fn calculate_deadline_for(&self, op: Operation, size: Option<u64>) -> Option<Duration> {
511 let op_stats = self.stats.stats_for(op);
512
513 if op_stats.total_samples(size, self.config.window) < self.config.min_samples {
514 return None;
515 }
516
517 let q = self.config.percentile as f64 / 100.0;
518 let pctl = op_stats.quantile(size, q, self.config.window)?;
519
520 let deadline = Duration::from_secs_f64(pctl.as_secs_f64() * self.config.safety_factor);
521 Some(deadline.clamp(self.config.min_deadline, self.config.max_deadline))
522 }
523
524 #[inline]
525 async fn with_io_deadline<F, T>(
526 deadline: Option<Duration>,
527 percentile: u8,
528 stats: &Arc<TailCutStats>,
529 size: Option<u64>,
530 op: Operation,
531 fut: F,
532 ) -> Result<T>
533 where
534 F: std::future::Future<Output = Result<T>>,
535 {
536 let start = Instant::now();
537
538 let result = if let Some(dl) = deadline {
539 match tokio::time::timeout(dl, fut).await {
540 Ok(res) => res,
541 Err(_) => Err(
542 Error::new(ErrorKind::Unexpected, "io cancelled by tail cut")
543 .with_operation(op)
544 .with_context("percentile", format!("P{}", percentile))
545 .with_context("deadline", format!("{:?}", dl))
546 .set_temporary(),
547 ),
548 }
549 } else {
550 fut.await
551 };
552
553 if result.is_ok() {
554 let latency = start.elapsed();
555 stats.stats_for(op).record(size, latency);
556 }
557
558 result
559 }
560}
561
562impl<R: oio::ReadStream> oio::ReadStream for TailCutWrapper<R> {
563 async fn read(&mut self) -> Result<Buffer> {
564 let deadline = self.calculate_deadline_for(Operation::Read, self.size);
565 Self::with_io_deadline(
566 deadline,
567 self.config.percentile,
568 &self.stats,
569 self.size,
570 Operation::Read,
571 self.inner.read(),
572 )
573 .await
574 }
575}
576
577impl<R: oio::Read> oio::Read for TailCutWrapper<R> {
578 async fn open(&self, range: BytesRange) -> Result<(RpRead, Box<dyn oio::ReadStreamDyn>)> {
579 let size = range.size();
580 let deadline = self.calculate_deadline_for(Operation::Read, size);
581 let (rp, stream) = Self::with_io_deadline(
582 deadline,
583 self.config.percentile,
584 &self.stats,
585 size,
586 Operation::Read,
587 self.inner.open(range),
588 )
589 .await?;
590
591 Ok((
592 rp,
593 Box::new(TailCutWrapper::new(
594 stream,
595 size,
596 self.config.clone(),
597 self.stats.clone(),
598 )) as Box<dyn oio::ReadStreamDyn>,
599 ))
600 }
601
602 async fn read(&self, range: BytesRange) -> Result<(RpRead, Buffer)> {
603 let size = range.size();
604 let deadline = self.calculate_deadline_for(Operation::Read, size);
605 Self::with_io_deadline(
606 deadline,
607 self.config.percentile,
608 &self.stats,
609 size,
610 Operation::Read,
611 self.inner.read(range),
612 )
613 .await
614 }
615}
616
617impl<R: oio::Write> oio::Write for TailCutWrapper<R> {
618 async fn write(&mut self, bs: Buffer) -> Result<()> {
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.write(bs),
627 )
628 .await
629 }
630
631 async fn copy_from(&mut self, path: &str, args: OpRead, range: BytesRange) -> 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.copy_from(path, args, range),
640 )
641 .await
642 }
643
644 async fn close(&mut self) -> Result<Metadata> {
645 let deadline = self.calculate_deadline(Operation::Write);
646 Self::with_io_deadline(
647 deadline,
648 self.config.percentile,
649 &self.stats,
650 self.size,
651 Operation::Write,
652 self.inner.close(),
653 )
654 .await
655 }
656
657 async fn abort(&mut self) -> Result<()> {
658 let deadline = self.calculate_deadline(Operation::Write);
659 Self::with_io_deadline(
660 deadline,
661 self.config.percentile,
662 &self.stats,
663 self.size,
664 Operation::Write,
665 self.inner.abort(),
666 )
667 .await
668 }
669}
670
671impl<R: oio::List> oio::List for TailCutWrapper<R> {
672 async fn next(&mut self) -> Result<Option<oio::Entry>> {
673 let deadline = self.calculate_deadline(Operation::List);
674 Self::with_io_deadline(
675 deadline,
676 self.config.percentile,
677 &self.stats,
678 self.size,
679 Operation::List,
680 self.inner.next(),
681 )
682 .await
683 }
684}
685
686impl<R: oio::Delete> oio::Delete for TailCutWrapper<R> {
687 async fn delete(&mut self, path: &str, args: OpDelete) -> Result<()> {
688 self.inner.delete(path, args).await
689 }
690
691 async fn close(&mut self) -> Result<()> {
692 let deadline = self.calculate_deadline(Operation::Delete);
693 Self::with_io_deadline(
694 deadline,
695 self.config.percentile,
696 &self.stats,
697 self.size,
698 Operation::Delete,
699 self.inner.close(),
700 )
701 .await
702 }
703}
704
705impl<C: oio::Copy> oio::Copy for TailCutWrapper<C> {
706 async fn next(&mut self) -> Result<Option<usize>> {
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.next(),
715 )
716 .await
717 }
718
719 async fn close(&mut self) -> Result<Metadata> {
720 let deadline = self.calculate_deadline(Operation::Copy);
721 Self::with_io_deadline(
722 deadline,
723 self.config.percentile,
724 &self.stats,
725 self.size,
726 Operation::Copy,
727 self.inner.close(),
728 )
729 .await
730 }
731
732 async fn abort(&mut self) -> Result<()> {
733 let deadline = self.calculate_deadline(Operation::Copy);
734 Self::with_io_deadline(
735 deadline,
736 self.config.percentile,
737 &self.stats,
738 self.size,
739 Operation::Copy,
740 self.inner.abort(),
741 )
742 .await
743 }
744}
745
746struct TailCutStats {
748 operations: [Arc<OperationStats>; 7],
750}
751
752impl TailCutStats {
753 fn new() -> Self {
754 Self {
755 operations: std::array::from_fn(|_| Arc::new(OperationStats::new())),
756 }
757 }
758
759 fn stats_for(&self, op: Operation) -> &Arc<OperationStats> {
760 let idx = match op {
761 Operation::Read => 0,
762 Operation::Write => 1,
763 Operation::Stat => 2,
764 Operation::List => 3,
765 Operation::Delete => 4,
766 Operation::Copy => 5,
767 Operation::Rename => 6,
768 _ => 2, };
770 &self.operations[idx]
771 }
772}
773
774struct OperationStats {
776 buckets: Vec<SizeBucket>,
777}
778
779impl OperationStats {
780 fn new() -> Self {
781 Self {
782 buckets: vec![
783 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), ],
790 }
791 }
792
793 fn bucket_for(&self, size: Option<u64>) -> &SizeBucket {
794 let size = size.unwrap_or(u64::MAX);
795
796 self.buckets
797 .iter()
798 .find(|b| b.contains(size))
799 .unwrap_or(&self.buckets[self.buckets.len() - 1])
800 }
801
802 fn record(&self, size: Option<u64>, latency: Duration) {
803 self.bucket_for(size).histogram.record(latency);
804 }
805
806 fn quantile(&self, size: Option<u64>, q: f64, window: Duration) -> Option<Duration> {
807 self.bucket_for(size).histogram.quantile(q, window)
808 }
809
810 fn total_samples(&self, size: Option<u64>, window: Duration) -> usize {
811 self.bucket_for(size).histogram.total_samples(window)
812 }
813}
814
815struct SizeBucket {
817 min_size: u64,
818 max_size: Option<u64>,
819 histogram: WindowedHistogram,
820}
821
822impl SizeBucket {
823 fn new(min_size: u64, max_size: Option<u64>) -> Self {
824 Self {
825 min_size,
826 max_size,
827 histogram: WindowedHistogram::new(),
828 }
829 }
830
831 fn contains(&self, size: u64) -> bool {
832 size >= self.min_size && self.max_size.is_none_or(|max| size < max)
833 }
834}
835
836const SLICE_DURATION_MS: u64 = 10_000; const NUM_SLICES: usize = 12; const NUM_BUCKETS: usize = 17; struct WindowedHistogram {
842 slices: Box<[TimeSlice; NUM_SLICES]>,
843 current_idx: AtomicUsize,
844 last_rotate: AtomicU64,
845}
846
847impl WindowedHistogram {
848 fn new() -> Self {
849 Self {
850 slices: Box::new(std::array::from_fn(|_| TimeSlice::new())),
851 current_idx: AtomicUsize::new(0),
852 last_rotate: AtomicU64::new(Self::now_ms()),
853 }
854 }
855
856 fn record(&self, latency: Duration) {
857 self.maybe_rotate();
858
859 let bucket_idx = Self::latency_to_bucket(latency);
860 let slice_idx = self.current_idx.load(Ordering::Relaxed);
861
862 self.slices[slice_idx].buckets[bucket_idx].fetch_add(1, Ordering::Relaxed);
863 }
864
865 fn quantile(&self, q: f64, window: Duration) -> Option<Duration> {
866 debug_assert!((0.0..=1.0).contains(&q), "quantile must be in [0, 1]");
867
868 let snapshot = self.snapshot(window);
869 let total: u64 = snapshot.iter().sum();
870
871 if total == 0 {
872 return None;
873 }
874
875 let target = (total as f64 * q).ceil() as u64;
876 let mut cumsum = 0u64;
877
878 for (bucket_idx, &count) in snapshot.iter().enumerate() {
879 cumsum += count;
880 if cumsum >= target {
881 return Some(Self::bucket_to_latency(bucket_idx));
882 }
883 }
884
885 Some(Self::bucket_to_latency(NUM_BUCKETS - 1))
886 }
887
888 fn total_samples(&self, window: Duration) -> usize {
889 self.snapshot(window).iter().map(|&v| v as usize).sum()
890 }
891
892 fn snapshot(&self, window: Duration) -> [u64; NUM_BUCKETS] {
893 let mut result = [0u64; NUM_BUCKETS];
894 let now_ms = Self::now_ms();
895 let window_ms = window.as_millis() as u64;
896
897 for slice in self.slices.iter() {
898 let start = slice.start_epoch_ms.load(Ordering::Acquire);
899
900 if start > 0 && now_ms.saturating_sub(start) < window_ms + SLICE_DURATION_MS {
901 for (i, bucket) in slice.buckets.iter().enumerate() {
902 result[i] += bucket.load(Ordering::Relaxed);
903 }
904 }
905 }
906
907 result
908 }
909
910 fn maybe_rotate(&self) {
911 let now = Self::now_ms();
912 let last_rotate = self.last_rotate.load(Ordering::Relaxed);
913
914 if now - last_rotate >= SLICE_DURATION_MS
915 && self
916 .last_rotate
917 .compare_exchange(last_rotate, now, Ordering::Release, Ordering::Relaxed)
918 .is_ok()
919 {
920 let old_idx = self.current_idx.load(Ordering::Relaxed);
921 let new_idx = (old_idx + 1) % NUM_SLICES;
922
923 let new_slice = &self.slices[new_idx];
924 new_slice.start_epoch_ms.store(now, Ordering::Release);
925 for bucket in &new_slice.buckets {
926 bucket.store(0, Ordering::Relaxed);
927 }
928
929 self.current_idx.store(new_idx, Ordering::Release);
930 }
931 }
932
933 fn latency_to_bucket(latency: Duration) -> usize {
934 let ms = latency.as_millis() as u64;
935
936 if ms == 0 {
937 return 0;
938 }
939
940 let bucket = 64 - ms.leading_zeros();
941 (bucket as usize).min(NUM_BUCKETS - 1)
942 }
943
944 fn bucket_to_latency(bucket_idx: usize) -> Duration {
945 if bucket_idx == 0 {
946 Duration::from_millis(1)
947 } else if bucket_idx >= NUM_BUCKETS - 1 {
948 Duration::from_secs(64)
949 } else {
950 Duration::from_millis(1u64 << bucket_idx)
951 }
952 }
953
954 fn now_ms() -> u64 {
955 u64::try_from(Timestamp::now().into_inner().as_millisecond()).unwrap()
957 }
958}
959
960struct TimeSlice {
962 buckets: [AtomicU64; NUM_BUCKETS],
964 start_epoch_ms: AtomicU64,
965}
966
967impl TimeSlice {
968 fn new() -> Self {
969 Self {
970 buckets: std::array::from_fn(|_| AtomicU64::new(0)),
971 start_epoch_ms: AtomicU64::new(0),
972 }
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979
980 #[test]
981 fn test_latency_to_bucket() {
982 assert_eq!(
983 WindowedHistogram::latency_to_bucket(Duration::from_millis(0)),
984 0
985 );
986 assert_eq!(
987 WindowedHistogram::latency_to_bucket(Duration::from_millis(1)),
988 1
989 );
990 assert_eq!(
991 WindowedHistogram::latency_to_bucket(Duration::from_millis(2)),
992 2
993 );
994 assert_eq!(
995 WindowedHistogram::latency_to_bucket(Duration::from_millis(4)),
996 3
997 );
998 assert_eq!(
999 WindowedHistogram::latency_to_bucket(Duration::from_millis(8)),
1000 4
1001 );
1002 assert_eq!(
1003 WindowedHistogram::latency_to_bucket(Duration::from_millis(500)),
1004 9
1005 );
1006 assert_eq!(
1007 WindowedHistogram::latency_to_bucket(Duration::from_secs(1)),
1008 10
1009 );
1010 assert_eq!(
1011 WindowedHistogram::latency_to_bucket(Duration::from_secs(2)),
1012 11
1013 );
1014 assert_eq!(
1015 WindowedHistogram::latency_to_bucket(Duration::from_secs(64)),
1016 16
1017 );
1018 assert_eq!(
1019 WindowedHistogram::latency_to_bucket(Duration::from_secs(1000)),
1020 16
1021 );
1022 }
1023
1024 #[test]
1025 fn test_size_bucket_contains() {
1026 let bucket = SizeBucket::new(0, Some(4096));
1027 assert!(bucket.contains(0));
1028 assert!(bucket.contains(4095));
1029 assert!(!bucket.contains(4096));
1030
1031 let bucket = SizeBucket::new(4096, None);
1032 assert!(!bucket.contains(4095));
1033 assert!(bucket.contains(4096));
1034 assert!(bucket.contains(u64::MAX));
1035 }
1036
1037 #[tokio::test]
1038 async fn test_histogram_basic() {
1039 let hist = WindowedHistogram::new();
1040 let now = WindowedHistogram::now_ms();
1041 hist.slices[0].start_epoch_ms.store(now, Ordering::Release);
1042
1043 hist.record(Duration::from_millis(10));
1044 hist.record(Duration::from_millis(20));
1045 hist.record(Duration::from_millis(30));
1046
1047 let samples = hist.total_samples(Duration::from_secs(60));
1048 assert_eq!(samples, 3);
1049
1050 let p50 = hist.quantile(0.5, Duration::from_secs(60));
1051 assert!(p50.is_some());
1052 }
1053
1054 #[tokio::test]
1055 async fn test_tail_cut_layer_build() {
1056 let layer = TailCutLayer::builder()
1057 .percentile(95)
1058 .safety_factor(1.5)
1059 .window(Duration::from_secs(60))
1060 .min_samples(100)
1061 .min_deadline(Duration::from_millis(200))
1062 .max_deadline(Duration::from_secs(20))
1063 .build();
1064
1065 assert_eq!(layer.config.percentile, 95);
1066 assert_eq!(layer.config.safety_factor, 1.5);
1067 assert_eq!(layer.config.window, Duration::from_secs(60));
1068 assert_eq!(layer.config.min_samples, 100);
1069 assert_eq!(layer.config.min_deadline, Duration::from_millis(200));
1070 assert_eq!(layer.config.max_deadline, Duration::from_secs(20));
1071 }
1072
1073 #[tokio::test]
1074 async fn test_layer_clone_shares_stats() {
1075 let layer = TailCutLayer::new();
1076 let cloned = layer.clone();
1077
1078 assert!(Arc::ptr_eq(&layer.stats, &cloned.stats));
1079 }
1080}