1use crate::{
42 Image, Node, NodeId, PixelsError, Plan, PlanOptions, Region, Result, ThreadPool, Tile, TileBuf,
43 TileCache, TileKey,
44};
45use std::collections::HashMap;
46use std::sync::Arc;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct SchedulerOptions {
52 pub threads: usize,
54 pub cache_budget: usize,
56 pub batch_tiles: usize,
61 pub plan: PlanOptions,
63}
64
65impl SchedulerOptions {
66 #[must_use]
71 pub const fn with_threads(mut self, threads: usize) -> Self {
72 self.threads = threads;
73 self
74 }
75
76 #[must_use]
78 pub const fn with_cache_budget(mut self, bytes: usize) -> Self {
79 self.cache_budget = bytes;
80 self
81 }
82
83 #[must_use]
85 pub const fn with_batch_tiles(mut self, tiles: usize) -> Self {
86 self.batch_tiles = tiles;
87 self
88 }
89
90 #[must_use]
92 pub const fn with_plan(mut self, plan: PlanOptions) -> Self {
93 self.plan = plan;
94 self
95 }
96}
97
98impl Default for SchedulerOptions {
99 fn default() -> Self {
100 Self {
101 threads: 0,
102 cache_budget: TileCache::DEFAULT_BUDGET,
103 batch_tiles: 0,
104 plan: PlanOptions::default(),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111#[non_exhaustive]
112pub struct RunStats {
113 pub output_tiles: u64,
115 pub materialized_nodes: u64,
117 pub peak_bytes: u64,
122 pub reduction: Option<crate::Reduction>,
138}
139
140#[derive(Debug)]
144pub struct Scheduler {
145 pool: ThreadPool,
146 cache: Arc<TileCache>,
147 options: SchedulerOptions,
148}
149
150impl Scheduler {
151 pub fn new(options: SchedulerOptions) -> Result<Self> {
157 let threads = if options.threads == 0 {
158 ThreadPool::default_threads()
159 } else {
160 options.threads
161 };
162 Ok(Self {
163 pool: ThreadPool::new(threads)?,
164 cache: Arc::new(TileCache::new(options.cache_budget)),
165 options,
166 })
167 }
168
169 pub fn with_defaults() -> Result<Self> {
175 Self::new(SchedulerOptions::default())
176 }
177
178 #[must_use]
180 pub const fn threads(&self) -> usize {
181 self.pool.threads()
182 }
183
184 #[must_use]
186 pub fn cache(&self) -> &Arc<TileCache> {
187 &self.cache
188 }
189
190 pub fn run(
201 &self,
202 image: &Image,
203 mut consume: impl FnMut(Region, &Tile<'_>) -> Result<()>,
204 ) -> Result<RunStats> {
205 let plan = Plan::build(image, self.options.plan)?;
206 let root = Arc::clone(image.node());
207 let mut stats = RunStats::default();
208
209 let cacheable: Arc<std::collections::HashSet<NodeId>> = Arc::new(
211 dependency_order(&root)
212 .iter()
213 .filter(|node| plan.node(node.id()).is_some_and(|p| p.cacheable))
214 .map(|node| node.id())
215 .collect(),
216 );
217
218 let materialized = self.materialize(&root, &plan, &cacheable, &mut stats)?;
220
221 let batch_size = if self.options.batch_tiles == 0 {
223 self.threads()
224 } else {
225 self.options.batch_tiles
226 }
227 .max(1);
228 let tiles = plan.output_tiles().to_vec();
229
230 for batch in tiles.chunks(batch_size) {
231 let sources = self.pull_sources(&root, batch, &materialized)?;
234
235 let context = Arc::new(Context {
236 cache: Arc::clone(&self.cache),
237 materialized: Arc::clone(&materialized),
238 sources: Arc::new(sources),
239 cacheable: Arc::clone(&cacheable),
240 });
241
242 let outputs: Arc<Vec<std::sync::Mutex<Option<Arc<TileBuf>>>>> =
244 Arc::new(batch.iter().map(|_| std::sync::Mutex::new(None)).collect());
245 let tasks: Vec<_> = batch
246 .iter()
247 .enumerate()
248 .map(|(index, region)| {
249 let (root, context, outputs, region) = (
250 Arc::clone(&root),
251 Arc::clone(&context),
252 Arc::clone(&outputs),
253 *region,
254 );
255 move || {
256 let tile = produce(&root, region, &context)?;
257 if let Some(slot) = outputs.get(index) {
258 *slot
259 .lock()
260 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(tile);
261 }
262 Ok(())
263 }
264 })
265 .collect();
266 self.pool.run_all(tasks)?;
267
268 let mut batch_bytes = 0_u64;
270 for (index, region) in batch.iter().enumerate() {
271 let slot = outputs
272 .get(index)
273 .ok_or_else(|| PixelsError::graph("output slot vanished"))?;
274 let buffer = slot
275 .lock()
276 .unwrap_or_else(std::sync::PoisonError::into_inner)
277 .take()
278 .ok_or_else(|| {
279 PixelsError::graph(format!("output tile {region} was never produced"))
280 })?;
281 batch_bytes += buffer.bytes().len() as u64;
282 consume(*region, &buffer.as_tile()?)?;
283 stats.output_tiles += 1;
284 }
285 stats.peak_bytes = stats
286 .peak_bytes
287 .max(batch_bytes + materialized_bytes(&materialized));
288 }
289 Ok(stats)
290 }
291
292 fn materialize(
294 &self,
295 root: &Arc<Node>,
296 plan: &Plan,
297 cacheable: &Arc<std::collections::HashSet<NodeId>>,
298 stats: &mut RunStats,
299 ) -> Result<Arc<HashMap<NodeId, Arc<TileBuf>>>> {
300 let mut materialized: HashMap<NodeId, Arc<TileBuf>> = HashMap::new();
301 for node in dependency_order(root) {
302 let should = plan.node(node.id()).is_some_and(|p| p.materialize);
303 if !should {
304 continue;
305 }
306 let context = Context {
309 cache: Arc::clone(&self.cache),
310 materialized: Arc::new(materialized.clone()),
311 sources: Arc::new(HashMap::new()),
312 cacheable: Arc::clone(cacheable),
313 };
314 let whole = node.descriptor().region();
315 let buffer = produce_uncached(&node, whole, &context)?;
316 stats.materialized_nodes += 1;
317 materialized.insert(node.id(), buffer);
318 }
319 Ok(Arc::new(materialized))
320 }
321
322 fn pull_sources(
324 &self,
325 root: &Arc<Node>,
326 batch: &[Region],
327 materialized: &HashMap<NodeId, Arc<TileBuf>>,
328 ) -> Result<HashMap<TileKey, Arc<TileBuf>>> {
329 let mut sources = HashMap::new();
330 for region in batch {
331 for (node, needed) in source_demand(root, *region)? {
332 if materialized.contains_key(&node.id()) {
333 continue;
334 }
335 let key = TileKey::new(node.id(), needed);
336 if sources.contains_key(&key) {
337 continue;
338 }
339 let Some(producer) = node.producer() else {
340 continue;
341 };
342 let mut buffer = TileBuf::zeroed(needed, node.descriptor().pixel)?;
343 producer.produce(needed, &mut buffer.as_tile_mut()?)?;
344 sources.insert(key, Arc::new(buffer));
345 }
346 }
347 Ok(sources)
348 }
349}
350
351fn materialized_bytes(materialized: &HashMap<NodeId, Arc<TileBuf>>) -> u64 {
356 materialized.values().map(|b| b.bytes().len() as u64).sum()
357}
358
359#[derive(Debug)]
361struct Context {
362 cache: Arc<TileCache>,
363 materialized: Arc<HashMap<NodeId, Arc<TileBuf>>>,
364 sources: Arc<HashMap<TileKey, Arc<TileBuf>>>,
365 cacheable: Arc<std::collections::HashSet<NodeId>>,
369}
370
371fn produce(node: &Arc<Node>, region: Region, context: &Context) -> Result<Arc<TileBuf>> {
378 if !context.cacheable.contains(&node.id()) {
379 return produce_uncached(node, region, context);
380 }
381 let key = TileKey::new(node.id(), region);
382 if let Some(tile) = context.cache.get(&key) {
383 return Ok(tile);
384 }
385 let buffer = produce_uncached(node, region, context)?;
386 Ok(context.cache.insert(key, buffer))
387}
388
389fn produce_uncached(node: &Arc<Node>, region: Region, context: &Context) -> Result<Arc<TileBuf>> {
391 if let Some(whole) = context.materialized.get(&node.id()) {
393 let mut buffer = TileBuf::zeroed(region, node.descriptor().pixel)?;
394 crate::copy_region(&whole.as_tile()?, &mut buffer.as_tile_mut()?, region)?;
395 return Ok(Arc::new(buffer));
396 }
397
398 if let Some(producer) = node.producer() {
399 let key = TileKey::new(node.id(), region);
402 if let Some(band) = context.sources.get(&key) {
403 return Ok(Arc::clone(band));
404 }
405 let mut buffer = TileBuf::zeroed(region, node.descriptor().pixel)?;
406 producer.produce(region, &mut buffer.as_tile_mut()?)?;
407 return Ok(Arc::new(buffer));
408 }
409
410 let op = node.op().ok_or_else(|| {
411 PixelsError::graph(format!("node `{}` is neither op nor source", node.name()))
412 })?;
413 let descriptors: Vec<_> = node.inputs().iter().map(|n| n.descriptor()).collect();
414 let requested = op.input_regions(region, &descriptors)?;
415 if requested.len() != node.inputs().len() {
416 return Err(PixelsError::graph(format!(
417 "op `{}` requested {} input region(s) for {} input(s)",
418 op.name(),
419 requested.len(),
420 node.inputs().len()
421 )));
422 }
423
424 let mut inputs = Vec::with_capacity(requested.len());
427 for (input, needed) in node.inputs().iter().zip(&requested) {
428 inputs.push(produce(input, *needed, context)?);
429 }
430 let mut tiles = Vec::with_capacity(inputs.len());
431 for buffer in &inputs {
432 tiles.push(buffer.as_tile()?);
433 }
434
435 let mut output = TileBuf::zeroed(region, node.descriptor().pixel)?;
436 op.compute(&tiles, &mut output.as_tile_mut()?)?;
437 Ok(Arc::new(output))
438}
439
440fn source_demand(root: &Arc<Node>, region: Region) -> Result<Vec<(Arc<Node>, Region)>> {
442 let mut found = Vec::new();
443 let mut stack = vec![(Arc::clone(root), region)];
444 while let Some((node, region)) = stack.pop() {
445 let Some(op) = node.op() else {
446 found.push((node, region));
447 continue;
448 };
449 let descriptors: Vec<_> = node.inputs().iter().map(|n| n.descriptor()).collect();
450 let requested = op.input_regions(region, &descriptors)?;
451 for (input, needed) in node.inputs().iter().zip(requested) {
452 stack.push((Arc::clone(input), needed));
453 }
454 }
455 found.sort_by_key(|(_, region)| (region.y, region.x));
458 Ok(found)
459}
460
461fn dependency_order(root: &Arc<Node>) -> Vec<Arc<Node>> {
463 enum Step {
464 Visit(Arc<Node>),
465 Emit(Arc<Node>),
466 }
467 let mut seen = std::collections::HashSet::new();
468 let mut order = Vec::new();
469 let mut stack = vec![Step::Visit(Arc::clone(root))];
470 while let Some(step) = stack.pop() {
471 match step {
472 Step::Visit(node) => {
473 if seen.contains(&node.id()) {
474 continue;
475 }
476 stack.push(Step::Emit(Arc::clone(&node)));
477 for input in node.inputs() {
478 stack.push(Step::Visit(Arc::clone(input)));
479 }
480 }
481 Step::Emit(node) => {
482 if seen.insert(node.id()) {
483 order.push(node);
484 }
485 }
486 }
487 }
488 order
489}
490
491pub fn evaluate_tiled(image: &Image, options: SchedulerOptions) -> Result<TileBuf> {
500 let scheduler = Scheduler::new(options)?;
501 let descriptor = image.descriptor();
502 let mut out = TileBuf::for_image(&descriptor)?;
503 {
504 let mut view = out.as_tile_mut()?;
505 scheduler.run(image, |region, tile| {
506 crate::copy_region(tile, &mut view, region)
507 })?;
508 }
509 Ok(out)
510}
511
512#[cfg(test)]
513#[allow(
514 clippy::unwrap_used,
515 clippy::indexing_slicing,
516 clippy::panic,
517 reason = "tests operate on known-good values and assert shapes directly"
518)]
519mod tests {
520 use super::*;
521 use crate::testing::{ConstantOp, RampProducer, SumOp};
522 use crate::{
523 AccessPattern, BufferSource, DecodedSource, Decoder, Format, ImageDescriptor, Op,
524 PixelFormat, PixelsError, TileMut, evaluate,
525 };
526
527 fn ramp_image(width: u32, height: u32) -> Image {
528 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
529 Image::from_producer(Arc::new(RampProducer::new(descriptor)), Format::Raw)
530 }
531
532 #[derive(Debug)]
534 struct RampDecoder {
535 descriptor: ImageDescriptor,
536 row: u32,
537 }
538
539 impl Decoder for RampDecoder {
540 fn descriptor(&self) -> ImageDescriptor {
541 self.descriptor
542 }
543 fn read_row(&mut self, out: &mut [u8]) -> Result<()> {
544 if self.row >= self.descriptor.height {
545 return Err(PixelsError::invalid_argument("out", "past the end"));
546 }
547 for (x, cell) in out.iter_mut().enumerate() {
548 *cell = (self.row as usize).wrapping_mul(7).wrapping_add(x) as u8;
549 }
550 self.row += 1;
551 Ok(())
552 }
553 }
554
555 fn streamed_image(width: u32, height: u32) -> Image {
557 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
558 let decoder = RampDecoder { descriptor, row: 0 };
559 Image::from_producer(Arc::new(DecodedSource::new(Box::new(decoder))), Format::Raw)
560 }
561
562 fn buffered_image(width: u32, height: u32) -> Image {
564 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Gray8).unwrap();
565 let bytes: Vec<u8> = (0..descriptor.byte_len().unwrap())
566 .map(|i| (i % 251) as u8)
567 .collect();
568 let buffer = TileBuf::from_vec(descriptor.region(), descriptor.pixel, bytes).unwrap();
569 let source = BufferSource::new(descriptor, Arc::new(buffer)).unwrap();
570 Image::from_producer(Arc::new(source), Format::Raw)
571 }
572
573 #[derive(Debug)]
575 struct Reverse;
576 impl Op for Reverse {
577 fn name(&self) -> &'static str {
578 "reverse"
579 }
580 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
581 inputs
582 .first()
583 .copied()
584 .ok_or_else(|| PixelsError::graph("no input"))
585 }
586 fn input_regions(&self, out: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
587 let input = inputs
588 .first()
589 .ok_or_else(|| PixelsError::graph("no input"))?;
590 let y = (u64::from(input.height) - out.bottom()) as u32;
591 Ok(vec![Region::new(out.x, y, out.width, out.height)])
592 }
593 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
594 let input = inputs
595 .first()
596 .ok_or_else(|| PixelsError::graph("no input tile"))?;
597 let (out_region, in_region) = (output.region(), input.region());
598 for offset in 0..out_region.height {
599 let from = input
600 .row(in_region.y + (in_region.height - 1 - offset))
601 .ok_or_else(|| PixelsError::graph("missing input row"))?;
602 let into = output
603 .row_mut(out_region.y + offset)
604 .ok_or_else(|| PixelsError::graph("missing output row"))?;
605 into.copy_from_slice(from);
606 }
607 Ok(())
608 }
609 }
610
611 #[derive(Debug)]
613 struct RowBlur;
614 impl Op for RowBlur {
615 fn name(&self) -> &'static str {
616 "row-blur"
617 }
618 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
619 inputs
620 .first()
621 .copied()
622 .ok_or_else(|| PixelsError::graph("no input"))
623 }
624 fn input_regions(&self, out: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
625 let input = inputs
626 .first()
627 .ok_or_else(|| PixelsError::graph("no input"))?;
628 let top = out.y.saturating_sub(1);
629 let bottom = (out.bottom() + 1).min(u64::from(input.height));
630 Ok(vec![Region::new(
631 out.x,
632 top,
633 out.width,
634 (bottom - u64::from(top)) as u32,
635 )])
636 }
637 fn access_pattern(&self) -> AccessPattern {
638 AccessPattern::Spatial
639 }
640 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
641 let input = inputs
642 .first()
643 .ok_or_else(|| PixelsError::graph("no input tile"))?;
644 let out_region = output.region();
645 let in_region = input.region();
646 for y in out_region.y..out_region.y.saturating_add(out_region.height) {
647 let mut sums = vec![0_u32; out_region.width as usize];
649 let mut count = 0_u32;
650 for neighbour in [y.saturating_sub(1), y, y + 1] {
651 if neighbour < in_region.y || u64::from(neighbour) >= in_region.bottom() {
652 continue;
653 }
654 let Some(row) = input.row(neighbour) else {
655 continue;
656 };
657 let start = (out_region.x - in_region.x) as usize;
658 for (i, sum) in sums.iter_mut().enumerate() {
659 *sum += u32::from(row[start + i]);
660 }
661 count += 1;
662 }
663 let into = output
664 .row_mut(y)
665 .ok_or_else(|| PixelsError::graph("missing output row"))?;
666 for (cell, sum) in into.iter_mut().zip(&sums) {
667 *cell = (sum / count.max(1)) as u8;
668 }
669 }
670 Ok(())
671 }
672 }
673
674 fn options(threads: usize, strip_rows: u32) -> SchedulerOptions {
675 SchedulerOptions {
676 threads,
677 plan: PlanOptions {
678 strip_rows,
679 square_size: 8,
680 },
681 ..SchedulerOptions::default()
682 }
683 }
684
685 #[test]
688 fn a_bare_source_matches_the_m1_evaluator() {
689 let image = ramp_image(16, 40);
690 let expected = evaluate(&image).unwrap();
691 let actual = evaluate_tiled(&image, options(4, 8)).unwrap();
692 assert_eq!(actual.bytes(), expected.bytes());
693 }
694
695 #[test]
696 fn a_chain_matches_the_m1_evaluator() {
697 let image = buffered_image(23, 37)
698 .apply(Arc::new(ConstantOp::new(9)))
699 .unwrap()
700 .apply(Arc::new(Reverse))
701 .unwrap();
702 let expected = evaluate(&image).unwrap();
703 let actual = evaluate_tiled(&image, options(4, 8)).unwrap();
704 assert_eq!(actual.bytes(), expected.bytes());
705 }
706
707 #[test]
708 fn agreement_holds_across_tile_sizes_and_thread_counts() {
709 let image = buffered_image(31, 53).apply(Arc::new(Reverse)).unwrap();
712 let expected = evaluate(&image).unwrap();
713 for threads in [1, 2, 4, 8] {
714 for strip_rows in [1, 3, 8, 64, 1000] {
715 let actual = evaluate_tiled(&image, options(threads, strip_rows)).unwrap();
716 assert_eq!(
717 actual.bytes(),
718 expected.bytes(),
719 "threads={threads} strip_rows={strip_rows}"
720 );
721 }
722 }
723 }
724
725 #[test]
726 fn a_spatial_op_matches_the_m1_evaluator() {
727 let image = buffered_image(20, 20).apply(Arc::new(RowBlur)).unwrap();
730 let expected = evaluate(&image).unwrap();
731 for threads in [1, 4] {
732 let actual = evaluate_tiled(&image, options(threads, 4)).unwrap();
733 assert_eq!(actual.bytes(), expected.bytes(), "threads={threads}");
734 }
735 }
736
737 #[test]
738 fn a_branching_graph_matches_the_m1_evaluator() {
739 let base = buffered_image(16, 24);
740 let left = base.apply(Arc::new(ConstantOp::new(3))).unwrap();
741 let right = base.apply(Arc::new(ConstantOp::new(4))).unwrap();
742 let joined = Image::combine(&[left, right], Arc::new(SumOp)).unwrap();
743 let expected = evaluate(&joined).unwrap();
744 let actual = evaluate_tiled(&joined, options(4, 8)).unwrap();
745 assert_eq!(actual.bytes(), expected.bytes());
746 assert_eq!(actual.bytes()[0], 7);
747 }
748
749 #[test]
750 fn a_streaming_source_matches_the_m1_evaluator() {
751 let build = || {
752 streamed_image(12, 40)
753 .apply(Arc::new(ConstantOp::new(2)))
754 .unwrap()
755 };
756 let expected = evaluate(&build()).unwrap();
757 let actual = evaluate_tiled(&build(), options(4, 8)).unwrap();
758 assert_eq!(actual.bytes(), expected.bytes());
759 }
760
761 #[test]
762 fn reversal_over_a_streaming_source_matches_the_m1_evaluator() {
763 let build = || streamed_image(12, 40).apply(Arc::new(Reverse)).unwrap();
766 let expected = evaluate(&build()).unwrap();
767 let actual = evaluate_tiled(&build(), options(4, 8)).unwrap();
768 assert_eq!(actual.bytes(), expected.bytes());
769 }
770
771 #[test]
774 fn output_tiles_reach_the_sink_in_order() {
775 let image = buffered_image(16, 50);
777 let scheduler = Scheduler::new(options(8, 4)).unwrap();
778 let mut seen = Vec::new();
779 scheduler
780 .run(&image, |region, _| {
781 seen.push(region.y);
782 Ok(())
783 })
784 .unwrap();
785 let mut sorted = seen.clone();
786 sorted.sort_unstable();
787 assert_eq!(seen, sorted, "tiles arrived out of order");
788 assert_eq!(seen.first(), Some(&0));
789 }
790
791 #[test]
792 fn a_forward_pipeline_does_not_materialize() {
793 let image = streamed_image(8, 64)
794 .apply(Arc::new(ConstantOp::new(1)))
795 .unwrap();
796 let scheduler = Scheduler::new(options(4, 8)).unwrap();
797 let stats = scheduler.run(&image, |_, _| Ok(())).unwrap();
798 assert_eq!(stats.materialized_nodes, 0);
799 assert_eq!(stats.output_tiles, 8);
800 }
801
802 #[test]
803 fn reversal_over_a_streaming_source_materializes_exactly_once() {
804 let image = streamed_image(8, 64).apply(Arc::new(Reverse)).unwrap();
805 let scheduler = Scheduler::new(options(4, 8)).unwrap();
806 let stats = scheduler.run(&image, |_, _| Ok(())).unwrap();
807 assert_eq!(stats.materialized_nodes, 1, "ADR-0009 buffer");
808 }
809
810 #[test]
811 fn reversal_over_a_buffer_source_materializes_nothing() {
812 let image = buffered_image(8, 64).apply(Arc::new(Reverse)).unwrap();
814 let scheduler = Scheduler::new(options(4, 8)).unwrap();
815 let stats = scheduler.run(&image, |_, _| Ok(())).unwrap();
816 assert_eq!(
817 stats.materialized_nodes, 0,
818 "a memory source needs no buffer"
819 );
820 }
821
822 #[test]
825 fn peak_memory_does_not_grow_with_image_height() {
826 let measure = |height: u32| {
829 let image = streamed_image(64, height)
830 .apply(Arc::new(ConstantOp::new(1)))
831 .unwrap();
832 let scheduler = Scheduler::new(options(2, 8)).unwrap();
833 scheduler.run(&image, |_, _| Ok(())).unwrap().peak_bytes
834 };
835 let small = measure(64);
836 let large = measure(4096);
837 assert_eq!(
838 small, large,
839 "peak memory scaled with height: {small} vs {large}"
840 );
841 }
842
843 #[test]
844 fn materializing_pipelines_report_their_buffer() {
845 let image = streamed_image(64, 256).apply(Arc::new(Reverse)).unwrap();
848 let scheduler = Scheduler::new(options(2, 8)).unwrap();
849 let stats = scheduler.run(&image, |_, _| Ok(())).unwrap();
850 assert!(
851 stats.peak_bytes >= 64 * 256,
852 "a materialized full image should be accounted: {}",
853 stats.peak_bytes
854 );
855 }
856
857 #[test]
858 fn a_tiny_cache_budget_changes_cost_not_correctness() {
859 let image = buffered_image(16, 32)
862 .apply(Arc::new(ConstantOp::new(5)))
863 .unwrap();
864 let expected = evaluate(&image).unwrap();
865 for budget in [0, 1, 64, 1024, 1 << 20] {
866 let options = SchedulerOptions {
867 threads: 4,
868 cache_budget: budget,
869 plan: PlanOptions {
870 strip_rows: 4,
871 square_size: 8,
872 },
873 ..SchedulerOptions::default()
874 };
875 let actual = evaluate_tiled(&image, options).unwrap();
876 assert_eq!(actual.bytes(), expected.bytes(), "cache_budget={budget}");
877 }
878 }
879
880 #[test]
883 fn a_failing_op_fails_the_run() {
884 let image = buffered_image(16, 32)
885 .apply(Arc::new(crate::testing::FailingOp))
886 .unwrap();
887 let err = evaluate_tiled(&image, options(4, 8)).unwrap_err();
888 assert_eq!(err.code(), crate::ErrorCode::Malformed);
889 }
890
891 #[test]
892 fn a_failing_sink_stops_the_run() {
893 let image = buffered_image(16, 32);
894 let scheduler = Scheduler::new(options(4, 8)).unwrap();
895 let mut seen = 0;
896 let err = scheduler
897 .run(&image, |_, _| {
898 seen += 1;
899 Err(PixelsError::unsupported("sink refused"))
900 })
901 .unwrap_err();
902 assert_eq!(err.code(), crate::ErrorCode::Unsupported);
903 assert_eq!(seen, 1, "the sink should not be called again after failing");
904 }
905
906 #[test]
907 fn failures_are_deterministic_across_runs() {
908 let image = buffered_image(16, 64)
910 .apply(Arc::new(crate::testing::FailingOp))
911 .unwrap();
912 let first = evaluate_tiled(&image, options(8, 4))
913 .unwrap_err()
914 .to_string();
915 for _ in 0..10 {
916 assert_eq!(
917 evaluate_tiled(&image, options(8, 4))
918 .unwrap_err()
919 .to_string(),
920 first
921 );
922 }
923 }
924
925 #[test]
928 fn defaults_produce_a_usable_scheduler() {
929 let scheduler = Scheduler::with_defaults().unwrap();
930 assert!(scheduler.threads() >= 1);
931 assert_eq!(scheduler.cache().budget(), TileCache::DEFAULT_BUDGET);
932 let image = buffered_image(8, 8);
933 let stats = scheduler.run(&image, |_, _| Ok(())).unwrap();
934 assert_eq!(stats.output_tiles, 1);
935 }
936
937 #[test]
938 fn one_scheduler_runs_many_pipelines() {
939 let scheduler = Scheduler::new(options(4, 8)).unwrap();
940 for height in [8_u32, 16, 32] {
941 let image = buffered_image(8, height);
942 let expected = evaluate(&image).unwrap();
943 let mut out = TileBuf::for_image(&image.descriptor()).unwrap();
944 {
945 let mut view = out.as_tile_mut().unwrap();
946 scheduler
947 .run(&image, |region, tile| {
948 crate::copy_region(tile, &mut view, region)
949 })
950 .unwrap();
951 }
952 assert_eq!(out.bytes(), expected.bytes(), "height={height}");
953 }
954 }
955}