rsloop 0.1.23__tar.gz → 0.1.25__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 (88) hide show
  1. {rsloop-0.1.23 → rsloop-0.1.25}/Cargo.lock +31 -31
  2. {rsloop-0.1.23 → rsloop-0.1.25}/Cargo.toml +2 -2
  3. {rsloop-0.1.23 → rsloop-0.1.25}/PKG-INFO +14 -14
  4. {rsloop-0.1.23 → rsloop-0.1.25}/README.md +13 -13
  5. {rsloop-0.1.23 → rsloop-0.1.25}/benchmarks/README.md +1 -1
  6. {rsloop-0.1.23 → rsloop-0.1.25}/benchmarks/compare_event_loops.py +6 -3
  7. {rsloop-0.1.23 → rsloop-0.1.25}/pyproject.toml +1 -1
  8. {rsloop-0.1.23 → rsloop-0.1.25}/src/callbacks.rs +16 -22
  9. {rsloop-0.1.23 → rsloop-0.1.25}/src/fast_streams.rs +33 -20
  10. {rsloop-0.1.23 → rsloop-0.1.25}/src/fd_ops.rs +11 -1
  11. {rsloop-0.1.23 → rsloop-0.1.25}/src/loop_core/commands.rs +10 -10
  12. {rsloop-0.1.23 → rsloop-0.1.25}/src/loop_core.rs +164 -140
  13. rsloop-0.1.25/src/python_api/pre_exec.rs +118 -0
  14. {rsloop-0.1.23 → rsloop-0.1.25}/src/python_api.rs +150 -53
  15. {rsloop-0.1.23 → rsloop-0.1.25}/src/runtime.rs +46 -141
  16. {rsloop-0.1.23 → rsloop-0.1.25}/src/stream_transport.rs +161 -30
  17. {rsloop-0.1.23 → rsloop-0.1.25}/tests/test_compat.py +76 -0
  18. {rsloop-0.1.23 → rsloop-0.1.25}/uv.lock +410 -281
  19. rsloop-0.1.23/src/python_api/pre_exec.rs +0 -109
  20. {rsloop-0.1.23 → rsloop-0.1.25}/.cargo/config.toml +0 -0
  21. {rsloop-0.1.23 → rsloop-0.1.25}/.github/workflows/mkdocs.yml +0 -0
  22. {rsloop-0.1.23 → rsloop-0.1.25}/.github/workflows/tests.yml +0 -0
  23. {rsloop-0.1.23 → rsloop-0.1.25}/.github/workflows/wheels.yml +0 -0
  24. {rsloop-0.1.23 → rsloop-0.1.25}/.gitignore +0 -0
  25. {rsloop-0.1.23 → rsloop-0.1.25}/.vscode/settings.json +0 -0
  26. {rsloop-0.1.23 → rsloop-0.1.25}/LICENSE +0 -0
  27. {rsloop-0.1.23 → rsloop-0.1.25}/build.rs +0 -0
  28. {rsloop-0.1.23 → rsloop-0.1.25}/demo/README.md +0 -0
  29. {rsloop-0.1.23 → rsloop-0.1.25}/demo/fastapi_service.py +0 -0
  30. {rsloop-0.1.23 → rsloop-0.1.25}/demo/picows_server.py +0 -0
  31. {rsloop-0.1.23 → rsloop-0.1.25}/demo/picows_test.py +0 -0
  32. {rsloop-0.1.23 → rsloop-0.1.25}/demo/wsbench_websockets.py +0 -0
  33. {rsloop-0.1.23 → rsloop-0.1.25}/docs/development.md +0 -0
  34. {rsloop-0.1.23 → rsloop-0.1.25}/docs/examples.md +0 -0
  35. {rsloop-0.1.23 → rsloop-0.1.25}/docs/getting-started.md +0 -0
  36. {rsloop-0.1.23 → rsloop-0.1.25}/docs/how-it-works.md +0 -0
  37. {rsloop-0.1.23 → rsloop-0.1.25}/docs/index.md +0 -0
  38. {rsloop-0.1.23 → rsloop-0.1.25}/docs/project-structure.md +0 -0
  39. {rsloop-0.1.23 → rsloop-0.1.25}/docs/rust-extensions.md +0 -0
  40. {rsloop-0.1.23 → rsloop-0.1.25}/examples/01_basics.py +0 -0
  41. {rsloop-0.1.23 → rsloop-0.1.25}/examples/02_fd_and_sockets.py +0 -0
  42. {rsloop-0.1.23 → rsloop-0.1.25}/examples/03_streams.py +0 -0
  43. {rsloop-0.1.23 → rsloop-0.1.25}/examples/04_unix_and_accepted_socket.py +0 -0
  44. {rsloop-0.1.23 → rsloop-0.1.25}/examples/05_pipes_signals_subprocesses.py +0 -0
  45. {rsloop-0.1.23 → rsloop-0.1.25}/justfile +0 -0
  46. {rsloop-0.1.23 → rsloop-0.1.25}/mkdocs.yml +0 -0
  47. {rsloop-0.1.23 → rsloop-0.1.25}/python/rsloop/__init__.py +0 -0
  48. {rsloop-0.1.23 → rsloop-0.1.25}/python/rsloop/_bootstrap.py +0 -0
  49. {rsloop-0.1.23 → rsloop-0.1.25}/python/rsloop/_loop_compat.py +0 -0
  50. {rsloop-0.1.23 → rsloop-0.1.25}/python/rsloop/_profile.py +0 -0
  51. {rsloop-0.1.23 → rsloop-0.1.25}/python/rsloop/_run.py +0 -0
  52. {rsloop-0.1.23 → rsloop-0.1.25}/qualirs.toml +0 -0
  53. {rsloop-0.1.23 → rsloop-0.1.25}/rsloop.png +0 -0
  54. {rsloop-0.1.23 → rsloop-0.1.25}/scripts/build-wheels.sh +0 -0
  55. {rsloop-0.1.23 → rsloop-0.1.25}/scripts/generate-test-tls-certs.sh +0 -0
  56. {rsloop-0.1.23 → rsloop-0.1.25}/scripts/python-versions.sh +0 -0
  57. {rsloop-0.1.23 → rsloop-0.1.25}/scripts/test-supported-pythons.sh +0 -0
  58. {rsloop-0.1.23 → rsloop-0.1.25}/src/async_event.rs +0 -0
  59. {rsloop-0.1.23 → rsloop-0.1.25}/src/blocking.rs +0 -0
  60. {rsloop-0.1.23 → rsloop-0.1.25}/src/context.rs +0 -0
  61. {rsloop-0.1.23 → rsloop-0.1.25}/src/errors.rs +0 -0
  62. {rsloop-0.1.23 → rsloop-0.1.25}/src/fd_ops/windows.rs +0 -0
  63. {rsloop-0.1.23 → rsloop-0.1.25}/src/lib.rs +0 -0
  64. {rsloop-0.1.23 → rsloop-0.1.25}/src/module_init.rs +0 -0
  65. {rsloop-0.1.23 → rsloop-0.1.25}/src/process_transport.rs +0 -0
  66. {rsloop-0.1.23 → rsloop-0.1.25}/src/profiler.rs +0 -0
  67. {rsloop-0.1.23 → rsloop-0.1.25}/src/python_api/ffi_helpers.rs +0 -0
  68. {rsloop-0.1.23 → rsloop-0.1.25}/src/python_api/process_handles.rs +0 -0
  69. {rsloop-0.1.23 → rsloop-0.1.25}/src/python_names.rs +0 -0
  70. {rsloop-0.1.23 → rsloop-0.1.25}/src/runtime/timer_entry.rs +0 -0
  71. {rsloop-0.1.23 → rsloop-0.1.25}/src/rust_async.rs +0 -0
  72. {rsloop-0.1.23 → rsloop-0.1.25}/src/tls/material.rs +0 -0
  73. {rsloop-0.1.23 → rsloop-0.1.25}/src/tls.rs +0 -0
  74. {rsloop-0.1.23 → rsloop-0.1.25}/src/windows_vibeio.rs +0 -0
  75. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/_smoke.py +0 -0
  76. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/aiohttp_test.py +0 -0
  77. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/django_asgi_test.py +0 -0
  78. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/falcon_test.py +0 -0
  79. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/fastapi_test.py +0 -0
  80. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/faststream_test.py +0 -0
  81. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/litestar_granian_test.py +0 -0
  82. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/litestar_test.py +0 -0
  83. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/quart_test.py +0 -0
  84. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/sanic_test.py +0 -0
  85. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/starlette_test.py +0 -0
  86. {rsloop-0.1.23 → rsloop-0.1.25}/tests/packages/uvicorn_test.py +0 -0
  87. {rsloop-0.1.23 → rsloop-0.1.25}/tests/test_run.py +0 -0
  88. {rsloop-0.1.23 → rsloop-0.1.25}/tests/test_tls.py +0 -0
