# asyncio Debug Mode

## Enabling Debug Mode

Three ways to enable, from simplest to most configurable:

### 1. Environment variable (recommended for CI/testing)

```bash
PYTHONASYNCIODEBUG=1 $RUNNER script.py
PYTHONASYNCIODEBUG=1 $RUNNER -m pytest tests/ -v
```

### 2. Python dev mode (enables debug mode + extra warnings)

```bash
$RUNNER -X dev script.py
```

This enables asyncio debug mode AND shows ResourceWarning for unclosed transports, RuntimeWarning for unawaited coroutines, etc.

### 3. Programmatic (for fine-grained control)

```python
import asyncio

async def main():
    loop = asyncio.get_running_loop()
    loop.set_debug(True)
    loop.slow_callback_duration = 0.05  # 50ms threshold (default: 100ms)
    # ... rest of your code

asyncio.run(main(), debug=True)  # Alternative: pass debug=True to run()
```

## What Debug Mode Detects

### 1. Slow callbacks (event loop blocking)

When any callback or coroutine step takes longer than `slow_callback_duration` (default 100ms):

```
Executing <Task pending name='Task-1' coro=<process_data() running at app.py:42>>
took 0.534 seconds
```

This means `process_data` blocked the event loop for 534ms. Common causes:
- Sync I/O (file read, HTTP request, DB query)
- CPU-bound computation
- `time.sleep()` instead of `asyncio.sleep()`

### 2. Unawaited coroutines

```
RuntimeWarning: coroutine 'fetch_data' was never awaited
```

This means a coroutine was created but never scheduled:
```python
async def handler():
    fetch_data(url)  # Missing await! Creates coroutine object, discards it
    # Should be: await fetch_data(url)
```

### 3. Unclosed resources

```
ResourceWarning: unclosed <ssl.SSLSocket ...>
ResourceWarning: unclosed transport <_SelectorSocketTransport ...>
```

### 4. Coroutine creation tracebacks

In debug mode, each coroutine stores where it was created. When exceptions occur, you see both the exception traceback AND the creation site. This is invaluable for debugging fire-and-forget tasks.

## Interpreting Slow Callback Output

### Format
```
Executing <HANDLE_TYPE> took X.XXX seconds
```

Handle types:
- `<Task pending name='...' coro=<func() running at file:line>>` — a coroutine step blocked
- `<Handle callback at file:line>` — a plain callback blocked
- `<TimerHandle when=... callback at file:line>` — a scheduled callback blocked

### Finding the blocking call

The "running at file:line" tells you where execution was when the slow step ended, not where the blocking call started. To find the actual blocking call:

1. Read the function at that file:line
2. Look for sync I/O, CPU work, or `time.sleep`
3. Check functions it calls — the blocking call may be in a callee

### Adjusting the threshold

```python
loop.slow_callback_duration = 0.02  # 20ms — catch more issues
```

Lower thresholds produce more output but catch subtler blocking. For initial analysis, use 100ms (default). For thorough analysis, lower to 20-50ms.

## Practical Workflow

### Step 1: Baseline with debug mode

```bash
PYTHONASYNCIODEBUG=1 $RUNNER -m pytest tests/test_api.py -v 2>&1 | tee /tmp/async_debug.log
```

### Step 2: Extract findings

```bash
# Slow callbacks
grep -n "took .* seconds" /tmp/async_debug.log

# Unawaited coroutines
grep -n "was never awaited" /tmp/async_debug.log

# Unclosed resources
grep -n "unclosed\|ResourceWarning" /tmp/async_debug.log
```

### Step 3: Rank by severity

1. Slow callbacks > 500ms — Critical, blocks event loop for half a second
2. Slow callbacks 100-500ms — High, noticeable latency impact
3. Unawaited coroutines — High (correctness bug, work silently lost)
4. Slow callbacks 20-100ms — Medium, matters under high concurrency
5. Unclosed resources — Low (resource leak, but usually not a performance issue)

### Step 4: Fix and re-run

After fixing a blocking call, re-run with debug mode to verify the warning is gone. New slow callbacks may appear as previously-masked issues become visible.

## Combining with pytest-asyncio

```python
# conftest.py
import pytest

@pytest.fixture(autouse=True)
def enable_asyncio_debug(event_loop):
    """Enable debug mode for all async tests."""
    event_loop.set_debug(True)
    event_loop.slow_callback_duration = 0.05
```

This makes every async test report slow callbacks, giving continuous feedback during development.

## Performance Overhead

Debug mode adds overhead:
- ~10-20% slower due to extra bookkeeping
- Creation tracebacks stored for every coroutine (memory)
- Slow callback timing checked after every callback

**Never use in production.** It's a diagnostic tool for development and CI.

## Common False Positives

- **Test fixture setup**: Heavy fixtures (DB seeding, file creation) may trigger slow callback warnings. These don't reflect production behavior.
- **Import-time initialization**: First import of heavy modules triggers a slow callback. Ignore if it only happens once.
- **GC pauses**: Garbage collection can trigger slow callback warnings. Usually <50ms and intermittent.
