rsloop 0.1.6__tar.gz → 0.1.7__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.
- {rsloop-0.1.6 → rsloop-0.1.7}/Cargo.lock +60 -3
- {rsloop-0.1.6 → rsloop-0.1.7}/Cargo.toml +3 -1
- {rsloop-0.1.6 → rsloop-0.1.7}/PKG-INFO +58 -14
- {rsloop-0.1.6 → rsloop-0.1.7}/README.md +57 -13
- {rsloop-0.1.6 → rsloop-0.1.7}/benchmarks/README.md +3 -1
- {rsloop-0.1.6 → rsloop-0.1.7}/benchmarks/compare_event_loops.py +110 -7
- {rsloop-0.1.6 → rsloop-0.1.7}/demo/README.md +2 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/pyproject.toml +1 -1
- {rsloop-0.1.6 → rsloop-0.1.7}/src/fd_ops.rs +78 -18
- {rsloop-0.1.6 → rsloop-0.1.7}/src/lib.rs +2 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/loop_core.rs +5 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/runtime.rs +218 -19
- {rsloop-0.1.6 → rsloop-0.1.7}/src/stream_transport.rs +141 -6
- rsloop-0.1.7/src/windows_vibeio.rs +137 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/uv.lock +1 -1
- {rsloop-0.1.6 → rsloop-0.1.7}/.github/workflows/tests.yml +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/.github/workflows/wheels.yml +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/.gitignore +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/LICENSE +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/build.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/demo/fastapi_service.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/examples/01_basics.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/examples/02_fd_and_sockets.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/examples/03_streams.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/examples/04_unix_and_accepted_socket.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/examples/05_pipes_signals_subprocesses.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/justfile +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/python/rsloop/__init__.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/rsloop.png +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/scripts/build-wheels.sh +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/scripts/generate-test-tls-certs.sh +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/scripts/python-versions.sh +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/scripts/test-supported-pythons.sh +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/async_event.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/blocking.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/callbacks.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/context.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/errors.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/fast_streams.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/process_transport.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/profiler.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/python_api.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/python_names.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/src/tls.rs +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/tests/packages/aiohttp_test.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/tests/test_compat.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/tests/test_run.py +0 -0
- {rsloop-0.1.6 → rsloop-0.1.7}/tests/test_tls.py +0 -0
|
@@ -374,7 +374,7 @@ dependencies = [
|
|
|
374
374
|
"crossbeam-queue",
|
|
375
375
|
"flume",
|
|
376
376
|
"futures-util",
|
|
377
|
-
"io-uring",
|
|
377
|
+
"io-uring 0.7.11",
|
|
378
378
|
"io_uring_buf_ring",
|
|
379
379
|
"libc",
|
|
380
380
|
"once_cell",
|
|
@@ -878,6 +878,16 @@ dependencies = [
|
|
|
878
878
|
"str_stack",
|
|
879
879
|
]
|
|
880
880
|
|
|
881
|
+
[[package]]
|
|
882
|
+
name = "io-uring"
|
|
883
|
+
version = "0.6.4"
|
|
884
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
885
|
+
checksum = "595a0399f411a508feb2ec1e970a4a30c249351e30208960d58298de8660b0e5"
|
|
886
|
+
dependencies = [
|
|
887
|
+
"bitflags 1.3.2",
|
|
888
|
+
"libc",
|
|
889
|
+
]
|
|
890
|
+
|
|
881
891
|
[[package]]
|
|
882
892
|
name = "io-uring"
|
|
883
893
|
version = "0.7.11"
|
|
@@ -896,7 +906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
896
906
|
checksum = "1838759bb8c2f24cf05a35429d83145c4aa6af43f8ad38477295e12a7320a80e"
|
|
897
907
|
dependencies = [
|
|
898
908
|
"bytes",
|
|
899
|
-
"io-uring",
|
|
909
|
+
"io-uring 0.7.11",
|
|
900
910
|
"rustix",
|
|
901
911
|
]
|
|
902
912
|
|
|
@@ -1035,6 +1045,18 @@ dependencies = [
|
|
|
1035
1045
|
"adler2",
|
|
1036
1046
|
]
|
|
1037
1047
|
|
|
1048
|
+
[[package]]
|
|
1049
|
+
name = "mio"
|
|
1050
|
+
version = "1.1.1"
|
|
1051
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1052
|
+
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
|
1053
|
+
dependencies = [
|
|
1054
|
+
"libc",
|
|
1055
|
+
"log",
|
|
1056
|
+
"wasi",
|
|
1057
|
+
"windows-sys 0.61.2",
|
|
1058
|
+
]
|
|
1059
|
+
|
|
1038
1060
|
[[package]]
|
|
1039
1061
|
name = "nix"
|
|
1040
1062
|
version = "0.26.4"
|
|
@@ -1090,6 +1112,12 @@ version = "1.21.4"
|
|
|
1090
1112
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1091
1113
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
1092
1114
|
|
|
1115
|
+
[[package]]
|
|
1116
|
+
name = "oneshot"
|
|
1117
|
+
version = "0.2.1"
|
|
1118
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1119
|
+
checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c"
|
|
1120
|
+
|
|
1093
1121
|
[[package]]
|
|
1094
1122
|
name = "openssl-probe"
|
|
1095
1123
|
version = "0.2.1"
|
|
@@ -1346,7 +1374,7 @@ dependencies = [
|
|
|
1346
1374
|
|
|
1347
1375
|
[[package]]
|
|
1348
1376
|
name = "rsloop"
|
|
1349
|
-
version = "0.1.
|
|
1377
|
+
version = "0.1.7"
|
|
1350
1378
|
dependencies = [
|
|
1351
1379
|
"async-std",
|
|
1352
1380
|
"compio",
|
|
@@ -1362,6 +1390,8 @@ dependencies = [
|
|
|
1362
1390
|
"rustls-pemfile",
|
|
1363
1391
|
"signal-hook",
|
|
1364
1392
|
"socket2",
|
|
1393
|
+
"tokio",
|
|
1394
|
+
"vibeio",
|
|
1365
1395
|
"windows-sys 0.61.2",
|
|
1366
1396
|
]
|
|
1367
1397
|
|
|
@@ -1729,6 +1759,16 @@ dependencies = [
|
|
|
1729
1759
|
"cfg-if",
|
|
1730
1760
|
]
|
|
1731
1761
|
|
|
1762
|
+
[[package]]
|
|
1763
|
+
name = "tokio"
|
|
1764
|
+
version = "1.50.0"
|
|
1765
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1766
|
+
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
|
1767
|
+
dependencies = [
|
|
1768
|
+
"bytes",
|
|
1769
|
+
"pin-project-lite",
|
|
1770
|
+
]
|
|
1771
|
+
|
|
1732
1772
|
[[package]]
|
|
1733
1773
|
name = "tracing"
|
|
1734
1774
|
version = "0.1.44"
|
|
@@ -1824,6 +1864,23 @@ version = "0.9.5"
|
|
|
1824
1864
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1825
1865
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|
1826
1866
|
|
|
1867
|
+
[[package]]
|
|
1868
|
+
name = "vibeio"
|
|
1869
|
+
version = "0.2.5"
|
|
1870
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1871
|
+
checksum = "78dce705a708cacdd5822824342a8843abcceb9619e7079dc0e97b77dcc4ce1c"
|
|
1872
|
+
dependencies = [
|
|
1873
|
+
"crossbeam-queue",
|
|
1874
|
+
"futures-util",
|
|
1875
|
+
"io-uring 0.6.4",
|
|
1876
|
+
"libc",
|
|
1877
|
+
"mio",
|
|
1878
|
+
"oneshot",
|
|
1879
|
+
"slab",
|
|
1880
|
+
"tokio",
|
|
1881
|
+
"windows-sys 0.61.2",
|
|
1882
|
+
]
|
|
1883
|
+
|
|
1827
1884
|
[[package]]
|
|
1828
1885
|
name = "wasi"
|
|
1829
1886
|
version = "0.11.1+wasi-snapshot-preview1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "rsloop"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.7"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "An event loop for asyncio written in Rust"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -33,6 +33,8 @@ signal-hook = "0.4"
|
|
|
33
33
|
socket2 = "0.6"
|
|
34
34
|
|
|
35
35
|
[target.'cfg(windows)'.dependencies]
|
|
36
|
+
tokio = { version = "1", default-features = false, features = ["io-util"] }
|
|
37
|
+
vibeio = { version = "0.2.5", default-features = false }
|
|
36
38
|
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Networking_WinSock", "Win32_System_Pipes", "Win32_System_Threading"] }
|
|
37
39
|
|
|
38
40
|
[build-dependencies]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rsloop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -49,7 +49,7 @@ coroutines still run on the thread that calls `run_forever()` or
|
|
|
49
49
|
The package exposes:
|
|
50
50
|
|
|
51
51
|
- a native extension module at `rsloop._loop`
|
|
52
|
-
- a Python wrapper in `python/rsloop/__init__.py`
|
|
52
|
+
- a Python wrapper in [`python/rsloop/__init__.py`](./python/rsloop/__init__.py)
|
|
53
53
|
- `rsloop.Loop`, `rsloop.new_event_loop()`, and `rsloop.run(...)`
|
|
54
54
|
|
|
55
55
|
Repository metadata currently targets Python `>=3.8`. The packaged project now
|
|
@@ -100,7 +100,7 @@ finally:
|
|
|
100
100
|
|
|
101
101
|
Importing `rsloop` also patches `asyncio.set_event_loop()` so Python 3.8 can
|
|
102
102
|
accept an `rsloop.Loop` instance, matching the behavior exercised by
|
|
103
|
-
`tests/test_run.py
|
|
103
|
+
[`tests/test_run.py`](./tests/test_run.py).
|
|
104
104
|
|
|
105
105
|
## Verified Surface Area
|
|
106
106
|
|
|
@@ -177,8 +177,9 @@ The native fast-stream path is used only when:
|
|
|
177
177
|
|
|
178
178
|
Otherwise `rsloop` falls back to the stdlib `asyncio.streams` helpers.
|
|
179
179
|
|
|
180
|
-
The implementation lives in `src/fast_streams.rs` and
|
|
181
|
-
level transport code in
|
|
180
|
+
The implementation lives in [`src/fast_streams.rs`](./src/fast_streams.rs) and
|
|
181
|
+
is backed by the lower level transport code in
|
|
182
|
+
[`src/stream_transport.rs`](./src/stream_transport.rs).
|
|
182
183
|
|
|
183
184
|
## Runtime Model
|
|
184
185
|
|
|
@@ -239,9 +240,9 @@ Build release wheels into `dist/wheels`:
|
|
|
239
240
|
scripts/build-wheels.sh
|
|
240
241
|
```
|
|
241
242
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
locate interpreters.
|
|
243
|
+
[`scripts/build-wheels.sh`](./scripts/build-wheels.sh) currently defaults to
|
|
244
|
+
CPython `3.8 3.9 3.10 3.11 3.12 3.13 3.14` plus free-threaded `3.14t`, and
|
|
245
|
+
uses `uv python install` / `uv python find` to locate interpreters.
|
|
245
246
|
|
|
246
247
|
## Profiling
|
|
247
248
|
|
|
@@ -290,12 +291,19 @@ uv run python examples/04_unix_and_accepted_socket.py
|
|
|
290
291
|
uv run python examples/05_pipes_signals_subprocesses.py
|
|
291
292
|
```
|
|
292
293
|
|
|
294
|
+
Example files:
|
|
295
|
+
[`examples/01_basics.py`](./examples/01_basics.py),
|
|
296
|
+
[`examples/02_fd_and_sockets.py`](./examples/02_fd_and_sockets.py),
|
|
297
|
+
[`examples/03_streams.py`](./examples/03_streams.py),
|
|
298
|
+
[`examples/04_unix_and_accepted_socket.py`](./examples/04_unix_and_accepted_socket.py),
|
|
299
|
+
[`examples/05_pipes_signals_subprocesses.py`](./examples/05_pipes_signals_subprocesses.py).
|
|
300
|
+
|
|
293
301
|
The repository also includes:
|
|
294
302
|
|
|
295
|
-
- `demo/fastapi_service.py` for running the same
|
|
296
|
-
`asyncio`, `uvloop`, or `rsloop`
|
|
297
|
-
- `benchmarks/compare_event_loops.py`
|
|
298
|
-
comparisons
|
|
303
|
+
- [`demo/fastapi_service.py`](./demo/fastapi_service.py) for running the same
|
|
304
|
+
FastAPI app on stdlib `asyncio`, `uvloop`, or `rsloop`
|
|
305
|
+
- [`benchmarks/compare_event_loops.py`](./benchmarks/compare_event_loops.py)
|
|
306
|
+
for callback, task, and TCP stream comparisons
|
|
299
307
|
|
|
300
308
|
## Benchmark
|
|
301
309
|
|
|
@@ -304,6 +312,42 @@ uv run --with maturin maturin develop --release
|
|
|
304
312
|
uv run --with uvloop python benchmarks/compare_event_loops.py
|
|
305
313
|
```
|
|
306
314
|
|
|
307
|
-
|
|
308
|
-
|
|
315
|
+
An example output from that script on Linux with CPython 3.14.0:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
callbacks (200,000 ops)
|
|
319
|
+
loop median_s best_s ops_per_s vs_fastest
|
|
320
|
+
rsloop 0.041608 0.040585 4,806,807 1.00x
|
|
321
|
+
uvloop 0.087539 0.086690 2,284,707 2.10x
|
|
322
|
+
asyncio 0.229563 0.221348 871,222 5.52x
|
|
323
|
+
|
|
324
|
+
tasks (50,000 ops)
|
|
325
|
+
loop median_s best_s ops_per_s vs_fastest
|
|
326
|
+
uvloop 0.084425 0.083497 592,239 1.00x
|
|
327
|
+
rsloop 0.091845 0.090982 544,397 1.09x
|
|
328
|
+
asyncio 0.138782 0.137716 360,276 1.64x
|
|
329
|
+
tcp_streams mode: rsloop native fast streams
|
|
330
|
+
|
|
331
|
+
tcp_streams (5,000 ops)
|
|
332
|
+
loop median_s best_s ops_per_s vs_fastest
|
|
333
|
+
rsloop 0.119483 0.118451 41,847 1.00x
|
|
334
|
+
uvloop 0.119582 0.116446 41,812 1.00x
|
|
335
|
+
asyncio 0.138408 0.134438 36,125 1.16x
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
See [`benchmarks/README.md`](./benchmarks/README.md) for workload details and
|
|
339
|
+
extra flags, and [`demo/README.md`](./demo/README.md) for the FastAPI loop
|
|
340
|
+
comparison demo.
|
|
341
|
+
|
|
342
|
+
## Acknowledgements
|
|
343
|
+
|
|
344
|
+
`rsloop` builds on the Python `asyncio` model and is implemented with
|
|
345
|
+
[PyO3](https://pyo3.rs/) on the Rust side. The runtime and I/O work in the
|
|
346
|
+
current implementation rely in part on
|
|
347
|
+
[compio](https://github.com/compio-rs/compio).
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
This project is licensed under the Apache License, Version 2.0. See
|
|
352
|
+
[`LICENSE`](./LICENSE) for the full text.
|
|
309
353
|
|
|
@@ -17,7 +17,7 @@ coroutines still run on the thread that calls `run_forever()` or
|
|
|
17
17
|
The package exposes:
|
|
18
18
|
|
|
19
19
|
- a native extension module at `rsloop._loop`
|
|
20
|
-
- a Python wrapper in `python/rsloop/__init__.py`
|
|
20
|
+
- a Python wrapper in [`python/rsloop/__init__.py`](./python/rsloop/__init__.py)
|
|
21
21
|
- `rsloop.Loop`, `rsloop.new_event_loop()`, and `rsloop.run(...)`
|
|
22
22
|
|
|
23
23
|
Repository metadata currently targets Python `>=3.8`. The packaged project now
|
|
@@ -68,7 +68,7 @@ finally:
|
|
|
68
68
|
|
|
69
69
|
Importing `rsloop` also patches `asyncio.set_event_loop()` so Python 3.8 can
|
|
70
70
|
accept an `rsloop.Loop` instance, matching the behavior exercised by
|
|
71
|
-
`tests/test_run.py
|
|
71
|
+
[`tests/test_run.py`](./tests/test_run.py).
|
|
72
72
|
|
|
73
73
|
## Verified Surface Area
|
|
74
74
|
|
|
@@ -145,8 +145,9 @@ The native fast-stream path is used only when:
|
|
|
145
145
|
|
|
146
146
|
Otherwise `rsloop` falls back to the stdlib `asyncio.streams` helpers.
|
|
147
147
|
|
|
148
|
-
The implementation lives in `src/fast_streams.rs` and
|
|
149
|
-
level transport code in
|
|
148
|
+
The implementation lives in [`src/fast_streams.rs`](./src/fast_streams.rs) and
|
|
149
|
+
is backed by the lower level transport code in
|
|
150
|
+
[`src/stream_transport.rs`](./src/stream_transport.rs).
|
|
150
151
|
|
|
151
152
|
## Runtime Model
|
|
152
153
|
|
|
@@ -207,9 +208,9 @@ Build release wheels into `dist/wheels`:
|
|
|
207
208
|
scripts/build-wheels.sh
|
|
208
209
|
```
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
locate interpreters.
|
|
211
|
+
[`scripts/build-wheels.sh`](./scripts/build-wheels.sh) currently defaults to
|
|
212
|
+
CPython `3.8 3.9 3.10 3.11 3.12 3.13 3.14` plus free-threaded `3.14t`, and
|
|
213
|
+
uses `uv python install` / `uv python find` to locate interpreters.
|
|
213
214
|
|
|
214
215
|
## Profiling
|
|
215
216
|
|
|
@@ -258,12 +259,19 @@ uv run python examples/04_unix_and_accepted_socket.py
|
|
|
258
259
|
uv run python examples/05_pipes_signals_subprocesses.py
|
|
259
260
|
```
|
|
260
261
|
|
|
262
|
+
Example files:
|
|
263
|
+
[`examples/01_basics.py`](./examples/01_basics.py),
|
|
264
|
+
[`examples/02_fd_and_sockets.py`](./examples/02_fd_and_sockets.py),
|
|
265
|
+
[`examples/03_streams.py`](./examples/03_streams.py),
|
|
266
|
+
[`examples/04_unix_and_accepted_socket.py`](./examples/04_unix_and_accepted_socket.py),
|
|
267
|
+
[`examples/05_pipes_signals_subprocesses.py`](./examples/05_pipes_signals_subprocesses.py).
|
|
268
|
+
|
|
261
269
|
The repository also includes:
|
|
262
270
|
|
|
263
|
-
- `demo/fastapi_service.py` for running the same
|
|
264
|
-
`asyncio`, `uvloop`, or `rsloop`
|
|
265
|
-
- `benchmarks/compare_event_loops.py`
|
|
266
|
-
comparisons
|
|
271
|
+
- [`demo/fastapi_service.py`](./demo/fastapi_service.py) for running the same
|
|
272
|
+
FastAPI app on stdlib `asyncio`, `uvloop`, or `rsloop`
|
|
273
|
+
- [`benchmarks/compare_event_loops.py`](./benchmarks/compare_event_loops.py)
|
|
274
|
+
for callback, task, and TCP stream comparisons
|
|
267
275
|
|
|
268
276
|
## Benchmark
|
|
269
277
|
|
|
@@ -272,5 +280,41 @@ uv run --with maturin maturin develop --release
|
|
|
272
280
|
uv run --with uvloop python benchmarks/compare_event_loops.py
|
|
273
281
|
```
|
|
274
282
|
|
|
275
|
-
|
|
276
|
-
|
|
283
|
+
An example output from that script on Linux with CPython 3.14.0:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
callbacks (200,000 ops)
|
|
287
|
+
loop median_s best_s ops_per_s vs_fastest
|
|
288
|
+
rsloop 0.041608 0.040585 4,806,807 1.00x
|
|
289
|
+
uvloop 0.087539 0.086690 2,284,707 2.10x
|
|
290
|
+
asyncio 0.229563 0.221348 871,222 5.52x
|
|
291
|
+
|
|
292
|
+
tasks (50,000 ops)
|
|
293
|
+
loop median_s best_s ops_per_s vs_fastest
|
|
294
|
+
uvloop 0.084425 0.083497 592,239 1.00x
|
|
295
|
+
rsloop 0.091845 0.090982 544,397 1.09x
|
|
296
|
+
asyncio 0.138782 0.137716 360,276 1.64x
|
|
297
|
+
tcp_streams mode: rsloop native fast streams
|
|
298
|
+
|
|
299
|
+
tcp_streams (5,000 ops)
|
|
300
|
+
loop median_s best_s ops_per_s vs_fastest
|
|
301
|
+
rsloop 0.119483 0.118451 41,847 1.00x
|
|
302
|
+
uvloop 0.119582 0.116446 41,812 1.00x
|
|
303
|
+
asyncio 0.138408 0.134438 36,125 1.16x
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
See [`benchmarks/README.md`](./benchmarks/README.md) for workload details and
|
|
307
|
+
extra flags, and [`demo/README.md`](./demo/README.md) for the FastAPI loop
|
|
308
|
+
comparison demo.
|
|
309
|
+
|
|
310
|
+
## Acknowledgements
|
|
311
|
+
|
|
312
|
+
`rsloop` builds on the Python `asyncio` model and is implemented with
|
|
313
|
+
[PyO3](https://pyo3.rs/) on the Rust side. The runtime and I/O work in the
|
|
314
|
+
current implementation rely in part on
|
|
315
|
+
[compio](https://github.com/compio-rs/compio).
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
This project is licensed under the Apache License, Version 2.0. See
|
|
320
|
+
[`LICENSE`](./LICENSE) for the full text.
|
|
@@ -13,6 +13,8 @@ uv run --with maturin maturin develop --release
|
|
|
13
13
|
uv run --with uvloop python benchmarks/compare_event_loops.py
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
Benchmark runner: [`benchmarks/compare_event_loops.py`](./compare_event_loops.py)
|
|
17
|
+
|
|
16
18
|
The Rust prototype should be installed in release mode before benchmarking.
|
|
17
19
|
Using the default debug build will heavily skew the comparison against
|
|
18
20
|
`asyncio` and `uvloop`.
|
|
@@ -42,7 +44,7 @@ uv run --with uvloop python benchmarks/compare_event_loops.py \
|
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
This writes one SVG per workload, such as
|
|
45
|
-
`benchmarks/profiles/rsloop-callbacks.svg
|
|
47
|
+
[`benchmarks/profiles/rsloop-callbacks.svg`](./profiles/rsloop-callbacks.svg), before the warmup and measured
|
|
46
48
|
passes.
|
|
47
49
|
|
|
48
50
|
The runner executes each loop/workload in a fresh subprocess and reports:
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
5
|
import asyncio
|
|
6
|
+
import ctypes
|
|
7
|
+
from ctypes import wintypes
|
|
6
8
|
import gc
|
|
7
9
|
import importlib
|
|
8
10
|
import json
|
|
@@ -34,6 +36,7 @@ class ChildResult:
|
|
|
34
36
|
workload: str
|
|
35
37
|
seconds: float
|
|
36
38
|
operations: int
|
|
39
|
+
peak_rss_bytes: int
|
|
37
40
|
|
|
38
41
|
@property
|
|
39
42
|
def ops_per_sec(self) -> float:
|
|
@@ -195,6 +198,65 @@ def is_loop_available(loop_name: str) -> tuple[bool, str | None]:
|
|
|
195
198
|
return True, None
|
|
196
199
|
|
|
197
200
|
|
|
201
|
+
def get_peak_rss_bytes() -> int:
|
|
202
|
+
if sys.platform == "win32":
|
|
203
|
+
class PROCESS_MEMORY_COUNTERS(ctypes.Structure):
|
|
204
|
+
_fields_ = [
|
|
205
|
+
("cb", wintypes.DWORD),
|
|
206
|
+
("PageFaultCount", wintypes.DWORD),
|
|
207
|
+
("PeakWorkingSetSize", ctypes.c_size_t),
|
|
208
|
+
("WorkingSetSize", ctypes.c_size_t),
|
|
209
|
+
("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
|
|
210
|
+
("QuotaPagedPoolUsage", ctypes.c_size_t),
|
|
211
|
+
("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
|
|
212
|
+
("QuotaNonPagedPoolUsage", ctypes.c_size_t),
|
|
213
|
+
("PagefileUsage", ctypes.c_size_t),
|
|
214
|
+
("PeakPagefileUsage", ctypes.c_size_t),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
218
|
+
psapi = ctypes.WinDLL("psapi", use_last_error=True)
|
|
219
|
+
get_current_process = kernel32.GetCurrentProcess
|
|
220
|
+
get_current_process.restype = wintypes.HANDLE
|
|
221
|
+
|
|
222
|
+
get_process_memory_info = psapi.GetProcessMemoryInfo
|
|
223
|
+
get_process_memory_info.argtypes = [
|
|
224
|
+
wintypes.HANDLE,
|
|
225
|
+
ctypes.POINTER(PROCESS_MEMORY_COUNTERS),
|
|
226
|
+
wintypes.DWORD,
|
|
227
|
+
]
|
|
228
|
+
get_process_memory_info.restype = wintypes.BOOL
|
|
229
|
+
|
|
230
|
+
counters = PROCESS_MEMORY_COUNTERS()
|
|
231
|
+
counters.cb = ctypes.sizeof(PROCESS_MEMORY_COUNTERS)
|
|
232
|
+
if not get_process_memory_info(
|
|
233
|
+
get_current_process(),
|
|
234
|
+
ctypes.byref(counters),
|
|
235
|
+
counters.cb,
|
|
236
|
+
):
|
|
237
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
238
|
+
return int(counters.PeakWorkingSetSize)
|
|
239
|
+
|
|
240
|
+
import resource
|
|
241
|
+
|
|
242
|
+
peak_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
243
|
+
if sys.platform == "darwin":
|
|
244
|
+
return int(peak_rss)
|
|
245
|
+
return int(peak_rss * 1024)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def format_bytes(num_bytes: int) -> str:
|
|
249
|
+
units = ["B", "KiB", "MiB", "GiB", "TiB"]
|
|
250
|
+
value = float(num_bytes)
|
|
251
|
+
for unit in units:
|
|
252
|
+
if value < 1024.0 or unit == units[-1]:
|
|
253
|
+
if unit == "B":
|
|
254
|
+
return f"{int(value)} {unit}"
|
|
255
|
+
return f"{value:.1f} {unit}"
|
|
256
|
+
value /= 1024.0
|
|
257
|
+
raise AssertionError("unreachable")
|
|
258
|
+
|
|
259
|
+
|
|
198
260
|
def run_with_loop(loop_name: str, coro: asyncio.coroutines) -> ChildResult:
|
|
199
261
|
loop_factory = loop_factory_for(loop_name)
|
|
200
262
|
if sys.version_info[:2] >= (3, 12):
|
|
@@ -225,7 +287,13 @@ async def bench_callbacks(loop_name: str, iterations: int) -> ChildResult:
|
|
|
225
287
|
start = time.perf_counter()
|
|
226
288
|
loop.call_soon(callback)
|
|
227
289
|
await done
|
|
228
|
-
return ChildResult(
|
|
290
|
+
return ChildResult(
|
|
291
|
+
loop_name,
|
|
292
|
+
"callbacks",
|
|
293
|
+
time.perf_counter() - start,
|
|
294
|
+
iterations,
|
|
295
|
+
peak_rss_bytes=0,
|
|
296
|
+
)
|
|
229
297
|
|
|
230
298
|
|
|
231
299
|
async def bench_tasks(loop_name: str, iterations: int, batch_size: int) -> ChildResult:
|
|
@@ -238,7 +306,13 @@ async def bench_tasks(loop_name: str, iterations: int, batch_size: int) -> Child
|
|
|
238
306
|
current_batch = min(batch_size, remaining)
|
|
239
307
|
await asyncio.gather(*(tiny_task() for _ in range(current_batch)))
|
|
240
308
|
remaining -= current_batch
|
|
241
|
-
return ChildResult(
|
|
309
|
+
return ChildResult(
|
|
310
|
+
loop_name,
|
|
311
|
+
"tasks",
|
|
312
|
+
time.perf_counter() - start,
|
|
313
|
+
iterations,
|
|
314
|
+
peak_rss_bytes=0,
|
|
315
|
+
)
|
|
242
316
|
|
|
243
317
|
|
|
244
318
|
async def maybe_wait_closed(writer: asyncio.StreamWriter) -> None:
|
|
@@ -285,7 +359,13 @@ async def bench_tcp_streams(
|
|
|
285
359
|
duration = time.perf_counter() - start
|
|
286
360
|
writer.close()
|
|
287
361
|
await maybe_wait_closed(writer)
|
|
288
|
-
return ChildResult(
|
|
362
|
+
return ChildResult(
|
|
363
|
+
loop_name,
|
|
364
|
+
"tcp_streams",
|
|
365
|
+
duration,
|
|
366
|
+
roundtrips,
|
|
367
|
+
peak_rss_bytes=0,
|
|
368
|
+
)
|
|
289
369
|
finally:
|
|
290
370
|
server.close()
|
|
291
371
|
await server.wait_closed()
|
|
@@ -315,6 +395,14 @@ def child_main(args: argparse.Namespace) -> int:
|
|
|
315
395
|
finally:
|
|
316
396
|
gc.enable()
|
|
317
397
|
|
|
398
|
+
result = ChildResult(
|
|
399
|
+
loop=result.loop,
|
|
400
|
+
workload=result.workload,
|
|
401
|
+
seconds=result.seconds,
|
|
402
|
+
operations=result.operations,
|
|
403
|
+
peak_rss_bytes=get_peak_rss_bytes(),
|
|
404
|
+
)
|
|
405
|
+
|
|
318
406
|
print(
|
|
319
407
|
json.dumps(
|
|
320
408
|
{
|
|
@@ -323,6 +411,7 @@ def child_main(args: argparse.Namespace) -> int:
|
|
|
323
411
|
"seconds": result.seconds,
|
|
324
412
|
"operations": result.operations,
|
|
325
413
|
"ops_per_sec": result.ops_per_sec,
|
|
414
|
+
"peak_rss_bytes": result.peak_rss_bytes,
|
|
326
415
|
}
|
|
327
416
|
)
|
|
328
417
|
)
|
|
@@ -387,19 +476,31 @@ def run_child(
|
|
|
387
476
|
workload=payload["workload"],
|
|
388
477
|
seconds=payload["seconds"],
|
|
389
478
|
operations=payload["operations"],
|
|
479
|
+
peak_rss_bytes=payload["peak_rss_bytes"],
|
|
390
480
|
)
|
|
391
481
|
|
|
392
482
|
|
|
393
483
|
def print_workload_table(workload: str, runs: dict[str, list[ChildResult]]) -> None:
|
|
394
|
-
rows: list[tuple[str, float, float, float, int]] = []
|
|
484
|
+
rows: list[tuple[str, float, float, float, int, int]] = []
|
|
395
485
|
for loop_name, loop_runs in runs.items():
|
|
396
486
|
seconds = [item.seconds for item in loop_runs]
|
|
487
|
+
peak_rss_values = [item.peak_rss_bytes for item in loop_runs]
|
|
397
488
|
median_seconds = statistics.median(seconds)
|
|
489
|
+
median_peak_rss = int(statistics.median(peak_rss_values))
|
|
398
490
|
operations = loop_runs[0].operations
|
|
399
491
|
ops_per_sec = (
|
|
400
492
|
operations / median_seconds if median_seconds > 0 else float("inf")
|
|
401
493
|
)
|
|
402
|
-
rows.append(
|
|
494
|
+
rows.append(
|
|
495
|
+
(
|
|
496
|
+
loop_name,
|
|
497
|
+
median_seconds,
|
|
498
|
+
ops_per_sec,
|
|
499
|
+
min(seconds),
|
|
500
|
+
operations,
|
|
501
|
+
median_peak_rss,
|
|
502
|
+
)
|
|
503
|
+
)
|
|
403
504
|
|
|
404
505
|
rows.sort(key=lambda item: item[1])
|
|
405
506
|
fastest = rows[0][1]
|
|
@@ -407,15 +508,16 @@ def print_workload_table(workload: str, runs: dict[str, list[ChildResult]]) -> N
|
|
|
407
508
|
print()
|
|
408
509
|
print(f"{workload} ({rows[0][4]:,} ops)")
|
|
409
510
|
print(
|
|
410
|
-
f"{'loop':<10} {'median_s':>12} {'best_s':>12} {'ops_per_s':>14} {'vs_fastest':>12}"
|
|
511
|
+
f"{'loop':<10} {'median_s':>12} {'best_s':>12} {'ops_per_s':>14} {'peak_rss':>12} {'vs_fastest':>12}"
|
|
411
512
|
)
|
|
412
|
-
for loop_name, median_seconds, ops_per_sec, best_seconds, _ in rows:
|
|
513
|
+
for loop_name, median_seconds, ops_per_sec, best_seconds, _, median_peak_rss in rows:
|
|
413
514
|
relative = median_seconds / fastest if fastest > 0 else 1.0
|
|
414
515
|
print(
|
|
415
516
|
f"{loop_name:<10} "
|
|
416
517
|
f"{median_seconds:>12.6f} "
|
|
417
518
|
f"{best_seconds:>12.6f} "
|
|
418
519
|
f"{ops_per_sec:>14,.0f} "
|
|
520
|
+
f"{format_bytes(median_peak_rss):>12} "
|
|
419
521
|
f"{relative:>11.2f}x"
|
|
420
522
|
)
|
|
421
523
|
|
|
@@ -489,6 +591,7 @@ def parent_main(args: argparse.Namespace) -> int:
|
|
|
489
591
|
"seconds": item.seconds,
|
|
490
592
|
"operations": item.operations,
|
|
491
593
|
"ops_per_sec": item.ops_per_sec,
|
|
594
|
+
"peak_rss_bytes": item.peak_rss_bytes,
|
|
492
595
|
}
|
|
493
596
|
for item in measured
|
|
494
597
|
],
|
|
@@ -30,6 +30,8 @@ uv run --with fastapi --with uvicorn --with uvloop python demo/fastapi_service.p
|
|
|
30
30
|
uv run --with fastapi --with uvicorn --with uvloop python demo/fastapi_service.py --event-loop rsloop
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
Service entrypoint: [`demo/fastapi_service.py`](./fastapi_service.py)
|
|
34
|
+
|
|
33
35
|
`std-async` is also accepted as an alias for stdlib `asyncio`:
|
|
34
36
|
|
|
35
37
|
```bash
|