polyphony 1.0.1 → 1.1

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/.github/workflows/test_io_uring.yml +1 -1
  4. data/.yardopts +1 -0
  5. data/CHANGELOG.md +9 -0
  6. data/README.md +1 -0
  7. data/TODO.md +6 -12
  8. data/docs/advanced-io.md +224 -0
  9. data/docs/cheat-sheet.md +2 -2
  10. data/docs/readme.md +1 -0
  11. data/examples/core/debug.rb +12 -0
  12. data/examples/core/rpc_benchmark.rb +136 -0
  13. data/examples/core/stream_mockup.rb +68 -0
  14. data/examples/core/throttled_loop_inside_move_on_after.rb +13 -0
  15. data/ext/polyphony/backend_common.c +3 -5
  16. data/ext/polyphony/backend_common.h +10 -1
  17. data/ext/polyphony/backend_io_uring.c +6 -6
  18. data/ext/polyphony/backend_libev.c +5 -5
  19. data/ext/polyphony/extconf.rb +6 -0
  20. data/ext/polyphony/fiber.c +21 -1
  21. data/lib/polyphony/extensions/fiber.rb +1 -0
  22. data/lib/polyphony/extensions/io.rb +74 -74
  23. data/lib/polyphony/extensions/object.rb +6 -0
  24. data/lib/polyphony/extensions/socket.rb +39 -39
  25. data/lib/polyphony/version.rb +1 -1
  26. data/polyphony.gemspec +3 -1
  27. data/test/stress.rb +1 -1
  28. data/test/test_fiber.rb +45 -1
  29. data/test/test_io.rb +46 -0
  30. data/test/test_process_supervision.rb +1 -1
  31. data/test/test_resource_pool.rb +1 -1
  32. data/test/test_scenarios.rb +38 -0
  33. data/test/test_socket.rb +1 -2
  34. data/test/test_thread_pool.rb +4 -2
  35. data/test/test_timer.rb +2 -2
  36. metadata +36 -149
  37. data/vendor/liburing/man/IO_URING_CHECK_VERSION.3 +0 -1
  38. data/vendor/liburing/man/IO_URING_VERSION_MAJOR.3 +0 -1
  39. data/vendor/liburing/man/IO_URING_VERSION_MINOR.3 +0 -1
  40. data/vendor/liburing/man/io_uring.7 +0 -781
  41. data/vendor/liburing/man/io_uring_buf_ring_add.3 +0 -53
  42. data/vendor/liburing/man/io_uring_buf_ring_advance.3 +0 -31
  43. data/vendor/liburing/man/io_uring_buf_ring_cq_advance.3 +0 -41
  44. data/vendor/liburing/man/io_uring_buf_ring_init.3 +0 -30
  45. data/vendor/liburing/man/io_uring_buf_ring_mask.3 +0 -27
  46. data/vendor/liburing/man/io_uring_check_version.3 +0 -72
  47. data/vendor/liburing/man/io_uring_close_ring_fd.3 +0 -43
  48. data/vendor/liburing/man/io_uring_cq_advance.3 +0 -49
  49. data/vendor/liburing/man/io_uring_cq_has_overflow.3 +0 -25
  50. data/vendor/liburing/man/io_uring_cq_ready.3 +0 -26
  51. data/vendor/liburing/man/io_uring_cqe_get_data.3 +0 -53
  52. data/vendor/liburing/man/io_uring_cqe_get_data64.3 +0 -1
  53. data/vendor/liburing/man/io_uring_cqe_seen.3 +0 -42
  54. data/vendor/liburing/man/io_uring_enter.2 +0 -1700
  55. data/vendor/liburing/man/io_uring_enter2.2 +0 -1
  56. data/vendor/liburing/man/io_uring_free_probe.3 +0 -27
  57. data/vendor/liburing/man/io_uring_get_events.3 +0 -33
  58. data/vendor/liburing/man/io_uring_get_probe.3 +0 -30
  59. data/vendor/liburing/man/io_uring_get_sqe.3 +0 -57
  60. data/vendor/liburing/man/io_uring_major_version.3 +0 -1
  61. data/vendor/liburing/man/io_uring_minor_version.3 +0 -1
  62. data/vendor/liburing/man/io_uring_opcode_supported.3 +0 -30
  63. data/vendor/liburing/man/io_uring_peek_cqe.3 +0 -38
  64. data/vendor/liburing/man/io_uring_prep_accept.3 +0 -197
  65. data/vendor/liburing/man/io_uring_prep_accept_direct.3 +0 -1
  66. data/vendor/liburing/man/io_uring_prep_cancel.3 +0 -118
  67. data/vendor/liburing/man/io_uring_prep_cancel64.3 +0 -1
  68. data/vendor/liburing/man/io_uring_prep_close.3 +0 -59
  69. data/vendor/liburing/man/io_uring_prep_close_direct.3 +0 -1
  70. data/vendor/liburing/man/io_uring_prep_connect.3 +0 -66
  71. data/vendor/liburing/man/io_uring_prep_fadvise.3 +0 -59
  72. data/vendor/liburing/man/io_uring_prep_fallocate.3 +0 -59
  73. data/vendor/liburing/man/io_uring_prep_fgetxattr.3 +0 -1
  74. data/vendor/liburing/man/io_uring_prep_files_update.3 +0 -92
  75. data/vendor/liburing/man/io_uring_prep_fsetxattr.3 +0 -1
  76. data/vendor/liburing/man/io_uring_prep_fsync.3 +0 -70
  77. data/vendor/liburing/man/io_uring_prep_getxattr.3 +0 -61
  78. data/vendor/liburing/man/io_uring_prep_link.3 +0 -1
  79. data/vendor/liburing/man/io_uring_prep_link_timeout.3 +0 -94
  80. data/vendor/liburing/man/io_uring_prep_linkat.3 +0 -91
  81. data/vendor/liburing/man/io_uring_prep_madvise.3 +0 -56
  82. data/vendor/liburing/man/io_uring_prep_mkdir.3 +0 -1
  83. data/vendor/liburing/man/io_uring_prep_mkdirat.3 +0 -83
  84. data/vendor/liburing/man/io_uring_prep_msg_ring.3 +0 -92
  85. data/vendor/liburing/man/io_uring_prep_msg_ring_cqe_flags.3 +0 -1
  86. data/vendor/liburing/man/io_uring_prep_multishot_accept.3 +0 -1
  87. data/vendor/liburing/man/io_uring_prep_multishot_accept_direct.3 +0 -1
  88. data/vendor/liburing/man/io_uring_prep_nop.3 +0 -28
  89. data/vendor/liburing/man/io_uring_prep_openat.3 +0 -117
  90. data/vendor/liburing/man/io_uring_prep_openat2.3 +0 -117
  91. data/vendor/liburing/man/io_uring_prep_openat2_direct.3 +0 -1
  92. data/vendor/liburing/man/io_uring_prep_openat_direct.3 +0 -1
  93. data/vendor/liburing/man/io_uring_prep_poll_add.3 +0 -72
  94. data/vendor/liburing/man/io_uring_prep_poll_multishot.3 +0 -1
  95. data/vendor/liburing/man/io_uring_prep_poll_remove.3 +0 -55
  96. data/vendor/liburing/man/io_uring_prep_poll_update.3 +0 -89
  97. data/vendor/liburing/man/io_uring_prep_provide_buffers.3 +0 -140
  98. data/vendor/liburing/man/io_uring_prep_read.3 +0 -69
  99. data/vendor/liburing/man/io_uring_prep_read_fixed.3 +0 -72
  100. data/vendor/liburing/man/io_uring_prep_readv.3 +0 -85
  101. data/vendor/liburing/man/io_uring_prep_readv2.3 +0 -111
  102. data/vendor/liburing/man/io_uring_prep_recv.3 +0 -105
  103. data/vendor/liburing/man/io_uring_prep_recv_multishot.3 +0 -1
  104. data/vendor/liburing/man/io_uring_prep_recvmsg.3 +0 -124
  105. data/vendor/liburing/man/io_uring_prep_recvmsg_multishot.3 +0 -1
  106. data/vendor/liburing/man/io_uring_prep_remove_buffers.3 +0 -52
  107. data/vendor/liburing/man/io_uring_prep_rename.3 +0 -1
  108. data/vendor/liburing/man/io_uring_prep_renameat.3 +0 -96
  109. data/vendor/liburing/man/io_uring_prep_send.3 +0 -66
  110. data/vendor/liburing/man/io_uring_prep_send_set_addr.3 +0 -38
  111. data/vendor/liburing/man/io_uring_prep_send_zc.3 +0 -96
  112. data/vendor/liburing/man/io_uring_prep_send_zc_fixed.3 +0 -1
  113. data/vendor/liburing/man/io_uring_prep_sendmsg.3 +0 -89
  114. data/vendor/liburing/man/io_uring_prep_sendmsg_zc.3 +0 -1
  115. data/vendor/liburing/man/io_uring_prep_setxattr.3 +0 -64
  116. data/vendor/liburing/man/io_uring_prep_shutdown.3 +0 -53
  117. data/vendor/liburing/man/io_uring_prep_socket.3 +0 -118
  118. data/vendor/liburing/man/io_uring_prep_socket_direct.3 +0 -1
  119. data/vendor/liburing/man/io_uring_prep_socket_direct_alloc.3 +0 -1
  120. data/vendor/liburing/man/io_uring_prep_splice.3 +0 -120
  121. data/vendor/liburing/man/io_uring_prep_statx.3 +0 -74
  122. data/vendor/liburing/man/io_uring_prep_symlink.3 +0 -1
  123. data/vendor/liburing/man/io_uring_prep_symlinkat.3 +0 -85
  124. data/vendor/liburing/man/io_uring_prep_sync_file_range.3 +0 -59
  125. data/vendor/liburing/man/io_uring_prep_tee.3 +0 -74
  126. data/vendor/liburing/man/io_uring_prep_timeout.3 +0 -95
  127. data/vendor/liburing/man/io_uring_prep_timeout_remove.3 +0 -1
  128. data/vendor/liburing/man/io_uring_prep_timeout_update.3 +0 -98
  129. data/vendor/liburing/man/io_uring_prep_unlink.3 +0 -1
  130. data/vendor/liburing/man/io_uring_prep_unlinkat.3 +0 -82
  131. data/vendor/liburing/man/io_uring_prep_write.3 +0 -67
  132. data/vendor/liburing/man/io_uring_prep_write_fixed.3 +0 -72
  133. data/vendor/liburing/man/io_uring_prep_writev.3 +0 -85
  134. data/vendor/liburing/man/io_uring_prep_writev2.3 +0 -111
  135. data/vendor/liburing/man/io_uring_queue_exit.3 +0 -26
  136. data/vendor/liburing/man/io_uring_queue_init.3 +0 -89
  137. data/vendor/liburing/man/io_uring_queue_init_params.3 +0 -1
  138. data/vendor/liburing/man/io_uring_recvmsg_cmsg_firsthdr.3 +0 -1
  139. data/vendor/liburing/man/io_uring_recvmsg_cmsg_nexthdr.3 +0 -1
  140. data/vendor/liburing/man/io_uring_recvmsg_name.3 +0 -1
  141. data/vendor/liburing/man/io_uring_recvmsg_out.3 +0 -82
  142. data/vendor/liburing/man/io_uring_recvmsg_payload.3 +0 -1
  143. data/vendor/liburing/man/io_uring_recvmsg_payload_length.3 +0 -1
  144. data/vendor/liburing/man/io_uring_recvmsg_validate.3 +0 -1
  145. data/vendor/liburing/man/io_uring_register.2 +0 -834
  146. data/vendor/liburing/man/io_uring_register_buf_ring.3 +0 -140
  147. data/vendor/liburing/man/io_uring_register_buffers.3 +0 -104
  148. data/vendor/liburing/man/io_uring_register_buffers_sparse.3 +0 -1
  149. data/vendor/liburing/man/io_uring_register_buffers_tags.3 +0 -1
  150. data/vendor/liburing/man/io_uring_register_buffers_update_tag.3 +0 -1
  151. data/vendor/liburing/man/io_uring_register_eventfd.3 +0 -51
  152. data/vendor/liburing/man/io_uring_register_eventfd_async.3 +0 -1
  153. data/vendor/liburing/man/io_uring_register_file_alloc_range.3 +0 -52
  154. data/vendor/liburing/man/io_uring_register_files.3 +0 -112
  155. data/vendor/liburing/man/io_uring_register_files_sparse.3 +0 -1
  156. data/vendor/liburing/man/io_uring_register_files_tags.3 +0 -1
  157. data/vendor/liburing/man/io_uring_register_files_update.3 +0 -1
  158. data/vendor/liburing/man/io_uring_register_files_update_tag.3 +0 -1
  159. data/vendor/liburing/man/io_uring_register_iowq_aff.3 +0 -61
  160. data/vendor/liburing/man/io_uring_register_iowq_max_workers.3 +0 -71
  161. data/vendor/liburing/man/io_uring_register_ring_fd.3 +0 -49
  162. data/vendor/liburing/man/io_uring_register_sync_cancel.3 +0 -71
  163. data/vendor/liburing/man/io_uring_setup.2 +0 -669
  164. data/vendor/liburing/man/io_uring_sq_ready.3 +0 -31
  165. data/vendor/liburing/man/io_uring_sq_space_left.3 +0 -25
  166. data/vendor/liburing/man/io_uring_sqe_set_data.3 +0 -48
  167. data/vendor/liburing/man/io_uring_sqe_set_data64.3 +0 -1
  168. data/vendor/liburing/man/io_uring_sqe_set_flags.3 +0 -87
  169. data/vendor/liburing/man/io_uring_sqring_wait.3 +0 -34
  170. data/vendor/liburing/man/io_uring_submit.3 +0 -46
  171. data/vendor/liburing/man/io_uring_submit_and_get_events.3 +0 -31
  172. data/vendor/liburing/man/io_uring_submit_and_wait.3 +0 -38
  173. data/vendor/liburing/man/io_uring_submit_and_wait_timeout.3 +0 -56
  174. data/vendor/liburing/man/io_uring_unregister_buf_ring.3 +0 -30
  175. data/vendor/liburing/man/io_uring_unregister_buffers.3 +0 -27
  176. data/vendor/liburing/man/io_uring_unregister_eventfd.3 +0 -1
  177. data/vendor/liburing/man/io_uring_unregister_files.3 +0 -27
  178. data/vendor/liburing/man/io_uring_unregister_iowq_aff.3 +0 -1
  179. data/vendor/liburing/man/io_uring_unregister_ring_fd.3 +0 -32
  180. data/vendor/liburing/man/io_uring_wait_cqe.3 +0 -40
  181. data/vendor/liburing/man/io_uring_wait_cqe_nr.3 +0 -43
  182. data/vendor/liburing/man/io_uring_wait_cqe_timeout.3 +0 -53
  183. data/vendor/liburing/man/io_uring_wait_cqes.3 +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f60a881ccb01cfce59bb3c84420bba574c2b1cd502df987b958a02b649fb4415
