1#![forbid(unsafe_code)]
2#![deny(unsafe_op_in_unsafe_fn)]
3use std::fmt::Debug;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Arc, Condvar, Mutex, MutexGuard};
23use std::time::{Duration, Instant};
24
25use rayon::prelude::*;
26use rayon::{ThreadPool, ThreadPoolBuilder};
27
28const PROGRESS_INTERVAL: Duration = Duration::from_millis(100);
29const CANCEL_POLL_INTERVAL: Duration = Duration::from_millis(5);
30
31trait PaceClock: Send + Sync + Debug {
32 fn now(&self) -> Duration;
33 fn rest(&self, duration: Duration);
34}
35
36#[derive(Debug)]
37struct RealPaceClock {
38 started: Instant,
39}
40
41impl PaceClock for RealPaceClock {
42 fn now(&self) -> Duration {
43 self.started.elapsed()
44 }
45
46 fn rest(&self, duration: Duration) {
47 std::thread::sleep(duration);
48 }
49}
50
51#[derive(Debug)]
52struct Pacing {
53 slice: Duration,
54 last_rest: Mutex<Duration>,
55 clock: Arc<dyn PaceClock>,
56}
57
58#[derive(Clone, Debug)]
60pub struct CancelToken {
61 cancelled: Arc<AtomicBool>,
62 pacing: Option<Arc<Pacing>>,
63}
64
65impl Default for CancelToken {
66 fn default() -> Self {
67 Self {
68 cancelled: Arc::new(AtomicBool::new(false)),
69 pacing: None,
70 }
71 }
72}
73
74impl CancelToken {
75 #[must_use]
77 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn cancel(&self) {
83 self.cancelled.store(true, Ordering::Release);
84 }
85
86 #[must_use]
89 pub fn paced(&self, slice: Duration) -> CancelToken {
90 self.paced_with_clock(
91 slice,
92 Arc::new(RealPaceClock {
93 started: Instant::now(),
94 }),
95 )
96 }
97
98 fn paced_with_clock(&self, slice: Duration, clock: Arc<dyn PaceClock>) -> CancelToken {
99 let now = clock.now();
100 CancelToken {
101 cancelled: Arc::clone(&self.cancelled),
102 pacing: Some(Arc::new(Pacing {
103 slice,
104 last_rest: Mutex::new(now),
105 clock,
106 })),
107 }
108 }
109
110 fn pace(&self) {
111 let Some(pacing) = &self.pacing else {
112 return;
113 };
114 let mut last_rest = lock_unpoisoned(&pacing.last_rest);
115 let now = pacing.clock.now();
116 let worked = now.saturating_sub(*last_rest);
117 if !pacing.slice.is_zero() && worked >= pacing.slice {
118 pacing
119 .clock
120 .rest(worked.min(pacing.slice.saturating_mul(4)));
121 *last_rest = pacing.clock.now();
122 }
123 }
124
125 #[must_use]
128 pub fn is_cancelled(&self) -> bool {
129 self.pace();
130 self.cancelled.load(Ordering::Acquire)
131 }
132
133 pub fn check(&self) -> Result<(), Cancelled> {
135 if self.is_cancelled() {
136 Err(Cancelled)
137 } else {
138 Ok(())
139 }
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
145#[error("cancelled")]
146pub struct Cancelled;
147
148#[derive(Debug, Clone, PartialEq)]
150pub struct Progress {
151 pub stage: &'static str,
153 pub done: u64,
155 pub total: u64,
157 pub eta: Option<Duration>,
159}
160
161pub trait ProgressSink: Send + Sync + Debug {
163 fn report(&self, progress: &Progress);
165}
166
167#[derive(Debug, Default)]
169pub struct NoProgress;
170
171impl ProgressSink for NoProgress {
172 fn report(&self, _progress: &Progress) {}
173}
174
175pub trait MemoryProbe: Send + Sync + Debug {
177 fn available_bytes(&self) -> u64;
179}
180
181#[derive(Debug)]
183pub struct FixedMemory(pub u64);
184
185impl MemoryProbe for FixedMemory {
186 fn available_bytes(&self) -> u64 {
187 self.0
188 }
189}
190
191#[derive(Debug)]
192struct BudgetState {
193 available: u64,
194}
195
196#[derive(Debug)]
198pub struct MemoryBudget {
199 total: u64,
200 state: Mutex<BudgetState>,
201 released: Condvar,
202}
203
204impl MemoryBudget {
205 #[must_use]
207 pub fn new(total_bytes: u64) -> Self {
208 Self {
209 total: total_bytes,
210 state: Mutex::new(BudgetState {
211 available: total_bytes,
212 }),
213 released: Condvar::new(),
214 }
215 }
216
217 #[must_use]
219 pub fn from_probe(probe: &dyn MemoryProbe, fraction: f64, floor_bytes: u64) -> Self {
220 let scaled = (probe.available_bytes() as f64 * fraction).max(floor_bytes as f64);
221 let total = if scaled.is_finite() {
222 scaled as u64
223 } else if scaled.is_sign_positive() {
224 u64::MAX
225 } else {
226 floor_bytes
227 };
228 Self::new(total.max(floor_bytes))
229 }
230
231 #[must_use]
233 pub fn total(&self) -> u64 {
234 self.total
235 }
236
237 #[must_use]
239 pub fn available(&self) -> u64 {
240 lock_unpoisoned(&self.state).available
241 }
242
243 pub fn acquire(
245 &self,
246 bytes: u64,
247 cancel: &CancelToken,
248 ) -> Result<MemoryPermit<'_>, MemoryError> {
249 if bytes > self.total {
250 return Err(MemoryError::TooLarge {
251 requested: bytes,
252 total: self.total,
253 });
254 }
255
256 let mut state = lock_unpoisoned(&self.state);
257 loop {
258 cancel.check().map_err(|_| MemoryError::Cancelled)?;
259 if state.available >= bytes {
260 state.available -= bytes;
261 return Ok(MemoryPermit {
262 budget: self,
263 bytes,
264 });
265 }
266
267 state = match self.released.wait_timeout(state, CANCEL_POLL_INTERVAL) {
268 Ok((guard, _)) => guard,
269 Err(poisoned) => poisoned.into_inner().0,
270 };
271 }
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
277pub enum MemoryError {
278 #[error("request of {requested} bytes exceeds memory budget of {total} bytes")]
280 TooLarge {
281 requested: u64,
283 total: u64,
285 },
286 #[error("cancelled")]
288 Cancelled,
289}
290
291#[derive(Debug)]
293pub struct MemoryPermit<'a> {
294 budget: &'a MemoryBudget,
295 bytes: u64,
296}
297
298impl MemoryPermit<'_> {
299 pub fn shrink_to(&mut self, bytes: u64) {
302 if bytes < self.bytes {
303 let excess = self.bytes - bytes;
304 self.bytes = bytes;
305 self.budget.release(excess);
306 }
307 }
308}
309
310impl MemoryBudget {
311 fn release(&self, bytes: u64) {
312 let mut state = lock_unpoisoned(&self.state);
313 state.available = state
314 .available
315 .checked_add(bytes)
316 .expect("memory permit release cannot exceed u64");
317 debug_assert!(state.available <= self.total);
318 drop(state);
319 self.released.notify_all();
320 }
321}
322
323impl Drop for MemoryPermit<'_> {
324 fn drop(&mut self) {
325 self.budget.release(self.bytes);
326 }
327}
328
329#[derive(Debug)]
330enum Executor {
331 Inline,
332 Pool(ThreadPool),
333}
334
335#[derive(Debug)]
337pub struct Context {
338 cancel: CancelToken,
339 progress: Arc<dyn ProgressSink>,
340 executor: Executor,
341 budget: Arc<MemoryBudget>,
342}
343
344impl Context {
345 #[must_use]
347 pub fn new(
348 threads: usize,
349 budget: MemoryBudget,
350 progress: Arc<dyn ProgressSink>,
351 cancel: CancelToken,
352 ) -> Self {
353 let threads = threads.max(1);
354 let executor = if threads == 1 {
355 Executor::Inline
356 } else {
357 Executor::Pool(
358 ThreadPoolBuilder::new()
359 .num_threads(threads)
360 .build()
361 .expect("building a rayon thread pool should succeed"),
362 )
363 };
364 Self {
365 cancel,
366 progress,
367 executor,
368 budget: Arc::new(budget),
369 }
370 }
371
372 #[must_use]
376 pub fn child_serial(&self) -> Context {
377 Self {
378 cancel: self.cancel.clone(),
379 progress: Arc::new(NoProgress),
380 executor: Executor::Inline,
381 budget: Arc::clone(&self.budget),
382 }
383 }
384
385 #[must_use]
387 pub fn serial() -> Self {
388 Self::new(
389 1,
390 MemoryBudget::new(u64::MAX),
391 Arc::new(NoProgress),
392 CancelToken::new(),
393 )
394 }
395
396 #[must_use]
398 pub fn cancel(&self) -> &CancelToken {
399 &self.cancel
400 }
401
402 #[must_use]
404 pub fn budget(&self) -> &MemoryBudget {
405 &self.budget
406 }
407
408 #[must_use]
410 pub fn stage(&self, stage: &'static str, total: u64) -> StageProgress<'_> {
411 StageProgress::new(stage, total, self.progress.as_ref())
412 }
413
414 pub fn map<T: Send, U: Send>(
416 &self,
417 items: Vec<T>,
418 f: impl Fn(T) -> U + Sync + Send,
419 ) -> Result<Vec<U>, Cancelled> {
420 match &self.executor {
421 Executor::Inline => items
422 .into_iter()
423 .map(|item| {
424 self.cancel.check()?;
425 Ok(f(item))
426 })
427 .collect(),
428 Executor::Pool(pool) => pool.install(|| {
429 items
430 .into_par_iter()
431 .map(|item| {
432 self.cancel.check()?;
433 Ok(f(item))
434 })
435 .collect()
436 }),
437 }
438 }
439
440 pub fn try_map<T: Send, U: Send, E: Send>(
443 &self,
444 items: Vec<T>,
445 f: impl Fn(T) -> Result<U, E> + Sync + Send,
446 ) -> Result<Vec<U>, MapError<E>> {
447 match &self.executor {
448 Executor::Inline => {
449 let mut output = Vec::with_capacity(items.len());
450 for (index, item) in items.into_iter().enumerate() {
451 self.cancel.check().map_err(|_| MapError::Cancelled)?;
452 output.push(f(item).map_err(|error| MapError::Item { index, error })?);
453 }
454 Ok(output)
455 }
456 Executor::Pool(pool) => {
457 let outcomes: Vec<_> = pool.install(|| {
458 items
459 .into_par_iter()
460 .map(|item| {
461 if self.cancel.is_cancelled() {
462 ItemOutcome::Cancelled
463 } else {
464 ItemOutcome::Value(f(item))
465 }
466 })
467 .collect()
468 });
469 collect_outcomes(outcomes)
470 }
471 }
472 }
473}
474
475#[derive(Debug)]
476enum ItemOutcome<U, E> {
477 Cancelled,
478 Value(Result<U, E>),
479}
480
481fn collect_outcomes<U, E>(outcomes: Vec<ItemOutcome<U, E>>) -> Result<Vec<U>, MapError<E>> {
482 let mut output = Vec::with_capacity(outcomes.len());
483 for (index, outcome) in outcomes.into_iter().enumerate() {
484 match outcome {
485 ItemOutcome::Cancelled => return Err(MapError::Cancelled),
486 ItemOutcome::Value(Ok(value)) => output.push(value),
487 ItemOutcome::Value(Err(error)) => return Err(MapError::Item { index, error }),
488 }
489 }
490 Ok(output)
491}
492
493#[derive(Debug)]
494struct StageState {
495 done: u64,
496 last_reported: u64,
497 last_report_at: Instant,
498}
499
500#[derive(Debug)]
502pub struct StageProgress<'a> {
503 stage: &'static str,
504 total: u64,
505 started_at: Instant,
506 sink: &'a dyn ProgressSink,
507 state: Mutex<StageState>,
508}
509
510impl<'a> StageProgress<'a> {
511 fn new(stage: &'static str, total: u64, sink: &'a dyn ProgressSink) -> Self {
512 let started_at = Instant::now();
513 let tracker = Self {
514 stage,
515 total,
516 started_at,
517 sink,
518 state: Mutex::new(StageState {
519 done: 0,
520 last_reported: 0,
521 last_report_at: started_at,
522 }),
523 };
524 tracker.sink.report(&Progress {
525 stage,
526 done: 0,
527 total,
528 eta: None,
529 });
530 tracker
531 }
532
533 pub fn advance(&self, n: u64) {
535 let mut state = lock_unpoisoned(&self.state);
536 let done = state.done.saturating_add(n).min(self.total);
537 self.update_locked(&mut state, done);
538 }
539
540 pub fn set(&self, done: u64) {
542 let mut state = lock_unpoisoned(&self.state);
543 let done = state.done.max(done.min(self.total));
544 self.update_locked(&mut state, done);
545 }
546
547 fn update_locked(&self, state: &mut StageState, done: u64) {
548 state.done = done;
549 if done == state.last_reported {
550 return;
551 }
552
553 let now = Instant::now();
554 let is_last = done == self.total;
555 if !is_last && now.duration_since(state.last_report_at) < PROGRESS_INTERVAL {
556 return;
557 }
558
559 state.last_reported = done;
560 state.last_report_at = now;
561 self.sink.report(&Progress {
562 stage: self.stage,
563 done,
564 total: self.total,
565 eta: eta(self.started_at, now, done, self.total),
566 });
567 }
568}
569
570fn eta(started_at: Instant, now: Instant, done: u64, total: u64) -> Option<Duration> {
571 if done == 0 {
572 return None;
573 }
574 let remaining = total.saturating_sub(done) as f64;
575 let seconds_per_item = now.duration_since(started_at).as_secs_f64() / done as f64;
576 Some(Duration::from_secs_f64(seconds_per_item * remaining))
577}
578
579fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
580 mutex
581 .lock()
582 .unwrap_or_else(|poisoned| poisoned.into_inner())
583}
584
585#[derive(Debug, PartialEq, Eq)]
587pub enum MapError<E> {
588 Cancelled,
590 Item {
592 index: usize,
594 error: E,
596 },
597}
598
599#[cfg(test)]
600mod tests {
601 use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
602 use std::sync::mpsc;
603 use std::thread;
604
605 use super::*;
606
607 fn context(threads: usize, cancel: CancelToken) -> Context {
608 Context::new(
609 threads,
610 MemoryBudget::new(1024),
611 Arc::new(NoProgress),
612 cancel,
613 )
614 }
615
616 #[derive(Debug, Default)]
617 struct FakePaceClock {
618 now_ms: AtomicU64,
619 rested_ms: AtomicU64,
620 }
621
622 impl FakePaceClock {
623 fn work(&self, duration: Duration) {
624 self.now_ms.fetch_add(
625 u64::try_from(duration.as_millis()).unwrap(),
626 Ordering::SeqCst,
627 );
628 }
629 }
630
631 impl PaceClock for FakePaceClock {
632 fn now(&self) -> Duration {
633 Duration::from_millis(self.now_ms.load(Ordering::SeqCst))
634 }
635
636 fn rest(&self, duration: Duration) {
637 let millis = u64::try_from(duration.as_millis()).unwrap();
638 self.rested_ms.fetch_add(millis, Ordering::SeqCst);
639 self.now_ms.fetch_add(millis, Ordering::SeqCst);
640 }
641 }
642
643 #[test]
644 fn paced_token_rests_for_about_half_of_simulated_time() {
645 let clock = Arc::new(FakePaceClock::default());
646 let token = CancelToken::new().paced_with_clock(Duration::from_millis(50), clock.clone());
647 while clock.now() < Duration::from_secs(1) {
648 clock.work(Duration::from_millis(5));
649 assert!(!token.is_cancelled());
650 }
651 let rested = clock.rested_ms.load(Ordering::SeqCst);
652 assert!((450..=550).contains(&rested), "rested {rested} ms");
653 token.cancel();
654 assert!(token.clone().is_cancelled());
655 }
656
657 #[test]
658 fn paced_token_caps_rest_after_long_gaps_between_checks() {
659 let clock = Arc::new(FakePaceClock::default());
660 let token = CancelToken::new().paced_with_clock(Duration::from_millis(50), clock.clone());
661
662 clock.work(Duration::from_millis(300));
663 assert!(!token.is_cancelled());
664 assert_eq!(clock.rested_ms.load(Ordering::SeqCst), 200);
665
666 clock.work(Duration::from_millis(300));
667 assert!(!token.is_cancelled());
668 assert_eq!(clock.rested_ms.load(Ordering::SeqCst), 400);
669 }
670
671 #[test]
672 fn parallel_map_is_ordered_and_thread_count_independent() {
673 let items: Vec<u64> = (0..48).collect();
674 let expected: Vec<_> = items.iter().map(|value| value * value + 3).collect();
675
676 for threads in [1, 2, 8] {
677 let caller = thread::current().id();
678 let output = context(threads, CancelToken::new())
679 .map(items.clone(), |value| {
680 thread::sleep(Duration::from_millis((7 - value % 7) / 2));
681 if threads == 1 {
682 assert_eq!(thread::current().id(), caller);
683 }
684 value * value + 3
685 })
686 .unwrap();
687 assert_eq!(output, expected);
688 }
689 }
690
691 #[test]
692 fn cancellation_stops_map_quickly_and_skips_remaining() {
693 let cancel = CancelToken::new();
694 let cancel_from_thread = cancel.clone();
695 let started = Arc::new(AtomicUsize::new(0));
696 let closure_started = Arc::clone(&started);
697 let (cancelled_tx, cancelled_rx) = mpsc::channel();
698
699 let canceller = thread::spawn(move || {
700 thread::sleep(Duration::from_millis(25));
701 let at = Instant::now();
702 cancel_from_thread.cancel();
703 cancelled_tx.send(at).unwrap();
704 });
705 let result = context(2, cancel).map((0..100).collect(), move |_| {
706 closure_started.fetch_add(1, Ordering::SeqCst);
707 thread::sleep(Duration::from_millis(10));
708 });
709 let cancelled_at = cancelled_rx.recv().unwrap();
710 let response_time = cancelled_at.elapsed();
711 canceller.join().unwrap();
712
713 eprintln!("cancellation response: {response_time:?}");
714 assert_eq!(result, Err(Cancelled));
715 assert!(
718 response_time < Duration::from_millis(250),
719 "cancellation took {response_time:?}"
720 );
721 assert!(started.load(Ordering::SeqCst) < 100);
722 }
723
724 #[test]
725 fn try_map_reports_lowest_failing_index() {
726 let result = context(8, CancelToken::new()).try_map((0..12).collect(), |index| {
727 if index == 2 {
728 thread::sleep(Duration::from_millis(20));
729 Err("low")
730 } else if index == 9 {
731 Err("high")
732 } else {
733 Ok(index)
734 }
735 });
736
737 assert_eq!(
738 result,
739 Err(MapError::Item {
740 index: 2,
741 error: "low"
742 })
743 );
744 }
745
746 #[test]
747 fn memory_budget_bounds_concurrent_acquirers() {
748 let budget = MemoryBudget::new(3);
749 let current = AtomicUsize::new(0);
750 let maximum = AtomicUsize::new(0);
751 thread::scope(|scope| {
752 for _ in 0..12 {
753 scope.spawn(|| {
754 let _permit = budget.acquire(1, &CancelToken::new()).unwrap();
755 let now = current.fetch_add(1, Ordering::SeqCst) + 1;
756 maximum.fetch_max(now, Ordering::SeqCst);
757 thread::sleep(Duration::from_millis(3));
758 current.fetch_sub(1, Ordering::SeqCst);
759 });
760 }
761 });
762
763 assert_eq!(current.load(Ordering::SeqCst), 0);
764 assert!(maximum.load(Ordering::SeqCst) <= 3);
765 }
766
767 #[test]
768 fn memory_budget_rejects_oversized_requests() {
769 let budget = MemoryBudget::new(10);
770 assert_eq!(
771 budget.acquire(11, &CancelToken::new()).unwrap_err(),
772 MemoryError::TooLarge {
773 requested: 11,
774 total: 10
775 }
776 );
777 }
778
779 #[test]
780 fn memory_budget_waiter_observes_cancellation() {
781 let budget = MemoryBudget::new(1);
782 let holder = budget.acquire(1, &CancelToken::new()).unwrap();
783 let cancel = CancelToken::new();
784 thread::scope(|scope| {
785 let waiter_cancel = cancel.clone();
786 let budget = &budget;
787 let waiter = scope.spawn(move || budget.acquire(1, &waiter_cancel));
788 thread::sleep(Duration::from_millis(15));
789 cancel.cancel();
790 assert_eq!(waiter.join().unwrap().unwrap_err(), MemoryError::Cancelled);
791 });
792 drop(holder);
793 }
794
795 #[test]
796 fn child_serial_shares_budget_and_cancel_without_progress() {
797 let sink = Arc::new(RecordingSink::default());
798 let cancel = CancelToken::new();
799 let parent = Context::new(4, MemoryBudget::new(10), sink.clone(), cancel.clone());
800 let child = parent.child_serial();
801 let caller = thread::current().id();
802 let output = child
803 .map((0..8).collect(), |value: u64| {
804 assert_eq!(thread::current().id(), caller);
805 value * 2
806 })
807 .unwrap();
808 assert_eq!(output, (0..8).map(|v| v * 2).collect::<Vec<_>>());
809
810 let permit = child.budget().acquire(7, child.cancel()).unwrap();
811 assert_eq!(parent.budget().available(), 3);
812 assert_eq!(child.budget().total(), 10);
813 drop(permit);
814 assert_eq!(parent.budget().available(), 10);
815
816 let _stage = child.stage("engine.child", 1);
817 assert!(lock_unpoisoned(&sink.reports).is_empty());
818 cancel.cancel();
819 assert!(child.cancel().is_cancelled());
820 }
821
822 #[test]
823 fn memory_permit_shrink_returns_the_excess() {
824 let budget = MemoryBudget::new(10);
825 let mut permit = budget.acquire(8, &CancelToken::new()).unwrap();
826 assert_eq!(budget.available(), 2);
827 permit.shrink_to(5);
828 assert_eq!(budget.available(), 5);
829 permit.shrink_to(9);
830 assert_eq!(budget.available(), 5);
831 drop(permit);
832 assert_eq!(budget.available(), 10);
833 }
834
835 #[test]
836 fn memory_permit_shrink_wakes_a_waiter() {
837 let budget = MemoryBudget::new(4);
838 let mut holder = budget.acquire(4, &CancelToken::new()).unwrap();
839 thread::scope(|scope| {
840 let waiter = scope.spawn(|| budget.acquire(3, &CancelToken::new()).map(drop));
841 thread::sleep(Duration::from_millis(15));
842 holder.shrink_to(1);
843 assert_eq!(waiter.join().unwrap(), Ok(()));
844 });
845 drop(holder);
846 assert_eq!(budget.available(), 4);
847 }
848
849 #[test]
850 fn memory_permit_releases_after_panic() {
851 let budget = Arc::new(MemoryBudget::new(7));
852 let panicking_budget = Arc::clone(&budget);
853 let panicked = thread::spawn(move || {
854 let _permit = panicking_budget.acquire(7, &CancelToken::new()).unwrap();
855 panic!("intentional panic while holding permit");
856 })
857 .join();
858
859 assert!(panicked.is_err());
860 let _permit = budget.acquire(7, &CancelToken::new()).unwrap();
861 }
862
863 #[derive(Debug, Default)]
864 struct RecordingSink {
865 reports: Mutex<Vec<(Instant, Progress)>>,
866 }
867
868 impl ProgressSink for RecordingSink {
869 fn report(&self, progress: &Progress) {
870 lock_unpoisoned(&self.reports).push((Instant::now(), progress.clone()));
871 }
872 }
873
874 #[test]
875 fn progress_reports_endpoints_monotonically_and_throttles() {
876 let sink = Arc::new(RecordingSink::default());
877 let context = Context::new(1, MemoryBudget::new(1), sink.clone(), CancelToken::new());
878 let progress = context.stage("engine.test", 100);
879 for _ in 0..10 {
880 progress.advance(1);
881 }
882 thread::sleep(Duration::from_millis(110));
883 progress.advance(10);
884 for done in 21..100 {
885 progress.set(done);
886 }
887 progress.set(100);
888
889 let reports = lock_unpoisoned(&sink.reports);
890 let done: Vec<_> = reports.iter().map(|(_, report)| report.done).collect();
891 assert_eq!(done.first(), Some(&0));
892 assert!(done.contains(&20));
893 assert_eq!(done.last(), Some(&100));
894 assert!(done.windows(2).all(|window| window[0] <= window[1]));
895 assert_eq!(reports.first().unwrap().1.eta, None);
896 assert_eq!(reports.last().unwrap().1.eta, Some(Duration::ZERO));
897 for reports in reports.windows(2) {
898 if reports[1].1.done != 100 {
899 assert!(reports[1].0.duration_since(reports[0].0) >= PROGRESS_INTERVAL);
900 }
901 }
902 }
903
904 #[test]
905 fn fixed_memory_and_probe_floor_set_budget() {
906 assert_eq!(
907 MemoryBudget::from_probe(&FixedMemory(1_000), 0.5, 100).total(),
908 500
909 );
910 assert_eq!(
911 MemoryBudget::from_probe(&FixedMemory(10), 0.5, 100).total(),
912 100
913 );
914 }
915}