feat: dagrunner, initial version

This commit is contained in:
swaits committed 2025-10-07 16:31:03 -07:00
commit 7412287ee4
122 files changed
+23101

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
//! Deep chain comparison: dagx vs dagrs
//!
//! Long sequential chain: 100 tasks in sequence
use criterion::Criterion;
pub fn bench_deep_chain(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("deep_chain_100_tasks");
// dagx implementation
group.bench_function("dagx", |b| {
b.iter(|| {
rt.block_on(async {
use dagx::{task_fn, DagRunner};
let dag = DagRunner::new();
let first = dag.add_task(task_fn(|_: ()| async { 0 }));
let mut prev = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(&first);
for i in 2..100 {
prev = dag
.add_task(task_fn(move |x: i32| async move { x + i }))
.depends_on(prev);
}
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
dag.get(prev).unwrap()
})
});
});
// dagrs implementation
group.bench_function("dagrs", |b| {
b.iter(|| {
use async_trait::async_trait;
use dagrs::*;
use std::sync::Arc;
struct Initial;
#[async_trait]
impl Action for Initial {
async fn run(
&self,
_: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
out.broadcast(Content::new(0i32)).await;
Output::Out(Some(Content::new(0i32)))
}
}
struct Increment(i32);
#[async_trait]
impl Action for Increment {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result = vals[0] + self.0;
out.broadcast(Content::new(result)).await;
Output::Out(Some(Content::new(result)))
}
}
let mut table = NodeTable::new();
let mut nodes = vec![];
let mut node_ids = vec![];
let initial = DefaultNode::with_action("task_0".to_string(), Initial, &mut table);
node_ids.push(initial.id());
nodes.push(initial);
for i in 1..100 {
let node =
DefaultNode::with_action(format!("task_{}", i), Increment(i), &mut table);
node_ids.push(node.id());
nodes.push(node);
}
let mut graph = Graph::new();
for node in nodes {
graph.add_node(node);
}
for i in 1..100 {
graph.add_edge(node_ids[i - 1], vec![node_ids[i]]);
}
graph.set_env(EnvVar::new(table));
graph.start().unwrap();
});
});
group.finish();
}
+140
View File
@@ -0,0 +1,140 @@
//! Diamond pattern comparison: dagx vs dagrs
//!
//! Classic diamond: A → B,C → D
use criterion::Criterion;
pub fn bench_diamond_pattern(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("diamond_pattern");
// dagx implementation
group.bench_function("dagx", |b| {
b.iter(|| {
rt.block_on(async {
use dagx::{task_fn, DagRunner};
let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 10 }));
let b = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(&a);
let c = dag
.add_task(task_fn(|x: i32| async move { x + 5 }))
.depends_on(&a);
let d = dag
.add_task(task_fn(|(x, y): (i32, i32)| async move { x + y }))
.depends_on((&b, &c));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
dag.get(d).unwrap()
})
});
});
// dagrs implementation
group.bench_function("dagrs", |b| {
b.iter(|| {
use async_trait::async_trait;
use dagrs::*;
use std::sync::Arc;
struct TaskA;
#[async_trait]
impl Action for TaskA {
async fn run(
&self,
_: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
out.broadcast(Content::new(10i32)).await;
Output::Out(Some(Content::new(10i32)))
}
}
struct TaskB;
#[async_trait]
impl Action for TaskB {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result = vals[0] * 2;
out.broadcast(Content::new(result)).await;
Output::Out(Some(Content::new(result)))
}
}
struct TaskC;
#[async_trait]
impl Action for TaskC {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result = vals[0] + 5;
out.broadcast(Content::new(result)).await;
Output::Out(Some(Content::new(result)))
}
}
struct TaskD;
#[async_trait]
impl Action for TaskD {
async fn run(
&self,
input: &mut InChannels,
_: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result: i32 = vals.iter().sum();
Output::Out(Some(Content::new(result)))
}
}
let mut table = NodeTable::new();
let a = DefaultNode::with_action("a".to_string(), TaskA, &mut table);
let a_id = a.id();
let b = DefaultNode::with_action("b".to_string(), TaskB, &mut table);
let b_id = b.id();
let c = DefaultNode::with_action("c".to_string(), TaskC, &mut table);
let c_id = c.id();
let d = DefaultNode::with_action("d".to_string(), TaskD, &mut table);
let d_id = d.id();
let mut graph = Graph::new();
graph.add_node(a);
graph.add_node(b);
graph.add_node(c);
graph.add_node(d);
graph.add_edge(a_id, vec![b_id, c_id]);
graph.add_edge(b_id, vec![d_id]);
graph.add_edge(c_id, vec![d_id]);
graph.set_env(EnvVar::new(table));
graph.start().unwrap();
});
});
group.finish();
}
+241
View File
@@ -0,0 +1,241 @@
//! Realistic ETL pipeline comparison: dagx vs dagrs
//!
//! Extract → Transform → Validate → Load pattern
use criterion::Criterion;
pub fn bench_etl_pipeline(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("etl_pipeline");
// dagx implementation
group.bench_function("dagx", |b| {
b.iter(|| {
rt.block_on(async {
use dagx::{task_fn, DagRunner};
let dag = DagRunner::new();
// Extract from 3 sources
let source1 = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] }));
let source2 = dag.add_task(task_fn(|_: ()| async { vec![6, 7, 8, 9, 10] }));
let source3 = dag.add_task(task_fn(|_: ()| async { vec![11, 12, 13, 14, 15] }));
// Transform each source
let transform1 = dag
.add_task(task_fn(|data: Vec<i32>| async move {
data.into_iter().map(|x| x * 2).collect::<Vec<_>>()
}))
.depends_on(&source1);
let transform2 = dag
.add_task(task_fn(|data: Vec<i32>| async move {
data.into_iter().map(|x| x * 2).collect::<Vec<_>>()
}))
.depends_on(&source2);
let transform3 = dag
.add_task(task_fn(|data: Vec<i32>| async move {
data.into_iter().map(|x| x * 2).collect::<Vec<_>>()
}))
.depends_on(&source3);
// Merge all transformed data
let merge = dag
.add_task(task_fn(
|(d1, d2, d3): (Vec<i32>, Vec<i32>, Vec<i32>)| async move {
let mut result = d1;
result.extend(d2);
result.extend(d3);
result
},
))
.depends_on((&transform1, &transform2, &transform3));
// Validate
let validate = dag
.add_task(task_fn(|data: Vec<i32>| async move {
data.into_iter().filter(|&x| x > 0).collect::<Vec<_>>()
}))
.depends_on(merge);
// Load (compute sum)
let load = dag
.add_task(task_fn(|data: Vec<i32>| async move {
data.into_iter().sum::<i32>()
}))
.depends_on(validate);
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
dag.get(load).unwrap()
})
});
});
// dagrs implementation
group.bench_function("dagrs", |b| {
b.iter(|| {
use async_trait::async_trait;
use dagrs::*;
use std::sync::Arc;
struct Extract(Vec<i32>);
#[async_trait]
impl Action for Extract {
async fn run(
&self,
_: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
out.broadcast(Content::new(self.0.clone())).await;
Output::Out(Some(Content::new(self.0.clone())))
}
}
struct Transform;
#[async_trait]
impl Action for Transform {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let inputs: Vec<Vec<i32>> = input
.map(|c| (**c.unwrap().into_inner::<Vec<i32>>().unwrap()).to_vec())
.await;
let data = &inputs[0];
let transformed: Vec<i32> = data.iter().map(|&x| x * 2).collect();
out.broadcast(Content::new(transformed.clone())).await;
Output::Out(Some(Content::new(transformed)))
}
}
struct Merge;
#[async_trait]
impl Action for Merge {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let inputs: Vec<Vec<i32>> = input
.map(|c| (**c.unwrap().into_inner::<Vec<i32>>().unwrap()).to_vec())
.await;
let mut result = Vec::new();
for input in inputs {
result.extend(input);
}
out.broadcast(Content::new(result.clone())).await;
Output::Out(Some(Content::new(result)))
}
}
struct Validate;
#[async_trait]
impl Action for Validate {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let inputs: Vec<Vec<i32>> = input
.map(|c| (**c.unwrap().into_inner::<Vec<i32>>().unwrap()).to_vec())
.await;
let data = &inputs[0];
let validated: Vec<i32> = data.iter().filter(|&&x| x > 0).copied().collect();
out.broadcast(Content::new(validated.clone())).await;
Output::Out(Some(Content::new(validated)))
}
}
struct Load;
#[async_trait]
impl Action for Load {
async fn run(
&self,
input: &mut InChannels,
_: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let inputs: Vec<Vec<i32>> = input
.map(|c| (**c.unwrap().into_inner::<Vec<i32>>().unwrap()).to_vec())
.await;
let data = &inputs[0];
let sum: i32 = data.iter().sum();
Output::Out(Some(Content::new(sum)))
}
}
let mut table = NodeTable::new();
let source1 = DefaultNode::with_action(
"source1".to_string(),
Extract(vec![1, 2, 3, 4, 5]),
&mut table,
);
let source1_id = source1.id();
let source2 = DefaultNode::with_action(
"source2".to_string(),
Extract(vec![6, 7, 8, 9, 10]),
&mut table,
);
let source2_id = source2.id();
let source3 = DefaultNode::with_action(
"source3".to_string(),
Extract(vec![11, 12, 13, 14, 15]),
&mut table,
);
let source3_id = source3.id();
let transform1 =
DefaultNode::with_action("transform1".to_string(), Transform, &mut table);
let transform1_id = transform1.id();
let transform2 =
DefaultNode::with_action("transform2".to_string(), Transform, &mut table);
let transform2_id = transform2.id();
let transform3 =
DefaultNode::with_action("transform3".to_string(), Transform, &mut table);
let transform3_id = transform3.id();
let merge = DefaultNode::with_action("merge".to_string(), Merge, &mut table);
let merge_id = merge.id();
let validate = DefaultNode::with_action("validate".to_string(), Validate, &mut table);
let validate_id = validate.id();
let load = DefaultNode::with_action("load".to_string(), Load, &mut table);
let load_id = load.id();
let mut graph = Graph::new();
graph.add_node(source1);
graph.add_node(source2);
graph.add_node(source3);
graph.add_node(transform1);
graph.add_node(transform2);
graph.add_node(transform3);
graph.add_node(merge);
graph.add_node(validate);
graph.add_node(load);
graph.add_edge(source1_id, vec![transform1_id]);
graph.add_edge(source2_id, vec![transform2_id]);
graph.add_edge(source3_id, vec![transform3_id]);
graph.add_edge(transform1_id, vec![merge_id]);
graph.add_edge(transform2_id, vec![merge_id]);
graph.add_edge(transform3_id, vec![merge_id]);
graph.add_edge(merge_id, vec![validate_id]);
graph.add_edge(validate_id, vec![load_id]);
graph.set_env(EnvVar::new(table));
graph.start().unwrap();
});
});
group.finish();
}
+68
View File
@@ -0,0 +1,68 @@
//! Large scale comparison: dagx vs dagrs
//!
//! 10,000 independent tasks to test scaling
use criterion::Criterion;
pub fn bench_large_scale(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("large_scale_10k_tasks");
group.sample_size(10);
group.measurement_time(std::time::Duration::from_secs(20));
// dagx implementation
group.bench_function("dagx", |b| {
b.iter(|| {
rt.block_on(async {
use dagx::{task_fn, DagRunner};
let dag = DagRunner::new();
for i in 0..10_000 {
dag.add_task(task_fn(move |_: ()| async move { i * 2 }));
}
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
})
});
});
// dagrs implementation
group.bench_function("dagrs", |b| {
b.iter(|| {
use async_trait::async_trait;
use dagrs::*;
use std::sync::Arc;
struct Task(i32);
#[async_trait]
impl Action for Task {
async fn run(
&self,
_: &mut InChannels,
_: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let result = self.0 * 2;
Output::Out(Some(Content::new(result)))
}
}
let mut table = NodeTable::new();
let mut graph = Graph::new();
for i in 0..10_000 {
let node = DefaultNode::with_action(format!("task_{}", i), Task(i), &mut table);
graph.add_node(node);
}
graph.set_env(EnvVar::new(table));
graph.start().unwrap();
});
});
group.finish();
}
+165
View File
@@ -0,0 +1,165 @@
//! Linear pipeline comparison: dagx vs dagrs
//!
//! Simple chain of tasks: A → B → C → D → E
use criterion::Criterion;
pub fn bench_linear_pipeline(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("linear_pipeline_5_tasks");
// dagx implementation
group.bench_function("dagx", |b| {
b.iter(|| {
rt.block_on(async {
use dagx::{task_fn, DagRunner};
let dag = DagRunner::new();
let a = dag.add_task(task_fn(|_: ()| async { 1 }));
let b = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(&a);
let c = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(b);
let d = dag
.add_task(task_fn(|x: i32| async move { x - 1 }))
.depends_on(c);
let e = dag
.add_task(task_fn(|x: i32| async move { x * 3 }))
.depends_on(d);
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
dag.get(e).unwrap()
})
});
});
// dagrs implementation
group.bench_function("dagrs", |b| {
b.iter(|| {
use async_trait::async_trait;
use dagrs::*;
use std::sync::Arc;
struct TaskA;
#[async_trait]
impl Action for TaskA {
async fn run(
&self,
_: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
out.broadcast(Content::new(1i32)).await;
Output::Out(Some(Content::new(1i32)))
}
}
struct TaskB;
#[async_trait]
impl Action for TaskB {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result = vals[0] + 1;
out.broadcast(Content::new(result)).await;
Output::Out(Some(Content::new(result)))
}
}
struct TaskC;
#[async_trait]
impl Action for TaskC {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result = vals[0] * 2;
out.broadcast(Content::new(result)).await;
Output::Out(Some(Content::new(result)))
}
}
struct TaskD;
#[async_trait]
impl Action for TaskD {
async fn run(
&self,
input: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result = vals[0] - 1;
out.broadcast(Content::new(result)).await;
Output::Out(Some(Content::new(result)))
}
}
struct TaskE;
#[async_trait]
impl Action for TaskE {
async fn run(
&self,
input: &mut InChannels,
_: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result = vals[0] * 3;
Output::Out(Some(Content::new(result)))
}
}
let mut table = NodeTable::new();
let a = DefaultNode::with_action("a".to_string(), TaskA, &mut table);
let a_id = a.id();
let b = DefaultNode::with_action("b".to_string(), TaskB, &mut table);
let b_id = b.id();
let c = DefaultNode::with_action("c".to_string(), TaskC, &mut table);
let c_id = c.id();
let d = DefaultNode::with_action("d".to_string(), TaskD, &mut table);
let d_id = d.id();
let e = DefaultNode::with_action("e".to_string(), TaskE, &mut table);
let e_id = e.id();
let mut graph = Graph::new();
graph.add_node(a);
graph.add_node(b);
graph.add_node(c);
graph.add_node(d);
graph.add_node(e);
graph.add_edge(a_id, vec![b_id]);
graph.add_edge(b_id, vec![c_id]);
graph.add_edge(c_id, vec![d_id]);
graph.add_edge(d_id, vec![e_id]);
graph.set_env(EnvVar::new(table));
graph.start().unwrap();
});
});
group.finish();
}
+11
View File
@@ -0,0 +1,11 @@
//! Head-to-head comparison benchmarks: dagx vs dagrs
//!
//! These benchmarks compare dagx performance against dagrs (the most popular
//! Rust DAG library) across various workload patterns.
pub mod deep_chain;
pub mod diamond_pattern;
pub mod etl_pipeline;
pub mod large_scale;
pub mod linear_pipeline;
pub mod wide_fanout;
+96
View File
@@ -0,0 +1,96 @@
//! Wide fanout comparison: dagx vs dagrs
//!
//! One source task feeding 100 dependent tasks
use criterion::Criterion;
pub fn bench_wide_fanout(c: &mut Criterion) {
let rt = tokio::runtime::Runtime::new().unwrap();
let mut group = c.benchmark_group("wide_fanout_100_tasks");
// dagx implementation
group.bench_function("dagx", |b| {
b.iter(|| {
rt.block_on(async {
use dagx::{task_fn, DagRunner};
let dag = DagRunner::new();
let source = dag.add_task(task_fn(|_: ()| async { 42 }));
// Create 100 tasks that all depend on source
for i in 0..100 {
dag.add_task(task_fn(move |x: i32| async move { x + i }))
.depends_on(&source);
}
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
})
});
});
// dagrs implementation
group.bench_function("dagrs", |b| {
b.iter(|| {
use async_trait::async_trait;
use dagrs::*;
use std::sync::Arc;
struct Source;
#[async_trait]
impl Action for Source {
async fn run(
&self,
_: &mut InChannels,
out: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
out.broadcast(Content::new(42i32)).await;
Output::Out(Some(Content::new(42i32)))
}
}
struct Consumer(i32);
#[async_trait]
impl Action for Consumer {
async fn run(
&self,
input: &mut InChannels,
_: &mut OutChannels,
_: Arc<EnvVar>,
) -> Output {
let vals: Vec<i32> = input
.map(|c| *c.unwrap().into_inner::<i32>().unwrap())
.await;
let result = vals[0] + self.0;
Output::Out(Some(Content::new(result)))
}
}
let mut table = NodeTable::new();
let source = DefaultNode::with_action("source".to_string(), Source, &mut table);
let source_id = source.id();
let mut graph = Graph::new();
graph.add_node(source);
let mut consumer_ids = Vec::new();
for i in 0..100 {
let consumer =
DefaultNode::with_action(format!("consumer_{}", i), Consumer(i), &mut table);
consumer_ids.push(consumer.id());
graph.add_node(consumer);
}
graph.add_edge(source_id, consumer_ids);
graph.set_env(EnvVar::new(table));
graph.start().unwrap();
});
});
group.finish();
}