@@ -234,9 +234,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
234
234
 
235
235
  [[package]]
236
236
  name = "bytes"
237
- version = "1.12.0"
237
+ version = "1.12.1"
238
238
  source = "registry+https://github.com/rust-lang/crates.io-index"
239
- checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
239
+ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
240
240
 
241
241
  [[package]]
242
242
  name = "cc"
@@ -476,18 +476,18 @@ dependencies = [
476
476
 
477
477
  [[package]]
478
478
  name = "crossbeam-queue"
479
- version = "0.3.12"
479
+ version = "0.3.13"
480
480
  source = "registry+https://github.com/rust-lang/crates.io-index"
481
- checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
481
+ checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
482
482
  dependencies = [
483
483
  "crossbeam-utils",
484
484
  ]
485
485
 
486
486
  [[package]]
487
487
  name = "crossbeam-utils"
488
- version = "0.8.21"
488
+ version = "0.8.22"
489
489
  source = "registry+https://github.com/rust-lang/crates.io-index"
490
- checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
490
+ checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
491
491
 
492
492
  [[package]]
493
493
  name = "dunce"
@@ -581,9 +581,9 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "futures-channel"
584
- version = "0.3.32"
584
+ version = "0.3.33"
585
585
  source = "registry+https://github.com/rust-lang/crates.io-index"
586
- checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
586
+ checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
587
587
  dependencies = [
588
588
  "futures-core",
589
589
  "futures-sink",
@@ -591,15 +591,15 @@ dependencies = [
591
591
 
592
592
  [[package]]
593
593
  name = "futures-core"
594
- version = "0.3.32"
594
+ version = "0.3.33"
595
595
  source = "registry+https://github.com/rust-lang/crates.io-index"
596
- checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
596
+ checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
597
597
 
598
598
  [[package]]
599
599
  name = "futures-io"
600
- version = "0.3.32"
600
+ version = "0.3.33"
601
601
  source = "registry+https://github.com/rust-lang/crates.io-index"
602
- checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
602
+ checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
603
603
 
604
604
  [[package]]
605
605
  name = "futures-lite"
@@ -616,9 +616,9 @@ dependencies = [
616
616
 
617
617
  [[package]]
618
618
  name = "futures-macro"
619
- version = "0.3.32"
619
+ version = "0.3.33"
620
620
  source = "registry+https://github.com/rust-lang/crates.io-index"
621
- checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
621
+ checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
622
622
  dependencies = [
623
623
  "proc-macro2",
624
624
  "quote",
@@ -627,21 +627,21 @@ dependencies = [
627
627
 
628
628
  [[package]]
629
629
  name = "futures-sink"
630
- version = "0.3.32"
630
+ version = "0.3.33"
631
631
  source = "registry+https://github.com/rust-lang/crates.io-index"
632
- checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
632
+ checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
633
633
 
634
634
  [[package]]
635
635
  name = "futures-task"
636
- version = "0.3.32"
636
+ version = "0.3.33"
637
637
  source = "registry+https://github.com/rust-lang/crates.io-index"
638
- checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
638
+ checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
639
639
 
640
640
  [[package]]
641
641
  name = "futures-util"
642
- version = "0.3.32"
642
+ version = "0.3.33"
643
643
  source = "registry+https://github.com/rust-lang/crates.io-index"
644
- checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
644
+ checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
645
645
  dependencies = [
646
646
  "futures-channel",
647
647
  "futures-core",
@@ -842,15 +842,15 @@ dependencies = [
842
842
 
843
843
  [[package]]
844
844
  name = "memchr"
845
- version = "2.8.2"
845
+ version = "2.8.3"
846
846
  source = "registry+https://github.com/rust-lang/crates.io-index"
847
- checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
847
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
848
848
 
849
849
  [[package]]
850
850
  name = "mio"
851
- version = "1.2.1"
851
+ version = "1.2.2"
852
852
  source = "registry+https://github.com/rust-lang/crates.io-index"
853
- checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
853
+ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
854
854
  dependencies = [
855
855
  "libc",
856
856
  "log",
@@ -1116,7 +1116,7 @@ dependencies = [
1116
1116
 
1117
1117
  [[package]]
1118
1118
  name = "rsloop"
1119
- version = "0.1.23"
1119
+ version = "0.1.25"
1120
1120
  dependencies = [
1121
1121
  "async-std",
1122
1122
  "compio",
@@ -1384,9 +1384,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
1384
1384
 
1385
1385
  [[package]]
1386
1386
  name = "syn"
1387
- version = "2.0.118"
1387
+ version = "2.0.119"
1388
1388
  source = "registry+https://github.com/rust-lang/crates.io-index"
1389
- checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
1389
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
1390
1390
  dependencies = [
1391
1391
  "proc-macro2",
1392
1392
  "quote",
@@ -1431,9 +1431,9 @@ dependencies = [
1431
1431
 
1432
1432
  [[package]]
1433
1433
  name = "tokio"
1434
- version = "1.52.3"
1434
+ version = "1.53.0"
1435
1435
  source = "registry+https://github.com/rust-lang/crates.io-index"
1436
- checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
1436
+ checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee"
1437
1437
  dependencies = [
1438
1438
  "bytes",
1439
1439
  "pin-project-lite",
@@ -1541,9 +1541,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1541
1541
 
1542
1542
  [[package]]
1543
1543
  name = "vibeio"
1544
- version = "0.2.13"
1544
+ version = "0.2.15"
1545
1545
  source = "registry+https://github.com/rust-lang/crates.io-index"
1546
- checksum = "cfd556b0b3efd91cb88566b6aa6d0ceef475b37443b6d898e170ce9da73a2f67"
1546
+ checksum = "63b968e30d91bc5c14d23f9af184f26ea7f0d886ab309d11b37148b244d7dfa4"
1547
1547
  dependencies = [
1548
1548
  "crossbeam-queue",
1549
1549
  "futures-util",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rsloop"
3
- version = "0.1.23"
3
+ version = "0.1.25"
4
4
  edition = "2024"
5
5
  description = "An event loop for asyncio written in Rust"
6
6
  license = "Apache-2.0"
@@ -35,7 +35,7 @@ tracy-client = { version = "0.18", default-features = false, features = ["enable
35
35
 
36
36
  [target.'cfg(windows)'.dependencies]
37
37
  tokio = { version = "1", default-features = false, features = ["io-util"] }
38
- vibeio = { version = "0.2.13", default-features = false }
38
+ vibeio = { version = "0.2.15", default-features = false }
39
39
  windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Networking_WinSock", "Win32_System_Pipes", "Win32_System_Threading"] }
40
40
 
41
41
  [build-dependencies]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rsloop
3
- Version: 0.1.23
3
+ Version: 0.1.25
4
4
  Classifier: Development Status :: 3 - Alpha
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: Apache Software License
@@ -379,26 +379,26 @@ uv run --with maturin maturin develop --release
379
379
  uv run --with uvloop python benchmarks/compare_event_loops.py
380
380
  ```
381
381
 
382
- An example output from that script on Linux with CPython 3.14.0:
382
+ An example output from that script on macOS (arm64) with CPython 3.14:
383
383
 
384
384
  ```
385
385
  callbacks (200,000 ops)
386
- loop median_s best_s ops_per_s vs_fastest
387
- rsloop 0.041608 0.040585 4,806,807 1.00x
388
- uvloop 0.087539 0.086690 2,284,707 2.10x
389
- asyncio 0.229563 0.221348 871,222 5.52x
386
+ loop median_s best_s ops_per_s peak_rss vs_fastest
387
+ rsloop 0.033083 0.032710 6,045,401 67.5 MiB 1.00x
388
+ uvloop 0.040958 0.040721 4,883,026 72.8 MiB 1.24x
389
+ asyncio 0.082233 0.082093 2,432,114 65.3 MiB 2.49x
390
390
 
391
391
  tasks (50,000 ops)
392
- loop median_s best_s ops_per_s vs_fastest
393
- uvloop 0.084425 0.083497 592,239 1.00x
394
- rsloop 0.091845 0.090982 544,397 1.09x
395
- asyncio 0.138782 0.137716 360,276 1.64x
392
+ loop median_s best_s ops_per_s peak_rss vs_fastest
393
+ rsloop 0.063593 0.063286 786,247 37.6 MiB 1.00x
394
+ uvloop 0.069614 0.069420 718,251 38.4 MiB 1.09x
395
+ asyncio 0.108114 0.107502 462,473 36.1 MiB 1.70x
396
396
 
397
397
  tcp_streams (5,000 ops)
398
- loop median_s best_s ops_per_s vs_fastest
399
- rsloop 0.119483 0.118451 41,847 1.00x
400
- uvloop 0.119582 0.116446 41,812 1.00x
401
- asyncio 0.138408 0.134438 36,125 1.16x
398
+ loop median_s best_s ops_per_s peak_rss vs_fastest
399
+ rsloop 0.090940 0.083355 54,981 32.2 MiB 1.00x
400
+ uvloop 0.133182 0.127404 37,543 31.5 MiB 1.46x
401
+ asyncio 0.302337 0.299813 16,538 29.6 MiB 3.32x
402
402
  ```
403
403
 
404
404
  See [`benchmarks/README.md`](./benchmarks/README.md) for workload details and
@@ -347,26 +347,26 @@ uv run --with maturin maturin develop --release
347
347
  uv run --with uvloop python benchmarks/compare_event_loops.py
348
348
  ```
349
349
 
350
- An example output from that script on Linux with CPython 3.14.0:
350
+ An example output from that script on macOS (arm64) with CPython 3.14:
351
351
 
352
352
  ```
353
353
  callbacks (200,000 ops)
354
- loop median_s best_s ops_per_s vs_fastest
355
- rsloop 0.041608 0.040585 4,806,807 1.00x
356
- uvloop 0.087539 0.086690 2,284,707 2.10x
357
- asyncio 0.229563 0.221348 871,222 5.52x
354
+ loop median_s best_s ops_per_s peak_rss vs_fastest
355
+ rsloop 0.033083 0.032710 6,045,401 67.5 MiB 1.00x
356
+ uvloop 0.040958 0.040721 4,883,026 72.8 MiB 1.24x
357
+ asyncio 0.082233 0.082093 2,432,114 65.3 MiB 2.49x
358
358
 
359
359
  tasks (50,000 ops)
360
- loop median_s best_s ops_per_s vs_fastest
361
- uvloop 0.084425 0.083497 592,239 1.00x
362
- rsloop 0.091845 0.090982 544,397 1.09x
363
- asyncio 0.138782 0.137716 360,276 1.64x
360
+ loop median_s best_s ops_per_s peak_rss vs_fastest
361
+ rsloop 0.063593 0.063286 786,247 37.6 MiB 1.00x
362
+ uvloop 0.069614 0.069420 718,251 38.4 MiB 1.09x
363
+ asyncio 0.108114 0.107502 462,473 36.1 MiB 1.70x
364
364
 
365
365
  tcp_streams (5,000 ops)
366
- loop median_s best_s ops_per_s vs_fastest
367
- rsloop 0.119483 0.118451 41,847 1.00x
368
- uvloop 0.119582 0.116446 41,812 1.00x
369
- asyncio 0.138408 0.134438 36,125 1.16x
366
+ loop median_s best_s ops_per_s peak_rss vs_fastest
367
+ rsloop 0.090940 0.083355 54,981 32.2 MiB 1.00x
368
+ uvloop 0.133182 0.127404 37,543 31.5 MiB 1.46x
369
+ asyncio 0.302337 0.299813 16,538 29.6 MiB 3.32x
370
370
  ```
371
371
 
372
372
  See [`benchmarks/README.md`](./benchmarks/README.md) for workload details and
@@ -57,7 +57,7 @@ The runner executes each loop/workload in a fresh subprocess and reports:
57
57
 
58
58
  Current workloads:
59
59
 
60
- - `callbacks`: chained `call_soon()` dispatch
60
+ - `callbacks`: pre-scheduled batch of `call_soon()` callbacks (schedule + dispatch cost)
61
61
  - `tasks`: many tiny `asyncio.sleep(0)` tasks
62
62
  - `tcp_streams`: local `asyncio.start_server()` / `asyncio.open_connection()` echo round trips
63
63
 
@@ -294,6 +294,10 @@ def run_with_loop(loop_name: str, coro: asyncio.coroutines) -> ChildResult:
294
294
 
295
295
 
296
296
  async def bench_callbacks(loop_name: str, iterations: int) -> ChildResult:
297
+ # Pre-schedule the full batch instead of chaining callbacks: a chain
298
+ # forces asyncio/uvloop into one event-loop iteration (including a
299
+ # kernel poll) per callback, which measures selector overhead rather
300
+ # than call_soon dispatch cost.
297
301
  loop = asyncio.get_running_loop()
298
302
  done = loop.create_future()
299
303
  remaining = iterations
@@ -303,11 +307,10 @@ async def bench_callbacks(loop_name: str, iterations: int) -> ChildResult:
303
307
  remaining -= 1
304
308
  if remaining == 0:
305
309
  done.set_result(None)
306
- return
307
- loop.call_soon(callback)
308
310
 
309
311
  start = time.perf_counter()
310
- loop.call_soon(callback)
312
+ for _ in range(iterations):
313
+ loop.call_soon(callback)
311
314
  await done
312
315
  return ChildResult(
313
316
  loop_name,
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "rsloop"
7
- version = "0.1.23"
7
+ version = "0.1.25"
8
8
  description = "An event loop for asyncio written in Rust"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -60,7 +60,6 @@ pub struct ReadyCallback {
60
60
  kind: CallbackKind,
61
61
  callback: Py<PyAny>,
62
62
  args: CallbackArgs,
63
- args_tuple: Py<PyTuple>,
64
63
  context: Py<PyAny>,
65
64
  context_needs_run: bool,
66
65
  cancelled: AtomicBool,
@@ -94,7 +93,6 @@ impl ReadyCallback {
94
93
  kind,
95
94
  callback,
96
95
  args,
97
- args_tuple,
98
96
  context,
99
97
  context_needs_run,
100
98
  cancelled: AtomicBool::new(false),
@@ -159,10 +157,6 @@ impl ReadyCallback {
159
157
  }
160
158
  }
161
159
 
162
- pub fn clone_args_tuple(&self, py: Python<'_>) -> Py<PyTuple> {
163
- self.args_tuple.clone_ref(py)
164
- }
165
-
166
160
  #[inline]
167
161
  pub fn cancelled(&self) -> bool {
168
162
  self.cancelled.load(Ordering::Relaxed)
@@ -174,42 +168,42 @@ impl ReadyCallback {
174
168
  }
175
169
  }
176
170
 
177
- #[pyclass(name = "Handle", module = "rsloop._loop", weakref, freelist = 4096)]
171
+ // The handle owns its callback inline (instead of a separate
172
+ // `Arc<ReadyCallback>` allocation) and the ready queue holds `Py<PyHandle>`
173
+ // clones: one allocation per call_soon instead of two. `frozen` lets the
174
+ // event loop read the callback through `Py::get` without borrow checking.
175
+ #[pyclass(name = "Handle", module = "rsloop._loop", weakref, frozen)]
178
176
  pub struct PyHandle {
179
- callback_id: CallbackId,
180
- callback: Weak<ReadyCallback>,
181
- cancelled: AtomicBool,
177
+ callback: ReadyCallback,
182
178
  }
183
179
 
184
180
  impl PyHandle {
185
181
  #[inline]
186
- pub fn new(callback_id: CallbackId, callback: &Arc<ReadyCallback>) -> Self {
187
- Self {
188
- callback_id,
189
- callback: Arc::downgrade(callback),
190
- cancelled: AtomicBool::new(false),
191
- }
182
+ pub fn new(callback: ReadyCallback) -> Self {
183
+ Self { callback }
184
+ }
185
+
186
+ #[inline]
187
+ pub fn ready(&self) -> &ReadyCallback {
188
+ &self.callback
192
189
  }
193
190
  }
194
191
 
195
192
  #[pymethods]
196
193
  impl PyHandle {
197
194
  fn cancel(&self) -> PyResult<()> {
198
- self.cancelled.store(true, Ordering::Relaxed);
199
- if let Some(callback) = self.callback.upgrade() {
200
- callback.cancel();
201
- }
195
+ self.callback.cancel();
202
196
  Ok(())
203
197
  }
204
198
 
205
199
  fn cancelled(&self) -> bool {
206
- self.cancelled.load(Ordering::Relaxed)
200
+ self.callback.cancelled()
207
201
  }
208
202
 
209
203
  fn __repr__(&self) -> String {
210
204
  format!(
211
205
  "<Handle id={} cancelled={}>",
212
- self.callback_id,
206
+ self.callback.id(),
213
207
  self.cancelled()
214
208
  )
215
209
  }
@@ -5,10 +5,19 @@ use pyo3_async_runtimes::TaskLocals;
5
5
 
6
6
  use crate::python_api::PyLoop;
7
7
  use crate::python_names;
8
- use crate::stream_transport::task_locals_for_loop;
8
+ use crate::stream_transport::{PyStreamTransport, task_locals_for_loop};
9
9
 
10
10
  const DEFAULT_STREAM_LIMIT: usize = 65_536;
11
11
 
12
+ /// Create a future on `loop_obj`, skipping the Python-level method dispatch
13
+ /// when the loop is a native rsloop instance running on this thread.
14
+ fn loop_create_future(py: Python<'_>, loop_obj: &Py<PyAny>) -> PyResult<Py<PyAny>> {
15
+ if let Some(future) = crate::python_api::try_fast_create_future(py, loop_obj)? {
16
+ return Ok(future);
17
+ }
18
+ python_names::call_method0(py, loop_obj.bind(py), python_names::create_future(py))
19
+ }
20
+
12
21
  struct ReadBuffer {
13
22
  bytes: Vec<u8>,
14
23
  start: usize,
@@ -167,7 +176,7 @@ impl PyFastStreamReader {
167
176
 
168
177
  #[inline]
169
178
  fn create_future(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
170
- python_names::call_method0(py, self.loop_obj.bind(py), python_names::create_future(py))
179
+ loop_create_future(py, &self.loop_obj)
171
180
  }
172
181
 
173
182
  fn ready_result_future(&self, py: Python<'_>, value: Py<PyAny>) -> PyResult<Py<PyAny>> {
@@ -235,7 +244,15 @@ impl PyFastStreamReader {
235
244
  }
236
245
 
237
246
  fn maybe_pause_transport(&mut self, py: Python<'_>) -> PyResult<()> {
238
- if !self.transport.bind(py).is_none() && !self.paused && self.buffer.len() > 2 * self.limit
247
+ // Never pause while a coroutine is waiting for data: a pending
248
+ // readexactly()/read() waiter may need more than 2 * limit bytes to
249
+ // complete, and pausing would starve it forever. This mirrors
250
+ // asyncio.StreamReader._wait_for_data, which resumes the transport
251
+ // while a reader is waiting.
252
+ if self.waiter.is_none()
253
+ && !self.transport.bind(py).is_none()
254
+ && !self.paused
255
+ && self.buffer.len() > 2 * self.limit
239
256
  {
240
257
  match python_names::call_method0(
241
258
  py,
@@ -541,10 +558,8 @@ impl PyFastStreamProtocol {
541
558
  reader: Py<PyFastStreamReader>,
542
559
  client_connected_cb: Py<PyAny>,
543
560
  ) -> PyResult<Self> {
544
- let closed =
545
- python_names::call_method0(py, loop_obj.bind(py), python_names::create_future(py))?;
546
- let ready_none =
547
- python_names::call_method0(py, loop_obj.bind(py), python_names::create_future(py))?;
561
+ let closed = loop_create_future(py, &loop_obj)?;
562
+ let ready_none = loop_create_future(py, &loop_obj)?;
548
563
  python_names::call_method1(
549
564
  py,
550
565
  ready_none.bind(py),
@@ -578,11 +593,7 @@ impl PyFastStreamProtocol {
578
593
  }
579
594
 
580
595
  fn ready_exception_future(&self, py: Python<'_>, exc: Py<PyAny>) -> PyResult<Py<PyAny>> {
581
- let future = python_names::call_method0(
582
- py,
583
- self.loop_obj.bind(py),
584
- python_names::create_future(py),
585
- )?;
596
+ let future = loop_create_future(py, &self.loop_obj)?;
586
597
  python_names::call_method1(
587
598
  py,
588
599
  future.bind(py),
@@ -593,11 +604,7 @@ impl PyFastStreamProtocol {
593
604
  }
594
605
 
595
606
  fn push_drain_waiter(&mut self, py: Python<'_>) -> PyResult<Py<PyAny>> {
596
- let future = python_names::call_method0(
597
- py,
598
- self.loop_obj.bind(py),
599
- python_names::create_future(py),
600
- )?;
607
+ let future = loop_create_future(py, &self.loop_obj)?;
601
608
  self.drain_waiters.push(future.clone_ref(py));
602
609
  Ok(future)
603
610
  }
@@ -832,6 +839,11 @@ impl PyFastStreamWriter {
832
839
  }
833
840
 
834
841
  fn write(&self, py: Python<'_>, data: &Bound<'_, PyAny>) -> PyResult<()> {
842
+ // Native transports take the direct Rust path; only foreign
843
+ // transports go through Python method dispatch.
844
+ if let Ok(transport) = self.transport.bind(py).cast_exact::<PyStreamTransport>() {
845
+ return transport.borrow().write_data(py, data);
846
+ }
835
847
  self.transport.call_method1(py, "write", (data,))?;
836
848
  Ok(())
837
849
  }
@@ -1003,9 +1015,10 @@ fn native_stream_loop(
1003
1015
  }
1004
1016
  if let Some(kwargs) = kwargs
1005
1017
  && let Some(ssl) = kwargs.get_item("ssl")?
1006
- && !ssl.is_none() {
1007
- return Ok(None);
1008
- }
1018
+ && !ssl.is_none()
1019
+ {
1020
+ return Ok(None);
1021
+ }
1009
1022
  Ok(Some(loop_obj))
1010
1023
  }
1011
1024
 
@@ -85,7 +85,17 @@ pub fn poll_fd(fd: RawFd, read: bool, write: bool, timeout_ms: i32) -> io::Resul
85
85
  }
86
86
 
87
87
  let revents = pollfd.revents as i32;
88
- let error_bits = (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL) as i32;
88
+ // POLLNVAL means the descriptor is not open (e.g. the socket was closed
89
+ // out from under the watcher). Reporting it as "ready" would make
90
+ // watchers fire forever on a dead fd, so surface it as an error instead;
91
+ // a closed fd produces no readiness events on asyncio either.
92
+ if revents & (libc::POLLNVAL as i32) != 0 {
93
+ return Err(io::Error::new(
94
+ io::ErrorKind::InvalidInput,
95
+ "file descriptor is not open",
96
+ ));
97
+ }
98
+ let error_bits = (libc::POLLERR | libc::POLLHUP) as i32;
89
99
  Ok((
90
100
  read && (revents & ((libc::POLLIN as i32) | error_bits)) != 0,
91
101
  write && (revents & ((libc::POLLOUT as i32) | error_bits)) != 0,
@@ -3,9 +3,8 @@ use std::sync::{Arc, Mutex};
3
3
  use std::time::Instant;
4
4
 
5
5
  use pyo3::prelude::*;
6
- use pyo3::types::PyTuple;
7
6
 
8
- use crate::callbacks::ReadyCallback;
7
+ use crate::callbacks::{PyHandle, ReadyCallback};
9
8
  use crate::fd_ops::RawFd;
10
9
  use crate::process_transport::ProcessTransportCore;
11
10
  use crate::stream_transport::{
@@ -14,6 +13,7 @@ use crate::stream_transport::{
14
13
 
15
14
  pub enum ReadyItem {
16
15
  Callback(Arc<ReadyCallback>),
16
+ HandleCallback(Py<PyHandle>),
17
17
  FutureSetResult {
18
18
  future: Py<PyAny>,
19
19
  value: Py<PyAny>,
@@ -33,6 +33,7 @@ pub enum ReadyItem {
33
33
 
34
34
  pub enum LoopCommand {
35
35
  ScheduleReady(Arc<ReadyCallback>),
36
+ ScheduleReadyHandle(Py<PyHandle>),
36
37
  ScheduleTimer {
37
38
  callback: Arc<ReadyCallback>,
38
39
  when: Instant,
@@ -50,6 +51,7 @@ pub enum LoopRunCommand {
50
51
  EnterRun {
51
52
  pending_ready: Arc<Mutex<VecDeque<ReadyItem>>>,
52
53
  wake_tx: std::sync::mpsc::Sender<()>,
54
+ wake_pending: Arc<std::sync::atomic::AtomicBool>,
53
55
  },
54
56
  FinishRun {
55
57
  done_tx: std::sync::mpsc::Sender<()>,
@@ -63,20 +65,18 @@ pub enum LoopSignalCommand {
63
65
  }
64
66
 
65
67
  pub enum LoopIoCommand {
68
+ // Fd watches hold one persistent ReadyCallback per registration, shared
69
+ // with LoopState's keepalive map. Every readiness event schedules the
70
+ // same callback, so remove_reader()/remove_writer() can cancel pending
71
+ // fires exactly like asyncio's Handle.cancel() on its reader handle.
66
72
  StartReader {
67
73
  fd: RawFd,
68
- callback: Py<PyAny>,
69
- args: Py<PyTuple>,
70
- context: Py<PyAny>,
71
- context_needs_run: bool,
74
+ callback: Arc<ReadyCallback>,
72
75
  },
73
76
  StopReader(RawFd),
74
77
  StartWriter {
75
78
  fd: RawFd,
76
- callback: Py<PyAny>,
77
- args: Py<PyTuple>,
78
- context: Py<PyAny>,
79
- context_needs_run: bool,
79
+ callback: Arc<ReadyCallback>,
80
80
  },
81
81
  StopWriter(RawFd),
82
82
  StartSocketReader {