refactor: consolidate some tests/examples/benchmarks
This commit is contained in:
1 parent
49be497964
commit
b8204c4ed9
83 files changed
+569
-10068
No files matched your search
@@ -1,6 +1,6 @@
|
||||
//! Concurrent access tests
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
//! Tests for execution correctness and parallelism
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
// Test that tasks in the same topological layer execute in parallel
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let layer_timing = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct Layer0Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer0Task {
|
||||
async fn run(&self) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L0_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
self.idx * 10
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 0: 3 independent sources
|
||||
let sources: Vec<_> = (0..3)
|
||||
.map(|i| {
|
||||
dag.add_task(Layer0Task {
|
||||
timing: layer_timing.clone(),
|
||||
idx: i,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
struct Layer1Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer1Task {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L1_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 1: Tasks depending on layer 0
|
||||
let layer1: Vec<_> = sources
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, source)| {
|
||||
dag.add_task(Layer1Task {
|
||||
idx: i,
|
||||
timing: layer_timing.clone(),
|
||||
})
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: Final aggregation
|
||||
let final_task = {
|
||||
let timing = layer_timing.clone();
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| {
|
||||
let timing = timing.clone();
|
||||
let start = Instant::now();
|
||||
timing.lock().unwrap().push(("L2_final".to_string(), start));
|
||||
a + b + c
|
||||
},
|
||||
))
|
||||
.depends_on((&layer1[0], &layer1[1], &layer1[2]))
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1)
|
||||
|
||||
// Analyze timing
|
||||
let timing = layer_timing.lock().unwrap();
|
||||
|
||||
// Get layer 0 start times
|
||||
let l0_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L0_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Get layer 1 start times
|
||||
let l1_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L1_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 0 tasks should all start nearly simultaneously
|
||||
let l0_spread = l0_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l0_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l0_spread < Duration::from_millis(5),
|
||||
"Layer 0 tasks should start together, spread was {:?}",
|
||||
l0_spread
|
||||
);
|
||||
|
||||
// Layer 1 tasks should also start nearly simultaneously (after layer 0 completes)
|
||||
let l1_spread = l1_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l1_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l1_spread < Duration::from_millis(5),
|
||||
"Layer 1 tasks should start together, spread was {:?}",
|
||||
l1_spread
|
||||
);
|
||||
|
||||
// Layer 1 should start after layer 0
|
||||
let earliest_l1 = *l1_times.iter().min().unwrap();
|
||||
let earliest_l0 = *l0_times.iter().min().unwrap();
|
||||
assert!(
|
||||
earliest_l1 >= earliest_l0 + Duration::from_millis(15),
|
||||
"Layer 1 should start after layer 0 completes"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_completion_order_matches_dependencies() -> DagResult<()> {
|
||||
// Verify that tasks complete in dependency order
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let completion_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct ATask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ATask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
self.order.lock().unwrap().push("A");
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// Create a complex DAG with clear ordering requirements
|
||||
let a = dag.add_task(ATask {
|
||||
order: completion_order.clone(),
|
||||
});
|
||||
|
||||
struct BTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl BTask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
self.order.lock().unwrap().push("B");
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
let b = dag.add_task(BTask {
|
||||
order: completion_order.clone(),
|
||||
});
|
||||
|
||||
let c = {
|
||||
let order = completion_order.clone();
|
||||
dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("C");
|
||||
x + y
|
||||
}))
|
||||
.depends_on((a, b))
|
||||
};
|
||||
|
||||
let d = {
|
||||
let order = completion_order.clone();
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("D");
|
||||
x * 2
|
||||
}))
|
||||
.depends_on(c)
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(d)?, 6); // (1 + 2) * 2
|
||||
|
||||
// Check completion order
|
||||
let order = completion_order.lock().unwrap();
|
||||
|
||||
// A and B can complete in any order (parallel)
|
||||
assert!(order.contains(&"A"));
|
||||
assert!(order.contains(&"B"));
|
||||
|
||||
// C must come after both A and B
|
||||
let c_pos = order.iter().position(|name| *name == "C").unwrap();
|
||||
let a_pos = order.iter().position(|name| *name == "A").unwrap();
|
||||
let b_pos = order.iter().position(|name| *name == "B").unwrap();
|
||||
assert!(c_pos > a_pos, "C should complete after A");
|
||||
assert!(c_pos > b_pos, "C should complete after B");
|
||||
|
||||
// D must come after C
|
||||
let d_pos = order.iter().position(|name| *name == "D").unwrap();
|
||||
assert!(d_pos > c_pos, "D should complete after C");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
|
||||
// Prove that dependent tasks wait for their dependencies
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let execution_order = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct ATask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ATask {
|
||||
async fn run(&self) -> i32 {
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
self.order.lock().unwrap().push("A");
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
// Create a chain: A -> B -> C -> D
|
||||
let a: TaskHandle<_> = dag
|
||||
.add_task(ATask {
|
||||
order: execution_order.clone(),
|
||||
})
|
||||
.into();
|
||||
|
||||
struct BTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl BTask {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.order.lock().unwrap().push("B");
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let b = dag
|
||||
.add_task(BTask {
|
||||
order: execution_order.clone(),
|
||||
})
|
||||
.depends_on(a);
|
||||
|
||||
struct CTask {
|
||||
order: Arc<Mutex<Vec<&'static str>>>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl CTask {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.order.lock().unwrap().push("C");
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
let c = dag
|
||||
.add_task(CTask {
|
||||
order: execution_order.clone(),
|
||||
})
|
||||
.depends_on(b);
|
||||
|
||||
let d = {
|
||||
let order = execution_order.clone();
|
||||
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
|
||||
let order = order.clone();
|
||||
order.lock().unwrap().push("D");
|
||||
x + 1
|
||||
}))
|
||||
.depends_on(c)
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify results
|
||||
assert_eq!(dag.get(a)?, 1);
|
||||
assert_eq!(dag.get(b)?, 2);
|
||||
assert_eq!(dag.get(c)?, 3);
|
||||
assert_eq!(dag.get(d)?, 4);
|
||||
|
||||
// Verify execution order
|
||||
let order = execution_order.lock().unwrap();
|
||||
let expected = vec!["A", "B", "C", "D"];
|
||||
assert_eq!(order.as_slice(), expected.as_slice());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Parallelism tests module
|
||||
|
||||
pub mod concurrent;
|
||||
pub mod execution;
|
||||
pub mod spawning;
|
||||
pub mod threads;
|
||||
pub mod timing;
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Tests for spawner functionality
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{DagResult, DagRunner};
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
@@ -1,81 +1,13 @@
|
||||
//! Tests for multi-threading and concurrent execution
|
||||
|
||||
use crate::common::task_fn;
|
||||
use dagx::task_fn;
|
||||
use dagx::{task, DagResult, DagRunner, TaskHandle};
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::ThreadId;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tasks_run_on_different_threads() -> DagResult<()> {
|
||||
// Prove that tasks CAN run on multiple different threads (true parallelism capability)
|
||||
// Note: Tokio may choose to run fast tasks on the same thread for efficiency
|
||||
use std::thread;
|
||||
|
||||
let dag = DagRunner::new();
|
||||
let thread_ids = Arc::new(Mutex::new(std::collections::HashSet::new()));
|
||||
|
||||
struct LayerTask {
|
||||
ids: Arc<Mutex<HashSet<ThreadId>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl LayerTask {
|
||||
async fn run(&self) -> i32 {
|
||||
let tid = thread::current().id();
|
||||
self.ids.lock().unwrap().insert(tid);
|
||||
|
||||
// Yield to give tokio reason to distribute
|
||||
sleep(Duration::from_millis(5)).await;
|
||||
|
||||
// Some CPU work too
|
||||
let mut sum = 0;
|
||||
for j in 0..1000 {
|
||||
sum += j;
|
||||
}
|
||||
self.idx + sum
|
||||
}
|
||||
}
|
||||
|
||||
// Create 50 tasks with actual work to encourage multi-threading
|
||||
let tasks: Vec<_> = (0..50)
|
||||
.map(|i| {
|
||||
dag.add_task(LayerTask {
|
||||
ids: thread_ids.clone(),
|
||||
idx: i,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
// Verify all tasks completed
|
||||
for task in tasks.into_iter() {
|
||||
let _ = dag.get(task)?;
|
||||
}
|
||||
|
||||
// With a multi-threaded runtime and sufficient work, we typically see multiple threads
|
||||
// But we can't guarantee it (scheduler's choice), so we just verify the capability exists
|
||||
let unique_threads = thread_ids.lock().unwrap().len();
|
||||
|
||||
// On a multi-core system with sufficient work, tokio should use multiple threads
|
||||
// This is a soft assertion - if it fails, it means tasks CAN'T use multiple threads
|
||||
if unique_threads == 1 {
|
||||
eprintln!(
|
||||
"Warning: All tasks ran on 1 thread. This may indicate parallelism isn't working, \
|
||||
or tokio chose not to distribute work (valid for small/fast tasks)"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
// Prove concurrent execution using atomic counters to track simultaneous tasks
|
||||
@@ -155,73 +87,6 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diamond_parallel_execution() -> DagResult<()> {
|
||||
// Test diamond pattern: source -> (parallel1, parallel2) -> sink
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let concurrent_middle = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into();
|
||||
|
||||
struct ParallelTask1 {
|
||||
counter: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ParallelTask1 {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.counter.fetch_add(1, Ordering::SeqCst);
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
let count = self.counter.load(Ordering::SeqCst);
|
||||
self.counter.fetch_sub(1, Ordering::SeqCst);
|
||||
assert!(count > 0, "Should see concurrent execution");
|
||||
x * 2
|
||||
}
|
||||
}
|
||||
|
||||
// These should run in parallel
|
||||
let parallel1 = dag
|
||||
.add_task(ParallelTask1 {
|
||||
counter: concurrent_middle.clone(),
|
||||
})
|
||||
.depends_on(source);
|
||||
|
||||
struct ParallelTask2 {
|
||||
counter: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl ParallelTask2 {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
self.counter.fetch_add(1, Ordering::SeqCst);
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
let count = self.counter.load(Ordering::SeqCst);
|
||||
self.counter.fetch_sub(1, Ordering::SeqCst);
|
||||
// Both should be running
|
||||
assert!(count > 0, "Should see concurrent execution");
|
||||
x * 3
|
||||
}
|
||||
}
|
||||
|
||||
let parallel2 = dag
|
||||
.add_task(ParallelTask2 {
|
||||
counter: concurrent_middle.clone(),
|
||||
})
|
||||
.depends_on(source);
|
||||
|
||||
let sink = dag
|
||||
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
|
||||
.depends_on((¶llel1, ¶llel2));
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(sink)?, 500); // (100 * 2) + (100 * 3)
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_massive_parallel_fanout() -> DagResult<()> {
|
||||
// Test extreme parallelism with 1000 parallel tasks
|
||||
|
||||
+151
-3
@@ -1,8 +1,11 @@
|
||||
//! Tests for timing and speedup verification
|
||||
|
||||
use dagx::{task, DagResult, DagRunner};
|
||||
use dagx::{task, task_fn, DagResult, DagRunner};
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::time::sleep;
|
||||
|
||||
struct SleepTask(usize, Duration);
|
||||
@@ -49,7 +52,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
|
||||
// Parallel execution should be significantly faster
|
||||
assert!(
|
||||
elapsed < sequential_time / 2,
|
||||
elapsed < sequential_time / 4,
|
||||
"Parallel execution too slow: {:?} vs sequential {:?}",
|
||||
elapsed,
|
||||
sequential_time
|
||||
@@ -57,3 +60,148 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_layers_execute_in_parallel() -> DagResult<()> {
|
||||
// Test that tasks in the same topological layer execute in parallel
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let layer_timing = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct Layer0Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: i32,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer0Task {
|
||||
async fn run(&self) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L0_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
self.idx * 10
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 0: 3 independent sources
|
||||
let sources: Vec<_> = (0..3)
|
||||
.map(|i| {
|
||||
dag.add_task(Layer0Task {
|
||||
timing: layer_timing.clone(),
|
||||
idx: i,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
struct Layer1Task {
|
||||
timing: Arc<Mutex<Vec<(String, Instant)>>>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
#[task]
|
||||
impl Layer1Task {
|
||||
async fn run(&self, x: &i32) -> i32 {
|
||||
let start = Instant::now();
|
||||
self.timing
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((format!("L1_{}", self.idx), start));
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
x + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Layer 1: Tasks depending on layer 0
|
||||
let layer1: Vec<_> = sources
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, source)| {
|
||||
dag.add_task(Layer1Task {
|
||||
idx: i,
|
||||
timing: layer_timing.clone(),
|
||||
})
|
||||
.depends_on(source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 2: Final aggregation
|
||||
let final_task = {
|
||||
let timing = layer_timing.clone();
|
||||
dag.add_task(task_fn::<(i32, i32, i32), _, _>(
|
||||
move |(a, b, c): (&i32, &i32, &i32)| {
|
||||
let timing = timing.clone();
|
||||
let start = Instant::now();
|
||||
timing.lock().unwrap().push(("L2_final".to_string(), start));
|
||||
a + b + c
|
||||
},
|
||||
))
|
||||
.depends_on((&layer1[0], &layer1[1], &layer1[2]))
|
||||
};
|
||||
|
||||
dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })
|
||||
.await?;
|
||||
|
||||
assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1)
|
||||
|
||||
// Analyze timing
|
||||
let timing = layer_timing.lock().unwrap();
|
||||
|
||||
// Get layer 0 start times
|
||||
let l0_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L0_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Get layer 1 start times
|
||||
let l1_times: Vec<Instant> = (0..3)
|
||||
.map(|i| {
|
||||
timing
|
||||
.iter()
|
||||
.find(|(name, _)| name == &format!("L1_{}", i))
|
||||
.map(|(_, t)| *t)
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Layer 0 tasks should all start nearly simultaneously
|
||||
let l0_spread = l0_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l0_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l0_spread < Duration::from_millis(5),
|
||||
"Layer 0 tasks should start together, spread was {:?}",
|
||||
l0_spread
|
||||
);
|
||||
|
||||
// Layer 1 tasks should also start nearly simultaneously (after layer 0 completes)
|
||||
let l1_spread = l1_times
|
||||
.iter()
|
||||
.max()
|
||||
.unwrap()
|
||||
.duration_since(*l1_times.iter().min().unwrap());
|
||||
assert!(
|
||||
l1_spread < Duration::from_millis(5),
|
||||
"Layer 1 tasks should start together, spread was {:?}",
|
||||
l1_spread
|
||||
);
|
||||
|
||||
// Layer 1 should start after layer 0
|
||||
let earliest_l1 = *l1_times.iter().min().unwrap();
|
||||
let earliest_l0 = *l0_times.iter().min().unwrap();
|
||||
assert!(
|
||||
earliest_l1 >= earliest_l0 + Duration::from_millis(15),
|
||||
"Layer 1 should start after layer 0 completes"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user