# Auto-Search Reference

## Profiling Tools

```bash
# asyncio debug mode (detect blocking calls at runtime):
PYTHONASYNCIODEBUG=1 $RUNNER -X dev -m pytest <test_file> -v 2>&1 | tee /tmp/async_debug.log
grep -E "took .* seconds|was never awaited|slow callback" /tmp/async_debug.log

# cProfile for function-level hotspots (still useful for async code):
$RUNNER -m cProfile -s cumtime -m pytest <test_file> -v 2>&1 | head -50

# yappi — per-coroutine wall-clock timing across await points (pip install yappi):
$RUNNER -c "
import yappi, asyncio
yappi.set_clock_type('WALL')
with yappi.run():
    asyncio.run(your_target())
stats = yappi.get_func_stats()
stats.sort('ttot', 'desc')
stats.print_all(columns={0: ('name', 60), 1: ('ncall', 8), 2: ('ttot', 8), 3: ('tsub', 8)})
"
# ttot = total time (including awaited children), tsub = self time.
# High ttot + low tsub = this coroutine awaits something slow.
# High tsub = this coroutine itself is the bottleneck.
```

### Python 3.12+ — Eager Task Factory

Zero-code-change optimization for coroutines that often complete synchronously (cache hits, validation, fast returns):
```python
asyncio.run(main(), loop_factory=lambda: asyncio.new_event_loop().set_task_factory(asyncio.eager_task_factory))
```
Benchmark with and without to measure impact. 2-5x faster for coroutines that rarely actually block.

### uvloop benchmark template

Compare default asyncio loop vs uvloop on your actual workload:
```bash
$RUNNER -c "
import asyncio, time
async def bench():
    # ... your async workload
    pass
start = time.perf_counter()
asyncio.run(bench())
print(f'default: {time.perf_counter() - start:.3f}s')

import uvloop
start = time.perf_counter()
uvloop.run(bench())
print(f'uvloop: {time.perf_counter() - start:.3f}s')
"
```

Adapt the commands for the project's specific setup (virtualenv, PYTHONPATH, working directory, etc.).

## Micro-Benchmark Template

> See `../shared/micro-benchmark.md` for the A/B pattern, runner commands, and keep criteria.

**Async-specific**: Use `asyncio.run(fn())` with `time.perf_counter()` for wall-clock. Include `CONCURRENCY` and `N_OPERATIONS` constants. **Keep threshold**: >20% or >2x at representative concurrency.

## Antipattern Catalog

### HIGH impact

**Sequential awaits** — Total latency = sum of all individual latencies
- Detect: consecutive `await` statements on independent coroutines
- Fix: `asyncio.gather()` or `TaskGroup` (3.11+)
- Typical speedup: 2-10x depending on number of independent operations
- Example: 5 independent API calls of 200ms each: 1000ms -> 200ms

**Await in loop** — N sequential round trips
- Detect: `await` inside `for`/`while` loop body
- Fix: collect coroutines, `gather` with semaphore
- Typical speedup: proportional to number of iterations
- Example: 100 items at 50ms each: 5000ms -> 50ms (with sufficient concurrency)

**Blocking call in async** — Event loop frozen for duration
- Detect: `time.sleep`, `requests.*`, `open()`, `subprocess.*` in `async def`
- Fix: async equivalent or `run_in_executor`/`to_thread`
- Impact: all other coroutines stalled during block

**CPU-bound in event loop** — Event loop starvation
- Detect: asyncio debug mode "took X seconds" + no I/O in the function
- Fix: `asyncio.to_thread()` or `ProcessPoolExecutor`
- Impact: latency spike for all concurrent requests

**@cache on async def** — Correctness bug
- Detect: `@cache`/`@lru_cache` decorator on `async def`
- Fix: async-aware caching (manual dict, aiocache)
- Impact: RuntimeError or wrong results on cache hit

### MEDIUM impact

**Unbounded gather** — Resource exhaustion
- Detect: `gather(*[coro for x in large_iterable])`
- Fix: semaphore-bounded concurrency
- Impact: connection pool overflow, rate limit violations, memory spikes

**Missing connection reuse** — Per-request handshake overhead
- Detect: `async with httpx.AsyncClient()` inside request handler
- Fix: shared client instance with connection pooling
- Typical speedup: 50-200ms per request (TCP+TLS handshake saved)

**Fire-and-forget tasks** — Silent failures, resource leaks
- Detect: `create_task()` without storing reference or using TaskGroup
- Fix: TaskGroup (3.11+) or explicit task tracking
- Impact: lost errors, leaked connections

**Unnecessary serialization** — Broad locks defeating concurrency
- Detect: `async with lock:` around large blocks that could be narrowed
- Fix: minimize critical section, use lock-free alternatives
- Impact: throughput limited to serial execution

### BACKPRESSURE (HIGH — causes memory growth and latency degradation)

**Unbounded asyncio.Queue** — Memory growth, increasing latency
- Detect: `asyncio.Queue()` without `maxsize` or `maxsize=0` (static analyzer catches this)
- Fix: `asyncio.Queue(maxsize=N)` — `put()` blocks producer when full
- Impact: fast producers outpace slow consumers → OOM over time

**Missing drain() after write()** — Unbounded write buffer
- Detect: `writer.write(data)` without `await writer.drain()` (static analyzer catches this)
- Fix: always pair `write()` with `await drain()`
- Impact: memory growth when receiver can't keep up

**Too many tasks instead of worker pool** — Scheduling starvation
- Detect: `create_task()` in a loop without semaphore/queue. 200+ tasks starves monitoring coroutines
- Fix: bounded queue + fixed worker pool (e.g., `Queue(maxsize=1)` + 4 workers)
- Impact: heartbeat/health check latency spikes to seconds

