This page is for contributors and readers of the codebase.
Local development uses Python 3.14.7, pinned in .python-version. Install that
interpreter before running the uv commands below. This development pin does
not change the package's Python 3.10+ support or the multi-version test matrix.
Local builds and build/test CI use Rust nightly-2026-09-25, pinned in
rust-toolchain.toml.
Rustup selects it automatically
inside this repository. LLVM tools remain optional for PGO builds.
Quick Rust check:
cargo checkThe nightly pin supplies the allocator API merged in
rust-lang/rust#156882.
Scheduler batch reuse uses the stabilized Allocator and
Vec::with_capacity_in APIs in every normal build. The
scheduler-batch-cache feature remains as a compatibility flag.
Each embedded runtime caches one exact-layout System allocation
between block_on calls: normally 2 KiB on 64-bit platforms, with a hard 16 KiB
retention ceiling. The cache belongs to the runtime's owner thread and needs no
mutex or reference-counted allocator handles. Simultaneous allocations remain
disjoint, all task values are dropped before storage is recycled, and cached
storage is freed when the runtime is destroyed.
A full-batch drain retains the vector's capacity within each block_on call;
it uses stabilized vector primitives because allocator-aware Vec::drain is
still outside the stabilized subset. Unconsumed elements are dropped on early
exit or unwind. The cache lives outside the shared scheduler state to preserve
that hot structure's layout.
This replaces the rejected shared, size-class recycler experiment. Ready queues,
timers, task futures, transport queues, and stream payload pools keep their
existing allocation strategies. There is no allocator_ext feature gate or
runtime allocator-selection API. Default builds now cache scheduler batches; older benchmark results predate
the wider runtime optimizations. Kani's older compiler uses ordinary Vec for
scheduler proofs; strict-provenance Miri tests exercise the actual allocator.
For an isolated measurement of scheduler entry/exit costs, build
tools/vibeio-check/examples/runtime_turns.rs against both revisions with the
same toolchain, lockfile, profile, and benchmark source, then run:
cargo build --manifest-path tools/vibeio-check/Cargo.toml --example runtime_turns --release --locked --features scheduler-batch-cache
.venv/bin/python scripts/compare_runtime_turns.py \
--baseline /path/to/baseline/runtime_turns \
--candidate tools/vibeio-check/target/release/examples/runtime_turns \
--out target/runtime-turns-comparisonThe runner records binary hashes and randomized paired process order before measurement. This benchmark isolates runtime turns; evaluate application timing and peak RSS separately.
Normal release builds include the cache. Measure application timing and peak RSS for your workload; the compatibility feature does not select another path.
Build the extension and install it into the current environment:
cargo build --release
uv run --with maturin maturin develop --releaseBuild release wheels into dist/wheels:
scripts/build-wheels.shscripts/build-wheels.sh
currently defaults to
CPython 3.10 3.11 3.12 3.13 3.14 3.14t 3.15 3.15t, and uses
uv python install / uv python find to locate interpreters.
just test-vibeio-allocations checks that 1,000 stable completion-buffer owner
handoffs, TCP vectored writes, and embedded scheduler turns each perform zero Rust heap allocations
inside the measured operations. Socket/runtime setup, payload creation, and
completion cancellation are outside these budgets. This is not a claim that
all of rsloop, CPython, or the kernel runs without allocations.
TCP and Unix polling writes pass borrowed IoSlice descriptors directly to
standard-library socket I/O, avoiding intermediate descriptor allocations and
copies. Completion I/O retains the payload owner. IoBuf::STABLE_ON_MOVE is an
unsafe opt-in guarantee that both readable and writable payload addresses survive
owner moves; vectors, strings, boxes, static slices, and eligible cursor wrappers
provide it. Other buffers retain boxed address stability. Cancellation still
retains the owner until the driver acknowledges completion.
The embedded poll_once() services one batch of up to 256 tasks using stack
storage, without creating a root future or waker. Unpolled task references are
released on panic, and task wakeups keep their existing scheduling order. The
allocation check covers idle turns and 16 continuously ready tasks, with runtime
and task creation outside the measurement.
General block_on() turns also reuse their batch allocation across calls, as
described above. Root wakers are cached and deadline-bound parking uses the
driver timeout without allocating a Sleep timer entry.
Optionally build wheels with profile-guided optimization (PGO):
rustup component add llvm-tools-preview
scripts/build-pgo-wheels.shFor each requested Python ABI, the PGO wrapper creates an instrumented wheel, trains it on sustained HTTP, TLS, WebSocket, mixed-stream, bulk-transfer, idle-connection, callback, task, and TCP workloads, merges the resulting LLVM profiles, and builds that ABI's final wheel with its matching profile. Per-ABI training avoids discarding counters when PyO3's generated control flow differs between Python versions or free-threaded builds. The target must be native because the instrumented extension runs during training.
Set RSLOOP_PGO_SCENARIOS to override the comma-separated network scenarios.
The Wheels CI workflow disables PGO by default: tagged releases and ordinary
manual runs use the normal release-wheel builder. To opt in, enable the pgo
checkbox when manually running the workflow. LLVM tools are installed only for
PGO runs; source-distribution and publishing steps are unchanged.
When enabled, PGO is used on every supported platform except Windows ARM64. Rust profile-generation binaries currently crash on that target (rust-lang/rust#156675), so it temporarily falls back to the normal fat-LTO release build.
scripts/test-supported-pythons.sh --debugThe matrix explicitly installs the test dependency group, including the
WebSocket dependency used by the TLS tests. Optional framework scripts in
tests/integration/packages use the separate integration group. Install that
group explicitly when working on integrations; it is intentionally excluded
from the default dev environment because some frameworks have native
dependencies that do not support every Python interpreter.
The default matrix covers CPython 3.10 through 3.15 plus the free-threaded
CPython 3.14 and 3.15 builds (3.14t, 3.15t). Until Python 3.15 is final,
uv resolves 3.15 and 3.15t to
the latest available prerelease standalone build.
Clippy uses a risk-focused policy in Cargo.toml: correctness is denied,
and suspicious, perf, and complexity are warnings. Safety documentation,
pointer alignment, and holding locks or RefCell borrows across await points
are explicitly checked. Blanket style and pedantic checks are disabled;
nursery and restriction are not enabled as groups.
The same policy covers the embedded vibeio runtime, without a blanket Clippy suppression. Exceptions should be scoped to the affected item and explain why the rule does not fit (for example, retaining inline driver storage). Existing vibeio allowances for Rust compiler compatibility are separate from this policy.
CI and the development command treat every enabled warning as an error:
uv run just clippyThe equivalent Cargo command is:
cargo clippy --all-targets --all-features -- -D warningsCI also passes --locked. Use rustfmt for formatting:
cargo fmt --all --checkRun Rust library tests with the repository's Python linking configuration:
uv run python scripts/run_rust_tests.py
uv run python scripts/run_rust_tests.py --all-featuresOn Windows, the runner keeps Cargo artifacts under an interpreter-specific
directory such as target/rust-tests/cp314 or target/rust-tests/cp314t.
This avoids a full relink when switching between regular, free-threaded, or
newer Python environments. The first run for each ABI builds its own cache.
CI runs both configurations. Networking and timers are always compiled; embedded
fs, process, signal, pipe, stdio, splice, and blocking-default
modules are opt-in Cargo features. They do not change the default wheel build.
To check an individual module, use --features fs (or another feature name)
with the test runner. Some modules have platform-specific implementations.
Run the Python compatibility suite:
uv run python -m pytestThe just recipe also regenerates the TLS fixtures before running the suite:
uv run just testThe complete recipe runs the independent Rust and Python suites concurrently after generating their shared TLS fixtures.
For a shorter local feedback loop, skip high-repetition and real-network slow tests. The normal suite uses merge-gating repetition counts; the stress recipe reruns marked tests with their original high counts:
uv run just test-fast
uv run just test-python
uv run just test-stressTo focus on one area, pass a test file or use pytest's -k selector:
uv run python -m pytest tests/test_run.py
uv run python -m pytest tests/test_compat.py
uv run python -m pytest tests/test_tls.py -k start_tlsWriter lifecycle contracts are exercised with:
uv run python -m pytest tests/test_stream_writer_lifecycle.pyThese tests compare asyncio, rsloop and optional uvloop over loopback TCP. They cover concurrent drains against a slow peer, cancellation and timeout cleanup while writing remains paused, ordered delivery through close/half-close, shutdown of pending drains, and error precedence. Events establish when drains are pending; a raw peer controls when bytes can be consumed. Protocol callbacks exercise prolonged pause deterministically without depending on kernel buffer sizes. Weak references check that cancelled waits are released before resume. Tests also check for leaked tasks and unhandled loop exceptions.
For real HTTP, WebSocket and database cancellation/recovery scenarios, see Async application lifecycle tests. The optional ecosystem dependency group has a dedicated cross-platform CI matrix.
CI and just test use scripts/run_python_tests.py, which forwards pytest
arguments and prints recurring stack dumps when tests stop making progress.
For the same diagnostics locally, run uv run python scripts/run_python_tests.py.
Repository benchmark and runner tests are kept out of the OS/Python matrix; run
them with uv run just test-tooling. Optional framework/database smoke scripts
remain under just test-frameworks and just test-databases; pytest does not
collect them by default.
Use pytest's monkeypatch fixture for attribute and environment overrides, and
pytest-mock's mocker fixture for mocks, spies, and call assertions. Use
monkeypatch.context() when a shared function must be restored before the test
finishes (for example, import machinery or warning handling).
Run uv run python -m pytest tests/test_stream_usecases.py after rebuilding the
native extension. These loopback TCP scenarios run against asyncio, rsloop and
uvloop when installed. The rsloop cases assert that both endpoints use the
native reader, so a fallback cannot silently bypass the implementation.
| Scenario | Contract |
|---|---|
| JSON Lines, UTF-8 and different write sizes | async for yields complete records, including a final line without a newline; repeated EOF stays exhausted. |
| Length-prefixed pipeline | Empty and large bodies retain exact framing; reads larger than the watermark complete; half-close preserves the reply direction. |
| Truncated final body | IncompleteReadError.partial and .expected describe the missing body accurately. |
| Oversized line followed by another request | readline and iteration discard the rejected line; readuntil preserves it for explicit recovery. |
| Cancelled iteration | Cancellation reaches the pending read; a later iteration receives the next record on the same connection. |
| Invalid or large declared size | Negative sizes fail when awaited without consuming data; EOF preserves the full expected unsigned size. |
Keep peer failures observable: await background tasks, close both endpoints and the server, and check for leaked tasks and event-loop exceptions. Use events or protocol acknowledgements to coordinate peers. Timeouts bound deadlocks; they do not establish ordering. A TCP write boundary is not a packet or read boundary. For a regression, first run the new test against the unchanged extension and record the failure, then rebuild and run the affected suite after the fix.
With MkDocs installed:
mkdocs serve
mkdocs build --strictOr with uv without adding a permanent dependency:
uvx --from mkdocs mkdocs serve
uvx --from mkdocs mkdocs build --strictIf you are new to the project, this order works well:
python/rsloop/__init__.pypython/rsloop/_run.pypython/rsloop/_loop_compat.pysrc/lib.rssrc/bindings/loop_api.rssrc/engine/loop_core.rs
This order moves from simple Python wrappers to the larger Rust internals.
When you add or debug a feature, it helps to ask:
- Is this a Python wrapper issue or a Rust implementation issue?
- Does the behavior need to match standard
asyncioexactly? - Is the feature cross-platform, Unix-only, or Windows-specific?
- Do the tests already describe the expected behavior?
Those four questions usually point you to the right part of the codebase.
Python 3.15 includes a low-overhead external sampling profiler that can run rsloop without a Cargo feature, special build, or in-process instrumentation. Generate an interactive flame graph with:
uv run --python 3.15 --with maturin maturin develop --release
uv run --python 3.15 python -m profiling.sampling run \
--all-threads --native --flamegraph \
-o rsloop-profile.html examples/01_basics.py--all-threads includes rsloop's runtime thread. --native adds synthetic
native-boundary markers; it does not unwind and name individual Rust frames.
The profiler and target must use the same Python 3.15 interpreter. Python 3.15
does not allow these options together with --async-aware; use a separate
async-aware pass when coroutine reconstruction is more important than native
and multi-thread visibility.
For repeatable workload profiles, use --profile-rsloop-dir with either
benchmark runner. Profile passes are unmeasured and produce HTML flame graphs.
See Python's special-frame documentation. Use a native stack profiler when attributing CPU time to individual Rust functions.
This is still an alpha-stage project.
It already covers a lot of asyncio surface area, but some areas are still evolving:
- TLS compatibility
- transport internals
- helper-thread removal in older paths
- platform-specific behavior differences
That makes the repository a good place to learn from, but also a project where careful testing matters.