feat: dagrunner, initial version
This commit is contained in:
122 files changed
+23101
No files matched your search
@@ -0,0 +1,30 @@
|
||||
//! DAG creation benchmarks
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{DagRunner, Task};
|
||||
use std::hint::black_box;
|
||||
|
||||
// Simple task for benchmarking
|
||||
struct Value(i32);
|
||||
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bench_dag_creation(c: &mut Criterion) {
|
||||
c.bench_function("create_empty_dag", |b| {
|
||||
b.iter(|| black_box(DagRunner::new()));
|
||||
});
|
||||
|
||||
c.bench_function("add_100_tasks", |b| {
|
||||
b.iter(|| {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..100 {
|
||||
black_box(dag.add_task(Value(i)));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//! Basic DAG execution benchmarks
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner, Task};
|
||||
|
||||
pub fn bench_dag_execution(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
c.bench_function("execute_10_independent_tasks", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..10 {
|
||||
dag.add_task(task_fn(move |_: ()| async move { i }));
|
||||
}
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("execute_100_independent_tasks", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..100 {
|
||||
dag.add_task(task_fn(move |_: ()| async move { i }));
|
||||
}
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
c.bench_function("execute_deep_chain_20", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Build a manual chain (API doesn't support loop-based chaining due to type system)
|
||||
struct Value(i32);
|
||||
#[dagx::task]
|
||||
impl Value {
|
||||
async fn run(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
let t0 = dag.add_task(Value(0));
|
||||
let t1 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(&t0);
|
||||
let t2 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t1);
|
||||
let t3 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t2);
|
||||
let t4 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t3);
|
||||
let t5 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t4);
|
||||
let t6 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t5);
|
||||
let t7 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t6);
|
||||
let t8 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t7);
|
||||
let t9 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t8);
|
||||
let t10 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t9);
|
||||
let t11 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t10);
|
||||
let t12 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t11);
|
||||
let t13 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t12);
|
||||
let t14 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t13);
|
||||
let t15 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t14);
|
||||
let t16 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t15);
|
||||
let t17 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t16);
|
||||
let t18 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t17);
|
||||
let _t19 = dag
|
||||
.add_task(task_fn(|x: i32| async move { x + 1 }))
|
||||
.depends_on(t18);
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Basic DAG benchmarks - creation, execution, scaling
|
||||
|
||||
pub mod creation;
|
||||
pub mod execution;
|
||||
pub mod scaling;
|
||||
@@ -0,0 +1,32 @@
|
||||
//! DAG scaling benchmarks - how performance scales with task count
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{task_fn, DagRunner};
|
||||
|
||||
pub fn bench_dag_scaling(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("scaling");
|
||||
|
||||
// Give extra time for 1000-task benchmark (needs ~5.8s, set to 8s for headroom)
|
||||
group.measurement_time(std::time::Duration::from_secs(8));
|
||||
|
||||
for size in [10, 50, 100, 500, 1000].iter() {
|
||||
group.bench_with_input(BenchmarkId::from_parameter(size), size, |b, &size| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..size {
|
||||
dag.add_task(task_fn(move |_: ()| async move { i }));
|
||||
}
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! dagx benchmark suite
|
||||
//!
|
||||
//! Organized into logical modules:
|
||||
//! - basic/: DAG creation, execution, and scaling benchmarks
|
||||
//! - patterns/: Common DAG patterns (fanout, diamond, 10k breakdown)
|
||||
//! - realistic/: Real-world workload simulations (ETL, config broadcast, etc.)
|
||||
//! - comparisons/: Head-to-head benchmarks against dagrs
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
mod basic;
|
||||
mod comparisons;
|
||||
mod patterns;
|
||||
mod realistic;
|
||||
|
||||
// Configure criterion with better defaults
|
||||
fn configure_criterion() -> Criterion {
|
||||
Criterion::default()
|
||||
.configure_from_args()
|
||||
.noise_threshold(0.05) // 5% noise threshold for detecting changes
|
||||
.significance_level(0.05) // 95% confidence interval
|
||||
.warm_up_time(std::time::Duration::from_secs(3))
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = creation_benches;
|
||||
config = configure_criterion();
|
||||
targets = basic::creation::bench_dag_creation
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = execution_benches;
|
||||
config = configure_criterion();
|
||||
targets = basic::execution::bench_dag_execution
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = scaling_benches;
|
||||
config = configure_criterion();
|
||||
targets = basic::scaling::bench_dag_scaling
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = pattern_benches;
|
||||
config = configure_criterion();
|
||||
targets =
|
||||
patterns::fanout::bench_fanout,
|
||||
patterns::diamond::bench_diamond,
|
||||
patterns::breakdown::bench_10k_breakdown
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = realistic_benches;
|
||||
config = configure_criterion();
|
||||
targets =
|
||||
realistic::etl::bench_etl_pipeline,
|
||||
realistic::config_broadcast::bench_config_broadcast,
|
||||
realistic::memory_efficiency::bench_memory_efficiency,
|
||||
realistic::mixed::bench_mixed_patterns
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = comparison_benches;
|
||||
config = configure_criterion();
|
||||
targets =
|
||||
comparisons::linear_pipeline::bench_linear_pipeline,
|
||||
comparisons::wide_fanout::bench_wide_fanout,
|
||||
comparisons::diamond_pattern::bench_diamond_pattern,
|
||||
comparisons::deep_chain::bench_deep_chain,
|
||||
comparisons::etl_pipeline::bench_etl_pipeline,
|
||||
comparisons::large_scale::bench_large_scale
|
||||
}
|
||||
|
||||
criterion_main!(
|
||||
creation_benches,
|
||||
execution_benches,
|
||||
scaling_benches,
|
||||
pattern_benches,
|
||||
realistic_benches,
|
||||
comparison_benches
|
||||
);
|
||||
@@ -0,0 +1,58 @@
|
||||
//! 10,000 task breakdown benchmarks - construction vs execution
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner};
|
||||
|
||||
pub fn bench_10k_breakdown(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("10k_tasks");
|
||||
group.measurement_time(std::time::Duration::from_secs(10));
|
||||
|
||||
// a) Adding 10k tasks (construction only)
|
||||
group.bench_function("construction", |b| {
|
||||
b.iter(|| {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn(move |_: ()| async move { i }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// b) Full lifecycle: add 10k tasks + run them
|
||||
group.bench_function("full_execution", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn(move |_: ()| async move { i }));
|
||||
}
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// c) Execution only (pre-built DAG, just run)
|
||||
group.bench_function("execution_only", |b| {
|
||||
// Pre-build the DAG outside the measurement
|
||||
let dag = DagRunner::new();
|
||||
for i in 0..10_000 {
|
||||
dag.add_task(task_fn(move |_: ()| async move { i }));
|
||||
}
|
||||
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! Diamond pattern benchmarks (fan-out then fan-in)
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner};
|
||||
|
||||
pub fn bench_diamond(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
c.bench_function("diamond_pattern_50", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
for i in 0..50 {
|
||||
let source = dag.add_task(task_fn(move |_: ()| async move { i }));
|
||||
|
||||
let left = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 2 }))
|
||||
.depends_on(&source);
|
||||
let right = dag
|
||||
.add_task(task_fn(|x: i32| async move { x * 3 }))
|
||||
.depends_on(&source);
|
||||
|
||||
dag.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
|
||||
.depends_on((&left, &right));
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Fan-out pattern benchmarks (1 → N dependencies)
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner};
|
||||
|
||||
pub fn bench_fanout(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
// Fan-out pattern (1 → n) with simple i32
|
||||
c.bench_function("fanout_1_to_100_i32", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let source = dag.add_task(task_fn(|_: ()| async { 42 }));
|
||||
|
||||
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();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Fan-out pattern with Vec (automatic Arc wrapping)
|
||||
c.bench_function("fanout_1_to_100_vec", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
let source = dag.add_task(task_fn(|_: ()| async {
|
||||
// Create 1000 strings - framework wraps in Arc automatically
|
||||
(0..1000).map(|i| format!("Item {}", i)).collect::<Vec<_>>()
|
||||
}));
|
||||
|
||||
for i in 0..100 {
|
||||
dag.add_task(task_fn(move |data: Vec<String>| async move {
|
||||
// Each consumer gets data extracted from Arc
|
||||
data.iter().filter(|s| s.contains(&i.to_string())).count()
|
||||
}))
|
||||
.depends_on(&source);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Arc overhead benchmark - shows cost on Copy types
|
||||
// For Copy types like usize, Arc adds overhead (atomic refcounting + indirection)
|
||||
// For heap types like Vec, Arc overhead is negligible compared to clone cost
|
||||
c.bench_function("arc_overhead_copy_type", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Source produces a simple Copy type (usize)
|
||||
// Framework wraps in Arc<usize> internally
|
||||
let source = dag.add_task(task_fn(|_: ()| async { 42usize }));
|
||||
|
||||
// 100 dependents - Arc overhead visible on Copy types
|
||||
for i in 0..100 {
|
||||
dag.add_task(task_fn(move |x: usize| async move { x + i }))
|
||||
.depends_on(&source);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! DAG pattern benchmarks
|
||||
|
||||
pub mod breakdown;
|
||||
pub mod diamond;
|
||||
pub mod fanout;
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Configuration broadcast pattern benchmark - one config shared across many workers
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use std::collections::HashMap;
|
||||
use std::hint::black_box;
|
||||
|
||||
pub fn bench_config_broadcast(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("config_broadcast");
|
||||
|
||||
for worker_count in [10, 50, 100, 200].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("config", worker_count),
|
||||
worker_count,
|
||||
|b, &workers| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Configuration source - framework wraps HashMap in Arc automatically
|
||||
let config = dag.add_task(task_fn(|_: ()| async {
|
||||
HashMap::from([
|
||||
("max_connections".to_string(), "100".to_string()),
|
||||
("timeout_ms".to_string(), "5000".to_string()),
|
||||
("batch_size".to_string(), "50".to_string()),
|
||||
("retry_count".to_string(), "3".to_string()),
|
||||
("worker_threads".to_string(), "4".to_string()),
|
||||
("buffer_size".to_string(), "8192".to_string()),
|
||||
("compression".to_string(), "true".to_string()),
|
||||
("log_level".to_string(), "info".to_string()),
|
||||
])
|
||||
}));
|
||||
|
||||
// Workers receive and process based on shared config
|
||||
for worker_id in 0..workers {
|
||||
dag.add_task(task_fn(move |cfg: HashMap<String, String>| async move {
|
||||
// Simulate work based on configuration
|
||||
let batch_size: usize = cfg
|
||||
.get("batch_size")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(10);
|
||||
let timeout: usize = cfg
|
||||
.get("timeout_ms")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1000);
|
||||
|
||||
// Simulate some computation
|
||||
black_box(worker_id * batch_size + timeout / 100)
|
||||
}))
|
||||
.depends_on(&config);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//! ETL (Extract, Transform, Load) pipeline benchmark
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{task_fn, DagRunner};
|
||||
|
||||
pub fn bench_etl_pipeline(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("etl_scaling");
|
||||
|
||||
// ETL pipeline needs more time for larger datasets (10k records needs ~12.3s, set to 15s for headroom)
|
||||
group.measurement_time(std::time::Duration::from_secs(15));
|
||||
|
||||
for record_count in [1_000, 5_000, 10_000].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("etl", record_count),
|
||||
record_count,
|
||||
|b, &count| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Extract: Load JSON-like data (framework wraps in Arc automatically)
|
||||
let extract = dag.add_task(task_fn(move |_: ()| async move {
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
format!(
|
||||
"{{\"id\":{},\"name\":\"User{}\",\"score\":{}}}",
|
||||
i,
|
||||
i,
|
||||
i * 10
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}));
|
||||
|
||||
// Transform: Three parallel transformations
|
||||
|
||||
// T1: Parse and validate
|
||||
let validate = dag
|
||||
.add_task(task_fn(move |data: Vec<String>| async move {
|
||||
data.iter()
|
||||
.filter(|s| {
|
||||
s.contains("\"id\"")
|
||||
&& s.contains("\"name\"")
|
||||
&& s.contains("\"score\"")
|
||||
})
|
||||
.count()
|
||||
}))
|
||||
.depends_on(&extract);
|
||||
|
||||
// T2: Extract scores
|
||||
let scores = dag
|
||||
.add_task(task_fn(move |data: Vec<String>| async move {
|
||||
data.iter()
|
||||
.filter_map(|s| {
|
||||
s.split("\"score\":").nth(1).and_then(|part| {
|
||||
part.trim_end_matches('}').parse::<i32>().ok()
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}))
|
||||
.depends_on(&extract);
|
||||
|
||||
// T3: Extract names
|
||||
let names = dag
|
||||
.add_task(task_fn(move |data: Vec<String>| async move {
|
||||
data.iter()
|
||||
.filter_map(|s| {
|
||||
s.split("\"name\":\"")
|
||||
.nth(1)
|
||||
.and_then(|part| part.split('"').next())
|
||||
.map(String::from)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}))
|
||||
.depends_on(&extract);
|
||||
|
||||
// Load: Aggregate all transformations
|
||||
dag.add_task(task_fn(
|
||||
move |(valid, scores, names): (usize, Vec<i32>, Vec<String>)| async move {
|
||||
let avg_score =
|
||||
scores.iter().sum::<i32>() / scores.len().max(1) as i32;
|
||||
format!(
|
||||
"Processed {} valid records, {} names, avg score: {}",
|
||||
valid,
|
||||
names.len(),
|
||||
avg_score
|
||||
)
|
||||
},
|
||||
))
|
||||
.depends_on((&validate, &scores, &names));
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Memory efficiency benchmark with large data structures
|
||||
|
||||
use criterion::{BenchmarkId, Criterion};
|
||||
use dagx::{task_fn, DagRunner};
|
||||
use std::hint::black_box;
|
||||
|
||||
pub fn bench_memory_efficiency(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let mut group = c.benchmark_group("memory_efficiency");
|
||||
|
||||
// Test with different data sizes (in MB)
|
||||
for mb_size in [1, 5, 10].iter() {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("large_data_20_consumers", format!("{}MB", mb_size)),
|
||||
mb_size,
|
||||
|b, &mb| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
let source = dag.add_task(task_fn(move |_: ()| async move {
|
||||
// Create MB of data - framework wraps in Arc automatically
|
||||
vec![0u8; mb * 1024 * 1024]
|
||||
}));
|
||||
|
||||
// 20 consumers share the data via automatic Arc wrapping
|
||||
for i in 0..20 {
|
||||
dag.add_task(task_fn(move |data: Vec<u8>| async move {
|
||||
// Process a small portion (data extracted from Arc)
|
||||
let sample_sum: u32 = data
|
||||
.iter()
|
||||
.skip(i * 1000)
|
||||
.take(1000)
|
||||
.map(|&b| b as u32)
|
||||
.sum();
|
||||
black_box(sample_sum)
|
||||
}))
|
||||
.depends_on(&source);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Mixed realistic patterns - fan-out, fan-in, and processing
|
||||
|
||||
use criterion::Criterion;
|
||||
use dagx::{task_fn, DagRunner};
|
||||
|
||||
pub fn bench_mixed_patterns(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
c.bench_function("mixed_patterns_realistic", |b| {
|
||||
b.iter(|| {
|
||||
rt.block_on(async {
|
||||
let dag = DagRunner::new();
|
||||
|
||||
// Stage 1: Load data (framework wraps in Arc automatically)
|
||||
let data = dag.add_task(task_fn(|_: ()| async {
|
||||
(0..1000)
|
||||
.map(|i| format!("Record-{:04}", i))
|
||||
.collect::<Vec<_>>()
|
||||
}));
|
||||
|
||||
// Stage 2: Fan-out for parallel analysis
|
||||
let analysis1 = dag
|
||||
.add_task(task_fn(|d: Vec<String>| async move {
|
||||
d.iter().filter(|s| s.contains("00")).count()
|
||||
}))
|
||||
.depends_on(&data);
|
||||
|
||||
let analysis2 = dag
|
||||
.add_task(task_fn(|d: Vec<String>| async move {
|
||||
d.iter().map(|s| s.len()).sum::<usize>()
|
||||
}))
|
||||
.depends_on(&data);
|
||||
|
||||
let analysis3 = dag
|
||||
.add_task(task_fn(|d: Vec<String>| async move { d.len() }))
|
||||
.depends_on(&data);
|
||||
|
||||
// Stage 3: Fan-in to aggregate
|
||||
let summary = dag
|
||||
.add_task(task_fn(|(a1, a2, a3): (usize, usize, usize)| async move {
|
||||
format!("Matches: {}, Total bytes: {}, Count: {}", a1, a2, a3)
|
||||
}))
|
||||
.depends_on((&analysis1, &analysis2, &analysis3));
|
||||
|
||||
// Stage 4: Fan-out final report to multiple destinations
|
||||
for i in 0..3 {
|
||||
dag.add_task(task_fn(move |report: String| async move {
|
||||
format!("Destination {}: {}", i, report)
|
||||
}))
|
||||
.depends_on(summary);
|
||||
}
|
||||
|
||||
dag.run(|fut| {
|
||||
tokio::spawn(fut);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
//! Realistic workload benchmarks
|
||||
|
||||
pub mod config_broadcast;
|
||||
pub mod etl;
|
||||
pub mod memory_efficiency;
|
||||
pub mod mixed;
|
||||
Reference in new issue
Block a user