4
- data.tar.gz: a138759174aba3944ca5e4faedff676d9c9807a50907ee03d18b2cec50f625c5
3
+ metadata.gz: 61fa840f595a0e75da1d6e1b761506b18fa90d5a94b25ce5832d22aae2e08fbd
4
+ data.tar.gz: 7d3a79225f68bb889ff0491ced1249c2679a222a3f79a5c4cf48f9571865ebc4
5
5
  SHA512:
6
- metadata.gz: 8167c82b2ebd31f4625dac5cbcf70a610b8e45d062b0ef5d4e2044493b6c9ca25a68b67d8b6948ece9ed3a414f2977c4f72175ce0fc63ce58545acc721f09d73
7
- data.tar.gz: b91049699af9824aeb2fa051d859f35ff0f1b4ba289b7ea143a347a5ad8a882ec5a4fb42502742b3e4d19bf295f9a1d5bf3a9e15f38a061efdcf50a1741f0bce
6
+ metadata.gz: 32d61cf7e0858e704fc39fe317048e3e531afcf97da70b9b3d1e4b59a078e2f5c8c65a3f72c656b61285df9e5c99d7430938403560ca6e8e7aa5d920dada274e
7
+ data.tar.gz: c5c1488b1cec55810ffccf8116d70e8312ccb7d93875c1047ab7608595eb81fda9601f44ef308b3e1f35319d683b6ceda423284c9cda76c6f44882a7341e681a
@@ -8,7 +8,7 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest, macos-latest]
11
- ruby: ['3.0', '3.1', '3.2']
11
+ ruby: ['3.0', '3.1', '3.2', 'head']
12
12
 
