Metadata-Version: 2.4
Name: skirr
Version: 0.1.1
Classifier: Development Status :: 3 - Alpha
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Rust
Requires-Dist: psutil ; extra == 'bench'
Requires-Dist: fastapi ; extra == 'bench'
Requires-Dist: uvloop ; extra == 'bench'
Requires-Dist: weft-server ; extra == 'bench'
Requires-Dist: peregrine-server ; extra == 'bench'
Requires-Dist: uvloop>=0.22 ; extra == 'speed'
Requires-Dist: pytest>=8 ; extra == 'test'
Requires-Dist: pytest-asyncio>=0.24 ; extra == 'test'
Requires-Dist: fastapi ; extra == 'test'
Provides-Extra: bench
Provides-Extra: speed
Provides-Extra: test
License-File: LICENSE
Summary: Experimental Linux HTTP/1 ASGI-only server
License-Expression: MIT
Requires-Python: >=3.12
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Repository, https://gitman.io/patx/skirr

# Skirr

A small, experimental **ASGI-only** Rust server for Linux. Python owns the
application's asyncio loop; Rust owns nonblocking sockets, HTTP/1 framing,
buffering, and native ASGI send/receive objects. The native epoll descriptor is
registered with `loop.add_reader`. There is no second async runtime or thread.

This is an architecture prototype, not a production release. Profile-driven
optimizations and same-session before/after comparisons live in
[the latest comparison](bench/results/latency-round4-final-report.txt). Results are
workload-specific; this is not a claim of overall superiority to other servers.
The fourth round improves FastAPI throughput by 1.9–3.4% over its starting
release and fixed-rate p99 by 4.4%. It does not consistently beat Peregrine on
both FastAPI throughput and latency; some saturation p99 regressions remain.
See [the profile and regression study](bench/profiles/latency-round4-study.md).

## Build and run

Requires Linux, Rust 1.88 or newer, and CPython 3.12 or newer. This initial implementation has
been tested on CPython 3.14.4 on Linux x86-64; other supported Python versions
and ARM64 still need qualification. Free-threaded Python is not supported.

```bash
python -m pip install .
skirr app:app --host 127.0.0.1 --port 8000
```

Run this from the directory containing `app.py`; `app:app` selects its ASGI
callable named `app`. The current directory is searched by default. To load an
application from another directory, use `--app-dir /path/to/project`.

`pip install '.[speed]'` installs optional uvloop. Standard asyncio also works.
Applications use the ordinary ASGI 3 `(scope, receive, send)` interface.
`--lifespan auto|on|off` defaults to `auto`. Only one worker is implemented;
other worker counts fail explicitly.

For development without installing the package:

```bash
python tools/build.py
PYTHONPATH=python python -m pytest tests -q
cargo test --lib
```

Before publishing, install the release wheel in a virtual environment and run
`/path/to/venv/bin/python tools/check_release.py`. This checks the installed
`skirr app:app` console command from a separate application directory.

## Implemented scope

- HTTP/1.0 and HTTP/1.1, sequential pipelining, fixed-length and chunked bodies.
- Request dispatch at headers, incremental upload consumption, demand-driven
  `100 Continue`, streaming responses, and HTTP/1.1 response trailers.
- Native completed awaitables; Futures for blocked receive and output drain.
- Eager task startup when no custom task factory is installed; normal factory
  behavior is preserved. Completed tasks are cleaned up without another callback.
- Read and write backpressure, per-callback I/O budgets and an ASGI operation
  budget per request so immediately completed sends and receives cooperate with the loop.
  Native event batches and eager application starts are also bounded.
- Request identities that prevent retained channels from writing into a later
  request, disconnect delivery, closed-send errors, and cancelled receive reuse.
- Lifespan startup/shutdown on the serving loop, request state copies and
  graceful drain. IPv4 and IPv6 TCP listeners.

No WSGI, routing, application framework, caching, compression, or static files.
WebSockets and other upgrades return 501. TLS, HTTP/2, HTTP/3, Unix sockets,
proxy-header processing, worker supervision and structured access logs are not
implemented. Informational-response extensions are not advertised. CONNECT is
not implemented and returns 501. An HTTP/1.0 response cannot carry trailers;
trailer events complete it without transmitting trailer fields.

## Standards and limits

The HTTP scope advertises ASGI 3.0 and HTTP sub-specification 2.5. Lifespan uses
2.0. The implementation targets RFC 9110/9112 for HTTP semantics/framing.
This describes the intended contract, not a claim of completed conformance
certification. See [the requirements matrix](COMPLIANCE.md) for tested behavior
and outstanding qualification. Supporting HTTP/1 does not imply HTTP/2 or HTTP/3
support, nor support for the ASGI WebSocket protocol.

Current fixed prototype limits:

| Resource | Limit |
|---|---:|
| Request head | 32 KiB / 100 fields |
| Decoded request body | 16 MiB |
| Chunk framing and trailer metadata combined | 32 KiB |
| Individual chunk-size line | 8 KiB |
| Buffered request-body high-water mark | 64 KiB, plus one decoded segment |
| Unconsumed wire buffer | 1 MiB |
| Output pause / resume thresholds | 512 KiB / 64 KiB |
| Connections | 10,000 |
| Header deadline / keepalive idle timeout | 10 s / 5 s |
| Stalled client I/O timeout | 30 s |

Output backpressure bounds queued work between awaited sends. One application
body message can exceed the threshold; any unsent suffix is copied into
its output buffer. There is no aggregate worker byte budget yet. Consequently,
the connection limit is not a sufficient production memory limit.