### CANCELLATION SAFETY (HIGH — correctness bugs)

**Swallowed CancelledError** — Task becomes unkillable
- Detect: `except CancelledError: pass` without re-raise (static analyzer catches this)
- Fix: `except CancelledError: await cleanup(); raise`
- Impact: TaskGroup hangs, parent tasks can't cancel children

**Fire-and-forget tasks without reference** — GC destroys mid-execution
- Detect: `create_task(coro())` as standalone expression (static analyzer catches this)
- Fix: `task = create_task(...); task_set.add(task); task.add_done_callback(task_set.discard)`
- Impact: silent task disappearance — classic heisenbug

**shield() without saving inner task** — Inner task lost on cancellation
- Detect: `await shield(create_task(...))` without intermediate variable (static analyzer catches this)
- Fix: `task = create_task(...); result = await shield(task)` + await task in CancelledError handler

**Cleanup code that hangs** — Graceful shutdown stalls
- Detect: `await` in `finally` block without timeout (source reading)
- Fix: wrap cleanup in `async with asyncio.timeout(5): await cleanup()`

### ASYNC GENERATOR LIFECYCLE (MEDIUM — resource leaks)

**Partial iteration without aclosing()** — Connection/file leaks
- Detect: `async for` with `break` where generator isn't wrapped in `aclosing()`
- Fix: `from contextlib import aclosing; async with aclosing(gen()) as agen: async for item in agen: ...`

**yield in finally block of async generator** — RuntimeError
- Detect: `yield` inside `finally` of an async generator (static analysis)
- Fix: only `await` (not `yield`) in finally blocks

### CONNECTION POOLING (MEDIUM — latency under load)

**Default pool limits cause silent queuing**
- Detect: `TCPConnector()` without `limit_per_host` (all concurrent requests to one host share the global limit)
- Fix: `TCPConnector(limit=100, limit_per_host=20)`

**DNS cache TTL too short** — Periodic DNS storms every 10 seconds (default)
- Detect: `TCPConnector()` without `ttl_dns_cache` override
- Fix: `TCPConnector(ttl_dns_cache=300)` for stable environments

**DNS resolution saturates default thread pool**
- Detect: `run_in_executor(None, ...)` + high-concurrency HTTP client sharing same pool
- Fix: separate `ThreadPoolExecutor` for DNS vs user blocking calls

### OPTIMIZATION PATTERNS (not antipatterns — opportunities)

**wait(FIRST_COMPLETED) for fan-out** — Only need fastest result
- Detect: `gather()` where only one result is used (e.g., redundant backends, cache replicas)
- Fix: `done, pending = await asyncio.wait(tasks, return_when=FIRST_COMPLETED); cancel pending`
- Typical improvement: latency = fastest backend, not slowest

**Request coalescing** — Deduplicate concurrent identical requests
- Detect: source reading — multiple concurrent requests for same data (user profiles, config)
- Fix: dict of `asyncio.Future` keyed by request identity; first request executes, others subscribe
- Impact: N concurrent identical queries → 1 query + N-1 fast returns

### LOW impact (only if on confirmed hot path)

**Connection pool sizing** — Suboptimal defaults
- Detect: default pool sizes with higher actual concurrency
- Fix: tune `max_connections`, `max_keepalive_connections`
- Typical improvement: 10-30% throughput at high concurrency

**Task scheduling overhead** — Too many fine-grained tasks
- Detect: creating thousands of very short-lived tasks
- Fix: batch operations, reduce task granularity
- Typical improvement: marginal unless task count is extreme

**Unnecessary async on pure functions** — Coroutine overhead for no benefit
- Detect: `async def` with no await/async-with/async-for (static analyzer catches this)
- Fix: make it a regular `def`
- Impact: eliminates coroutine creation overhead and reduces async "coloring"

**asyncio.sleep(0)** — Fragile scheduling workaround
- Detect: `asyncio.sleep(0)` (static analyzer catches this)
- Fix: restructure to use bounded queue/worker pool pattern

## Concurrency Scaling Tests

Test at geometric concurrency levels to find the **knee of the curve** — where throughput plateaus and latency climbs:

| Concurrency | Purpose |
|-------------|---------|
| 1 | Baseline — should match sync performance |
| 4 | Low — catch basic sequential-await issues |
| 16 | Medium-low — start to see contention |
| 64 | Medium — typical server load |
| 256 | High — expose pool exhaustion, scheduling starvation |

**Finding the knee**: throughput plateaus (< 5% gain from doubling concurrency) while p99 starts climbing (> 20% increase). This is the system's natural operating point.

**Little's Law cross-validation**: `concurrent_tasks ≈ throughput × avg_latency`. If measured values don't match, the benchmark has a measurement error.

An optimization that only helps at high concurrency is still valuable for production servers. Note the concurrency level in results.tsv.

## Benchmark Rigor

- **Warmup**: 5+ warmup rounds before recording (async has more cold-start variance than sync)
- **GC control**: `gc.disable(); gc.collect()` before benchmark, `gc.collect(); gc.enable()` after. GC pauses look like real latency issues.
- **Minimum rounds**: 20+ measurement rounds. For <10% effect sizes, use 50+.
- **Significance**: compute p50, p99 for both before/after. An optimization that improves p50 but degrades p99 is usually a net negative.
- **Coordinated omission**: our benchmarks use closed-loop (wait for response before next request). This hides worst-case latency. Be aware that real-world open-loop traffic will show worse tail latency.
