polyphony 1.0.1 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
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);