## Architecture and dependency budget

Three direct Rust dependencies: PyO3 (Python ownership/integration), httparse
(request-head syntax), and libc (isolated socket/poller calls). Cargo.lock pins the full
graph, including build dependencies. Python runtime dependencies are optional.

- `src/http.rs`: framing without Python or sockets, plus segmentation tests.
- `src/net.rs`: owned epoll descriptor and OS-specific socket/poller operations.
- `src/vectorcall.rs`: guarded CPython call entry for the existing PyO3 Sender.
- `src/completed.rs`: native ready-awaitable slots, with isolated CPython ownership.
- `src/lib.rs`: connection ownership, I/O, ASGI objects and worker callbacks.

The worker reuses one initialized read buffer. An empty socket remains armed
between requests, avoiding two epoll changes per request; buffered pipelined
input still pauses reads. Small writes are coalesced; large bodies use vectored
I/O with an owned copy of any unsent suffix. Python body pointers are never
retained after a send call. Header-name caching is capped at 512 names, each
at most 128 bytes; application-visible mutable scope dictionaries remain separate.
Identical short request-header blocks can share immutable tuples and bytes, with
a fresh mutable header list per request. Snapshots are bounded to eight fields
and 512 wire bytes per connection, with a weak hint for sharing across connections.
Native request-head storage is reused only after Python owns the scope values.
With explicit lifespan off, the optional state namespace is omitted; auto/on
still provide a fresh shallow copy.
Fixed scope keys are interned once; each request still gets separate mutable scope
and ASGI-version dictionaries. Header spans use one owned request block. Response-header storage is reused up
to 64 KiB per connection. Upload segments copy directly into owned Python bytes.
Completed sends allocate no iterator, while real I/O waits still use Futures.
Small final responses after more expensive application work copy into the kernel
with MSG_MORE and push at the end of the bounded native callback (or a queued
callback for independently resumed apps). Streaming sends flush
immediately. Queued flushes do not arm EPOLLOUT until an attempted write blocks.
- `python/skirr`: application loading, lifespan, CLI and loop orchestration.

Readability takes priority over a line-count target. CPython integration still
has overhead worth measuring: native callable dispatch, scope/message allocation,
buffer copies, task scheduling, and done callbacks. The Sender uses a guarded vectorcall entry while retaining PyO3 ownership,
thread checks and error/panic translation. This integration needs qualification
on every supported Python version.

Design research used pinned Weft `cec2030` and Peregrine `3f8cac1` source
snapshots. The implementation is new Rust code; their architectural ideas are
credited in [DESIGN.md](DESIGN.md). The latest study also reviews the exact
Peregrine 1.1.7 release source; [profiles and findings](bench/profiles/peregrine-study.md)
explain the retained and rejected experiments. The benchmark apps and resource-sampling
harness were adapted from our MRHTTP benchmark tooling.

## Benchmark reproduction

Install the optional benchmark dependencies and `wrk`. Run from this directory:

```bash
PYTHONPATH=python python bench/compare.py --workers 1 --connections 64 \
  --duration 5 --repeats 3 --output bench/results/my-c64
```

The default server CPU is core 0 and load-generator CPUs are 4–7; override
`--server-cpus` and `--client-cpus` for your machine. Their sets must not overlap.
Each result records versions, build/source hashes, CPU and memory sampling,
worker readiness, response validation, raw wrk output and streaming latency.
The main throughput run validates exact responses before and after load;
it does not validate every response body during the run.

These are closed-loop local microbenchmarks, not production capacity estimates.
wrk's timeout counter also includes completed responses outside its three-second
latency histogram. Compare that separately from connect/read/write failures.
A separate [fixed-rate comparison](bench/results/latency-round4-final-rate20k-report.txt)
uses wrk2 corrected histograms; do not pool those percentiles with wrk results.
Extended soak tests, fuzzing, broader sanitizer/leak qualification,
and broader platform testing remain required before a production release.

Verification: 69 CLI/socket/ASGI cases pass under both standard asyncio
and uvloop; three native framing-invariant tests pass; strict Clippy passes.
An installable CPython 3.14 Linux wheel was built and the same 69 cases
passed against that installed wheel. These checks cover the listed contracts,
not every requirement in the HTTP specifications. A separate AddressSanitizer
build passes the 64 cases under both loops; its embedding-host limitations and
commands are recorded in [the sanitizer log](bench/profiles/asan-round4-final-check.txt).

To reproduce the same-session before/after comparison, extract
`bench/profiles/round4-before-source.tar.gz` into a separate directory and include
`--servers skirr-before skirr weft peregrine --baseline /path/to/extracted`.
The archive includes the original source and CPython 3.14 x86-64 release binary.
Rebuild that binary when using another interpreter or architecture.

The latest before/after baseline is `bench/profiles/round3-before-source.tar.gz`;
its results are in `bench/results/latency-round4-final-report.txt`.
The [FastAPI study](bench/profiles/fastapi-study.md) records profiling, retained
changes, rejected prototypes, and measurement limitations. Earlier reports
and their distinct original baseline remain available for historical comparison.

For a comparable run on this laptop, the final matrices were launched with
`powerprofilesctl launch --profile performance --appid skirr-benchmark --` before
the Python benchmark command. This temporary hold releases on command exit;
it is optional benchmark tooling, not a Skirr runtime dependency. CPU-frequency
samples and the active power profile are recorded in each result. Discarded
power-saver and mixed-clock trials remain clearly named in the results folder.