13
13
  name: >-
14
14
  ${{matrix.os}}, ${{matrix.ruby}}
@@ -8,7 +8,7 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest]
11
- ruby: ['3.0', '3.1', '3.2']
11
+ ruby: ['3.0', '3.1', '3.2', 'head']
12
12
 
13
13
  name: >-
14
14
  ${{matrix.os}}, ${{matrix.ruby}}
data/.yardopts CHANGED
@@ -20,6 +20,7 @@
20
20
  docs/readme.md
21
21
  docs/overview.md
22
22
  docs/tutorial.md
23
+ docs/advanced-io.md
23
24
  docs/cheat-sheet.md
24
25
  docs/faq.md
25
26
  docs/concurrency.md
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## 1.1 2023-06-08
2
+
3
+ - Add advanced I/O doc page
4
+ - Add `Fiber#receive_loop` API
5
+
6
+ ## 1.0.2 2023-05-28
7
+
8
+ - Remove liburing man files from gemspec (#103)
9
+
1
10
  ## 1.0.1 2023-05-14
2
11
 
3
12
  - Add cheat-sheet
data/README.md CHANGED
@@ -77,6 +77,7 @@ $ gem install polyphony
77
77
 
78
78
  - [Overview](docs/overview.md)
79
79
  - [Tutorial](docs/tutorial.md)
80
+ - [Advanced I/O with Polyphony](docs/advanced-io.md)
80
81
  - [Cheat-Sheet](docs/cheat-sheet.md)
81
82
  - [FAQ](docs/faq.md)
82
83
 
data/TODO.md CHANGED
@@ -1,3 +1,6 @@
1
+ - Look at RPC benchmark more closely: is there a way to reduce the overhead of
2
+ the `backend_base_switch_fiber` function?
3
+
1
4
  - io_uring backend:
2
5
  - if `io_uring_get_sqe` returns null, call `io_uring_submit`, (snooze fiber)?
3
6
  and try again
@@ -11,24 +14,14 @@
11
14
  - Add support for IPv6:
12
15
  https://www.reddit.com/r/ruby/comments/lyen23/understanding_ipv6_and_why_its_important_to_you/
13
16
 
14
- - Check why `throttled_loop` inside of `move_on_after` fails to stop
15
-
16
17
  - Override stock `::SizedQueue` impl with Queue with capacity
17
18
 
18
- - Add support for `break` and `StopIteration` in all loops (with tests)
19
-
20
19
  - More tight loops
21
20
  - `IO#gets_loop`, `Socket#gets_loop`, `OpenSSL::Socket#gets_loop` (medium effort)
22
- - `Fiber#receive_loop` (very little effort, should be implemented in C)
23
21
 
24
22
  - Add support for `close` to io_uring backend
25
23
 
26
- ## Roadmap for Polyphony 1.0
27
-
28
- - Add test that mimics the original design for Monocrono:
29
- - 256 fibers each waiting for a message
30
- - When message received do some blocking work using a `ThreadPool`
31
- - Send messages, collect responses, check for correctness
24
+ ## Roadmap for Polyphony 1.1
32
25
 
33
26
  - io_uring
34
27
  - Use playground.c to find out why we when submitting and waiting for
@@ -113,7 +106,7 @@
113
106
 
114
107
  - Allow locking the scheduler on to one fiber
115
108
  - Add instance var `@fiber_lock`
116
- - API is `Thread#fiber_lock` which sets the fiber_lock instance varwhile
109
+ - API is `Thread#fiber_lock` which sets the fiber_lock instance var while
117
110
  running the block:
118
111
 
119
112
  ```ruby
@@ -123,6 +116,7 @@
123
116
  end
124
117
  end
125
118
  ```
119
+
126
120
  - When `@fiber_lock` is set, it is considered as the only one in the run
127
121
  queue:
128
122
 
@@ -0,0 +1,224 @@
1
+ # @title Advanced I/O with Polyphony
2
+
3
+ ## Using splice for moving data between files and sockets
4
+
5
+ Splice is linux-specific API that lets you move data between two file
6
+ descriptors without copying data between kernel-space and user-space. This is
7
+ not only useful for copying data between two files, but also for implementing
8
+ things such as web servers, where you might need to serve files of an arbitrary
9
+ size. Using splice, you can avoid the cost of having to load a file's content
10
+ into memory, in order to send it to a TCP connection.
11
+
12
+ In order to use `splice`, at least one of the file descriptors involved needs to
13
+ be a pipe. This is because in Linux, pipes are actually kernel buffers. The
14
+ normal way of using splice is that first you splice data from the source fd to
15
+ the pipe (to its *write* fd), and then you splice data from the pipe (from its
16
+ *read* fd) to the destination fd.
17
+
18
+ Here's how we do splicing using Polyphony:
19
+
20
+ ```ruby
21
+ def send_file_using_splice(src, dest)
22
+ # create a pipe. Polyphony::Pipe encapsulates a kernel pipe in a single
23
+ # IO-like object, but we can also use the stock IO.pipe method call that
24
+ # returns two separate pipe fds.
25
+ pipe = Polyphony::Pipe.new
26
+ loop do
27
+ # splices data from src to the pipe
28
+ bytes_spliced = IO.splice(src, pipe, 2**14)
29
+ break if bytes_spliced == 0 # EOF
30
+
31
+ # splices data from the pipe to the dest
32
+ IO.splice(pipe, dest, bytes_spliced)
33
+ end
34
+ end
35
+ ```
36
+
37
+ Let's examine the code above. First of all, we have a loop that repeatedly
38
+ splices data in chunks of 16KB. We break from the loop once EOF is encountered.
39
+ Secondly, on each iteration of the loop we perform two splice operations
40
+ sequentially. So, we need to repeatedly perform two splice operations, one after
41
+ the other. Would there be a better way to do this?
42
+
43
+ Fortunately, Polyphony provides just the tools needed to do that. Firstly, we
44
+ can tell Polyphony to splice data repeatedly until EOF is encountered by passing
45
+ a negative max size:
46
+
47
+ ```ruby
48
+ IO.splice(src, pipe, -2**14)
49
+ ```
50
+
51
+ Secondly, we can perform the two splice operations concurrently, by spinning up
52
+ a separate fiber that performs one of the splice operations, which gives us the
53
+ following:
54
+
55
+ ```ruby
56
+ def send_file_using_splice(src, dest)
57
+ pipe = Polyphony::Pipe.new
58
+ spin do
59
+ IO.splice(src, pipe, -2**14)
60
+ # We need to close the pipe in order to signal EOF for the 2nd splice call.
61
+ pipe.close
62
+ end
63
+ IO.splice(pipe, dest, -2**14)
64
+ end
65
+ ```
66
+
67
+ There are a few things to notice here: While we have two concurrent operations
68
+ running in two separate fibers, their are still inter-dependent in their
69
+ individual progress, as one is filling a kernel buffer, and the other is
70
+ flushing it, and thus the progress of whole will be bound by the slowest
71
+ operation.
72
+
73
+ Imagine an HTTP server that serves a large file to a slow client, or a client
74
+ with a bad network connection. The web server is perfectly capable of reading
75
+ the file from its disk very fast, but sending data to the HTTP client can be
76
+ much much slower. The second splice operation, splicing from the pipe to the
77
+ destination, will flush the kernel much more slowly that it is being filled. At
78
+ a certain point, the buffer is full, and the first splice operation from the
79
+ source to the pipe cannot continue. It will need to wait for the other splice
80
+ operation to progress, in order to continue filling the buffer. This is called
81
+ back-pressure propagation, and we get it automatically.
82
+
83
+ So let's look at all the things we didn't need to do: we didn't need to read
84
+ data into a Ruby string (which is costly in CPU time, in memory, and eventually
85
+ in GC pressure), we didn't need to manage a buffer and take care of
86
+ synchronizing access to the buffer. We got to move data from the source to the
87
+ destination concurrently, and we got back-pressure propagation for free. Can we
88
+ do any better than that?
89
+
90
+ Actually, we can! Polyphony also provides an API that does all of the above in a
91
+ single method call:
92
+
93
+ ```ruby
94
+ def send_file_using_splice(src, dest)
95
+ IO.double_splice(src, dest)
96
+ end
97
+ ```
98
+
99
+ The `IO.double_splice` creates a pipe and repeatedly splices data concurrently
100
+ from the source to pipe and from the pipe to the destination until the source is
101
+ exhausted. All this, without needing to instantiate a `Polyphony::Pipe` object,
102
+ and without needing to spin up a second fiber, further minimizing memory use and
103
+ GC pressure.
104
+
105
+ ## Compressing and decompressing in-flight data
106
+
107
+ You might be familiar with Ruby's [zlib](https://github.com/ruby/zlib) gem (docs
108
+ [here](https://rubyapi.org/3.2/o/zlib)), which can be used to compress and
109
+ uncompress data using the popular gzip format. Imagine we want to implement an
110
+ HTTP server that can serve files compresszed using gzip:
111
+
112
+ ```ruby
113
+ def serve_compressed_file(socket, file)
114
+ # we leave aside sending the HTTP headers and dealing with transfer encoding
115
+ compressed = Zlib.gzip(file.read)
116
+ socket << compressed
117
+ end
118
+ ```
119
+
120
+ In the above example, we have read the file contents into a Ruby string, then
121
+ passed the contents to `Zlib.gzip`, which returned the compressed contents in
122
+ another Ruby string, then wrote the compressed data to the socket. We can see
123
+ how this can lead to large allocations of memory (if the file is large), and
124
+ more pressure on the Ruby GC. How can we improve this?
125
+
126
+ One way would be to utilise Zlib's `GzipWriter` class:
127
+
128
+ ```ruby
129
+ def serve_compressed_file(socket, file)
130
+ # we leave aside sending the HTTP headers and dealing with transfer encoding
131
+ compressor = Zlib::GzipWriter.new(socket)
132
+ while (data = file.read(2**14))
133
+ compressor << data
134
+ end
135
+ end
136
+ ```
137
+
138
+ In the above code, we instantiate a `Zlib::GzipWriter`, which we then feed with
139
+ data from the file, with the compressor object writing the compressed data to
140
+ the socket. Notice how we still need to read the file contents into a Ruby
141
+ string and then pass it to the compressor. Could we avoid this? With Polyphony
142
+ the answer is yes we can!
143
+
144
+ Polyphony provides a number of APIs for compressing and decompressing data on
145
+ the fly between two file descriptors (i.e. `IO` instances), namely: `IO.gzip`,
146
+ `IO.gunzip`, `IO.deflate` and `IO.inflate`. Let's see how this can be used to
147
+ serve gzipped data to an HTTP client:
148
+
149
+ ```ruby
150
+ def serve_compressed_file(socket, file)
151
+ IO.gzip(file, socket) # and that's it!
152
+ end
153
+ ```
154
+
155
+ Using the `IO.gzip` API provided by Polyphony, we completely avoid instantiating
156
+ Ruby strings into which data is read, and in fact we avoid allocating any
157
+ buffers on the heap (apart from what `zlib` might be doing). *And* we get to
158
+ move data *and compress it* between the given file and the socket using a single
159
+ method call!
160
+
161
+ ## Feeding data from a file descriptor to a parser
162
+
163
+ Some times we want to process data from a given file or socket by passing
164
+ through some object that parses the data, or otherwise manipulates it. Normally,
165
+ we would write a loop that repeatedly reads the data from the source, then
166
+ passes it to the parser object. Imagine we have data transmitted using the
167
+ `MessagePack` format that we need to convert back into its original form. We
168
+ might do something like this:
169
+
170
+ ```ruby
171
+ def with_message_pack_data_from_io(io, &block)
172
+ unpacker = MessagePack::Unpacker.new
173
+ while (data = io.read(2**14))
174
+ unpacker.feed_each(data, &block)
175
+ end
176
+ end
177
+
178
+ # Which we can use as follows:
179
+ with_message_pack_data_from_io(socket) do |o|
180
+ puts "got: #{o.inspect}"
181
+ end
182
+ ```
183
+
184
+ Polyphony provides some APIs that help us write less code, and even optimize the
185
+ performance of our code. Let's look at the `IO#read_loop` (or `IO#recv_loop` for
186
+ sockets) API:
187
+
188
+ ```ruby
189
+ def with_message_pack_data_from_io(io, &block)
190
+ unpacker = MessagePack::Unpacker.new
191
+ io.read_loop do |data|
192
+ unpacker.feed_each(data, &block)
193
+ end
194
+ end
195
+ ```
196
+
197
+ In the above code, we replaced our `while` loop with a call to `IO#read_loop`,
198
+ which yields read data to the block given to it. In the block, we pass the data
199
+ to the MessagePack unpacker. While this does not like much different than the
200
+ previous implementation, the `IO#read_loop` API implements a tight loop at the
201
+ C-extension level, that provides slightly better performance.
202
+
203
+ But Polyphony goes even further than that and provides a `IO#feed_loop` API that
204
+ lets us feed read data to a given parser or processor object. Here's how we can
205
+ use it:
206
+
207
+ ```ruby
208
+ def with_message_pack_data_from_io(io, &block)
209
+ unpacker = MessagePack::Unpacker.new
210
+ io.feed_loop(unpacker, :feed_each, &block)
211
+ end
212
+ ```
213
+
214
+ With `IO#feed_loop` we get to write even less code, and as with `IO#read_loop`,
215
+ `IO#feed_loop` is implemented at the C-extension level using a tight loop that
216
+ maximizes performance.
217
+
218
+ ## Conclusion
219
+
220
+ In this article we have looked at some of the advanced I/O functionality
221
+ provided by Polyphony, which lets us write less code, have it run faster, and
222
+ minimize memory allocations and pressure on the Ruby GC. Feel free to browse the
223
+ [IO examples](https://github.com/digital-fabric/polyphony/tree/master/examples/io)
224
+ included in Polyphony.
data/docs/cheat-sheet.md CHANGED
@@ -71,7 +71,7 @@ def calculate_some_stuff(n)
71
71
  acc += big_calc(acc, i)
72
72
  snooze if (i % 1000) == 0
73
73
  end
74
- end
74
+ end
75
75
  ```
76
76
 
77
77
  ### Suspend fiber
@@ -191,7 +191,7 @@ dest2.tee_from(source, 8192)
191
191
  dest1.splice_from(source, 8192)
192
192
  # or:
193
193
  IO.tee(src, dest2)
194
- IO.splice(src, dest2)
194
+ IO.splice(src, dest1)
195
195
  ```
196
196
 
197
197
  ### Splice data between two arbitrary file descriptors, without creating a pipe
data/docs/readme.md CHANGED
@@ -79,6 +79,7 @@ $ gem install polyphony
79
79
 
80
80
  - {file:/docs/overview.md Overview}
81
81
  - {file:/docs/tutorial.md Tutorial}
82
+ - {file:/docs/advanced-io.md Advanced I/O with Polyphony}
82
83
  - {file:/docs/cheat-sheet.md Cheat-Sheet}
83
84
  - {file:/docs/faq.md FAQ}
84
85
 
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'debug'
5
+ require 'polyphony'
6
+
7
+ puts 'starting'
8
+ binding.break
9
+ sleep 1
10
+ puts 'hello'
11
+ sleep 1
12
+ puts 'bye'
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ require "benchmark/ips"
6
+
7
+ class Fiber
8
+ def call(*a, **b)
9
+ self << [Fiber.current, a, b]
10
+ Fiber.current.receive
11
+ end
12
+
13
+ def respond
14
+ peer, a, b = receive
15
+ result = yield(*a, **b)
16
+ (peer << result) rescue nil
17
+ rescue
18
+ peer.raise(result)
19
+ end
20
+
21
+ def respond_loop(&b)
22
+ while true
23
+ respond(&b)
24
+ end
25
+ end
26
+ end
27
+
28
+ $server = spin do
29
+ Fiber.current.respond_loop do |x, y|
30
+ x * y
31
+ end
32
+ end
33
+
34
+ $server_optimized = spin do
35
+ while true
36
+ fiber, x, y = receive
37
+ fiber << (x * y)
38
+ end
39
+ end
40
+
41
+ peer = Fiber.current
42
+ $server_single = spin do
43
+ while true
44
+ x = receive
45
+ peer << x * 4
46
+ end
47
+ end
48
+
49
+ $server_schedule = spin do
50
+ while true
51
+ x = suspend
52
+ peer.schedule x * 4
53
+ end
54
+ end
55
+
56
+ $server_raw = Fiber.new do |x|
57
+ while true
58
+ x = peer.transfer x * 4
59
+ end
60
+ end
61
+
62
+ def calc(x, y)
63
+ x * y
64
+ end
65
+
66
+ def bm_raw
67
+ calc(3, 4)
68
+ end
69
+
70
+ def bm_send
71
+ send(:calc, 3, 4)
72
+ end
73
+
74
+ def bm_fiber
75
+ $server.call(3, 4)
76
+ end
77
+
78
+ def bm_fiber_optimized
79
+ $server_optimized << [Fiber.current, 3, 4]
80
+ receive
81
+ end
82
+
83
+ def bm_fiber_single
84
+ $server_single << 3
85
+ receive
86
+ end
87
+
88
+ def bm_fiber_schedule
89
+ $server_schedule.schedule(3)
90
+ suspend
91
+ end
92
+
93
+ def bm_fiber_raw
94
+ $server_raw.transfer 3
95
+ end
96
+
97
+ # p bm_raw
98
+ # p bm_send
99
+ # p bm_fiber
100
+ # p bm_fiber_optimized
101
+ # p bm_fiber_single
102
+ p bm_fiber_raw
103
+ p bm_fiber_schedule
104
+
105
+ def warmup_jit
106
+ 10000.times do
107
+ bm_raw
108
+ bm_send
109
+ bm_fiber
110
+ bm_fiber_optimized
111
+ bm_fiber_single
112
+ bm_fiber_raw
113
+ bm_fiber_schedule
114
+ end
115
+ end
116
+
117
+ puts "warming up JIT..."
118
+
119
+ # 3.times do
120
+ # warmup_jit
121
+ # sleep 1
122
+ # end
123
+
124
+ Benchmark.ips do |x|
125
+ # x.report("raw") { bm_raw }
126
+ # x.report("send") { bm_send }
127
+ # x.report("fiber") { bm_fiber }
128
+ # x.report("fiber_optimized") { bm_fiber_optimized }
129
+ # x.report("fiber_single") { bm_fiber_single }
130
+ x.report("fiber_raw") { bm_fiber_raw }
131
+ x.report("fiber_schedule") { bm_fiber_schedule }
132
+ x.compare!
133
+ end
134
+
135
+ # p call_result: server.call(3, 4)
136
+
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ class Stream
7
+ def initialize(io)
8
+ @io = io
9
+ @buffer = +''
10
+ @length = 0
11
+ @pos = 0
12
+ end
13
+
14
+ def getbyte
15
+ if @pos == @length
16
+ return nil if !fill_buffer
17
+ end
18
+ byte = @buffer[@pos].getbyte(0)
19
+ @pos += 1
20
+ byte
21
+ end
22
+
23
+ def getc
24
+ if @pos == @length
25
+ return nil if !fill_buffer
26
+ end
27
+ char = @buffer[@pos]
28
+ @pos += 1
29
+ char
30
+ end
31
+
32
+ def ungetc(c)
33
+ @buffer.insert(@pos, c)
34
+ @length += 1
35
+ c
36
+ end
37
+
38
+ def gets
39
+ end
40
+
41
+ def read
42
+ end
43
+
44
+ def readpartial
45
+ end
46
+
47
+ private
48
+
49
+ def fill_buffer
50
+ Polyphony.backend_read(@io, @buffer, 8192, false, -1)
51
+ @length = @buffer.size
52
+ end
53
+ end
54
+
55
+ i, o = IO.pipe
56
+ s = Stream.new(i)
57
+
58
+ f = spin do
59
+ loop do
60
+ b = s.getbyte
61
+ p getbyte: b
62
+ s.ungetc(b.to_s) if rand > 0.5
63
+ end
64
+ end
65
+
66
+ o << 'hello'
67
+ sleep 0.1
68
+
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ i = 0
7
+ value = move_on_after(1, with_value: 42) do
8
+ throttled_loop(20) do
9
+ p (i += 1)
10
+ end
11
+ end
12
+
13
+ p value: value
@@ -389,12 +389,12 @@ inline void set_fd_blocking_mode(int fd, int blocking) {
389
389
  #endif
390
390
  }
391
391
 
392
- inline void io_verify_blocking_mode(rb_io_t *fptr, VALUE io, VALUE blocking) {
392
+ inline void io_verify_blocking_mode(VALUE io, int fd, VALUE blocking) {
393
393
  VALUE blocking_mode = rb_ivar_get(io, ID_ivar_blocking_mode);
394
394
  if (blocking == blocking_mode) return;
395
395
 
396
396
  rb_ivar_set(io, ID_ivar_blocking_mode, blocking);
397
- set_fd_blocking_mode(fptr->fd, blocking == Qtrue);
397
+ set_fd_blocking_mode(fd, blocking == Qtrue);
398
398
  }
399
399
 
400
400
  inline void backend_run_idle_tasks(struct Backend_base *base) {
@@ -455,9 +455,7 @@ VALUE Backend_stats(VALUE self) {
455
455
  }
456
456
 
457
457
  VALUE Backend_verify_blocking_mode(VALUE self, VALUE io, VALUE blocking) {
458
- rb_io_t *fptr;
459
- GetOpenFile(io, fptr);
460
- io_verify_blocking_mode(fptr, io, blocking);
458
+ io_verify_blocking_mode(io, rb_io_descriptor(io), blocking);
461
459
  return self;
462
460
  }
463
461
 
@@ -10,6 +10,15 @@
10
10
  #include "ruby/io.h"
11
11
  #include "runqueue.h"
12
12
 
13
+ #ifndef HAVE_RB_IO_DESCRIPTOR
14
+ static int rb_io_descriptor_fallback(VALUE io) {
15
+ rb_io_t *fptr;
16
+ GetOpenFile(io, fptr);
17
+ return fptr->fd;
18
+ }
19
+ #define rb_io_descriptor rb_io_descriptor_fallback
20
+ #endif
21
+
13
22
  struct backend_stats {
14
23
  unsigned int runqueue_size;
15
24
  unsigned int runqueue_length;
@@ -145,7 +154,7 @@ VALUE Backend_stats(VALUE self);
145
154
  VALUE Backend_verify_blocking_mode(VALUE self, VALUE io, VALUE blocking);
146
155
  void backend_run_idle_tasks(struct Backend_base *base);
147
156
  void set_fd_blocking_mode(int fd, int blocking);
148
- void io_verify_blocking_mode(rb_io_t *fptr, VALUE io, VALUE blocking);
157
+ void io_verify_blocking_mode(VALUE io, int fd, VALUE blocking);
149
158
  void backend_setup_stats_symbols();
150
159
  int backend_getaddrinfo(VALUE host, VALUE port, struct sockaddr **ai_addr);
151
160
  VALUE name_to_addrinfo(void *name, socklen_t len);
@@ -28,9 +28,9 @@ VALUE SYM_write;
28
28
  VALUE eArgumentError;
29
29
 
30
30
  #ifdef POLYPHONY_UNSET_NONBLOCK
31
- #define io_unset_nonblock(fptr, io) io_verify_blocking_mode(fptr, io, Qtrue)
31
+ #define io_unset_nonblock(io, fd) io_verify_blocking_mode(io, fd, Qtrue)
32
32
  #else
33
- #define io_unset_nonblock(fptr, io)
33
+ #define io_unset_nonblock(io, fd)
34
34
  #endif
35
35
 
36
36
  typedef struct Backend_t {
@@ -389,10 +389,10 @@ static inline int fd_from_io(VALUE io, rb_io_t **fptr, int write_mode, int recti
389
389
  if (underlying_io != Qnil) io = underlying_io;
390
390
 
391
391
  GetOpenFile(io, *fptr);
392
- io_unset_nonblock(*fptr, io);
392
+ int fd = rb_io_descriptor(io);
393
+ io_unset_nonblock(io, fd);
393
394
  if (rectify_file_pos) rectify_io_file_pos(*fptr);
394
-
395
- return (*fptr)->fd;
395
+ return fd;
396
396
  }
397
397
  }
398
398
 
@@ -1376,7 +1376,7 @@ VALUE Backend_wait_io(VALUE self, VALUE io, VALUE write) {
1376
1376
 
1377
1377
  // if (fd < 0) return Qnil;
1378
1378
 
1379
- // io_unset_nonblock(fptr, io);
1379
+ // io_unset_nonblock(io, fd);
1380
1380
 
1381
1381
  // ctx = context_store_acquire(&backend->store, OP_CLOSE);
1382
1382
  // sqe = io_uring_backend_get_sqe(backend);