async-inspect ๐
December 30, 2025 ยท View on GitHub
async-inspect ๐
X-ray vision for async Rust
async-inspect is a debugging tool that visualizes and inspects async state machines in Rust. See exactly what your futures are doing, where they're stuck, and why.
๐ฐ The Problem
Debugging async Rust is frustrating:
#[tokio::test]
async fn test_user_flow() {
let user = fetch_user(123).await; // Where is this stuck?
let posts = fetch_posts(user.id).await; // Or here?
let friends = fetch_friends(user.id).await; // Or here?
// Test hangs... but WHERE? WHY? ๐ฑ
}
What you see in a regular debugger:
Thread blocked in:
tokio::runtime::park
std::sys::unix::thread::Thread::sleep
???
โ Useless! You can't tell:
- Which
.awaitis blocked - What the future is waiting for
- How long it's been waiting
- What state the async state machine is in
Common async debugging nightmares:
- ๐ Tests hang forever (where?)
- ๐ Deadlocks with no stack trace
- โฐ Timeouts that shouldn't happen
- ๐ฒ Flaky tests (race conditions)
- ๐ Performance issues (lock contention? slow I/O?)
Current "solutions":
// Solution 1: Add prints everywhere ๐ญ
async fn fetch_user(id: u64) -> User {
println!("Starting fetch_user");
let result = http_get(url).await;
println!("Finished fetch_user");
result
}
// Solution 2: Use tokio-console (limited visibility)
// Solution 3: Give up and add timeouts everywhere ๐คท
๐ก The Solution
async-inspect gives you complete visibility into async execution:
$ async-inspect run ./my-app
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ async-inspect - Task Inspector โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Task #42: fetch_user_data(user_id=12345) โ
โ Status: BLOCKED (2.3s) โ
โ State: WaitingForPosts โ
โ โ
โ Progress: โโโโโโโโ 2/4 steps โ
โ โ
โ โ
fetch_user() - Completed (145ms) โ
โ โณ fetch_posts() - IN PROGRESS (2.3s) โโโโ STUCK HERE โ
โ โโ> http::get("api.example.com/posts/12345") โ
โ โโ> TCP: ESTABLISHED, waiting for response โ
โ โโ> Timeout in: 27.7s โ
โ โธ๏ธ fetch_friends() - Not started โ
โ โธ๏ธ build_response() - Not started โ
โ โ
โ State Machine Polls: 156 (avg: 14.7ms between polls) โ
โ โ
โ Press 'd' for details | 't' for timeline | 'g' for graph โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Now you know EXACTLY:
- โ
Which step is stuck (
fetch_posts) - โ What it's waiting for (HTTP response)
- โ How long it's been waiting (2.3s)
- โ What will happen next (timeout in 27.7s)
- โ Complete execution history
๐ฏ Why async-inspect?
Motivation
Async Rust is powerful but opaque. When you write:
async fn complex_operation() {
let a = step_a().await;
let b = step_b(a).await;
let c = step_c(b).await;
}
The compiler transforms this into a state machine:
// Simplified - the real thing is more complex
enum ComplexOperationState {
WaitingForStepA { /* ... */ },
WaitingForStepB { a: ResultA, /* ... */ },
WaitingForStepC { a: ResultA, b: ResultB, /* ... */ },
Done,
}
The problem: This state machine is invisible to debuggers!
Traditional debuggers show you:
- โ Stack frames (useless - points to runtime internals)
- โ Variable values (many are "moved" or "uninitialized")
- โ Current line (incorrect - shows scheduler code)
async-inspect understands async state machines and shows you:
- โ Current state name and position
- โ All captured variables and their values
- โ
Which
.awaityou're blocked on - โ Why you're blocked (I/O, lock, sleep, etc.)
- โ Complete execution timeline
- โ Dependencies between tasks
๐ Comparison with Existing Tools
tokio-console
tokio-console is excellent but limited:
$ tokio-console
What tokio-console shows:
Task Duration Polls State
#42 2.3s 156 Running
#43 0.1s 5 Idle
#44 5.2s 892 Running
What it DOESN'T show:
- โ Which
.awaitis blocked - โ Internal state machine state
- โ What the task is waiting for
- โ Variable values
- โ Deadlock detection
- โ Timeline visualization
Comparison Table
| Feature | async-inspect | tokio-console | gdb/lldb | println! |
|---|---|---|---|---|
See current .await | โ | โ | โ | โ ๏ธ Manual |
| State machine state | โ | โ | โ | โ |
| Variable inspection | โ | โ | โ ๏ธ Limited | โ |
| Waiting reason | โ | โ | โ | โ |
| Timeline view | โ | โ ๏ธ Basic | โ | โ |
| Deadlock detection | โ | โ | โ | โ |
| Dependency graph | โ | โ ๏ธ Basic | โ | โ |
| Runtime agnostic | โ | โ Tokio only | โ | โ |
| Zero code changes | โ | โ ๏ธ Requires tracing | โ | โ |
async-inspect is complementary to tokio-console:
- tokio-console: High-level task monitoring
- async-inspect: Deep state machine inspection
Use both together for complete visibility!
Runtime Support
async-inspect works with multiple async runtimes:
- โ
Tokio - Full support with
tokiofeature - โ
async-std - Full support with
async-std-runtimefeature - โ
smol - Full support with
smol-runtimefeature
Example usage with different runtimes:
// Tokio
use async_inspect::runtime::tokio::{spawn_tracked, InspectExt};
#[tokio::main]
async fn main() {
spawn_tracked("my_task", async {
// Your code here
}).await;
let result = fetch_data()
.inspect("fetch_data")
.await;
}
// async-std
use async_inspect::runtime::async_std::{spawn_tracked, InspectExt};
fn main() {
async_std::task::block_on(async {
spawn_tracked("my_task", async {
// Your code here
}).await;
});
}
// smol
use async_inspect::runtime::smol::{spawn_tracked, InspectExt};
fn main() {
smol::block_on(async {
spawn_tracked("my_task", async {
// Your code here
}).await;
});
}
See the examples/ directory for complete working examples.
โจ Features (Planned)
Core Features
- ๐ State Machine Inspection - See current state and variables
- โฑ๏ธ Execution Timeline - Visualize async execution over time
- ๐ฏ Breakpoints - Pause at specific states or
.awaitpoints - ๐ Dependency Tracking - See which tasks are waiting on others
- ๐ Deadlock Detection - Automatically find circular dependencies
- ๐ Performance Analysis - Identify slow operations and contention
- ๐ฎ Interactive Debugging - Step through async state transitions
- ๐ธ Snapshot & Replay - Record execution and replay later
Advanced Features
- ๐ Distributed Tracing - Track async across services
- ๐ฅ Flamegraphs - Visualize where time is spent
- ๐๏ธ Live Inspection - Attach to running processes
- ๐ Export & Share - Save traces for collaboration
- ๐ค CI Integration - Detect hangs in test suites
- ๐จ Custom Views - Plugin system for specialized visualization
๐ง Status
Work in Progress - Early development
Current version: 0.1.0-alpha
๐ Quick Start (Planned API)
Installation
# Not yet published
cargo install async-inspect
# Or build from source
git clone https://github.com/yourusername/async-inspect
cd async-inspect
cargo install --path .
Basic Usage
# Run your app with inspection enabled
async-inspect run ./my-app
# Attach to running process
async-inspect attach --pid 12345
# Run tests with inspection
async-inspect test
# Start web dashboard
async-inspect serve --port 8080
In Code (Optional Instrumentation)
// Add to Cargo.toml
[dependencies]
async-inspect = "0.1"
// Instrument specific functions
#[async_inspect::trace]
async fn fetch_user(id: u64) -> User {
// Automatically instrumented
let profile = fetch_profile(id).await;
let posts = fetch_posts(id).await;
User { profile, posts }
}
// Or use manual inspection points
use async_inspect::prelude::*;
async fn complex_operation() {
inspect_point!("starting");
let data = fetch_data().await;
inspect_point!("data_fetched", data.len());
process(data).await
}
๐ Use Cases
1. Find Where Test is Stuck
#[tokio::test]
async fn test_timeout() {
// This test hangs... but where?
let result = timeout(
Duration::from_secs(30),
long_operation()
).await;
}
With async-inspect:
$ async-inspect test
Found test stuck at:
test_timeout
โโ> long_operation()
โโ> fetch_data().await โโโโ BLOCKED (5m 23s)
โโ> Waiting for: HTTP response
โโ> URL: https://slow-api.example.com/data
โโ> Timeout: None (will wait forever!)
Suggestion: Add timeout to HTTP client
2. Debug Deadlock
async fn deadlock_example() {
let mutex_a = Arc::new(Mutex::new(0));
let mutex_b = Arc::new(Mutex::new(0));
// Task 1: locks A then B
tokio::spawn(async move {
let _a = mutex_a.lock().await;
tokio::time::sleep(Duration::from_millis(10)).await;
let _b = mutex_b.lock().await; // DEADLOCK!
});
// Task 2: locks B then A
tokio::spawn(async move {
let _b = mutex_b.lock().await;
tokio::time::sleep(Duration::from_millis(10)).await;
let _a = mutex_a.lock().await; // DEADLOCK!
});
}
With async-inspect:
๐ DEADLOCK DETECTED!
Task #42: waiting for Mutex<i32> @ 0x7f8a9c0
โโ> Held by: Task #89
Task #89: waiting for Mutex<i32> @ 0x7f8a9d0
โโ> Held by: Task #42
Circular dependency:
Task #42 โ Mutex A โ Task #89 โ Mutex B โ Task #42
Suggestion:
โข Acquire locks in consistent order (A before B)
โข Use try_lock() with timeout
โข Consider lock-free alternatives
3. Performance Investigation
$ async-inspect profile ./my-app
Performance Report:
Slowest Operations:
1. fetch_posts() - avg 2.3s (called 450x)
โโ> 98% time in: HTTP request
โโ> Suggestion: Add caching or batch requests
2. acquire_lock() - avg 340ms (called 1200x)
โโ> Lock contention: 50 tasks waiting
โโ> Suggestion: Reduce lock scope or use RwLock
Hot Paths:
1. process_request โ fetch_user โ fetch_posts (89% of requests)
2. handle_webhook โ validate โ store (11% of requests)
4. CI/CD Integration
# .github/workflows/test.yml
- name: Run tests with async inspection
run: async-inspect test --timeout 30s --fail-on-hang
- name: Upload trace on failure
if: failure()
uses: actions/upload-artifact@v3
with:
name: async-trace
path: async-inspect-trace.json
๐ ๏ธ How It Works
Compiler Instrumentation
// Your code
async fn fetch_user(id: u64) -> User {
let profile = fetch_profile(id).await;
let posts = fetch_posts(id).await;
User { profile, posts }
}
// With instrumentation (conceptual)
async fn fetch_user(id: u64) -> User {
__async_inspect_enter("fetch_user", id);
__async_inspect_await_start("fetch_profile");
let profile = fetch_profile(id).await;
__async_inspect_await_end("fetch_profile");
__async_inspect_await_start("fetch_posts");
let posts = fetch_posts(id).await;
__async_inspect_await_end("fetch_posts");
let result = User { profile, posts };
__async_inspect_exit("fetch_user", &result);
result
}
Runtime Integration
- Tokio: Hooks into task spawning and polling
- async-std: Custom executor wrapper
- smol: Runtime instrumentation
- Generic: Works with any runtime via proc macros
Zero Overhead When Disabled
# Production build - no overhead
[profile.release]
debug = false
# Debug build - full instrumentation
[profile.dev]
debug = true
๐ Ecosystem Integration
async-inspect works seamlessly with your existing Rust async ecosystem tools:
Prometheus Metrics
Export metrics for monitoring dashboards:
use async_inspect::integrations::prometheus::PrometheusExporter;
let exporter = PrometheusExporter::new()?;
exporter.update();
// In your /metrics endpoint:
let metrics = exporter.gather();
Available metrics:
async_inspect_tasks_total- Total tasks createdasync_inspect_active_tasks- Currently active tasksasync_inspect_blocked_tasks- Tasks waiting on I/Oasync_inspect_task_duration_seconds- Task execution timesasync_inspect_tasks_failed_total- Failed task count
OpenTelemetry Export
Send traces to Jaeger, Zipkin, or any OTLP backend:
use async_inspect::integrations::opentelemetry::OtelExporter;
let exporter = OtelExporter::new("my-service");
exporter.export_tasks();
Tracing Integration
Automatic capture via tracing-subscriber:
use tracing_subscriber::prelude::*;
use async_inspect::integrations::tracing_layer::AsyncInspectLayer;
tracing_subscriber::registry()
.with(AsyncInspectLayer::new())
.init();
Tokio Console Compatibility
Use alongside tokio-console for complementary insights:
# Terminal 1: Run with tokio-console
RUSTFLAGS="--cfg tokio_unstable" cargo run
# Terminal 2: Monitor with tokio-console
tokio-console
# async-inspect exports provide historical analysis
cargo run --example ecosystem_integration
Grafana Dashboards
Import async-inspect metrics into Grafana:
- Configure Prometheus scraping
- Import dashboard template (coming soon)
- Monitor key metrics:
- Task creation rate
- Active/blocked task ratio
- Task duration percentiles
- Error rates
Feature Flags:
[dependencies]
async-inspect = { version = "0.0.1", features = [
"prometheus-export", # Prometheus metrics
"opentelemetry-export", # OTLP traces
"tracing-sub", # Tracing integration
] }
๐ค Export Formats
async-inspect supports multiple industry-standard export formats for visualization and analysis:
JSON Export
Export complete task and event data as structured JSON:
use async_inspect::export::JsonExporter;
// Export to file
JsonExporter::export_to_file(&inspector, "data.json")?;
// Or get as string
let json = JsonExporter::export_to_string(&inspector)?;
Use with: jq, Python pandas, JavaScript tools, data pipelines
CSV Export
Export tasks and events in spreadsheet-compatible format:
use async_inspect::export::CsvExporter;
// Export tasks (id, name, duration, poll_count, etc.)
CsvExporter::export_tasks_to_file(&inspector, "tasks.csv")?;
// Export events (event_id, task_id, timestamp, kind, details)
CsvExporter::export_events_to_file(&inspector, "events.csv")?;
Use with: Excel, Google Sheets, pandas, data analysis
Chrome Trace Event Format
Export for visualization in chrome://tracing or Perfetto UI:
use async_inspect::export::ChromeTraceExporter;
ChromeTraceExporter::export_to_file(&inspector, "trace.json")?;
How to visualize:
-
Chrome DevTools (built-in):
- Open Chrome/Chromium
- Navigate to
chrome://tracing - Click "Load" and select
trace.json - Explore the interactive timeline!
-
Perfetto UI (recommended):
- Go to https://ui.perfetto.dev/
- Click "Open trace file"
- Select
trace.json - Get advanced analysis features:
- Thread-level view
- SQL-based queries
- Statistical summaries
- Custom tracks
What you see:
- Task spawning and completion as events
- Poll operations with precise durations
- Await points showing blocking time
- Complete async execution timeline
- Task relationships and dependencies
Flamegraph Export
Generate flamegraphs for performance analysis:
use async_inspect::export::{FlamegraphExporter, FlamegraphBuilder};
// Basic export (folded stack format)
FlamegraphExporter::export_to_file(&inspector, "flamegraph.txt")?;
// Customized export
FlamegraphBuilder::new()
.include_polls(false) // Exclude poll events
.include_awaits(true) // Include await points
.min_duration_ms(10) // Filter < 10ms operations
.export_to_file(&inspector, "flamegraph_filtered.txt")?;
// Generate SVG directly (requires 'flamegraph' feature)
#[cfg(feature = "flamegraph")]
FlamegraphExporter::generate_svg(&inspector, "flamegraph.svg")?;
How to visualize:
-
Speedscope (easiest, online):
- Go to https://www.speedscope.app/
- Drop
flamegraph.txtonto the page - Explore interactive flamegraph
-
inferno (local SVG generation):
cargo install inferno cat flamegraph.txt | inferno-flamegraph > output.svg open output.svg -
flamegraph.pl (classic):
git clone https://github.com/brendangregg/FlameGraph ./FlameGraph/flamegraph.pl flamegraph.txt > output.svg
What you see:
- Call stacks showing task hierarchies
- Time spent in each async operation
- Hotspots and bottlenecks
- Parent-child task relationships
Comprehensive Example
See examples/export_formats.rs for a complete example:
cargo run --example export_formats
This demonstrates:
- All export formats in one workflow
- Realistic async operations
- Multiple concurrent tasks
- Export to JSON, CSV, Chrome Trace, and Flamegraph
- Usage instructions for each format
Output files:
async_inspect_exports/
โโโ data.json # Complete JSON export
โโโ tasks.csv # Task metrics
โโโ events.csv # Event timeline
โโโ trace.json # Chrome Trace Event Format
โโโ flamegraph.txt # Basic flamegraph
โโโ flamegraph_filtered.txt # Filtered flamegraph
๐บ๏ธ Roadmap
Phase 1: Core Inspector (Current)
- Basic state machine inspection
- Task listing and status
- Simple TUI interface
- Tokio runtime integration
Phase 2: Advanced Debugging
- Variable inspection
- Breakpoints on states
- Step-by-step execution
- Timeline visualization
Phase 3: Analysis Tools
- Deadlock detection
- Performance profiling
- Lock contention analysis
- Flamegraphs
Phase 4: Production Ready
- Web dashboard
- Live process attachment
- Distributed tracing
- CI/CD integration
- Plugin system
Phase 5: Ecosystem
- async-std support
- smol support
- IDE integration (VS Code, IntelliJ)
- Cloud deployment monitoring
๐จ Interface Preview (Planned)
TUI (Terminal)
โโ async-inspect โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ [Tasks] [Timeline] [Graph] [Profile] [?] Help โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Active Tasks: 23 CPU: โโโโโโ 45% โ
โ Blocked: 8 Mem: โโโโโโ 20% โ
โ Running: 15 โ
โ โ
โ Task State Duration Details โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ #42 โณ WaitingPosts 2.3s http::get() โ
โ #43 โ
Done 0.1s Completed โ
โ #44 ๐ Deadlock 5.2s Mutex wait โ
โ #45 ๐ Running 0.03s Computing โ
โ โ
โ [โโ] Navigate [Enter] Details [g] Graph [q] Quit โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Web Dashboard
http://localhost:8080
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ async-inspect [Settings] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ ๐ Overview ๐ Last updated: 2s ago โ
โ โ
โ โ 23 Tasks Active โโโ
โโโโ
โโ Activity โ
โ โธ๏ธ 8 Blocked โ
โ ๐ 1 Deadlock [View Details โ] โ
โ โ
โ ๐ Performance โ
โ โโ Avg Response: 145ms โ
โ โโ 99th percentile: 2.3s โ
โ โโ Slowest: fetch_posts() - 5.2s โ
โ โ
โ [View Timeline] [Export Trace] [Filter...] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ค Contributing
Contributions welcome! This is a challenging project that needs expertise in:
- ๐ฆ Rust compiler internals
- ๐ง Async runtime implementation
- ๐จ UI/UX design
- ๐ Data visualization
- ๐ Debugger implementation
Priority areas:
- State machine introspection
- Runtime hooks (Tokio, async-std)
- TUI implementation
- Deadlock detection algorithms
- Documentation and examples
See CONTRIBUTING.md for details.
๐ Security
async-inspect is designed to be used in development and CI/CD environments for analyzing async code. We take security seriously:
Supply Chain Security
- SLSA Level 3 Provenance: All release binaries include SLSA provenance attestations for verifiable builds
- Dependency Scanning: Automated dependency review on all pull requests
- License Compliance: Only permissive licenses (MIT, Apache-2.0, BSD) - GPL/AGPL excluded
- Security Audits: Continuous monitoring via
cargo-auditandcargo-deny
Build Verification
You can verify the provenance of any release binary:
# Install GitHub CLI attestation verification
gh attestation verify async-inspect-linux-x86_64.tar.gz \
--owner ibrahimcesar
Reporting Security Issues
If you discover a security vulnerability, please email security@ibrahimcesar.com instead of using the issue tracker.
๐ License
MIT OR Apache-2.0
๐ Acknowledgments
Inspired by:
- tokio-console - Task monitoring for Tokio
- async-backtrace - Async stack traces
- tracing - Instrumentation framework
- Chrome DevTools - JavaScript async debugging
- Go's runtime tracer - Goroutine visualization
- rr - Time-travel debugging
async-inspect - Because async shouldn't be a black box ๐
Status: ๐ง Pre-alpha - Architecture design phase
Star โญ this repo to follow development!
๐ฌ Discussion
Have ideas or feedback? Open an issue or discussion!
Key questions we're exploring:
- How to minimize runtime overhead?
- Best UI for visualizing state machines?
- How to support multiple runtimes?
- What features would help you most?