chore: add logging under tracing feature

This commit is contained in:
swaits committed 2025-10-07 21:16:04 -07:00
1 parent ac848c6608
commit ec94ade329
11 files changed
+719 -8

No files matched your search

Generated
+120
View File
@@ -448,6 +448,8 @@ dependencies = [
"pollster", "pollster",
"smol", "smol",
"tokio", "tokio",
"tracing",
"tracing-subscriber",
] ]
[[package]] [[package]]
@@ -690,6 +692,12 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.176" version = "0.2.176"
@@ -721,6 +729,15 @@ dependencies = [
"value-bag", "value-bag",
] ]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.7.6"
@@ -747,6 +764,15 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -1052,6 +1078,15 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.6" version = "1.4.6"
@@ -1112,6 +1147,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "tinytemplate" name = "tinytemplate"
version = "1.2.1" version = "1.2.1"
@@ -1148,12 +1192,79 @@ dependencies = [
"syn 2.0.106", "syn 2.0.106",
] ]
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "tracing-core"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.19" version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "value-bag" name = "value-bag"
version = "1.11.1" version = "1.11.1"
@@ -1273,6 +1384,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
+7
View File
@@ -17,6 +17,10 @@ keywords = ["dag", "async", "task", "parallel", "executor"]
categories = ["asynchronous", "concurrency", "data-structures"] categories = ["asynchronous", "concurrency", "data-structures"]
exclude = [".claude/", ".jj/", "dagx-spec.md"] exclude = [".claude/", ".jj/", "dagx-spec.md"]
[features]
default = []
tracing = ["dep:tracing"]
[lints.rust] [lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)', 'cfg(tarpaulin_include)'] } unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)', 'cfg(tarpaulin_include)'] }
@@ -24,6 +28,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)', 'cfg(tarpauli
futures = "0.3" futures = "0.3"
parking_lot = "0.12" parking_lot = "0.12"
dagx-macros = { version = "0.2.2", path = "./dagx-macros" } dagx-macros = { version = "0.2.2", path = "./dagx-macros" }
tracing = { version = "0.1", optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] } tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
@@ -37,6 +42,8 @@ futures-executor = { version = "0.3", features = ["thread-pool"] }
# Comparison benchmarks # Comparison benchmarks
dagrs = { version = "0.5", default-features = false } dagrs = { version = "0.5", default-features = false }
async-trait = "0.1" async-trait = "0.1"
# Tracing support for tests and examples
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
[[bench]] [[bench]]
name = "dagx_bench" name = "dagx_bench"
+55
View File
@@ -20,6 +20,61 @@ A minimal, type-safe, runtime-agnostic async DAG (Directed Acyclic Graph) execut
- **Flexible task patterns**: Supports stateless, read-only, and mutable state tasks - **Flexible task patterns**: Supports stateless, read-only, and mutable state tasks
- **Simple API**: Just `#[task]`, `DagRunner`, `TaskHandle`, and `TaskBuilder` - **Simple API**: Just `#[task]`, `DagRunner`, `TaskHandle`, and `TaskBuilder`
- **Comprehensive error handling**: Result-based errors with actionable messages - **Comprehensive error handling**: Result-based errors with actionable messages
- **Optional tracing**: Zero-cost observability via optional `tracing` feature flag
## Optional Tracing Support
dagx provides optional observability through the `tracing` crate with **zero runtime overhead when disabled**. The tracing instrumentation is conditionally compiled using feature flags, meaning when the feature is off, the logging code doesn't exist in the compiled binary at all.
### Enabling Tracing
```toml
[dependencies]
dagx = { version = "0.2", features = ["tracing"] }
tracing-subscriber = "0.3"
```
### Usage Example
```rust
use tracing_subscriber::{fmt, EnvFilter};
// Initialize tracing subscriber
fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("dagx=info"))
)
.init();
// Your DAG code here - tracing will log execution details
```
### Log Levels
- **INFO**: DAG execution start/completion
- **DEBUG**: Task additions, dependency wiring, layer computation
- **TRACE**: Individual task execution (inline vs spawned), detailed execution flow
- **ERROR**: Panics, cycles, type errors
Run with different levels:
```bash
RUST_LOG=dagx=info cargo run # High-level execution info
RUST_LOG=dagx=debug cargo run # Task and layer details
RUST_LOG=dagx=trace cargo run # All execution details
```
See [`examples/tracing_example.rs`](examples/tracing_example.rs) for a complete working example.
### Zero-Cost Guarantee
When the `tracing` feature is disabled (the default), there is **literally 0ns overhead**:
- Logging code is removed at compile time via `#[cfg(feature = "tracing")]`
- No branches, no function calls, nothing
- The `tracing` crate isn't even linked
- Benchmarks verify identical performance with/without the feature
This follows the same pattern used by Tokio, Hyper, and other performance-critical Rust async libraries.
## Design Philosophy: Primitives as Scheduler ## Design Philosophy: Primitives as Scheduler
+168
View File
@@ -0,0 +1,168 @@
//! # Tracing Example
//!
//! This example demonstrates how to use the optional tracing feature to gain
//! visibility into DAG execution for debugging and performance analysis.
//!
//! ## Running This Example
//!
//! ```bash
//! # With default log level (info)
//! cargo run --example tracing_example --features tracing
//!
//! # With debug level (shows more details)
//! RUST_LOG=dagx=debug cargo run --example tracing_example --features tracing
//!
//! # With trace level (shows all execution details)
//! RUST_LOG=dagx=trace cargo run --example tracing_example --features tracing
//! ```
//!
//! ## What You'll See
//!
//! - Task additions to the DAG
//! - Dependency wiring between tasks
//! - Topological layer computation
//! - Layer-by-layer execution
//! - Inline vs spawned task execution
//! - Completion status
//!
//! ## Log Levels
//!
//! - **INFO**: High-level DAG execution start/completion
//! - **DEBUG**: Task additions, dependency wiring, layer computation
//! - **TRACE**: Individual task execution details (inline vs spawned)
//! - **ERROR**: Errors, panics, cycle detection
//!
use dagx::{task, DagRunner, Task};
use tracing_subscriber::{fmt, EnvFilter};
// Simple source task
struct Value(i32);
#[task]
impl Value {
async fn run(&self) -> i32 {
self.0
}
}
// Computation task
struct Add;
#[task]
impl Add {
async fn run(a: &i32, b: &i32) -> i32 {
a + b
}
}
// Another computation task
struct Multiply;
#[task]
impl Multiply {
async fn run(a: &i32, b: &i32) -> i32 {
a * b
}
}
#[tokio::main]
async fn main() {
// Initialize tracing subscriber with environment filter
// Use RUST_LOG environment variable to control log level
fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("dagx=info")),
)
.init();
println!("=== DAG Execution with Tracing ===\n");
println!("Run with RUST_LOG=dagx=debug or RUST_LOG=dagx=trace for more details\n");
// Example 1: Simple diamond pattern
println!("Example 1: Diamond Pattern");
println!("---------------------------");
{
let dag = DagRunner::new();
// Layer 0: Source
let source = dag.add_task(Value(10));
// Layer 1: Two parallel computations
let left = dag.add_task(Add).depends_on((&source, &source));
let right = dag.add_task(Multiply).depends_on((&source, &source));
// Layer 2: Combine results
let result = dag.add_task(Add).depends_on((&left, &right));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
println!("\nResult: {}", dag.get(result).unwrap());
println!("(Expected: 10+10 = 20, 10*10 = 100, 20+100 = 120)\n");
}
// Example 2: Linear chain (demonstrates inline execution)
println!("\nExample 2: Linear Chain (Inline Execution)");
println!("-------------------------------------------");
{
let dag = DagRunner::new();
// Create a linear chain - each layer has only one task
// This should trigger inline execution optimization
let a = dag.add_task(Value(1));
let b = dag.add_task(Add).depends_on((&a, &a));
let c = dag.add_task(Multiply).depends_on((&b, &b));
let d = dag.add_task(Add).depends_on((&c, &c));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
println!("\nResult: {}", dag.get(d).unwrap());
println!("(Expected: 1+1 = 2, 2*2 = 4, 4+4 = 8)\n");
}
// Example 3: Wide pattern (demonstrates spawned execution)
println!("\nExample 3: Wide Pattern (Spawned Execution)");
println!("--------------------------------------------");
{
let dag = DagRunner::new();
// Layer 0: Multiple sources
let v1 = dag.add_task(Value(1));
let v2 = dag.add_task(Value(2));
let v3 = dag.add_task(Value(3));
let v4 = dag.add_task(Value(4));
// Layer 1: Multiple parallel computations
let sum12 = dag.add_task(Add).depends_on((&v1, &v2));
let sum23 = dag.add_task(Add).depends_on((&v2, &v3));
let sum34 = dag.add_task(Add).depends_on((&v3, &v4));
// Layer 2: Final combination
let sum_all = dag.add_task(Add).depends_on((&sum12, &sum23));
let final_result = dag.add_task(Add).depends_on((&sum_all, &sum34));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
println!("\nResult: {}", dag.get(final_result).unwrap());
println!("(Expected: (1+2)+(2+3) = 8, 8+(3+4) = 15)\n");
}
println!("\n=== Tracing Example Complete ===");
println!("\nTry running with different log levels:");
println!(" RUST_LOG=dagx=info - High-level execution info");
println!(" RUST_LOG=dagx=debug - Task additions and layer details");
println!(" RUST_LOG=dagx=trace - All execution details including inline/spawned");
}
+11
View File
@@ -8,6 +8,9 @@ use crate::runner::DagRunner;
use crate::task::Task; use crate::task::Task;
use crate::types::{NodeId, TaskHandle}; use crate::types::{NodeId, TaskHandle};
#[cfg(feature = "tracing")]
use tracing::debug;
/// Marker trait to identify the unit type at compile time. /// Marker trait to identify the unit type at compile time.
/// ///
/// This is used to enforce that tasks with non-unit inputs MUST call `.depends_on()`, /// This is used to enforce that tasks with non-unit inputs MUST call `.depends_on()`,
@@ -132,6 +135,14 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> {
// Register dependencies in the DAG // Register dependencies in the DAG
let dep_ids = deps.to_node_ids(); let dep_ids = deps.to_node_ids();
#[cfg(feature = "tracing")]
debug!(
task_id = self.id.0,
dependency_ids = ?dep_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
dependency_count = dep_ids.len(),
"wiring task dependencies"
);
let mut edges = self.dag.edges.lock(); let mut edges = self.dag.edges.lock();
let mut dependents = self.dag.dependents.lock(); let mut dependents = self.dag.dependents.lock();
+57
View File
@@ -655,6 +655,63 @@
//! Run `cargo bench` to see comprehensive benchmarks including basic operations, scaling //! Run `cargo bench` to see comprehensive benchmarks including basic operations, scaling
//! characteristics, common patterns (fan-out, diamond), and realistic workloads. //! characteristics, common patterns (fan-out, diamond), and realistic workloads.
//! //!
//! # Optional Tracing Support
//!
//! dagx provides optional observability through the `tracing` crate with **zero runtime overhead
//! when disabled**. The tracing instrumentation is conditionally compiled using feature flags.
//!
//! ## Enabling Tracing
//!
//! Add the `tracing` feature to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! dagx = { version = "0.2", features = ["tracing"] }
//! tracing-subscriber = "0.3"
//! ```
//!
//! Then initialize a tracing subscriber in your application:
//!
//! ```no_run
//! use tracing_subscriber::{fmt, EnvFilter};
//!
//! fmt()
//! .with_env_filter(
//! EnvFilter::try_from_default_env()
//! .unwrap_or_else(|_| EnvFilter::new("dagx=info"))
//! )
//! .init();
//! ```
//!
//! ## Log Levels
//!
//! - **INFO**: DAG execution start/completion
//! - **DEBUG**: Task additions, dependency wiring, layer computation
//! - **TRACE**: Individual task execution (inline vs spawned), detailed execution flow
//! - **ERROR**: Panics, cycles, type errors
//!
//! Control log level with the `RUST_LOG` environment variable:
//!
//! ```bash
//! RUST_LOG=dagx=info cargo run # High-level execution info
//! RUST_LOG=dagx=debug cargo run # Task and layer details
//! RUST_LOG=dagx=trace cargo run # All execution details
//! ```
//!
//! ## Zero-Cost Guarantee
//!
//! When the `tracing` feature is disabled (the default), there is **literally 0ns overhead**:
//! - Logging code is removed at compile time via `#[cfg(feature = "tracing")]`
//! - No branches, no function calls, nothing in the compiled binary
//! - The `tracing` crate isn't even linked
//! - Benchmarks verify identical performance with/without the feature
//!
//! This follows the same zero-cost pattern used by Tokio, Hyper, and other performance-critical
//! Rust async libraries.
//!
//! See [`examples/tracing_example.rs`](https://github.com/swaits/dagx/blob/main/examples/tracing_example.rs)
//! for a complete working example.
//!
#![allow(private_bounds, private_interfaces)] #![allow(private_bounds, private_interfaces)]
+81 -8
View File
@@ -17,6 +17,9 @@ use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use parking_lot::Mutex; use parking_lot::Mutex;
#[cfg(feature = "tracing")]
use tracing::{debug, error, info, trace};
use crate::builder::TaskBuilder; use crate::builder::TaskBuilder;
use crate::error::{DagError, DagResult}; use crate::error::{DagError, DagResult};
use crate::extract::ExtractInput; use crate::extract::ExtractInput;
@@ -197,6 +200,14 @@ impl DagRunner {
Tk::Output: 'static + Clone, Tk::Output: 'static + Clone,
{ {
let id = self.alloc_id(); let id = self.alloc_id();
#[cfg(feature = "tracing")]
debug!(
task_id = id.0,
task_type = std::any::type_name::<Tk>(),
"adding task to DAG"
);
let node = TypedNode::new(id, task); let node = TypedNode::new(id, task);
self.nodes.lock().push(Some(Box::new(node))); self.nodes.lock().push(Some(Box::new(node)));
self.edges.lock().insert(id, Vec::new()); self.edges.lock().insert(id, Vec::new());
@@ -266,10 +277,13 @@ impl DagRunner {
/// This eliminates Mutex contention on outputs and enables true streaming execution. /// This eliminates Mutex contention on outputs and enables true streaming execution.
/// Type erasure occurs only at the ExecutableNode trait boundary - channels are created /// Type erasure occurs only at the ExecutableNode trait boundary - channels are created
/// with full type information and type-erased just before passing to execute_with_channels. /// with full type information and type-erased just before passing to execute_with_channels.
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))]
pub async fn run<S>(&self, spawner: S) -> DagResult<()> pub async fn run<S>(&self, spawner: S) -> DagResult<()>
where where
S: Fn(BoxFuture<'static, ()>), S: Fn(BoxFuture<'static, ()>),
{ {
#[cfg(feature = "tracing")]
info!("starting DAG execution");
// Acquire run lock to prevent concurrent executions // Acquire run lock to prevent concurrent executions
// Use atomic compare_exchange to check if already running // Use atomic compare_exchange to check if already running
if self if self
@@ -277,6 +291,9 @@ impl DagRunner {
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err() .is_err()
{ {
#[cfg(feature = "tracing")]
error!("DAG is already running - concurrent execution not supported");
return Err(DagError::CycleDetected { return Err(DagError::CycleDetected {
nodes: vec![], nodes: vec![],
description: "DAG is already running - concurrent execution not supported" description: "DAG is already running - concurrent execution not supported"
@@ -292,6 +309,13 @@ impl DagRunner {
// Build topological layers // Build topological layers
let layers = self.compute_layers()?; let layers = self.compute_layers()?;
#[cfg(feature = "tracing")]
debug!(
layer_count = layers.len(),
total_tasks = layers.iter().map(|l| l.len()).sum::<usize>(),
"computed topological layers"
);
let edges = self.edges.lock().clone(); let edges = self.edges.lock().clone();
// Create all channels upfront // Create all channels upfront
@@ -337,7 +361,19 @@ impl DagRunner {
)>(); )>();
// Execute layer by layer // Execute layer by layer
#[cfg(feature = "tracing")]
let mut layer_idx = 0;
for layer in layers { for layer in layers {
#[cfg(feature = "tracing")]
{
debug!(
layer = layer_idx,
task_count = layer.len(),
"executing layer"
);
layer_idx += 1;
}
// ============================================================================ // ============================================================================
// PERFORMANCE OPTIMIZATION: Inline execution for single-task layers // PERFORMANCE OPTIMIZATION: Inline execution for single-task layers
// ============================================================================ // ============================================================================
@@ -366,6 +402,13 @@ impl DagRunner {
// ============================================================================ // ============================================================================
if layer.len() == 1 { if layer.len() == 1 {
let node_id = layer[0]; let node_id = layer[0];
#[cfg(feature = "tracing")]
trace!(
task_id = node_id.0,
"executing task inline (single-task layer optimization)"
);
let out_tx = output_tx.clone(); let out_tx = output_tx.clone();
// Take ownership of the node // Take ownership of the node
@@ -392,16 +435,25 @@ impl DagRunner {
.await .await
.unwrap_or_else(|panic_payload| { .unwrap_or_else(|panic_payload| {
// Convert panic to error // Convert panic to error
let panic_message = if let Some(s) = panic_payload.downcast_ref::<&str>()
{
s.to_string()
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
#[cfg(feature = "tracing")]
error!(
task_id = node_id.0,
panic_message = %panic_message,
"task panicked during inline execution"
);
Err(DagError::TaskPanicked { Err(DagError::TaskPanicked {
task_id: node_id.0, task_id: node_id.0,
panic_message: if let Some(s) = panic_payload.downcast_ref::<&str>() panic_message,
{
s.to_string()
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
},
}) })
}); });
@@ -415,6 +467,8 @@ impl DagRunner {
// Spawn each task in this layer // Spawn each task in this layer
for &node_id in &layer { for &node_id in &layer {
#[cfg(feature = "tracing")]
trace!(task_id = node_id.0, "spawning task");
let mut task_tx = tx.clone(); let mut task_tx = tx.clone();
let out_tx = output_tx.clone(); let out_tx = output_tx.clone();
@@ -480,9 +534,14 @@ impl DagRunner {
// Return first error if any // Return first error if any
if let Some(err) = first_error { if let Some(err) = first_error {
#[cfg(feature = "tracing")]
error!(?err, "DAG execution failed");
return Err(err); return Err(err);
} }
#[cfg(feature = "tracing")]
info!("DAG execution completed successfully");
Ok(()) Ok(())
} }
@@ -551,6 +610,9 @@ impl DagRunner {
} }
fn compute_layers(&self) -> DagResult<Vec<Vec<NodeId>>> { fn compute_layers(&self) -> DagResult<Vec<Vec<NodeId>>> {
#[cfg(feature = "tracing")]
debug!("computing topological layers");
let mut in_degree: HashMap<NodeId, usize> = HashMap::new(); let mut in_degree: HashMap<NodeId, usize> = HashMap::new();
let mut layers = Vec::new(); let mut layers = Vec::new();
@@ -620,12 +682,23 @@ impl DagRunner {
unvisited.len(), unvisited.len(),
unvisited unvisited
); );
#[cfg(feature = "tracing")]
error!(
unvisited_count = unvisited.len(),
unvisited_nodes = ?unvisited,
"cycle detected in DAG"
);
return Err(DagError::CycleDetected { return Err(DagError::CycleDetected {
nodes: unvisited, nodes: unvisited,
description, description,
}); });
} }
#[cfg(feature = "tracing")]
debug!(layer_count = layers.len(), "topological layers computed");
Ok(layers) Ok(layers)
} }
} }
+1
View File
@@ -17,4 +17,5 @@ mod performance;
mod runtimes; mod runtimes;
mod task_patterns; mod task_patterns;
mod timing; mod timing;
mod tracing;
mod type_safety; mod type_safety;
+10
View File
@@ -0,0 +1,10 @@
//! Tests for tracing functionality
//!
//! These tests verify that the tracing feature works correctly when enabled
//! and that the library compiles and works without it when disabled.
#[cfg(feature = "tracing")]
mod with_tracing;
#[cfg(not(feature = "tracing"))]
mod without_tracing;
+148
View File
@@ -0,0 +1,148 @@
//! Tests with tracing feature enabled
use dagx::{task, DagRunner, Task};
use tracing_subscriber::{fmt, EnvFilter};
struct Value(i32);
#[task]
impl Value {
async fn run(&self) -> i32 {
self.0
}
}
struct Add;
#[task]
impl Add {
async fn run(a: &i32, b: &i32) -> i32 {
a + b
}
}
struct Multiply;
#[task]
impl Multiply {
async fn run(a: &i32, b: &i32) -> i32 {
a * b
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_tracing_with_subscriber() {
// Initialize tracing subscriber for this test
let _ = fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_test_writer()
.try_init();
let dag = DagRunner::new();
let a = dag.add_task(Value(2));
let b = dag.add_task(Value(3));
let sum = dag.add_task(Add).depends_on((&a, &b));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 5);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_tracing_with_complex_dag() {
// Initialize tracing subscriber
let _ = fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_test_writer()
.try_init();
let dag = DagRunner::new();
// Create a diamond pattern
let source = dag.add_task(Value(10));
let left = dag.add_task(Add).depends_on((&source, &source));
let right = dag.add_task(Multiply).depends_on((&source, &source));
let sink = dag.add_task(Add).depends_on((&left, &right));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
// source = 10
// left = 10 + 10 = 20
// right = 10 * 10 = 100
// sink = 20 + 100 = 120
assert_eq!(dag.get(sink).unwrap(), 120);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_tracing_inline_execution() {
// Initialize tracing subscriber
let _ = fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_test_writer()
.try_init();
let dag = DagRunner::new();
// Create a linear chain (should trigger inline execution)
let a = dag.add_task(Value(1));
let b = dag.add_task(Add).depends_on((&a, &a));
let c = dag.add_task(Multiply).depends_on((&b, &b));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
// a = 1
// b = 1 + 1 = 2
// c = 2 * 2 = 4
assert_eq!(dag.get(c).unwrap(), 4);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_tracing_multiple_layers() {
// Initialize tracing subscriber
let _ = fmt()
.with_env_filter(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("debug")),
)
.with_test_writer()
.try_init();
let dag = DagRunner::new();
// Layer 0
let v1 = dag.add_task(Value(1));
let v2 = dag.add_task(Value(2));
let v3 = dag.add_task(Value(3));
// Layer 1
let sum12 = dag.add_task(Add).depends_on((&v1, &v2));
let mul23 = dag.add_task(Multiply).depends_on((&v2, &v3));
// Layer 2
let final_sum = dag.add_task(Add).depends_on((&sum12, &mul23));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
// v1 = 1, v2 = 2, v3 = 3
// sum12 = 1 + 2 = 3
// mul23 = 2 * 3 = 6
// final_sum = 3 + 6 = 9
assert_eq!(dag.get(final_sum).unwrap(), 9);
}
+61
View File
@@ -0,0 +1,61 @@
//! Tests that the library works correctly without tracing feature
use dagx::{task, DagRunner, Task};
struct Value(i32);
#[task]
impl Value {
async fn run(&self) -> i32 {
self.0
}
}
struct Add;
#[task]
impl Add {
async fn run(a: &i32, b: &i32) -> i32 {
a + b
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_basic_dag_without_tracing() {
let dag = DagRunner::new();
let a = dag.add_task(Value(2));
let b = dag.add_task(Value(3));
let sum = dag.add_task(Add).depends_on((&a, &b));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
assert_eq!(dag.get(sum).unwrap(), 5);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_complex_dag_without_tracing() {
let dag = DagRunner::new();
// Create multiple layers
let v1 = dag.add_task(Value(1));
let v2 = dag.add_task(Value(2));
let v3 = dag.add_task(Value(3));
let v4 = dag.add_task(Value(4));
let sum12 = dag.add_task(Add).depends_on((&v1, &v2));
let sum34 = dag.add_task(Add).depends_on((&v3, &v4));
let final_sum = dag.add_task(Add).depends_on((&sum12, &sum34));
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
assert_eq!(dag.get(final_sum).unwrap(), 10);
}