Update tests, examples, benches

This commit is contained in:
TechnoPorg committed 2026-02-11 22:01:50 +01:00
1 parent 0137296fb4
commit 86787b3a0d
71 files changed
+1791 -3547

No files matched your search

+34 -29
View File
@@ -1,27 +1,30 @@
//! Concurrent access tests
use crate::common::task_fn;
use dagx::DagRunner;
use dagx::{task, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinSet;
struct SleepTask(usize);
#[task]
impl SleepTask {
async fn run(&self) -> usize {
tokio::time::sleep(Duration::from_millis(5)).await;
self.0
}
}
#[tokio::test]
async fn test_concurrent_run_calls() {
// Test that multiple run() calls are handled correctly
let dag = Arc::new(DagRunner::new());
// Add some tasks
let tasks: Vec<_> = (0..10)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
tokio::time::sleep(Duration::from_millis(5)).await;
i
}))
})
.collect();
let tasks: Vec<_> = (0..10).map(|i| dag.add_task(SleepTask(i))).collect();
// Try to run the DAG multiple times concurrently
let mut handles = JoinSet::new();
@@ -59,7 +62,7 @@ async fn test_concurrent_execution_and_building() {
// Start with some initial tasks
let initial: Vec<_> = (0..10)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
.collect();
// Execute the initial DAG
@@ -81,7 +84,7 @@ async fn test_concurrent_execution_and_building() {
for i in 10..20 {
let dag = Arc::clone(&dag);
handles.spawn(async move {
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
i
});
}
@@ -112,14 +115,14 @@ async fn test_dag_builder_thread_safety() {
handles.spawn(async move {
// Each thread creates a small pipeline
let source = dag.add_task(task_fn(move |_: ()| async move { thread_id }));
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| thread_id));
let transform1 = dag
.add_task(task_fn(move |x: i32| async move { x * 10 }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * 10))
.depends_on(source);
let transform2 = dag
.add_task(task_fn(move |x: i32| async move { x + thread_id }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + thread_id))
.depends_on(transform1);
counter.fetch_add(1, Ordering::SeqCst);
@@ -155,7 +158,7 @@ async fn test_concurrent_get_operations() {
// Create and execute tasks
let tasks: Vec<_> = (0..100)
.map(|i| dag.add_task(task_fn(move |_: ()| async move { i })))
.map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i)))
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap))
@@ -188,7 +191,7 @@ async fn test_concurrent_get_operations() {
async fn test_no_race_in_dependency_tracking() {
// Test that concurrent dependency modifications don't cause races
let dag = Arc::new(DagRunner::new());
let source = dag.add_task(task_fn(|_: ()| async { 42 }));
let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42));
let mut handles = JoinSet::new();
@@ -199,7 +202,7 @@ async fn test_no_race_in_dependency_tracking() {
let source_clone = source_handle;
handles.spawn(async move {
dag.add_task(task_fn(move |x: i32| async move { x + i }))
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(source_clone)
});
}
@@ -239,20 +242,22 @@ async fn test_high_concurrency_stress() {
match i % 3 {
0 => {
// Add a source task
dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
}
1 => {
// Add a task with a simple dependency
let source = dag.add_task(task_fn(move |_: ()| async move { i }));
dag.add_task(task_fn(move |x: i32| async move { x * 2 }))
let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| x * 2))
.depends_on(source);
}
2 => {
// Add a more complex subgraph
let a = dag.add_task(task_fn(move |_: ()| async move { i }));
let b = dag.add_task(task_fn(move |_: ()| async move { i + 1 }));
dag.add_task(task_fn(move |(x, y): (i32, i32)| async move { x + y }))
.depends_on((a, b));
let a = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
let b = dag.add_task(task_fn::<(), _, _>(move |_: ()| i + 1));
dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
x + y
}))
.depends_on((a, b));
}
_ => unreachable!(),
}
@@ -289,7 +294,7 @@ async fn test_lock_contention_measurement() {
handles.spawn(async move {
for j in 0..10 {
dag.add_task(task_fn(move |_: ()| async move { i * 10 + j }));
dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10 + j));
ops.fetch_add(1, Ordering::SeqCst);
// Small yield to increase contention likelihood
@@ -326,7 +331,7 @@ async fn test_atomic_dag_operations() {
let dag = Arc::new(DagRunner::new());
// Create a base task
let base = dag.add_task(task_fn(|_: ()| async { 100 }));
let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 100));
// Try to create dependent tasks concurrently
let mut handles = JoinSet::new();
@@ -340,7 +345,7 @@ async fn test_atomic_dag_operations() {
handles.spawn(async move {
let dep = dag
.add_task(task_fn(move |x: i32| async move { x + i }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + i))
.depends_on(base_clone);
success.fetch_add(1, Ordering::SeqCst);
@@ -381,12 +386,12 @@ async fn test_no_deadlock_on_circular_waiting() {
handles.spawn(async move {
// Each thread creates a chain of tasks
let first = dag.add_task(task_fn(move |_: ()| async move { i }));
let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| i));
let mut chain = vec![first.into()];
for j in 0..5 {
let next = dag
.add_task(task_fn(move |x: i32| async move { x + j }))
.add_task(task_fn::<i32, _, _>(move |&x: &i32| x + j))
.depends_on(chain.last().unwrap());
chain.push(next);
+146 -179
View File
@@ -1,7 +1,7 @@
//! Tests for execution correctness and parallelism
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
@@ -14,37 +14,61 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> {
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| {
let timing = layer_timing.clone();
dag.add_task(task_fn(move |_: ()| {
let timing = timing.clone();
async move {
let start = Instant::now();
timing.lock().unwrap().push((format!("L0_{}", i), start));
sleep(Duration::from_millis(20)).await;
i * 10
}
}))
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)| {
let timing = layer_timing.clone();
dag.add_task(task_fn(move |x: i32| {
let timing = timing.clone();
async move {
let start = Instant::now();
timing.lock().unwrap().push((format!("L1_{}", i), start));
sleep(Duration::from_millis(20)).await;
x + 1
}
}))
dag.add_task(Layer1Task {
idx: i,
timing: layer_timing.clone(),
})
.depends_on(source)
})
.collect();
@@ -52,14 +76,14 @@ async fn test_layers_execute_in_parallel() -> DagResult<()> {
// Layer 2: Final aggregation
let final_task = {
let timing = layer_timing.clone();
dag.add_task(task_fn(move |(a, b, c): (i32, i32, i32)| {
let timing = timing.clone();
async move {
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]))
};
@@ -134,51 +158,57 @@ async fn test_completion_order_matches_dependencies() -> DagResult<()> {
let completion_order = Arc::new(Mutex::new(Vec::new()));
// Create a complex DAG with clear ordering requirements
let a = {
let order = completion_order.clone();
dag.add_task(task_fn(move |_: ()| {
let order = order.clone();
async move {
sleep(Duration::from_millis(10)).await;
order.lock().unwrap().push("A");
1
}
}))
};
struct ATask {
order: Arc<Mutex<Vec<&'static str>>>,
}
let b = {
let order = completion_order.clone();
dag.add_task(task_fn(move |_: ()| {
let order = order.clone();
async move {
sleep(Duration::from_millis(5)).await;
order.lock().unwrap().push("B");
2
}
}))
};
#[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(move |(x, y): (i32, i32)| {
dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| {
let order = order.clone();
async move {
order.lock().unwrap().push("C");
x + y
}
order.lock().unwrap().push("C");
x + y
}))
.depends_on((a, b))
};
let d = {
let order = completion_order.clone();
dag.add_task(task_fn(move |x: i32| {
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let order = order.clone();
async move {
order.lock().unwrap().push("D");
x * 2
}
order.lock().unwrap().push("D");
x * 2
}))
.depends_on(c)
};
@@ -215,54 +245,70 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
let execution_order = Arc::new(Mutex::new(Vec::new()));
// Create a chain: A -> B -> C -> D
let a: TaskHandle<_> = {
let order = execution_order.clone();
dag.add_task(task_fn(move |_: ()| {
let order = order.clone();
async move {
sleep(Duration::from_millis(20)).await;
order.lock().unwrap().push("A");
1
}
}))
struct ATask {
order: Arc<Mutex<Vec<&'static str>>>,
}
.into();
let b = {
let order = execution_order.clone();
dag.add_task(task_fn(move |x: i32| {
let order = order.clone();
async move {
order.lock().unwrap().push("B");
sleep(Duration::from_millis(10)).await;
x + 1
}
}))
.depends_on(a)
};
#[task]
impl ATask {
async fn run(&self) -> i32 {
sleep(Duration::from_millis(20)).await;
self.order.lock().unwrap().push("A");
1
}
}
let c = {
let order = execution_order.clone();
dag.add_task(task_fn(move |x: i32| {
let order = order.clone();
async move {
order.lock().unwrap().push("C");
sleep(Duration::from_millis(10)).await;
x + 1
}
}))
.depends_on(b)
};
// 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(move |x: i32| {
dag.add_task(task_fn::<i32, _, _>(move |&x: &i32| {
let order = order.clone();
async move {
order.lock().unwrap().push("D");
x + 1
}
order.lock().unwrap().push("D");
x + 1
}))
.depends_on(c)
};
@@ -282,82 +328,3 @@ async fn test_dependent_tasks_respect_ordering() -> DagResult<()> {
Ok(())
}
#[tokio::test]
async fn test_independent_tasks_dont_wait() -> DagResult<()> {
// Prove that independent tasks don't wait for each other
let dag = DagRunner::new();
let task_started = Arc::new(Mutex::new(Vec::new()));
let task_finished = Arc::new(Mutex::new(Vec::new()));
// Create tasks with different durations
let fast1 = {
let started = task_started.clone();
let finished = task_finished.clone();
dag.add_task(task_fn(move |_: ()| {
let started = started.clone();
let finished = finished.clone();
async move {
started.lock().unwrap().push(("fast1", Instant::now()));
sleep(Duration::from_millis(10)).await;
finished.lock().unwrap().push(("fast1", Instant::now()));
"fast1"
}
}))
};
let slow = {
let started = task_started.clone();
let finished = task_finished.clone();
dag.add_task(task_fn(move |_: ()| {
let started = started.clone();
let finished = finished.clone();
async move {
started.lock().unwrap().push(("slow", Instant::now()));
sleep(Duration::from_millis(100)).await;
finished.lock().unwrap().push(("slow", Instant::now()));
"slow"
}
}))
};
let fast2 = {
let started = task_started.clone();
let finished = task_finished.clone();
dag.add_task(task_fn(move |_: ()| {
let started = started.clone();
let finished = finished.clone();
async move {
started.lock().unwrap().push(("fast2", Instant::now()));
sleep(Duration::from_millis(10)).await;
finished.lock().unwrap().push(("fast2", Instant::now()));
"fast2"
}
}))
};
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Check results
assert_eq!(dag.get(fast1)?, "fast1");
assert_eq!(dag.get(slow)?, "slow");
assert_eq!(dag.get(fast2)?, "fast2");
// Verify that fast tasks finished before slow task
let finished = task_finished.lock().unwrap();
let fast1_finish = finished.iter().find(|x| x.0 == "fast1").unwrap().1;
let fast2_finish = finished.iter().find(|x| x.0 == "fast2").unwrap().1;
let slow_finish = finished.iter().find(|x| x.0 == "slow").unwrap().1;
assert!(
fast1_finish < slow_finish,
"fast1 should finish before slow"
);
assert!(
fast2_finish < slow_finish,
"fast2 should finish before slow"
);
Ok(())
}
-1
View File
@@ -2,7 +2,6 @@
pub mod concurrent;
pub mod execution;
pub mod proof;
pub mod spawning;
pub mod threads;
pub mod timing;
-63
View File
@@ -1,63 +0,0 @@
//! Parallelism proof tests
use crate::common::task_fn;
use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::time::{Duration, Instant};
use tokio::time::sleep;
#[tokio::test]
async fn test_parallelism_proof_1000_tasks() -> DagResult<()> {
// Proof that 1000 tasks sleeping 100ms each complete in ~100ms (not 100 seconds)
let dag = DagRunner::new();
let sleep_duration = Duration::from_millis(100);
let num_tasks = 1000u32;
// Create 1000 independent tasks that each sleep for 100ms
let tasks: Vec<_> = (0..num_tasks)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
sleep(sleep_duration).await;
i
}))
})
.collect();
let start = Instant::now();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
let elapsed = start.elapsed();
// Verify all tasks completed
for (i, task) in tasks.into_iter().enumerate() {
assert_eq!(dag.get(task)?, i as u32);
}
// Sequential execution would take: 1000 * 100ms = 100 seconds
let sequential_time = sleep_duration * num_tasks;
println!(
"Parallelism proof: {} tasks × {}ms completed in {:?} (sequential would be {:?})",
num_tasks,
sleep_duration.as_millis(),
elapsed,
sequential_time
);
// Allow generous overhead: should complete in less than 500ms (not 100 seconds!)
assert!(
elapsed < Duration::from_millis(500),
"Tasks appear to be running sequentially! Took {:?} instead of ~100ms",
elapsed
);
// Verify we achieved massive speedup
let speedup = sequential_time.as_secs_f64() / elapsed.as_secs_f64();
assert!(
speedup > 200.0,
"Speedup too low: {:.1}x (expected > 200x)",
speedup
);
Ok(())
}
+3 -62
View File
@@ -5,9 +5,6 @@ use dagx::{DagResult, DagRunner};
use futures::FutureExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::time::sleep;
#[tokio::test]
async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
@@ -18,14 +15,12 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
// Create 10 independent tasks
let tasks: Vec<_> = (0..10)
.map(|i| {
dag.add_task(task_fn({
dag.add_task(task_fn::<(), _, _>({
let value = spawn_count.clone();
move |_: ()| {
let value = value.clone();
async move {
value.fetch_add(1, Ordering::SeqCst);
i
}
value.fetch_add(1, Ordering::SeqCst);
i
}
}))
})
@@ -49,57 +44,3 @@ async fn test_spawner_actually_spawns_tasks() -> DagResult<()> {
Ok(())
}
#[tokio::test]
async fn test_tasks_not_running_inline() -> DagResult<()> {
// Verify that tasks are spawned and CAN run on different threads than the caller
// Note: Small/fast tasks may run on the same thread if the scheduler chooses
use std::thread;
let dag = DagRunner::new();
let calling_thread = thread::current().id();
let task_threads = Arc::new(Mutex::new(Vec::new()));
// Create tasks with some work to encourage distribution
let tasks: Vec<_> = (0..20)
.map(|i| {
let threads = task_threads.clone();
dag.add_task(task_fn(move |_: ()| {
let threads = threads.clone();
async move {
threads.lock().unwrap().push(thread::current().id());
// Add some async work
sleep(Duration::from_millis(2)).await;
i
}
}))
})
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify results
for (i, task) in tasks.into_iter().enumerate() {
assert_eq!(dag.get(task)?, i as i32);
}
// Check if tasks ran on different threads than the caller
// This is a capability test - the scheduler MAY choose to use the same thread
let task_thread_ids = task_threads.lock().unwrap();
let different_threads = task_thread_ids
.iter()
.filter(|&&tid| tid != calling_thread)
.count();
if different_threads == 0 {
eprintln!(
"Warning: All tasks ran on the calling thread. This may indicate tasks aren't \
being spawned, or the scheduler chose not to distribute (valid for very fast tasks)"
);
}
// The key thing is that tasks completed successfully via spawner
Ok(())
}
+158 -310
View File
@@ -1,10 +1,12 @@
//! Tests for multi-threading and concurrent execution
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner, TaskHandle};
use futures::FutureExt;
use std::collections::HashSet;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::ThreadId;
use std::time::{Duration, Instant};
use tokio::time::sleep;
@@ -17,27 +19,36 @@ async fn test_tasks_run_on_different_threads() -> DagResult<()> {
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| {
let ids = thread_ids.clone();
dag.add_task(task_fn(move |_: ()| {
let ids = ids.clone();
async move {
let tid = thread::current().id();
ids.lock().unwrap().insert(tid);
// Do some actual async work 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;
}
i + sum
}
}))
dag.add_task(LayerTask {
ids: thread_ids.clone(),
idx: i,
})
})
.collect();
@@ -71,42 +82,50 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> {
let concurrent_count = Arc::new(AtomicUsize::new(0));
let max_concurrent = Arc::new(AtomicUsize::new(0));
struct LayerTask {
count: Arc<AtomicUsize>,
max: Arc<AtomicUsize>,
idx: usize,
}
#[task]
impl LayerTask {
async fn run(&self) -> usize {
// Increment concurrent counter
let current = self.count.fetch_add(1, Ordering::SeqCst) + 1;
// Track maximum concurrency
let mut prev_max = self.max.load(Ordering::SeqCst);
while current > prev_max {
match self.max.compare_exchange_weak(
prev_max,
current,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(x) => prev_max = x,
}
}
// Simulate work
sleep(Duration::from_millis(10)).await;
// Decrement counter
self.count.fetch_sub(1, Ordering::SeqCst);
self.idx
}
}
// Create 20 tasks that track concurrent execution
let tasks: Vec<_> = (0..20)
.map(|i| {
let count = concurrent_count.clone();
let max = max_concurrent.clone();
dag.add_task(task_fn(move |_: ()| {
let count = count.clone();
let max = max.clone();
async move {
// Increment concurrent counter
let current = count.fetch_add(1, Ordering::SeqCst) + 1;
// Track maximum concurrency
let mut prev_max = max.load(Ordering::SeqCst);
while current > prev_max {
match max.compare_exchange_weak(
prev_max,
current,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(x) => prev_max = x,
}
}
// Simulate work
sleep(Duration::from_millis(10)).await;
// Decrement counter
count.fetch_sub(1, Ordering::SeqCst);
i
}
}))
dag.add_task(LayerTask {
count: concurrent_count.clone(),
max: max_concurrent.clone(),
idx: i,
})
})
.collect();
@@ -141,44 +160,56 @@ async fn test_diamond_parallel_execution() -> DagResult<()> {
let concurrent_middle = Arc::new(AtomicUsize::new(0));
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 100 })).into();
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 = {
let counter = concurrent_middle.clone();
dag.add_task(task_fn(move |x: i32| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
sleep(Duration::from_millis(20)).await;
let count = counter.load(Ordering::SeqCst);
counter.fetch_sub(1, Ordering::SeqCst);
assert!(count > 0, "Should see concurrent execution");
x * 2
}
}))
.depends_on(source)
};
let parallel1 = dag
.add_task(ParallelTask1 {
counter: concurrent_middle.clone(),
})
.depends_on(source);
let parallel2 = {
let counter = concurrent_middle.clone();
dag.add_task(task_fn(move |x: i32| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
sleep(Duration::from_millis(20)).await;
let count = counter.load(Ordering::SeqCst);
counter.fetch_sub(1, Ordering::SeqCst);
// Both should be running
assert!(count > 0, "Should see concurrent execution");
x * 3
}
}))
.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(|(a, b): (i32, i32)| async move { a + b }))
.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b))
.depends_on((&parallel1, &parallel2));
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
@@ -188,197 +219,6 @@ async fn test_diamond_parallel_execution() -> DagResult<()> {
Ok(())
}
#[tokio::test]
async fn test_parallel_branches_with_dependencies() -> DagResult<()> {
// Test that parallel branches execute independently but respect their own dependencies
let dag = DagRunner::new();
let branch_progress = Arc::new(Mutex::new(Vec::new()));
// Source node
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
// Branch 1: source -> b1_1 -> b1_2
let b1_1 = {
let progress = branch_progress.clone();
dag.add_task(task_fn(move |x: i32| {
let progress = progress.clone();
async move {
progress
.lock()
.unwrap()
.push(("b1_1_start", Instant::now()));
sleep(Duration::from_millis(30)).await;
progress.lock().unwrap().push(("b1_1_end", Instant::now()));
x * 2
}
}))
.depends_on(source)
};
let b1_2 = {
let progress = branch_progress.clone();
dag.add_task(task_fn(move |x: i32| {
let progress = progress.clone();
async move {
progress
.lock()
.unwrap()
.push(("b1_2_start", Instant::now()));
sleep(Duration::from_millis(10)).await;
progress.lock().unwrap().push(("b1_2_end", Instant::now()));
x + 1
}
}))
.depends_on(b1_1)
};
// Branch 2: source -> b2_1 -> b2_2
let b2_1 = {
let progress = branch_progress.clone();
dag.add_task(task_fn(move |x: i32| {
let progress = progress.clone();
async move {
progress
.lock()
.unwrap()
.push(("b2_1_start", Instant::now()));
sleep(Duration::from_millis(10)).await;
progress.lock().unwrap().push(("b2_1_end", Instant::now()));
x * 3
}
}))
.depends_on(source)
};
let b2_2 = {
let progress = branch_progress.clone();
dag.add_task(task_fn(move |x: i32| {
let progress = progress.clone();
async move {
progress
.lock()
.unwrap()
.push(("b2_2_start", Instant::now()));
sleep(Duration::from_millis(30)).await;
progress.lock().unwrap().push(("b2_2_end", Instant::now()));
x + 2
}
}))
.depends_on(b2_1)
};
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify results
assert_eq!(dag.get(b1_2)?, 21); // (10 * 2) + 1
assert_eq!(dag.get(b2_2)?, 32); // (10 * 3) + 2
// Analyze timing to prove parallel execution
let progress = branch_progress.lock().unwrap();
// Find timestamps
let find_time = |name: &str| -> Instant {
progress
.iter()
.find(|(n, _)| *n == name)
.map(|(_, t)| *t)
.unwrap_or_else(|| panic!("Couldn't find {}", name))
};
let b1_1_start = find_time("b1_1_start");
let b1_1_end = find_time("b1_1_end");
let b1_2_start = find_time("b1_2_start");
let b2_1_start = find_time("b2_1_start");
// Branch 1 and Branch 2 first tasks should start nearly simultaneously
let branch_start_diff = if b1_1_start > b2_1_start {
b1_1_start - b2_1_start
} else {
b2_1_start - b1_1_start
};
// Use a more generous threshold to account for coverage instrumentation overhead
// 50ms is enough to prove they're parallel (not sequential) while being robust to overhead
assert!(
branch_start_diff < Duration::from_millis(50),
"Branches should start in parallel, diff was {:?}",
branch_start_diff
);
// b1_2 should only start after b1_1 ends
assert!(b1_2_start >= b1_1_end, "b1_2 started before b1_1 finished");
Ok(())
}
#[tokio::test]
async fn test_wide_fanout_parallel_execution() -> DagResult<()> {
// Test that a single source can fan out to many parallel tasks
let dag = DagRunner::new();
let max_concurrent = Arc::new(AtomicUsize::new(0));
let current_concurrent = Arc::new(AtomicUsize::new(0));
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 42 })).into();
// Create 50 tasks that all depend on the source
let dependents: Vec<_> = (0..50)
.map(|i| {
let max_c = max_concurrent.clone();
let curr_c = current_concurrent.clone();
dag.add_task(task_fn(move |x: i32| {
let max_c = max_c.clone();
let curr_c = curr_c.clone();
async move {
// Track concurrency
let current = curr_c.fetch_add(1, Ordering::SeqCst) + 1;
// Update max
let mut prev_max = max_c.load(Ordering::SeqCst);
while current > prev_max {
match max_c.compare_exchange_weak(
prev_max,
current,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(v) => prev_max = v,
}
}
// Do work
sleep(Duration::from_millis(5)).await;
curr_c.fetch_sub(1, Ordering::SeqCst);
x + i
}
}))
.depends_on(source)
})
.collect();
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify results
for (i, task) in dependents.iter().enumerate() {
assert_eq!(dag.get(task)?, 42 + i as i32);
}
// Check that we saw significant parallelism
let max_seen = max_concurrent.load(Ordering::SeqCst);
println!("Max concurrent in fanout: {}", max_seen);
assert!(
max_seen > 5,
"Expected significant parallelism in fanout, got {}",
max_seen
);
Ok(())
}
#[tokio::test]
async fn test_massive_parallel_fanout() -> DagResult<()> {
// Test extreme parallelism with 1000 parallel tasks
@@ -388,44 +228,52 @@ async fn test_massive_parallel_fanout() -> DagResult<()> {
let max_concurrent = Arc::new(AtomicUsize::new(0));
let current = Arc::new(AtomicUsize::new(0));
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 1 })).into();
let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into();
struct FanoutTask {
comp: Arc<AtomicUsize>,
max: Arc<AtomicUsize>,
curr: Arc<AtomicUsize>,
idx: i32,
}
#[task]
impl FanoutTask {
async fn run(&self, x: &i32) -> i32 {
// Track concurrency
let c = self.curr.fetch_add(1, Ordering::SeqCst) + 1;
let mut prev_max = self.max.load(Ordering::SeqCst);
while c > prev_max {
match self.max.compare_exchange_weak(
prev_max,
c,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(v) => prev_max = v,
}
}
// Quick work
sleep(Duration::from_millis(100)).await;
self.curr.fetch_sub(1, Ordering::SeqCst);
self.comp.fetch_add(1, Ordering::SeqCst);
x + self.idx
}
}
// Create 1000 tasks all depending on the same source
let tasks: Vec<_> = (0..1000)
.map(|i| {
let comp = completed.clone();
let max = max_concurrent.clone();
let curr = current.clone();
dag.add_task(task_fn(move |x: i32| {
let comp = comp.clone();
let max = max.clone();
let curr = curr.clone();
async move {
// Track concurrency
let c = curr.fetch_add(1, Ordering::SeqCst) + 1;
let mut prev_max = max.load(Ordering::SeqCst);
while c > prev_max {
match max.compare_exchange_weak(
prev_max,
c,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => break,
Err(v) => prev_max = v,
}
}
// Quick work
sleep(Duration::from_micros(100)).await;
curr.fetch_sub(1, Ordering::SeqCst);
comp.fetch_add(1, Ordering::SeqCst);
x + i
}
}))
dag.add_task(FanoutTask {
comp: completed.clone(),
max: max_concurrent.clone(),
curr: current.clone(),
idx: i,
})
.depends_on(source)
})
.collect();
+12 -87
View File
@@ -1,14 +1,20 @@
//! Tests for timing and speedup verification
use crate::common::task_fn;
use dagx::{DagResult, DagRunner, TaskHandle};
use dagx::{task, DagResult, DagRunner};
use futures::FutureExt;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use tokio::time::sleep;
struct SleepTask(usize, Duration);
#[task]
impl SleepTask {
async fn run(&self) -> usize {
sleep(self.1).await;
self.0
}
}
#[tokio::test]
async fn test_parallel_execution_speedup() -> DagResult<()> {
// Prove parallel execution by comparing timing
@@ -19,12 +25,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
// Create independent tasks that each take 50ms
let tasks: Vec<_> = (0..num_tasks)
.map(|i| {
dag.add_task(task_fn(move |_: ()| async move {
sleep(work_duration).await;
i
}))
})
.map(|i| dag.add_task(SleepTask(i, work_duration)))
.collect();
let start = Instant::now();
@@ -55,79 +56,3 @@ async fn test_parallel_execution_speedup() -> DagResult<()> {
Ok(())
}
struct SyncTask;
#[dagx::task]
impl SyncTask {
fn run(&self, x: &i32) -> i32 {
// Synchronous task - no async/await
std::thread::sleep(Duration::from_millis(1)); // Blocking sleep
x * 2
}
}
struct AsyncTask;
#[dagx::task]
impl AsyncTask {
async fn run(&self, x: &i32) -> i32 {
// Async task
sleep(Duration::from_millis(1)).await; // Non-blocking sleep
x * 3
}
}
#[tokio::test]
async fn test_sync_tasks_dont_block_async_runtime() -> DagResult<()> {
// Test that sync tasks don't block the async runtime
let dag = DagRunner::new();
let start_times = Arc::new(Mutex::new(Vec::new()));
// Mix of sync and async tasks
let source: TaskHandle<_> = dag.add_task(task_fn(|_: ()| async { 10 })).into();
// Sync tasks
let sync1 = dag.add_task(SyncTask).depends_on(source);
let sync2 = dag.add_task(SyncTask).depends_on(source);
// Async tasks
let async1 = dag.add_task(AsyncTask).depends_on(source);
let async2 = dag.add_task(AsyncTask).depends_on(source);
// Track timing
let timing_task = {
let times = start_times.clone();
dag.add_task(task_fn(move |_: ()| {
let times = times.clone();
async move {
for i in 0..5 {
times
.lock()
.unwrap()
.push((format!("tick_{}", i), Instant::now()));
sleep(Duration::from_millis(5)).await;
}
"timing"
}
}))
};
dag.run(|fut| tokio::spawn(fut).map(Result::unwrap)).await?;
// Verify results
assert_eq!(dag.get(sync1)?, 20);
assert_eq!(dag.get(sync2)?, 20);
assert_eq!(dag.get(async1)?, 30);
assert_eq!(dag.get(async2)?, 30);
assert_eq!(dag.get(timing_task)?, "timing");
// Check that timing task made progress (wasn't blocked by sync tasks)
let times = start_times.lock().unwrap();
assert_eq!(
times.len(),
5,
"Timing task should have completed all ticks"
);
Ok(())
}