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 +8 -0
- skirr-0.1.0/COMPLIANCE.md +56 -0
- skirr-0.1.0/Cargo.lock +140 -0
- skirr-0.1.0/Cargo.toml +27 -0
- skirr-0.1.0/DESIGN.md +142 -0
- skirr-0.1.0/LICENSE +21 -0
- skirr-0.1.0/PKG-INFO +216 -0
- skirr-0.1.0/README.md +189 -0
- skirr-0.1.0/pyproject.toml +37 -0
- skirr-0.1.0/python/skirr/__init__.py +159 -0
- skirr-0.1.0/src/completed.rs +128 -0
- skirr-0.1.0/src/http.rs +529 -0
- skirr-0.1.0/src/lib.rs +1626 -0
- skirr-0.1.0/src/net.rs +87 -0
- skirr-0.1.0/src/vectorcall.rs +104 -0
- skirr-0.1.0/tests/test_http.py +768 -0
- skirr-0.1.0/tools/build.py +13 -0
skirr-0.1.0/.gitignore
ADDED
|
@@ -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
|
+
|