skirr 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
skirr-0.1.0/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /target/
2
+ *.so
3
+ __pycache__/
4
+ .pytest_cache/
5
+ .venv/
6
+ *.egg-info/
7
+
8
+ /dist/
@@ -0,0 +1,56 @@
1
+ # HTTP/ASGI prototype requirements matrix
2
+
3
+ Normative references: [ASGI 3.0](https://asgi.readthedocs.io/en/latest/specs/main.html),
4
+ [HTTP/WebSocket 2.5](https://asgi.readthedocs.io/en/latest/specs/www.html),
5
+ [Lifespan 2.0](https://asgi.readthedocs.io/en/latest/specs/lifespan.html),
6
+ [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html),
7
+ [RFC 9112](https://www.rfc-editor.org/rfc/rfc9112.html).
8
+
9
+ This matrix tracks specific evidence. It is not a blanket compliance claim.
10
+ Specifications and verified errata need another review before a release.
11
+
12
+ | Contract | Evidence / state |
13
+ |---|---|
14
+ | ASGI 3 asynchronous application interface | Real ASGI apps, FastAPI benchmark |
15
+ | HTTP scopes, paths, query strings, repeated headers | `test_scope_headers_and_percent_decoding` |
16
+ | Absolute-target authority replaces Host | `test_absolute_target_authority_overrides_host` |
17
+ | Incremental fixed/chunked request bodies | Upload, fragmentation, and native segmentation tests |
18
+ | Exact request boundaries and ordered pipelining | Fixed-upload/pipeline test and native consumed-prefix test |
19
+ | Framing ambiguity closes the connection | Table of malformed requests plus a pipelined successor |
20
+ | Resource bounds and integer overflow | 100/101-header boundaries, oversized head/body cases; native chunk syntax/overflow test |
21
+ | Response start/body order and header validation | Response validation and iterable-header-pair tests; broader invalid-message matrix remains |
22
+ | Duplicate response fields preserved | Repeated Set-Cookie socket test |
23
+ | Header snapshots and owned scope values | Empty/obs-text/trimmed values, retained/mutated scope headers, changed same-length values, and large/duplicate header tables; mutation of response headers after start |
24
+ | Native send call signature | Invalid arity/type and positional/keyword send calls through a real application |
25
+ | Ready awaitable iterator returns the receive value | Explicit `__await__` iterator completion plus socket echo/fragmentation tests |
26
+ | HEAD/204/205/304 no response body | Parameterized bodyless pipeline test |
27
+ | Response Content-Length checked | Mismatch before/after response bytes begin, with no second response appended |
28
+ | Streaming sends flush before app completion | Event-controlled streaming socket test |
29
+ | ASGI response trailers | Multi-message trailer socket test |
30
+ | receive waits after the final request event | Disconnect test |
31
+ | Pending receive wakes when the response completes | Response-completion socket test |
32
+ | Closed send raises OSError | Backpressure/disconnect test |
33
+ | Cancelled receive may be replaced | Cancellation socket test |
34
+ | Retained request callables cannot cross requests | Retained-channel socket test |
35
+ | Immediate awaits preserve fairness | Streaming producer, ready disconnect receives and queued-marker pipeline checks |
36
+ | Buffered pipelining remains ordered while an app is held | 200-request pipeline test |
37
+ | Eager execution preserves Task and context isolation | Ready/waiting app context test |
38
+ | Embedding task factories are respected | Custom factory socket test |
39
+ | Disconnect during dispatch cannot panic or stop serving | Final/streaming TCP resets and subsequent healthy-request test |
40
+ | Server closed-send errors are quiet; application failures remain visible | Disconnect logging regression and ready/waiting Task failures, including application OSError and cancellation |
41
+ | Scope method/version defaults do not leak across requests | Mixed HTTP/1.0 and HTTP/1.1 methods on keepalive connections |
42
+ | Completion outside the native reactor flushes output | 24 independently held applications released together |
43
+ | Large body data and chunk framing survive partial writes | Byte-exact streamed payloads at 4095/4096/4097 bytes and 8 MiB |
44
+ | Lifespan on serving loop; request state shallow copies | Lifespan/state test for on/off; optional state omitted only when explicitly off |
45
+ | Explicit startup.failed prevents serving | Lifespan failure test |
46
+ | Graceful shutdown ordering | Cancellation cleanup precedes lifespan shutdown in embedding test; process-level qualification remains |
47
+ | Slow-client timeouts and truncated EOF | Implemented; broader deadline/EOF matrix remains |
48
+ | WebSockets | Unsupported; upgrade rejected with 501 |
49
+ | Optional ASGI extensions | None advertised |
50
+ | HTTP/2, HTTP/3, TLS | Unsupported |
51
+
52
+ Before production: complete the remaining HTTP requirement-by-requirement audit,
53
+ add property/fuzz coverage, extend the native/Python AddressSanitizer checks with leak and fuzz qualification,
54
+ qualify all advertised Python/platform combinations, and exercise prolonged
55
+ mixed-client load and shutdown failures. Differential server behavior is useful
56
+ evidence but does not replace the specifications as the authority.
skirr-0.1.0/Cargo.lock ADDED
@@ -0,0 +1,140 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "heck"
7
+ version = "0.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
10
+
11
+ [[package]]
12
+ name = "httparse"
13
+ version = "1.10.1"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
16
+
17
+ [[package]]
18
+ name = "libc"
19
+ version = "0.2.191"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
22
+
23
+ [[package]]
24
+ name = "once_cell"
25
+ version = "1.21.4"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
28
+
29
+ [[package]]
30
+ name = "portable-atomic"
31
+ version = "1.15.0"
32
+ source = "registry+https://github.com/rust-lang/crates.io-index"
33
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
34
+
35
+ [[package]]
36
+ name = "proc-macro2"
37
+ version = "1.0.107"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
40
+ dependencies = [
41
+ "unicode-ident",
42
+ ]
43
+
44
+ [[package]]
45
+ name = "pyo3"
46
+ version = "0.29.3"
47
+ source = "registry+https://github.com/rust-lang/crates.io-index"
48
+ checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
49
+ dependencies = [
50
+ "libc",
51
+ "once_cell",
52
+ "portable-atomic",
53
+ "pyo3-build-config",
54
+ "pyo3-ffi",
55
+ "pyo3-macros",
56
+ ]
57
+
58
+ [[package]]
59
+ name = "pyo3-build-config"
60
+ version = "0.29.3"
61
+ source = "registry+https://github.com/rust-lang/crates.io-index"
62
+ checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
63
+ dependencies = [
64
+ "target-lexicon",
65
+ ]
66
+
67
+ [[package]]
68
+ name = "pyo3-ffi"
69
+ version = "0.29.3"
70
+ source = "registry+https://github.com/rust-lang/crates.io-index"
71
+ checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
72
+ dependencies = [
73
+ "libc",
74
+ "pyo3-build-config",
75
+ ]
76
+
77
+ [[package]]
78
+ name = "pyo3-macros"
79
+ version = "0.29.3"
80
+ source = "registry+https://github.com/rust-lang/crates.io-index"
81
+ checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
82
+ dependencies = [
83
+ "proc-macro2",
84
+ "pyo3-macros-backend",
85
+ "quote",
86
+ "syn",
87
+ ]
88
+
89
+ [[package]]
90
+ name = "pyo3-macros-backend"
91
+ version = "0.29.3"
92
+ source = "registry+https://github.com/rust-lang/crates.io-index"
93
+ checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
94
+ dependencies = [
95
+ "heck",
96
+ "proc-macro2",
97
+ "quote",
98
+ "syn",
99
+ ]
100
+
101
+ [[package]]
102
+ name = "quote"
103
+ version = "1.0.47"
104
+ source = "registry+https://github.com/rust-lang/crates.io-index"
105
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
106
+ dependencies = [
107
+ "proc-macro2",
108
+ ]
109
+
110
+ [[package]]
111
+ name = "skirr"
112
+ version = "0.1.0"
113
+ dependencies = [
114
+ "httparse",
115
+ "libc",
116
+ "pyo3",
117
+ ]
118
+
119
+ [[package]]
120
+ name = "syn"
121
+ version = "2.0.119"
122
+ source = "registry+https://github.com/rust-lang/crates.io-index"
123
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
124
+ dependencies = [
125
+ "proc-macro2",
126
+ "quote",
127
+ "unicode-ident",
128
+ ]
129
+
130
+ [[package]]
131
+ name = "target-lexicon"
132
+ version = "0.13.5"
133
+ source = "registry+https://github.com/rust-lang/crates.io-index"
134
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
135
+
136
+ [[package]]
137
+ name = "unicode-ident"
138
+ version = "1.0.27"
139
+ source = "registry+https://github.com/rust-lang/crates.io-index"
140
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
skirr-0.1.0/Cargo.toml ADDED
@@ -0,0 +1,27 @@
1
+ [package]
2
+ name = "skirr"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+ rust-version = "1.88"
6
+ license = "MIT"
7
+ description = "A small experimental ASGI-only server"
8
+ repository = "https://gitman.io/patx/skirr"
9
+ exclude = ["bench/**", "docs/**"]
10
+ readme = "README.md"
11
+
12
+ [lib]
13
+ name = "_native"
14
+ crate-type = ["cdylib", "rlib"]
15
+
16
+ [dependencies]
17
+ pyo3 = "0.29"
18
+ httparse = "1.10"
19
+ libc = "0.2"
20
+
21
+ [features]
22
+ extension-module = ["pyo3/extension-module"]
23
+
24
+ [profile.release]
25
+ lto = "thin"
26
+ codegen-units = 1
27
+
skirr-0.1.0/DESIGN.md ADDED
@@ -0,0 +1,142 @@
1
+ # Prototype decisions
2
+
3
+ Skirr is an ASGI transport, not an application framework. Keep a single native
4
+ owner for each connection and run the application on the same thread and loop.
5
+
6
+ The prototype tests Peregrine's native-poller integration: epoll itself is
7
+ registered with asyncio, and the callback processes bounded native work. This
8
+ keeps uvloop usable and avoids another runtime and cross-thread request queues.
9
+ The alternative Weft design drives Tokio through a replacement asyncio selector.
10
+ Both deserve measurement; neither language nor reactor choice guarantees a win.
11
+
12
+ Sources reviewed:
13
+
14
+ - [Weft ASGI](https://github.com/grepjava/weft/blob/cec2030ed2a2f6e3fcb54df1270cc654cc9247e3/src/asgi.rs)
15
+ - [Weft callable types](https://github.com/grepjava/weft/blob/cec2030ed2a2f6e3fcb54df1270cc654cc9247e3/src/types.rs)
16
+ - [Peregrine ASGI](https://github.com/grepjava/peregrine/blob/3f8cac19b60ce6a151a07a5535a7e4ee8e3bbccf/Sources/PeregrineServer/ASGIDispatch.swift)
17
+ - [Peregrine connections](https://github.com/grepjava/peregrine/blob/3f8cac19b60ce6a151a07a5535a7e4ee8e3bbccf/Sources/PeregrineServer/Connection.swift)
18
+ - [Peregrine fuzzing](https://github.com/grepjava/peregrine/blob/3f8cac19b60ce6a151a07a5535a7e4ee8e3bbccf/Sources/PeregrineFuzzTargets/FuzzTargets.swift)
19
+
20
+ Native send and receive return a completed awaitable for ready operations and
21
+ Futures for waits. A shared completed-None awaitable avoids allocating a Future for every send.
22
+ A bounded operation budget forces cooperative yields even on fast sockets.
23
+
24
+ Connections have monotonically assigned identities; request callables refer to
25
+ their own request state and hold weak connection/worker references. Connections
26
+ are not recycled into the same identity. This is simpler than a slab for the
27
+ prototype and preserves the stale-callback safety property of generation tokens.
28
+ A slab is a later profiling decision.
29
+
30
+ Worker fields and connection state use thread-local ownership (Rc/RefCell),
31
+ not locks. Mutable state borrows must end before potentially reentrant Python
32
+ calls. CPython object ownership uses PyO3 rather than a parallel handwritten
33
+ reference-counting abstraction. Free-threaded Python is intentionally unqualified.
34
+
35
+ ## Profile-driven optimization
36
+
37
+ Native cycle sampling found a large read-buffer clearing cost (about 16% of
38
+ baseline raw-ASGI user-space cycles), while wall-time stack sampling also exposed
39
+ repeated epoll changes. The worker now reuses initialized scratch space and leaves
40
+ an empty socket readable across requests. Buffered pipelined bytes still pause
41
+ reads; a held application cannot make the reactor spin on its next request.
42
+
43
+ Response serialization writes into the connection's reusable buffer. A later
44
+ large-response profile still attributed about 42% of user-space cycles to copying.
45
+ Vectored writes now pass large immutable Python bodies directly to the socket;
46
+ partial writes immediately copy the unsent suffix into owned native memory.
47
+ The retained implementation's bulk profile reduced copying to about 2.5% of
48
+ user-space cycles. This avoids an intermediate copy, not the kernel's copy.
49
+
50
+ CPython Task eager startup (available in the supported Python 3.12+) avoids a
51
+ scheduler turn when the application does not need to wait. Each request still
52
+ has its own Task and context. Custom task factories use the normal loop path.
53
+ Already-finished tasks are cleaned up synchronously; pending tasks retain a done
54
+ callback. Pipeline progress belongs to the outer bounded pump, with explicit
55
+ deferred resumption when buffered work remains. It must never recurse once per
56
+ pipelined request. A request may disappear during eager dispatch on write failure,
57
+ so pump re-checks its active request rather than assuming one still exists.
58
+
59
+ The first optimization round shared an already-completed asyncio Future for sends. Real waits still get
60
+ their own Future. The operation budget still forces cooperative yields. Empty
61
+ request events are constructed lazily without allocating an otherwise unused
62
+ body queue. Parser header scratch uses httparse's safe uninitialized-header API.
63
+
64
+ Two experiments were discarded: borrowed native header views did not show a
65
+ clear benefit, and a PyCFunction vectorcall wrapper's modest improvements did not
66
+ justify extra unsafe code and two additional context/function objects per request.
67
+ That optimization round added neither dependencies nor unsafe blocks. A separate eager-work-budget/scope-completion experiment also failed to improve tail latency and was reverted; its patch and diagnostic runs are retained.
68
+
69
+ The next bottlenecks include request dispatch and the Python application itself.
70
+ FastAPI's baseline native CPU profile attributed roughly 45% of user-space cycles
71
+ to CPython's evaluator alone. The server does not change application GC policy,
72
+ skip response validation, disable fairness, or specialize for benchmark routes.
73
+
74
+ Cycle percentages are samples of user-space execution, not end-to-end wall time.
75
+ They must not be interpreted as predicted throughput gains. Raw perf reports,
76
+ commands and sampled build metadata live in `bench/profiles`; the authoritative
77
+ performance comparison is the repeated unprofiled run in `bench/results`.
78
+
79
+ Remaining roadmap: complete HTTP conformance evidence, aggregate byte budgets,
80
+ worker supervision and listener handoff, WebSockets via a maintained codec, then
81
+ optional TLS/HTTP2/HTTP3 dependencies with independent protocol qualification.
82
+
83
+ ## Exact-release Peregrine comparison
84
+
85
+ The second round reviewed the source of the installed Peregrine 1.1.7 release,
86
+ including its native ready awaitables, header spans and reusable buffers. The
87
+ [study](bench/profiles/peregrine-study.md) records profiles, source provenance,
88
+ syscall counts, tests and rejected experiments. Request headers now use one
89
+ owned block with safe ranges, response headers serialize once into bounded
90
+ reusable storage, and decoded uploads copy straight into owned Python bytes.
91
+ A level-triggered short read ends the current read batch instead of probing
92
+ again for EAGAIN. Another event services later data.
93
+
94
+ Completed sends now share a native stateless awaitable. Ready receives and
95
+ fairness yields use the same native slots with per-operation state; waits still
96
+ use Futures. CPython ownership for this small type is isolated in
97
+ `src/completed.rs`, with no thread-bound Rust pointers or global interpreter
98
+ references. Unlike the discarded vectorcall channel experiment, this is not a
99
+ parallel callable/connection ownership framework. The operation quota, Task
100
+ isolation, validation and scope isolation remain in force.
101
+
102
+ ## FastAPI fairness and allocation study
103
+
104
+ The third round profiles the exact release binaries and the actual FastAPI worker.
105
+ A worker-global send quota made some short requests yield behind an unrelated batch.
106
+ The quota now belongs to each request and includes ready receives. A 16-event
107
+ reactor batch and bounded eager starts preserve loop opportunities without making
108
+ every response asynchronous. Custom task factories and request contexts remain intact.
109
+
110
+ Scope construction uses startup-interned fixed keys, separate shallow copies of
111
+ mutable dictionaries, borrowed common method names and a pre-sized header list.
112
+ Completed Tasks are inspected once through result(), preserving application errors
113
+ and cancellation. Server-generated disconnect errors have an OSError subclass so
114
+ expected disconnects are quiet while application OSError failures remain reported.
115
+
116
+ The retained changes add 48 native lines and no dependencies or unsafe code.
117
+ Write-batching and alternative task schedulers were rejected after throughput and
118
+ disconnect checks. Detailed profiles, rejected patches and comparisons are in
119
+ [the FastAPI study](bench/profiles/fastapi-study.md). Native library sample shares
120
+ are small; CPython execution includes both application code and server object/API
121
+ work, so it is not valid to treat that entire share as unavoidable application cost.
122
+
123
+ ## Bounded final writes and FastAPI overhead
124
+
125
+ The fourth round replaces the adaptive-batch checkpoint with sixteen-event
126
+ cheap-work batches and at most 32 events while final flushes are queued. Small
127
+ final responses can copy into the kernel using MSG_MORE, then push at the native poll boundary or in a bounded
128
+ loop callback for independently resumed apps. Streaming responses retain immediate writes. A queued callback
129
+ tries the write before arming EPOLLOUT, avoiding redundant interest changes.
130
+
131
+ Native request-head scratch storage is recycled after Python owns its scope.
132
+ Short identical header blocks share immutable pairs behind fresh mutable lists;
133
+ the worker's cross-connection hint is weak and each connection retains only one
134
+ bounded snapshot. Optional state is omitted only for explicit lifespan off.
135
+ Loop methods are cached without caching the result of get_task_factory.
136
+
137
+ A guarded vectorcall entry removes generic call packaging for Sender while
138
+ keeping its PyO3 allocation, thread checks, errors and ownership. Layout checks
139
+ fall back to PyO3's ordinary call path; type slots are initialized under the GIL.
140
+ This adds unsafe CPython integration, so it needs independent qualification for
141
+ each Python version. The repeated profiles, rejected experiments and precise
142
+ limits of the results are in [the fourth-round study](bench/profiles/latency-round4-study.md).
skirr-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Skirr contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
skirr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: skirr
3
+ Version: 0.1.0
4
+ Classifier: Development Status :: 3 - Alpha
5
+ Classifier: Operating System :: POSIX :: Linux
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Rust
8
+ Requires-Dist: psutil ; extra == 'bench'
9
+ Requires-Dist: fastapi ; extra == 'bench'
10
+ Requires-Dist: uvloop ; extra == 'bench'
11
+ Requires-Dist: weft-server ; extra == 'bench'
12
+ Requires-Dist: peregrine-server ; extra == 'bench'
13
+ Requires-Dist: uvloop>=0.22 ; extra == 'speed'
14
+ Requires-Dist: pytest>=8 ; extra == 'test'
15
+ Requires-Dist: pytest-asyncio>=0.24 ; extra == 'test'
16
+ Requires-Dist: fastapi ; extra == 'test'
17
+ Provides-Extra: bench
18
+ Provides-Extra: speed
19
+ Provides-Extra: test
20
+ License-File: LICENSE
21
+ Summary: Experimental Linux HTTP/1 ASGI-only server
22
+ License-Expression: MIT
23
+ Requires-Python: >=3.12
24
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
25
+ Project-URL: Repository, https://gitman.io/patx/skirr
26
+
27
+ # Skirr
28
+
29
+ A small, experimental **ASGI-only** Rust server for Linux. Python owns the
30
+ application's asyncio loop; Rust owns nonblocking sockets, HTTP/1 framing,
31
+ buffering, and native ASGI send/receive objects. The native epoll descriptor is
32
+ registered with `loop.add_reader`. There is no second async runtime or thread.
33
+
34
+ This is an architecture prototype, not a production release. Profile-driven
35
+ optimizations and same-session before/after comparisons live in
36
+ [the latest comparison](bench/results/latency-round4-final-report.txt). Results are
37
+ workload-specific; this is not a claim of overall superiority to other servers.
38
+ The fourth round improves FastAPI throughput by 1.9–3.4% over its starting
39
+ release and fixed-rate p99 by 4.4%. It does not consistently beat Peregrine on
40
+ both FastAPI throughput and latency; some saturation p99 regressions remain.
41
+ See [the profile and regression study](bench/profiles/latency-round4-study.md).
42
+
43
+ ## Build and run
44
+
45
+ Requires Linux, Rust 1.88 or newer, and CPython 3.12 or newer. This initial implementation has
46
+ been tested on CPython 3.14.4 on Linux x86-64; other supported Python versions
47
+ and ARM64 still need qualification. Free-threaded Python is not supported.
48
+
49
+ ```bash
50
+ python -m pip install .
51
+ skirr app:app --host 127.0.0.1 --port 8000
52
+ ```
53
+
54
+ `pip install '.[speed]'` installs optional uvloop. Standard asyncio also works.
55
+ Applications use the ordinary ASGI 3 `(scope, receive, send)` interface.
56
+ `--lifespan auto|on|off` defaults to `auto`. Only one worker is implemented;
57
+ other worker counts fail explicitly.
58
+
59
+ For development without installing the package:
60
+
61
+ ```bash
62
+ python tools/build.py
63
+ PYTHONPATH=python python -m pytest tests -q
64
+ cargo test --lib
65
+ ```
66
+
67
+ ## Implemented scope
68
+
69
+ - HTTP/1.0 and HTTP/1.1, sequential pipelining, fixed-length and chunked bodies.
70
+ - Request dispatch at headers, incremental upload consumption, demand-driven
71
+ `100 Continue`, streaming responses, and HTTP/1.1 response trailers.
72
+ - Native completed awaitables; Futures for blocked receive and output drain.
73
+ - Eager task startup when no custom task factory is installed; normal factory
74
+ behavior is preserved. Completed tasks are cleaned up without another callback.
75
+ - Read and write backpressure, per-callback I/O budgets and an ASGI operation
76
+ budget per request so immediately completed sends and receives cooperate with the loop.
77
+ Native event batches and eager application starts are also bounded.
78
+ - Request identities that prevent retained channels from writing into a later
79
+ request, disconnect delivery, closed-send errors, and cancelled receive reuse.
80
+ - Lifespan startup/shutdown on the serving loop, request state copies and
81
+ graceful drain. IPv4 and IPv6 TCP listeners.
82
+
83
+ No WSGI, routing, application framework, caching, compression, or static files.
84
+ WebSockets and other upgrades return 501. TLS, HTTP/2, HTTP/3, Unix sockets,
85
+ proxy-header processing, worker supervision and structured access logs are not
86
+ implemented. Informational-response extensions are not advertised. CONNECT is
87
+ not implemented and returns 501. An HTTP/1.0 response cannot carry trailers;
88
+ trailer events complete it without transmitting trailer fields.
89
+
90
+ ## Standards and limits
91
+
92
+ The HTTP scope advertises ASGI 3.0 and HTTP sub-specification 2.5. Lifespan uses
93
+ 2.0. The implementation targets RFC 9110/9112 for HTTP semantics/framing.
94
+ This describes the intended contract, not a claim of completed conformance
95
+ certification. See [the requirements matrix](COMPLIANCE.md) for tested behavior
96
+ and outstanding qualification. Supporting HTTP/1 does not imply HTTP/2 or HTTP/3
97
+ support, nor support for the ASGI WebSocket protocol.
98
+
99
+ Current fixed prototype limits:
100
+
101
+ | Resource | Limit |
102
+ |---|---:|
103
+ | Request head | 32 KiB / 100 fields |
104
+ | Decoded request body | 16 MiB |
105
+ | Chunk framing and trailer metadata combined | 32 KiB |
106
+ | Individual chunk-size line | 8 KiB |
107
+ | Buffered request-body high-water mark | 64 KiB, plus one decoded segment |
108
+ | Unconsumed wire buffer | 1 MiB |
109
+ | Output pause / resume thresholds | 512 KiB / 64 KiB |
110
+ | Connections | 10,000 |
111
+ | Header deadline / keepalive idle timeout | 10 s / 5 s |
112
+ | Stalled client I/O timeout | 30 s |
113
+
114
+ Output backpressure bounds queued work between awaited sends. One application
115
+ body message can exceed the threshold; any unsent suffix is copied into
116
+ its output buffer. There is no aggregate worker byte budget yet. Consequently,
117
+ the connection limit is not a sufficient production memory limit.
118
+
119
+ ## Architecture and dependency budget
120
+
121
+ Three direct Rust dependencies: PyO3 (Python ownership/integration), httparse
122
+ (request-head syntax), and libc (isolated socket/poller calls). Cargo.lock pins the full
123
+ graph, including build dependencies. Python runtime dependencies are optional.
124
+
125
+ - `src/http.rs`: framing without Python or sockets, plus segmentation tests.
126
+ - `src/net.rs`: owned epoll descriptor and OS-specific socket/poller operations.
127
+ - `src/vectorcall.rs`: guarded CPython call entry for the existing PyO3 Sender.
128
+ - `src/completed.rs`: native ready-awaitable slots, with isolated CPython ownership.
129
+ - `src/lib.rs`: connection ownership, I/O, ASGI objects and worker callbacks.
130
+
131
+ The worker reuses one initialized read buffer. An empty socket remains armed
132
+ between requests, avoiding two epoll changes per request; buffered pipelined
133
+ input still pauses reads. Small writes are coalesced; large bodies use vectored
134
+ I/O with an owned copy of any unsent suffix. Python body pointers are never
135
+ retained after a send call. Header-name caching is capped at 512 names, each
136
+ at most 128 bytes; application-visible mutable scope dictionaries remain separate.
137
+ Identical short request-header blocks can share immutable tuples and bytes, with
138
+ a fresh mutable header list per request. Snapshots are bounded to eight fields
139
+ and 512 wire bytes per connection, with a weak hint for sharing across connections.
140
+ Native request-head storage is reused only after Python owns the scope values.
141
+ With explicit lifespan off, the optional state namespace is omitted; auto/on
142
+ still provide a fresh shallow copy.
143
+ Fixed scope keys are interned once; each request still gets separate mutable scope
144
+ and ASGI-version dictionaries. Header spans use one owned request block. Response-header storage is reused up
145
+ to 64 KiB per connection. Upload segments copy directly into owned Python bytes.
146
+ Completed sends allocate no iterator, while real I/O waits still use Futures.
147
+ Small final responses after more expensive application work copy into the kernel
148
+ with MSG_MORE and push at the end of the bounded native callback (or a queued
149
+ callback for independently resumed apps). Streaming sends flush
150
+ immediately. Queued flushes do not arm EPOLLOUT until an attempted write blocks.
151
+ - `python/skirr`: application loading, lifespan, CLI and loop orchestration.
152
+
153
+ Readability takes priority over a line-count target. CPython integration still
154
+ has overhead worth measuring: native callable dispatch, scope/message allocation,
155
+ buffer copies, task scheduling, and done callbacks. The Sender uses a guarded vectorcall entry while retaining PyO3 ownership,
156
+ thread checks and error/panic translation. This integration needs qualification
157
+ on every supported Python version.
158
+
159
+ Design research used pinned Weft `cec2030` and Peregrine `3f8cac1` source
160
+ snapshots. The implementation is new Rust code; their architectural ideas are
161
+ credited in [DESIGN.md](DESIGN.md). The latest study also reviews the exact
162
+ Peregrine 1.1.7 release source; [profiles and findings](bench/profiles/peregrine-study.md)
163
+ explain the retained and rejected experiments. The benchmark apps and resource-sampling
164
+ harness were adapted from our MRHTTP benchmark tooling.
165
+
166
+ ## Benchmark reproduction
167
+
168
+ Install the optional benchmark dependencies and `wrk`. Run from this directory:
169
+
170
+ ```bash
171
+ PYTHONPATH=python python bench/compare.py --workers 1 --connections 64 \
172
+ --duration 5 --repeats 3 --output bench/results/my-c64
173
+ ```
174
+
175
+ The default server CPU is core 0 and load-generator CPUs are 4–7; override
176
+ `--server-cpus` and `--client-cpus` for your machine. Their sets must not overlap.
177
+ Each result records versions, build/source hashes, CPU and memory sampling,
178
+ worker readiness, response validation, raw wrk output and streaming latency.
179
+ The main throughput run validates exact responses before and after load;
180
+ it does not validate every response body during the run.
181
+
182
+ These are closed-loop local microbenchmarks, not production capacity estimates.
183
+ wrk's timeout counter also includes completed responses outside its three-second
184
+ latency histogram. Compare that separately from connect/read/write failures.
185
+ A separate [fixed-rate comparison](bench/results/latency-round4-final-rate20k-report.txt)
186
+ uses wrk2 corrected histograms; do not pool those percentiles with wrk results.
187
+ Extended soak tests, fuzzing, broader sanitizer/leak qualification,
188
+ and broader platform testing remain required before a production release.
189
+
190
+ Verification: 64 real-socket/ASGI tests pass under both standard asyncio
191
+ and uvloop; three native framing-invariant tests pass; strict Clippy passes.
192
+ An installable CPython 3.14 Linux wheel was built and the same 64 socket tests
193
+ passed against that installed wheel. These checks cover the listed contracts,
194
+ not every requirement in the HTTP specifications. A separate AddressSanitizer
195
+ build passes the 64 cases under both loops; its embedding-host limitations and
196
+ commands are recorded in [the sanitizer log](bench/profiles/asan-round4-final-check.txt).
197
+
198
+ To reproduce the same-session before/after comparison, extract
199
+ `bench/profiles/round4-before-source.tar.gz` into a separate directory and include
200
+ `--servers skirr-before skirr weft peregrine --baseline /path/to/extracted`.
201
+ The archive includes the original source and CPython 3.14 x86-64 release binary.
202
+ Rebuild that binary when using another interpreter or architecture.
203
+
204
+ The latest before/after baseline is `bench/profiles/round3-before-source.tar.gz`;
205
+ its results are in `bench/results/latency-round4-final-report.txt`.
206
+ The [FastAPI study](bench/profiles/fastapi-study.md) records profiling, retained
207
+ changes, rejected prototypes, and measurement limitations. Earlier reports
208
+ and their distinct original baseline remain available for historical comparison.
209
+
210
+ For a comparable run on this laptop, the final matrices were launched with
211
+ `powerprofilesctl launch --profile performance --appid skirr-benchmark --` before
212
+ the Python benchmark command. This temporary hold releases on command exit;
213
+ it is optional benchmark tooling, not a Skirr runtime dependency. CPU-frequency
214
+ samples and the active power profile are recorded in each result. Discarded
215
+ power-saver and mixed-clock trials remain clearly named in the results folder.
216
+