From 6b79a401cd94e1de5e9922aeb584add54b0ffa04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adri=C3=A0=20Ardila?=
Date: Fri, 23 Jan 2026 14:52:23 +0100
Subject: [PATCH 1/7] wip uvloop compat
---
aiocop/core/callbacks.py | 4 +-
aiocop/core/slow_tasks.py | 165 ++++++++++++++++++-----
aiocop/core/state.py | 17 +++
aiocop/exceptions.py | 7 +-
pyproject.toml | 1 +
tests/test_uvloop.py | 274 ++++++++++++++++++++++++++++++++++++++
uv.lock | 46 +++++++
7 files changed, 478 insertions(+), 36 deletions(-)
create mode 100644 tests/test_uvloop.py
diff --git a/aiocop/core/callbacks.py b/aiocop/core/callbacks.py
index 4ecceb6..dfade47 100644
--- a/aiocop/core/callbacks.py
+++ b/aiocop/core/callbacks.py
@@ -66,7 +66,7 @@ def _capture_context() -> dict[str, Any]:
if provider_context is not None:
context.update(provider_context)
except Exception as e:
- logger.warning("Error in context provider %s: %s", provider.__name__, e)
+ logger.warning("Error in context provider %s: %s", getattr(provider, "__name__", repr(provider)), e)
return context
@@ -81,4 +81,4 @@ def _invoke_slow_task_callbacks(event: SlowTaskEvent) -> None:
try:
callback(event)
except Exception as e:
- logger.warning("Error in slow task callback %s: %s", callback.__name__, e)
+ logger.warning("Error in slow task callback %s: %s", getattr(callback, "__name__", repr(callback)), e)
diff --git a/aiocop/core/slow_tasks.py b/aiocop/core/slow_tasks.py
index a808108..acd7aab 100644
--- a/aiocop/core/slow_tasks.py
+++ b/aiocop/core/slow_tasks.py
@@ -1,10 +1,15 @@
-"""Slow task detection by patching asyncio Handle._run."""
-
+"""Slow task detection by patching event loop scheduling methods.
+This module patches the event loop's call_soon, call_later, call_at, and
+call_soon_threadsafe methods to wrap callbacks with monitoring logic.
+This approach works with both standard asyncio and uvloop, since it patches
+at the loop level rather than relying on asyncio's Handle._run which uvloop
+replaces with its own Cython implementation.
+"""
+import asyncio
import logging
-from asyncio.events import Handle
from collections.abc import Callable
from dataclasses import replace
from time import perf_counter_ns
@@ -27,32 +32,26 @@
logger = logging.getLogger(__name__)
-_detect_slow_tasks_already_applied = False
+_detect_slow_tasks_configured = False
_slow_task_threshold_ns: int = 30 * 1_000_000
SlowTaskCallback = Callable[[SlowTaskEvent], None]
-def _invoke_callbacks_with_context(event: SlowTaskEvent) -> None:
- """
- Capture context and invoke callbacks within the Handle's context.
-
- This function is called via self._context.run() to ensure that context
- providers (like ddtrace span) are captured from the correct contextvars.
- """
- captured_context = _capture_context()
- event_with_context = replace(event, context=captured_context)
- _invoke_slow_task_callbacks(event_with_context)
-
-
def detect_slow_tasks(
threshold_ms: int = 30,
on_slow_task: SlowTaskCallback | None = None,
) -> None:
"""
- Patch the asyncio event loop to detect slow tasks.
+ Configure slow task detection for the asyncio event loop.
+
+ This patches the event loop's scheduling methods (call_soon, call_later, etc.)
+ to measure execution time and capture blocking IO events. Works with both
+ standard asyncio and uvloop.
+
+ The loop is patched either immediately (if called from an async context) or
+ when activate() is called.
- This patches Handle._run to measure execution time and capture blocking IO events.
Callbacks are invoked for every task that has blocking events detected, with
the exceeded_threshold flag indicating if the task exceeded the threshold.
@@ -64,9 +63,9 @@ def detect_slow_tasks(
Should be called after start_blocking_io_detection().
"""
- global _detect_slow_tasks_already_applied, _slow_task_threshold_ns
+ global _detect_slow_tasks_configured, _slow_task_threshold_ns
- if _detect_slow_tasks_already_applied is True:
+ if _detect_slow_tasks_configured is True:
logger.warning("detect_slow_tasks called more than once, ignoring")
return
@@ -80,19 +79,103 @@ def detect_slow_tasks(
if raise_on_violations.get() is True:
logger.info("Exceptions raising on high severity IO blocking tasks enabled")
- old_run = Handle._run # noqa
+ _detect_slow_tasks_configured = True
+
+ # Register hook to patch the loop when activate() is called
+ # This handles the case where detect_slow_tasks() is called outside an async context
+ from aiocop.core.state import register_on_activate_hook
+
+ register_on_activate_hook(_ensure_loop_patched)
+
+ # Also try to patch immediately if we're already in an async context
+ _ensure_loop_patched()
+
+
+def _ensure_loop_patched() -> bool:
+ """
+ Ensure the current event loop is patched for slow task detection.
+
+ Returns True if the loop is patched (either now or previously).
+ Returns False if no running loop or detect_slow_tasks() hasn't been called.
+ """
+ if not _detect_slow_tasks_configured:
+ return False
+
+ try:
+ loop = asyncio.get_running_loop()
+ except RuntimeError:
+ # No running loop yet, will patch later when activate() is called
+ return False
+
+ # Check if this specific loop instance is already patched
+ if getattr(loop, "_aiocop_patched", False):
+ return True
+
+ _patch_loop(loop)
+ loop._aiocop_patched = True # type: ignore[attr-defined]
+ logger.info("Event loop patched for slow task detection (works with asyncio and uvloop)")
+ return True
+
+
+def _patch_loop(loop: asyncio.AbstractEventLoop) -> None:
+ """Patch the event loop's scheduling methods to monitor callback execution."""
+
+ # Patch call_soon
+ original_call_soon = loop.call_soon
- __class__ = Handle # noqa
+ def patched_call_soon(callback: Callable[..., Any], *args: Any, context: Any = None) -> Any:
+ wrapped = _make_monitored_callback(callback, args)
+ return original_call_soon(wrapped, context=context)
- def new_run(self) -> Any:
+ loop.call_soon = patched_call_soon # type: ignore[method-assign]
+
+ # Patch call_later
+ original_call_later = loop.call_later
+
+ def patched_call_later(delay: float, callback: Callable[..., Any], *args: Any, context: Any = None) -> Any:
+ wrapped = _make_monitored_callback(callback, args)
+ return original_call_later(delay, wrapped, context=context)
+
+ loop.call_later = patched_call_later # type: ignore[method-assign]
+
+ # Patch call_at
+ original_call_at = loop.call_at
+
+ def patched_call_at(when: float, callback: Callable[..., Any], *args: Any, context: Any = None) -> Any:
+ wrapped = _make_monitored_callback(callback, args)
+ return original_call_at(when, wrapped, context=context)
+
+ loop.call_at = patched_call_at # type: ignore[method-assign]
+
+ # Patch call_soon_threadsafe
+ original_call_soon_threadsafe = loop.call_soon_threadsafe
+
+ def patched_call_soon_threadsafe(callback: Callable[..., Any], *args: Any, context: Any = None) -> Any:
+ wrapped = _make_monitored_callback(callback, args)
+ return original_call_soon_threadsafe(wrapped, context=context)
+
+ loop.call_soon_threadsafe = patched_call_soon_threadsafe # type: ignore[method-assign]
+
+
+def _make_monitored_callback(callback: Callable[..., Any], args: tuple[Any, ...]) -> Callable[[], Any]:
+ """
+ Create a wrapper that monitors callback execution time and blocking events.
+
+ The wrapper is called with no arguments - the original args are captured
+ in the closure and passed to the callback.
+ """
+
+ def monitored_wrapper() -> Any:
+ # Fast path when monitoring is disabled
if not is_monitoring_active():
- return old_run(self)
+ if args:
+ return callback(*args)
+ return callback()
thread_local = _get_thread_local()
- captured_events: list = []
+ captured_events: list[Any] = []
previous_events = getattr(thread_local, "blocking_events", None)
-
thread_local.blocking_events = captured_events
thread_local.should_raise_for_this_handle = False
@@ -100,7 +183,10 @@ def new_run(self) -> Any:
t0 = perf_counter_ns()
try:
- return_value = old_run(self) # noqa
+ if args:
+ return_value = callback(*args)
+ else:
+ return_value = callback()
finally:
thread_local.blocking_events = previous_events
@@ -129,10 +215,12 @@ def new_run(self) -> Any:
blocking_events=formatted_events,
)
- self._context.run(_invoke_callbacks_with_context, slow_task_event)
+ # We're already running in the callback's context, so contextvars
+ # are accessible. Invoke callbacks directly.
+ _invoke_callbacks_with_context(slow_task_event)
if exceeded_threshold is True:
- self._context.run(_check_and_raise_if_needed, elapsed, formatted_events, should_raise)
+ _check_and_raise_if_needed(elapsed, formatted_events, should_raise)
elif exceeded_threshold is True:
slow_task_event = SlowTaskEvent(
@@ -145,7 +233,7 @@ def new_run(self) -> Any:
blocking_events=[],
)
- self._context.run(_invoke_callbacks_with_context, slow_task_event)
+ _invoke_callbacks_with_context(slow_task_event)
except HighSeverityBlockingIoException:
raise
@@ -154,14 +242,25 @@ def new_run(self) -> Any:
return return_value
- Handle._run = new_run # noqa # type: ignore[method-assign]
- _detect_slow_tasks_already_applied = True
+ return monitored_wrapper
+
+
+def _invoke_callbacks_with_context(event: SlowTaskEvent) -> None:
+ """
+ Capture context and invoke callbacks.
+
+ This is called from within the callback's execution context, so context
+ providers (like ddtrace span) can access the correct contextvars.
+ """
+ captured_context = _capture_context()
+ event_with_context = replace(event, context=captured_context)
+ _invoke_slow_task_callbacks(event_with_context)
def _check_and_raise_if_needed(
elapsed: int, blocking_events: list[BlockingEventInfo] | None, should_raise: bool
) -> None:
- """Check if high severity blocking IO should raise an exception within the Handle's context."""
+ """Check if high severity blocking IO should raise an exception."""
if should_raise is True and _has_exception_been_raised() is False:
io_severity = calculate_io_severity_score(blocking_events)
diff --git a/aiocop/core/state.py b/aiocop/core/state.py
index 40327cc..9c3cdaf 100644
--- a/aiocop/core/state.py
+++ b/aiocop/core/state.py
@@ -1,6 +1,7 @@
"""Shared state management for aiocop monitoring."""
import threading
+from collections.abc import Callable
from contextvars import ContextVar
_thread_local = threading.local()
@@ -11,12 +12,28 @@
_exception_raised = False
+_on_activate_hooks: list[Callable[[], object]] = []
+
+
+def register_on_activate_hook(hook: Callable[[], object]) -> None:
+ """Register a hook to be called when monitoring is activated.
+
+ This allows other modules to perform setup that requires a running event loop
+ without creating circular imports.
+ """
+ if hook not in _on_activate_hooks:
+ _on_activate_hooks.append(hook)
+
def activate() -> None:
"""Activate monitoring. Call this after startup when you want to start detecting blocking IO."""
global _monitoring_active
_monitoring_active = True
+ # Run registered hooks (e.g., to patch the event loop)
+ for hook in _on_activate_hooks:
+ hook()
+
def deactivate() -> None:
"""Deactivate monitoring. Pauses all detection without unregistering hooks."""
diff --git a/aiocop/exceptions.py b/aiocop/exceptions.py
index a48b570..bb87f7a 100644
--- a/aiocop/exceptions.py
+++ b/aiocop/exceptions.py
@@ -1,5 +1,10 @@
"""Exceptions for aiocop."""
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from aiocop.types.events import BlockingEventInfo
+
class HighSeverityBlockingIoException(Exception):
"""Exception raised when high-severity blocking I/O is detected in async context."""
@@ -10,7 +15,7 @@ def __init__(
severity_level: str,
elapsed_ms: float,
threshold_ms: float,
- events: list[dict[str, str]],
+ events: "list[BlockingEventInfo]",
) -> None:
self.severity_score = severity_score
self.severity_level = severity_level
diff --git a/pyproject.toml b/pyproject.toml
index 88dd210..c6f71a1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -45,6 +45,7 @@ test = [
"ruff", # linting
"ty", # checking types
"ipdb", # debugging
+ "uvloop; sys_platform != 'win32'", # uvloop testing (Unix only)
]
docs = [
"mkdocs-material", # documentation
diff --git a/tests/test_uvloop.py b/tests/test_uvloop.py
new file mode 100644
index 0000000..46fcb4a
--- /dev/null
+++ b/tests/test_uvloop.py
@@ -0,0 +1,274 @@
+"""Tests for uvloop compatibility.
+
+These tests verify that aiocop works correctly with uvloop.
+They are skipped on Windows or if uvloop is not installed.
+
+Note: pytest-asyncio manages its own event loops, so we use uvloop.run()
+for tests that need to verify uvloop-specific behavior.
+"""
+
+import asyncio
+import sys
+import tempfile
+import time
+from pathlib import Path
+from typing import Any
+
+import pytest
+
+# Skip entire module on Windows or if uvloop not available
+uvloop = pytest.importorskip("uvloop", reason="uvloop not available")
+
+if sys.platform == "win32":
+ pytest.skip("uvloop not supported on Windows", allow_module_level=True)
+
+import aiocop # noqa: E402
+from aiocop.core import slow_tasks, state # noqa: E402
+from aiocop.types.events import SlowTaskEvent # noqa: E402
+
+# =============================================================================
+# Fixtures
+# =============================================================================
+
+
+@pytest.fixture(autouse=True)
+def reset_aiocop_state():
+ """Reset aiocop state before and after each test.
+
+ This is critical for uvloop tests since we need fresh state
+ for each test to properly test loop patching.
+ """
+ # Reset before test
+ state._monitoring_active = False
+ state._on_activate_hooks.clear()
+ slow_tasks._detect_slow_tasks_configured = False
+ aiocop.clear_slow_task_callbacks()
+ aiocop.clear_context_providers()
+
+ yield
+
+ # Reset after test
+ state._monitoring_active = False
+ state._on_activate_hooks.clear()
+ slow_tasks._detect_slow_tasks_configured = False
+ aiocop.clear_slow_task_callbacks()
+ aiocop.clear_context_providers()
+
+
+# =============================================================================
+# Helper to run async code with uvloop
+# =============================================================================
+
+
+def run_with_uvloop(coro):
+ """Run a coroutine using uvloop.run() to ensure uvloop is used."""
+ return uvloop.run(coro)
+
+
+# =============================================================================
+# Tests using uvloop.run() directly
+# =============================================================================
+
+
+class TestUvloopWithRun:
+ """Tests that use uvloop.run() to ensure uvloop is actually used."""
+
+ def test_uvloop_run_uses_uvloop(self):
+ """Verify that uvloop.run() actually uses uvloop."""
+ async def check_loop():
+ loop = asyncio.get_running_loop()
+ return type(loop).__name__
+
+ loop_type = run_with_uvloop(check_loop())
+ assert loop_type == "Loop", f"Expected uvloop.Loop, got {loop_type}"
+
+ def test_detects_blocking_io_with_uvloop_run(self, reset_aiocop_state):
+ """Test blocking IO detection using uvloop.run()."""
+ captured_events: list[SlowTaskEvent] = []
+
+ def callback(event: SlowTaskEvent) -> None:
+ captured_events.append(event)
+
+ async def main():
+ aiocop.patch_audit_functions()
+ aiocop.start_blocking_io_detection(trace_depth=10)
+ aiocop.detect_slow_tasks(threshold_ms=10)
+ aiocop.register_slow_task_callback(callback)
+ aiocop.activate()
+
+ # Verify we're on uvloop
+ loop = asyncio.get_running_loop()
+ assert type(loop).__name__ == "Loop", "Not running on uvloop!"
+
+ async def task_with_sleep():
+ time.sleep(0.02)
+
+ task = asyncio.create_task(task_with_sleep())
+ await task
+ await asyncio.sleep(0)
+
+ run_with_uvloop(main())
+
+ assert len(captured_events) >= 1
+ event = captured_events[0]
+ assert event.reason == "io_blocking"
+ event_names = [e["event"] for e in event.blocking_events]
+ assert any("time.sleep" in name for name in event_names)
+
+ def test_loop_is_patched_with_uvloop_run(self, reset_aiocop_state):
+ """Test that uvloop loop gets patched correctly."""
+ async def main():
+ aiocop.patch_audit_functions()
+ aiocop.start_blocking_io_detection(trace_depth=10)
+ aiocop.detect_slow_tasks(threshold_ms=10)
+ aiocop.activate()
+
+ loop = asyncio.get_running_loop()
+ assert type(loop).__name__ == "Loop", "Not running on uvloop!"
+ assert hasattr(loop, "_aiocop_patched")
+ assert loop._aiocop_patched is True
+
+ run_with_uvloop(main())
+
+ def test_context_providers_with_uvloop_run(self, reset_aiocop_state):
+ """Test context providers work with uvloop."""
+ captured_events: list[SlowTaskEvent] = []
+
+ def callback(event: SlowTaskEvent) -> None:
+ captured_events.append(event)
+
+ def my_context_provider() -> dict[str, Any]:
+ return {"uvloop_test": True}
+
+ async def main():
+ aiocop.patch_audit_functions()
+ aiocop.start_blocking_io_detection(trace_depth=10)
+ aiocop.detect_slow_tasks(threshold_ms=10)
+ aiocop.register_slow_task_callback(callback)
+ aiocop.register_context_provider(my_context_provider)
+ aiocop.activate()
+
+ async def task_with_io():
+ time.sleep(0.015)
+
+ task = asyncio.create_task(task_with_io())
+ await task
+ await asyncio.sleep(0)
+
+ run_with_uvloop(main())
+
+ assert len(captured_events) >= 1
+ assert captured_events[0].context.get("uvloop_test") is True
+
+ def test_severity_calculation_with_uvloop_run(self, reset_aiocop_state):
+ """Test severity is calculated correctly with uvloop."""
+ captured_events: list[SlowTaskEvent] = []
+
+ def callback(event: SlowTaskEvent) -> None:
+ captured_events.append(event)
+
+ async def main():
+ aiocop.patch_audit_functions()
+ aiocop.start_blocking_io_detection(trace_depth=10)
+ aiocop.detect_slow_tasks(threshold_ms=10)
+ aiocop.register_slow_task_callback(callback)
+ aiocop.activate()
+
+ async def task_with_heavy_io():
+ time.sleep(0.015) # WEIGHT_HEAVY = 50
+
+ task = asyncio.create_task(task_with_heavy_io())
+ await task
+ await asyncio.sleep(0)
+
+ run_with_uvloop(main())
+
+ assert len(captured_events) >= 1
+ event = captured_events[0]
+ assert event.severity_score >= 50
+ assert event.severity_level == "high"
+
+ def test_file_io_detection_with_uvloop_run(self, reset_aiocop_state):
+ """Test file IO is detected with uvloop."""
+ captured_events: list[SlowTaskEvent] = []
+
+ def callback(event: SlowTaskEvent) -> None:
+ captured_events.append(event)
+
+ async def main():
+ aiocop.patch_audit_functions()
+ aiocop.start_blocking_io_detection(trace_depth=10)
+ aiocop.detect_slow_tasks(threshold_ms=10)
+ aiocop.register_slow_task_callback(callback)
+ aiocop.activate()
+
+ async def task_with_file_io():
+ with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
+ f.write("test")
+ temp_path = f.name
+ Path(temp_path).unlink()
+
+ task = asyncio.create_task(task_with_file_io())
+ await task
+ await asyncio.sleep(0)
+
+ run_with_uvloop(main())
+
+ assert len(captured_events) >= 1
+ event = captured_events[0]
+ assert event.reason == "io_blocking"
+ event_names = [e["event"] for e in event.blocking_events]
+ assert any("open" in name or "tempfile" in name for name in event_names)
+
+ def test_deactivated_no_detection_with_uvloop_run(self, reset_aiocop_state):
+ """Test no detection when deactivated with uvloop."""
+ captured_events: list[SlowTaskEvent] = []
+
+ def callback(event: SlowTaskEvent) -> None:
+ captured_events.append(event)
+
+ async def main():
+ aiocop.patch_audit_functions()
+ aiocop.start_blocking_io_detection(trace_depth=10)
+ aiocop.detect_slow_tasks(threshold_ms=10)
+ aiocop.register_slow_task_callback(callback)
+ # Note: NOT calling aiocop.activate()
+
+ async def task_with_io():
+ time.sleep(0.015)
+
+ task = asyncio.create_task(task_with_io())
+ await task
+ await asyncio.sleep(0)
+
+ run_with_uvloop(main())
+
+ assert len(captured_events) == 0
+
+ def test_exceeded_threshold_flag_with_uvloop_run(self, reset_aiocop_state):
+ """Test exceeded_threshold flag is set correctly with uvloop."""
+ captured_events: list[SlowTaskEvent] = []
+
+ def callback(event: SlowTaskEvent) -> None:
+ captured_events.append(event)
+
+ async def main():
+ aiocop.patch_audit_functions()
+ aiocop.start_blocking_io_detection(trace_depth=10)
+ aiocop.detect_slow_tasks(threshold_ms=10)
+ aiocop.register_slow_task_callback(callback)
+ aiocop.activate()
+
+ async def slow_task():
+ time.sleep(0.02) # 20ms, threshold is 10ms
+
+ task = asyncio.create_task(slow_task())
+ await task
+ await asyncio.sleep(0)
+
+ run_with_uvloop(main())
+
+ assert len(captured_events) >= 1
+ event = captured_events[0]
+ assert event.exceeded_threshold is True
+ assert event.elapsed_ms >= 10
diff --git a/uv.lock b/uv.lock
index 8e605cf..86c8213 100644
--- a/uv.lock
+++ b/uv.lock
@@ -22,6 +22,7 @@ test = [
{ name = "pytest-asyncio" },
{ name = "ruff" },
{ name = "ty" },
+ { name = "uvloop", marker = "sys_platform != 'win32'" },
]
[package.metadata]
@@ -33,6 +34,7 @@ requires-dist = [
{ name = "pytest-asyncio", marker = "extra == 'test'" },
{ name = "ruff", marker = "extra == 'test'" },
{ name = "ty", marker = "extra == 'test'" },
+ { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'test'" },
]
provides-extras = ["test", "docs"]
@@ -1042,6 +1044,50 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
+[[package]]
+name = "uvloop"
+version = "0.22.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" },
+ { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" },
+ { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
+ { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
+ { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
+ { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
+ { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
+ { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
+ { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
+ { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
+ { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
+ { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
+ { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
+ { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
+ { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
+ { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
+]
+
[[package]]
name = "watchdog"
version = "6.0.0"
From 147910d3601f957c67f0582f2c9b381499d8013f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adri=C3=A0=20Ardila?=
Date: Fri, 23 Jan 2026 14:54:37 +0100
Subject: [PATCH 2/7] benchmarks for uvloop
---
benchmarks/run_benchmark.py | 219 ++++++++++++++++++++++++++++--------
1 file changed, 172 insertions(+), 47 deletions(-)
diff --git a/benchmarks/run_benchmark.py b/benchmarks/run_benchmark.py
index 256d7c5..8ecdd0c 100644
--- a/benchmarks/run_benchmark.py
+++ b/benchmarks/run_benchmark.py
@@ -3,14 +3,18 @@
aiocop Benchmark Script
Measures the overhead of aiocop monitoring on async workloads.
+Supports both standard asyncio and uvloop.
Usage:
python benchmarks/run_benchmark.py
+ python benchmarks/run_benchmark.py --asyncio-only
+ python benchmarks/run_benchmark.py --uvloop-only
Or with uv:
uv run python benchmarks/run_benchmark.py
"""
+import argparse
import asyncio
import gc
import os
@@ -24,55 +28,71 @@
# Add parent directory to path for importing aiocop
sys.path.insert(0, str(Path(__file__).parent.parent))
-import aiocop
-
@dataclass
class BenchmarkResult:
"""Result of a single benchmark run."""
name: str
+ loop_type: str
num_tasks: int
without_aiocop_ms: float
with_aiocop_ms: float
overhead_per_task_us: float # microseconds per task
-def format_results(results: list[BenchmarkResult]) -> str:
- """Format benchmark results."""
- lines = []
- lines.append("")
- lines.append("=" * 70)
- lines.append("aiocop Benchmark Results")
- lines.append("=" * 70)
- lines.append("")
- lines.append("Per-Task Overhead (lower is better):")
- lines.append("")
- lines.append(f" {'Scenario':<40} {'Overhead':<15} {'Impact on 50ms request'}")
- lines.append(f" {'-' * 40} {'-' * 15} {'-' * 22}")
+def format_results(results: list[BenchmarkResult], loop_type: str) -> str:
+ """Format benchmark results for a specific loop type."""
+ filtered = [r for r in results if r.loop_type == loop_type]
+ if not filtered:
+ return ""
- for r in results:
- # Calculate impact on a 50ms request
- impact_percent = (r.overhead_per_task_us / 50000) * 100 # 50ms = 50000us
- lines.append(
- f" {r.name:<40} {r.overhead_per_task_us:>8.1f} us {impact_percent:.3f}%"
- )
+ lines = []
+ lines.append(f"\n{loop_type.upper()} Results:")
+ lines.append("-" * 70)
+ lines.append(f" {'Scenario':<40} {'Overhead':<15} {'Impact on 50ms'}")
+ lines.append(f" {'-' * 40} {'-' * 15} {'-' * 14}")
- lines.append("")
+ for r in filtered:
+ impact_percent = (r.overhead_per_task_us / 50000) * 100
+ lines.append(f" {r.name:<40} {r.overhead_per_task_us:>8.1f} us {impact_percent:.3f}%")
- # Calculate average
- avg_overhead = statistics.mean(r.overhead_per_task_us for r in results)
+ avg_overhead = statistics.mean(r.overhead_per_task_us for r in filtered)
avg_impact = (avg_overhead / 50000) * 100
+ lines.append(f"\n Average: {avg_overhead:.1f} us per task ({avg_impact:.3f}% on 50ms request)")
+
+ return "\n".join(lines)
+
+
+def format_comparison(results: list[BenchmarkResult]) -> str:
+ """Format a comparison between asyncio and uvloop results."""
+ asyncio_results = {r.name: r for r in results if r.loop_type == "asyncio"}
+ uvloop_results = {r.name: r for r in results if r.loop_type == "uvloop"}
+
+ if not asyncio_results or not uvloop_results:
+ return ""
+
+ lines = []
+ lines.append("\n" + "=" * 70)
+ lines.append("COMPARISON: asyncio vs uvloop")
+ lines.append("=" * 70)
+ lines.append(f" {'Scenario':<30} {'asyncio':<12} {'uvloop':<12} {'Difference'}")
+ lines.append(f" {'-' * 30} {'-' * 12} {'-' * 12} {'-' * 12}")
+
+ for name in asyncio_results:
+ if name in uvloop_results:
+ asyncio_us = asyncio_results[name].overhead_per_task_us
+ uvloop_us = uvloop_results[name].overhead_per_task_us
+ diff = uvloop_us - asyncio_us
+ diff_str = f"{diff:+.1f} us" if diff != 0 else "same"
+ lines.append(f" {name:<30} {asyncio_us:>8.1f} us {uvloop_us:>8.1f} us {diff_str}")
+
+ asyncio_avg = statistics.mean(r.overhead_per_task_us for r in asyncio_results.values())
+ uvloop_avg = statistics.mean(r.overhead_per_task_us for r in uvloop_results.values())
+ diff_avg = uvloop_avg - asyncio_avg
- lines.append(f" Average: {avg_overhead:.1f} us per task ({avg_impact:.3f}% on 50ms request)")
- lines.append("")
- lines.append("-" * 70)
- lines.append("")
- lines.append("What this means:")
- lines.append(f" - Each async task adds ~{avg_overhead:.0f} microseconds of overhead")
- lines.append(f" - A typical 50ms HTTP request sees {avg_impact:.2f}% overhead")
- lines.append(f" - A typical 100ms database query sees {avg_impact/2:.2f}% overhead")
lines.append("")
+ lines.append(f" {'Average':<30} {asyncio_avg:>8.1f} us {uvloop_avg:>8.1f} us {diff_avg:+.1f} us")
return "\n".join(lines)
@@ -81,9 +101,11 @@ async def run_scenario(
name: str,
task_fn: Callable[[], asyncio.Future],
num_tasks: int,
+ loop_type: str,
iterations: int = 5,
) -> BenchmarkResult:
"""Run a benchmark scenario with and without aiocop."""
+ import aiocop
async def run_tasks():
tasks = [asyncio.create_task(task_fn()) for _ in range(num_tasks)]
@@ -118,10 +140,11 @@ async def run_tasks():
with_ms = statistics.median(times_with)
overhead_ms = with_ms - without_ms
- overhead_per_task_us = (overhead_ms * 1000) / num_tasks # Convert to microseconds
+ overhead_per_task_us = (overhead_ms * 1000) / num_tasks
return BenchmarkResult(
name=name,
+ loop_type=loop_type,
num_tasks=num_tasks,
without_aiocop_ms=without_ms,
with_aiocop_ms=with_ms,
@@ -167,25 +190,27 @@ async def realistic_http_handler():
await asyncio.sleep(0.001) # 1ms async work
-def noop_callback(event: aiocop.SlowTaskEvent) -> None:
+def noop_callback(event) -> None:
"""No-op callback for benchmarking."""
pass
-async def main():
- print("")
- print("aiocop Performance Benchmark")
- print("=" * 50)
- print("")
- print("Setting up aiocop...")
+def reset_aiocop_state():
+ """Reset aiocop state for fresh benchmark run."""
+ from aiocop.core import slow_tasks, state
- # Setup aiocop with minimal trace depth for better performance
- aiocop.patch_audit_functions()
- aiocop.start_blocking_io_detection(trace_depth=5)
- aiocop.detect_slow_tasks(threshold_ms=1000, on_slow_task=noop_callback)
+ state._monitoring_active = False
+ state._on_activate_hooks.clear()
+ slow_tasks._detect_slow_tasks_configured = False
+
+ import aiocop
+
+ aiocop.clear_slow_task_callbacks()
+ aiocop.clear_context_providers()
- print("Running benchmarks...\n")
+async def run_all_scenarios(loop_type: str) -> list[BenchmarkResult]:
+ """Run all benchmark scenarios."""
results = []
# Scenario 1: Pure async (baseline - no blocking I/O to detect)
@@ -193,6 +218,7 @@ async def main():
name="Pure async (no blocking)",
task_fn=fast_async_task,
num_tasks=10_000,
+ loop_type=loop_type,
)
results.append(result)
print(f" [done] {result.name}")
@@ -202,6 +228,7 @@ async def main():
name="Trivial blocking (getcwd)",
task_fn=task_with_getcwd,
num_tasks=5_000,
+ loop_type=loop_type,
)
results.append(result)
print(f" [done] {result.name}")
@@ -211,6 +238,7 @@ async def main():
name="Light blocking (stat)",
task_fn=task_with_stat,
num_tasks=5_000,
+ loop_type=loop_type,
)
results.append(result)
print(f" [done] {result.name}")
@@ -220,6 +248,7 @@ async def main():
name="Moderate blocking (file read)",
task_fn=task_with_file_read,
num_tasks=2_000,
+ loop_type=loop_type,
)
results.append(result)
print(f" [done] {result.name}")
@@ -229,20 +258,116 @@ async def main():
name="Realistic HTTP handler",
task_fn=realistic_http_handler,
num_tasks=500,
+ loop_type=loop_type,
)
results.append(result)
print(f" [done] {result.name}")
+ return results
+
+
+def setup_aiocop():
+ """Setup aiocop for benchmarking."""
+ import aiocop
+
+ aiocop.patch_audit_functions()
+ aiocop.start_blocking_io_detection(trace_depth=5)
+ aiocop.detect_slow_tasks(threshold_ms=1000, on_slow_task=noop_callback)
+
+
+def run_asyncio_benchmark() -> list[BenchmarkResult]:
+ """Run benchmark with standard asyncio."""
+ reset_aiocop_state()
+ setup_aiocop()
+
+ async def main():
+ return await run_all_scenarios("asyncio")
+
+ return asyncio.run(main())
+
+
+def run_uvloop_benchmark() -> list[BenchmarkResult]:
+ """Run benchmark with uvloop."""
+ try:
+ import uvloop
+ except ImportError:
+ print(" [skip] uvloop not available")
+ return []
+
+ reset_aiocop_state()
+ setup_aiocop()
+
+ async def main():
+ # Verify we're on uvloop
+ loop = asyncio.get_running_loop()
+ assert type(loop).__name__ == "Loop", f"Not running on uvloop: {type(loop)}"
+ return await run_all_scenarios("uvloop")
+
+ return uvloop.run(main())
+
+
+def main():
+ parser = argparse.ArgumentParser(description="aiocop Performance Benchmark")
+ parser.add_argument("--asyncio-only", action="store_true", help="Only run asyncio benchmark")
+ parser.add_argument("--uvloop-only", action="store_true", help="Only run uvloop benchmark")
+ args = parser.parse_args()
+
+ import aiocop
+
+ print("")
+ print("aiocop Performance Benchmark")
+ print("=" * 50)
+ print("")
+
+ results: list[BenchmarkResult] = []
+
+ # Run asyncio benchmark
+ if not args.uvloop_only:
+ print("Running asyncio benchmarks...")
+ results.extend(run_asyncio_benchmark())
+ print("")
+
+ # Run uvloop benchmark
+ if not args.asyncio_only:
+ print("Running uvloop benchmarks...")
+ uvloop_results = run_uvloop_benchmark()
+ results.extend(uvloop_results)
+ print("")
+
# Print results
- print(format_results(results))
+ print("")
+ print("=" * 70)
+ print("aiocop Benchmark Results")
+ print("=" * 70)
+
+ if not args.uvloop_only:
+ print(format_results(results, "asyncio"))
+
+ if not args.asyncio_only and any(r.loop_type == "uvloop" for r in results):
+ print(format_results(results, "uvloop"))
- # Print system info
+ # Print comparison if both were run
+ if not args.asyncio_only and not args.uvloop_only:
+ print(format_comparison(results))
+
+ print("")
+ print("-" * 70)
+ print("")
print("System Info:")
print(f" Python: {sys.version.split()[0]}")
print(f" Platform: {sys.platform}")
print(f" aiocop: {aiocop.__version__}")
+
+ # Check uvloop version if available
+ try:
+ import uvloop
+
+ print(f" uvloop: {uvloop.__version__}")
+ except ImportError:
+ pass
+
print("")
if __name__ == "__main__":
- asyncio.run(main())
+ main()
From 77683b5068a9b09ac2f7ebcb99cf8b7198e63103 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adri=C3=A0=20Ardila?=
Date: Fri, 23 Jan 2026 15:00:51 +0100
Subject: [PATCH 3/7] cleanup
---
aiocop/core/slow_tasks.py | 21 +--------------------
aiocop/core/state.py | 7 +------
2 files changed, 2 insertions(+), 26 deletions(-)
diff --git a/aiocop/core/slow_tasks.py b/aiocop/core/slow_tasks.py
index acd7aab..7bb5aa1 100644
--- a/aiocop/core/slow_tasks.py
+++ b/aiocop/core/slow_tasks.py
@@ -25,6 +25,7 @@
_reset_exception_flag,
is_monitoring_active,
raise_on_violations,
+ register_on_activate_hook,
)
from aiocop.exceptions import HighSeverityBlockingIoException
from aiocop.types.events import BlockingEventInfo, SlowTaskEvent
@@ -81,22 +82,14 @@ def detect_slow_tasks(
_detect_slow_tasks_configured = True
- # Register hook to patch the loop when activate() is called
- # This handles the case where detect_slow_tasks() is called outside an async context
- from aiocop.core.state import register_on_activate_hook
-
register_on_activate_hook(_ensure_loop_patched)
- # Also try to patch immediately if we're already in an async context
_ensure_loop_patched()
def _ensure_loop_patched() -> bool:
"""
Ensure the current event loop is patched for slow task detection.
-
- Returns True if the loop is patched (either now or previously).
- Returns False if no running loop or detect_slow_tasks() hasn't been called.
"""
if not _detect_slow_tasks_configured:
return False
@@ -104,10 +97,8 @@ def _ensure_loop_patched() -> bool:
try:
loop = asyncio.get_running_loop()
except RuntimeError:
- # No running loop yet, will patch later when activate() is called
return False
- # Check if this specific loop instance is already patched
if getattr(loop, "_aiocop_patched", False):
return True
@@ -120,7 +111,6 @@ def _ensure_loop_patched() -> bool:
def _patch_loop(loop: asyncio.AbstractEventLoop) -> None:
"""Patch the event loop's scheduling methods to monitor callback execution."""
- # Patch call_soon
original_call_soon = loop.call_soon
def patched_call_soon(callback: Callable[..., Any], *args: Any, context: Any = None) -> Any:
@@ -129,7 +119,6 @@ def patched_call_soon(callback: Callable[..., Any], *args: Any, context: Any = N
loop.call_soon = patched_call_soon # type: ignore[method-assign]
- # Patch call_later
original_call_later = loop.call_later
def patched_call_later(delay: float, callback: Callable[..., Any], *args: Any, context: Any = None) -> Any:
@@ -138,7 +127,6 @@ def patched_call_later(delay: float, callback: Callable[..., Any], *args: Any, c
loop.call_later = patched_call_later # type: ignore[method-assign]
- # Patch call_at
original_call_at = loop.call_at
def patched_call_at(when: float, callback: Callable[..., Any], *args: Any, context: Any = None) -> Any:
@@ -147,7 +135,6 @@ def patched_call_at(when: float, callback: Callable[..., Any], *args: Any, conte
loop.call_at = patched_call_at # type: ignore[method-assign]
- # Patch call_soon_threadsafe
original_call_soon_threadsafe = loop.call_soon_threadsafe
def patched_call_soon_threadsafe(callback: Callable[..., Any], *args: Any, context: Any = None) -> Any:
@@ -166,7 +153,6 @@ def _make_monitored_callback(callback: Callable[..., Any], args: tuple[Any, ...]
"""
def monitored_wrapper() -> Any:
- # Fast path when monitoring is disabled
if not is_monitoring_active():
if args:
return callback(*args)
@@ -215,8 +201,6 @@ def monitored_wrapper() -> Any:
blocking_events=formatted_events,
)
- # We're already running in the callback's context, so contextvars
- # are accessible. Invoke callbacks directly.
_invoke_callbacks_with_context(slow_task_event)
if exceeded_threshold is True:
@@ -248,9 +232,6 @@ def monitored_wrapper() -> Any:
def _invoke_callbacks_with_context(event: SlowTaskEvent) -> None:
"""
Capture context and invoke callbacks.
-
- This is called from within the callback's execution context, so context
- providers (like ddtrace span) can access the correct contextvars.
"""
captured_context = _capture_context()
event_with_context = replace(event, context=captured_context)
diff --git a/aiocop/core/state.py b/aiocop/core/state.py
index 9c3cdaf..04cf62d 100644
--- a/aiocop/core/state.py
+++ b/aiocop/core/state.py
@@ -16,11 +16,7 @@
def register_on_activate_hook(hook: Callable[[], object]) -> None:
- """Register a hook to be called when monitoring is activated.
-
- This allows other modules to perform setup that requires a running event loop
- without creating circular imports.
- """
+ """Register a hook to be called when monitoring is activated."""
if hook not in _on_activate_hooks:
_on_activate_hooks.append(hook)
@@ -30,7 +26,6 @@ def activate() -> None:
global _monitoring_active
_monitoring_active = True
- # Run registered hooks (e.g., to patch the event loop)
for hook in _on_activate_hooks:
hook()
From 79daf7ffca6684256e2417074c9a4bc0f3ef1b3b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adri=C3=A0=20Ardila?=
Date: Fri, 23 Jan 2026 15:07:48 +0100
Subject: [PATCH 4/7] update docs
---
README.md | 4 +++-
docs/guide.md | 6 +++---
docs/index.md | 1 +
docs/installation.md | 1 +
4 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 821405e..d388938 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@
## Features
* **Production-Safe & Low Overhead**: Leverages Python's `sys.audit` hooks for minimal runtime overhead, making it safe for production use
+* **Works with asyncio and uvloop**: Compatible with both standard asyncio and uvloop event loops out of the box
* **Blocking I/O Detection**: Automatically detects blocking I/O calls (file operations, network calls, subprocess, etc.) in your async code
* **Stack Trace Capture**: Captures full stack traces to pinpoint exactly where blocking calls originate
* **Severity Scoring**: Assigns severity scores to blocking events to help prioritize fixes
@@ -30,7 +31,7 @@
-aiocop wraps `asyncio.Handle._run` (the method that executes every task in the event loop) and uses Python's `sys.audit` hooks to detect blocking calls. When your code calls a blocking function like `open()`, the audit event is captured along with the full stack trace—letting you know exactly where the problem is.
+aiocop wraps the event loop's scheduling methods (`call_soon`, `call_later`, etc.) and uses Python's `sys.audit` hooks to detect blocking calls. This approach works with both standard asyncio and uvloop. When your code calls a blocking function like `open()`, the audit event is captured along with the full stack trace—letting you know exactly where the problem is.
## Why aiocop?
@@ -47,6 +48,7 @@ aiocop was built to solve specific production constraints that existing approach
| Production Overhead | Low-Medium | High | Very Low (~13μs/task) |
| Stack Traces | Yes | No (timing only) | Yes |
| Runtime Control | Varies | Flag at startup | Dynamic on/off |
+| uvloop Support | Varies | No | Yes |
## Performance
diff --git a/docs/guide.md b/docs/guide.md
index 9a31b71..fa2aa5f 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -23,14 +23,14 @@ aiocop uses three mechanisms to detect blocking I/O:
2. **Audit Hook Registration** (`start_blocking_io_detection`): Registers a `sys.audit` hook that listens for blocking I/O events and captures stack traces.
-3. **Event Loop Patching** (`detect_slow_tasks`): Patches `asyncio.Handle._run` to measure task execution time and invoke callbacks when blocking is detected.
+3. **Event Loop Patching** (`detect_slow_tasks`): Patches the event loop's scheduling methods (`call_soon`, `call_later`, `call_at`) to measure task execution time and invoke callbacks when blocking is detected. This approach works with both standard asyncio and uvloop.

The diagram above shows the complete flow:
-1. The **Event Loop** schedules a task via `Handle._run`
-2. **aiocop's wrapper** starts a timer and creates an events list
+1. The **Event Loop** schedules a callback via `call_soon` (or similar)
+2. **aiocop's wrapper** intercepts the callback, starts a timer and creates an events list
3. Your **task code** executes
4. When a blocking function (like `open()`) is called, the **Python VM** (for native functions) or **aiocop's wrapper** (for patched functions) emits a `sys.audit` event
5. The **audit hook** captures the event and stack trace, appending it to the events list
diff --git a/docs/index.md b/docs/index.md
index 2026000..2eb96aa 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -45,6 +45,7 @@ aiocop.activate()
## Features
- **Production-Safe**: Minimal runtime overhead using Python's audit hooks
+- **asyncio & uvloop**: Works with both standard asyncio and uvloop out of the box
- **Blocking I/O Detection**: Detects file operations, network calls, subprocess, `time.sleep`, and more
- **Stack Trace Capture**: Full stack traces to pinpoint blocking calls
- **Severity Scoring**: Prioritize fixes based on impact
diff --git a/docs/installation.md b/docs/installation.md
index d7fd216..5833c41 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -4,6 +4,7 @@
- **Python 3.10 or higher**
- No additional dependencies required
+- **uvloop** (optional): aiocop works with both standard asyncio and uvloop out of the box
## Install from PyPI
From abd97b96165f8c377aeb6998a918c03a406a59e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adri=C3=A0=20Ardila?=
Date: Fri, 23 Jan 2026 15:11:18 +0100
Subject: [PATCH 5/7] more docs updates
---
docs/api.md | 2 +-
docs/benchmarks.md | 58 ++++++++++++++++++++++++++++++++++++----------
pyproject.toml | 5 ++--
uv.lock | 2 +-
4 files changed, 51 insertions(+), 16 deletions(-)
diff --git a/docs/api.md b/docs/api.md
index 74b6272..103407d 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -42,7 +42,7 @@ aiocop.detect_slow_tasks(
) -> None
```
-Patches the asyncio event loop to detect slow tasks.
+Patches the event loop to detect slow tasks. Works with both standard asyncio and uvloop.
**Parameters:**
- `threshold_ms`: Threshold in milliseconds. Tasks exceeding this have `exceeded_threshold=True`. Default: 30.
diff --git a/docs/benchmarks.md b/docs/benchmarks.md
index 229117f..c4b31da 100644
--- a/docs/benchmarks.md
+++ b/docs/benchmarks.md
@@ -2,8 +2,12 @@
aiocop is designed to be production-safe with minimal overhead. This page documents performance characteristics and how to run benchmarks yourself.
+aiocop works with both standard asyncio and uvloop, with similar low overhead on both.
+
## Summary
+### asyncio
+
| Scenario | Per-Task Overhead | Impact on 50ms Request |
|----------|-------------------|------------------------|
| Pure async (no blocking I/O) | ~1 us | 0.002% |
@@ -11,7 +15,16 @@ aiocop is designed to be production-safe with minimal overhead. This page docume
| Moderate blocking (file read) | ~12 us | 0.02% |
| Realistic HTTP handler | ~22 us | 0.04% |
-**Bottom line:** aiocop adds ~13 microseconds per task on average. For typical web applications where requests take 10-100ms, this translates to **less than 0.05% overhead**.
+### uvloop
+
+| Scenario | Per-Task Overhead | Impact on 50ms Request |
+|----------|-------------------|------------------------|
+| Pure async (no blocking I/O) | ~3 us | 0.006% |
+| Light blocking (os.stat) | ~29 us | 0.06% |
+| Moderate blocking (file read) | ~17 us | 0.03% |
+| Realistic HTTP handler | ~56 us | 0.11% |
+
+**Bottom line:** aiocop adds ~13 microseconds per task on asyncio and ~27 microseconds on uvloop. For typical web applications where requests take 10-100ms, this translates to **less than 0.1% overhead** on either event loop.
## Understanding the Numbers
@@ -41,11 +54,14 @@ If your application:
Run the included benchmark script:
```bash
-# With uv
+# Run both asyncio and uvloop benchmarks
uv run python benchmarks/run_benchmark.py
-# Or directly
-python benchmarks/run_benchmark.py
+# Run only asyncio benchmarks
+uv run python benchmarks/run_benchmark.py --asyncio-only
+
+# Run only uvloop benchmarks
+uv run python benchmarks/run_benchmark.py --uvloop-only
```
### Sample Output
@@ -55,10 +71,10 @@ python benchmarks/run_benchmark.py
aiocop Benchmark Results
======================================================================
-Per-Task Overhead (lower is better):
-
- Scenario Overhead Impact on 50ms request
- ---------------------------------------- --------------- ----------------------
+ASYNCIO Results:
+----------------------------------------------------------------------
+ Scenario Overhead Impact on 50ms
+ ---------------------------------------- --------------- --------------
Pure async (no blocking) 1.2 us 0.002%
Trivial blocking (getcwd) 15.3 us 0.031%
Light blocking (stat) 13.5 us 0.027%
@@ -67,12 +83,30 @@ Per-Task Overhead (lower is better):
Average: 12.8 us per task (0.026% on 50ms request)
+UVLOOP Results:
----------------------------------------------------------------------
+ Scenario Overhead Impact on 50ms
+ ---------------------------------------- --------------- --------------
+ Pure async (no blocking) 2.7 us 0.005%
+ Trivial blocking (getcwd) 29.5 us 0.059%
+ Light blocking (stat) 28.4 us 0.057%
+ Moderate blocking (file read) 16.8 us 0.034%
+ Realistic HTTP handler 56.4 us 0.113%
-What this means:
- - Each async task adds ~13 microseconds of overhead
- - A typical 50ms HTTP request sees 0.03% overhead
- - A typical 100ms database query sees 0.01% overhead
+ Average: 26.8 us per task (0.054% on 50ms request)
+
+======================================================================
+COMPARISON: asyncio vs uvloop
+======================================================================
+ Scenario asyncio uvloop Difference
+ ------------------------------ ------------ ------------ ------------
+ Pure async (no blocking) 1.2 us 2.7 us +1.5 us
+ Trivial blocking (getcwd) 15.3 us 29.5 us +14.2 us
+ Light blocking (stat) 13.5 us 28.4 us +14.9 us
+ Moderate blocking (file read) 12.3 us 16.8 us +4.5 us
+ Realistic HTTP handler 21.6 us 56.4 us +34.8 us
+
+ Average 12.8 us 26.8 us +14.0 us
```
## Tuning for Performance
diff --git a/pyproject.toml b/pyproject.toml
index c6f71a1..0e334cb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,9 +4,10 @@ build-backend = "hatchling.build"
[project]
name = "aiocop"
-version = "1.0.1"
-description = "Non-intrusive monitoring for Python asyncio. Detects, pinpoints, and logs blocking IO and CPU calls that freeze your event loop."
+version = "1.1.0"
+description = "Non-intrusive monitoring for Python asyncio and uvloop. Detects, pinpoints, and logs blocking IO and CPU calls that freeze your event loop."
readme = "README.md"
+keywords = ["asyncio", "uvloop", "monitoring", "blocking-io", "event-loop", "debugging", "performance"]
authors = [
{name = "Fever Labs, Inc.", email = "engineering@feverup.com"},
{name = "Adrià Ardila", email = "adria.ardila@feverup.com"}
diff --git a/uv.lock b/uv.lock
index 86c8213..f1dbcce 100644
--- a/uv.lock
+++ b/uv.lock
@@ -8,7 +8,7 @@ resolution-markers = [
[[package]]
name = "aiocop"
-version = "1.0.1"
+version = "1.1.0"
source = { editable = "." }
[package.optional-dependencies]
From f96ccc677f96fc0deea0ba40c044a2c4bb867e68 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adri=C3=A0=20Ardila?=
Date: Fri, 23 Jan 2026 15:21:29 +0100
Subject: [PATCH 6/7] update diagram
---
docs/images/explanation_diagram.png | Bin 999911 -> 1866704 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/docs/images/explanation_diagram.png b/docs/images/explanation_diagram.png
index 70a40c7d56b97a378d042abaab59fa62a26d3312..0e35a43136f507c1c5c5968bee3ed546d57415d9 100644
GIT binary patch
literal 1866704
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ⅆw=W6fA(&
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