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.
Files changed (48) hide show
  1. {rsloop-0.1.6 → rsloop-0.1.7}/Cargo.lock +60 -3
  2. {rsloop-0.1.6 → rsloop-0.1.7}/Cargo.toml +3 -1
  3. {rsloop-0.1.6 → rsloop-0.1.7}/PKG-INFO +58 -14
  4. {rsloop-0.1.6 → rsloop-0.1.7}/README.md +57 -13
  5. {rsloop-0.1.6 → rsloop-0.1.7}/benchmarks/README.md +3 -1
  6. {rsloop-0.1.6 → rsloop-0.1.7}/benchmarks/compare_event_loops.py +110 -7
  7. {rsloop-0.1.6 → rsloop-0.1.7}/demo/README.md +2 -0
  8. {rsloop-0.1.6 → rsloop-0.1.7}/pyproject.toml +1 -1
  9. {rsloop-0.1.6 → rsloop-0.1.7}/src/fd_ops.rs +78 -18
  10. {rsloop-0.1.6 → rsloop-0.1.7}/src/lib.rs +2 -0
  11. {rsloop-0.1.6 → rsloop-0.1.7}/src/loop_core.rs +5 -0
  12. {rsloop-0.1.6 → rsloop-0.1.7}/src/runtime.rs +218 -19
  13. {rsloop-0.1.6 → rsloop-0.1.7}/src/stream_transport.rs +141 -6
  14. rsloop-0.1.7/src/windows_vibeio.rs +137 -0
  15. {rsloop-0.1.6 → rsloop-0.1.7}/uv.lock +1 -1
  16. {rsloop-0.1.6 → rsloop-0.1.7}/.github/workflows/tests.yml +0 -0
  17. {rsloop-0.1.6 → rsloop-0.1.7}/.github/workflows/wheels.yml +0 -0
  18. {rsloop-0.1.6 → rsloop-0.1.7}/.gitignore +0 -0
  19. {rsloop-0.1.6 → rsloop-0.1.7}/LICENSE +0 -0
  20. {rsloop-0.1.6 → rsloop-0.1.7}/build.rs +0 -0
  21. {rsloop-0.1.6 → rsloop-0.1.7}/demo/fastapi_service.py +0 -0
  22. {rsloop-0.1.6 → rsloop-0.1.7}/examples/01_basics.py +0 -0
  23. {rsloop-0.1.6 → rsloop-0.1.7}/examples/02_fd_and_sockets.py +0 -0
  24. {rsloop-0.1.6 → rsloop-0.1.7}/examples/03_streams.py +0 -0
  25. {rsloop-0.1.6 → rsloop-0.1.7}/examples/04_unix_and_accepted_socket.py +0 -0
  26. {rsloop-0.1.6 → rsloop-0.1.7}/examples/05_pipes_signals_subprocesses.py +0 -0
  27. {rsloop-0.1.6 → rsloop-0.1.7}/justfile +0 -0
  28. {rsloop-0.1.6 → rsloop-0.1.7}/python/rsloop/__init__.py +0 -0
  29. {rsloop-0.1.6 → rsloop-0.1.7}/rsloop.png +0 -0
  30. {rsloop-0.1.6 → rsloop-0.1.7}/scripts/build-wheels.sh +0 -0
  31. {rsloop-0.1.6 → rsloop-0.1.7}/scripts/generate-test-tls-certs.sh +0 -0
  32. {rsloop-0.1.6 → rsloop-0.1.7}/scripts/python-versions.sh +0 -0
  33. {rsloop-0.1.6 → rsloop-0.1.7}/scripts/test-supported-pythons.sh +0 -0
  34. {rsloop-0.1.6 → rsloop-0.1.7}/src/async_event.rs +0 -0
  35. {rsloop-0.1.6 → rsloop-0.1.7}/src/blocking.rs +0 -0
  36. {rsloop-0.1.6 → rsloop-0.1.7}/src/callbacks.rs +0 -0
  37. {rsloop-0.1.6 → rsloop-0.1.7}/src/context.rs +0 -0
  38. {rsloop-0.1.6 → rsloop-0.1.7}/src/errors.rs +0 -0
  39. {rsloop-0.1.6 → rsloop-0.1.7}/src/fast_streams.rs +0 -0
  40. {rsloop-0.1.6 → rsloop-0.1.7}/src/process_transport.rs +0 -0
  41. {rsloop-0.1.6 → rsloop-0.1.7}/src/profiler.rs +0 -0
  42. {rsloop-0.1.6 → rsloop-0.1.7}/src/python_api.rs +0 -0
  43. {rsloop-0.1.6 → rsloop-0.1.7}/src/python_names.rs +0 -0
  44. {rsloop-0.1.6 → rsloop-0.1.7}/src/tls.rs +0 -0
  45. {rsloop-0.1.6 → rsloop-0.1.7}/tests/packages/aiohttp_test.py +0 -0
  46. {rsloop-0.1.6 → rsloop-0.1.7}/tests/test_compat.py +0 -0
  47. {rsloop-0.1.6 → rsloop-0.1.7}/tests/test_run.py +0 -0
  48. {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.6"
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.6"
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.6
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 is backed by the lower
181
- level transport code in `src/stream_transport.rs`.
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
- That script currently defaults to CPython `3.8 3.9 3.10 3.11 3.12 3.13 3.14`
243
- plus free-threaded `3.14t`, and uses `uv python install` / `uv python find` to
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 FastAPI app on stdlib
296
- `asyncio`, `uvloop`, or `rsloop`
297
- - `benchmarks/compare_event_loops.py` for callback, task, and TCP stream
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
- See `benchmarks/README.md` for workload details and extra flags, and
308
- `demo/README.md` for the FastAPI loop comparison demo.
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 is backed by the lower
149
- level transport code in `src/stream_transport.rs`.
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
- That script currently defaults to CPython `3.8 3.9 3.10 3.11 3.12 3.13 3.14`
211
- plus free-threaded `3.14t`, and uses `uv python install` / `uv python find` to
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 FastAPI app on stdlib
264
- `asyncio`, `uvloop`, or `rsloop`
265
- - `benchmarks/compare_event_loops.py` for callback, task, and TCP stream
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
- See `benchmarks/README.md` for workload details and extra flags, and
276
- `demo/README.md` for the FastAPI loop comparison demo.
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`, before the warmup and measured
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(loop_name, "callbacks", time.perf_counter() - start, iterations)
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(loop_name, "tasks", time.perf_counter() - start, iterations)
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(loop_name, "tcp_streams", duration, roundtrips)
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((loop_name, median_seconds, ops_per_sec, min(seconds), operations))
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
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "rsloop"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "An event loop for asyncio written in Rust"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }