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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +1 -1
- data/.github/workflows/test_io_uring.yml +1 -1
- data/.yardopts +1 -0
- data/CHANGELOG.md +9 -0
- data/README.md +1 -0
- data/TODO.md +6 -12
- data/docs/advanced-io.md +224 -0
- data/docs/cheat-sheet.md +2 -2
- data/docs/readme.md +1 -0
- data/examples/core/debug.rb +12 -0
- data/examples/core/rpc_benchmark.rb +136 -0
- data/examples/core/stream_mockup.rb +68 -0
- data/examples/core/throttled_loop_inside_move_on_after.rb +13 -0
- data/ext/polyphony/backend_common.c +3 -5
- data/ext/polyphony/backend_common.h +10 -1
- data/ext/polyphony/backend_io_uring.c +6 -6
- data/ext/polyphony/backend_libev.c +5 -5
- data/ext/polyphony/extconf.rb +6 -0
- data/ext/polyphony/fiber.c +21 -1
- data/lib/polyphony/extensions/fiber.rb +1 -0
- data/lib/polyphony/extensions/io.rb +74 -74
- data/lib/polyphony/extensions/object.rb +6 -0
- data/lib/polyphony/extensions/socket.rb +39 -39
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +3 -1
- data/test/stress.rb +1 -1
- data/test/test_fiber.rb +45 -1
- data/test/test_io.rb +46 -0
- data/test/test_process_supervision.rb +1 -1
- data/test/test_resource_pool.rb +1 -1
- data/test/test_scenarios.rb +38 -0
- data/test/test_socket.rb +1 -2
- data/test/test_thread_pool.rb +4 -2
- data/test/test_timer.rb +2 -2
- metadata +36 -149
- data/vendor/liburing/man/IO_URING_CHECK_VERSION.3 +0 -1
- data/vendor/liburing/man/IO_URING_VERSION_MAJOR.3 +0 -1
- data/vendor/liburing/man/IO_URING_VERSION_MINOR.3 +0 -1
- data/vendor/liburing/man/io_uring.7 +0 -781
- data/vendor/liburing/man/io_uring_buf_ring_add.3 +0 -53
- data/vendor/liburing/man/io_uring_buf_ring_advance.3 +0 -31
- data/vendor/liburing/man/io_uring_buf_ring_cq_advance.3 +0 -41
- data/vendor/liburing/man/io_uring_buf_ring_init.3 +0 -30
- data/vendor/liburing/man/io_uring_buf_ring_mask.3 +0 -27
- data/vendor/liburing/man/io_uring_check_version.3 +0 -72
- data/vendor/liburing/man/io_uring_close_ring_fd.3 +0 -43
- data/vendor/liburing/man/io_uring_cq_advance.3 +0 -49
- data/vendor/liburing/man/io_uring_cq_has_overflow.3 +0 -25
- data/vendor/liburing/man/io_uring_cq_ready.3 +0 -26
- data/vendor/liburing/man/io_uring_cqe_get_data.3 +0 -53
- data/vendor/liburing/man/io_uring_cqe_get_data64.3 +0 -1
- data/vendor/liburing/man/io_uring_cqe_seen.3 +0 -42
- data/vendor/liburing/man/io_uring_enter.2 +0 -1700
- data/vendor/liburing/man/io_uring_enter2.2 +0 -1
- data/vendor/liburing/man/io_uring_free_probe.3 +0 -27
- data/vendor/liburing/man/io_uring_get_events.3 +0 -33
- data/vendor/liburing/man/io_uring_get_probe.3 +0 -30
- data/vendor/liburing/man/io_uring_get_sqe.3 +0 -57
- data/vendor/liburing/man/io_uring_major_version.3 +0 -1
- data/vendor/liburing/man/io_uring_minor_version.3 +0 -1
- data/vendor/liburing/man/io_uring_opcode_supported.3 +0 -30
- data/vendor/liburing/man/io_uring_peek_cqe.3 +0 -38
- data/vendor/liburing/man/io_uring_prep_accept.3 +0 -197
- data/vendor/liburing/man/io_uring_prep_accept_direct.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_cancel.3 +0 -118
- data/vendor/liburing/man/io_uring_prep_cancel64.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_close.3 +0 -59
- data/vendor/liburing/man/io_uring_prep_close_direct.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_connect.3 +0 -66
- data/vendor/liburing/man/io_uring_prep_fadvise.3 +0 -59
- data/vendor/liburing/man/io_uring_prep_fallocate.3 +0 -59
- data/vendor/liburing/man/io_uring_prep_fgetxattr.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_files_update.3 +0 -92
- data/vendor/liburing/man/io_uring_prep_fsetxattr.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_fsync.3 +0 -70
- data/vendor/liburing/man/io_uring_prep_getxattr.3 +0 -61
- data/vendor/liburing/man/io_uring_prep_link.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_link_timeout.3 +0 -94
- data/vendor/liburing/man/io_uring_prep_linkat.3 +0 -91
- data/vendor/liburing/man/io_uring_prep_madvise.3 +0 -56
- data/vendor/liburing/man/io_uring_prep_mkdir.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_mkdirat.3 +0 -83
- data/vendor/liburing/man/io_uring_prep_msg_ring.3 +0 -92
- data/vendor/liburing/man/io_uring_prep_msg_ring_cqe_flags.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_multishot_accept.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_multishot_accept_direct.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_nop.3 +0 -28
- data/vendor/liburing/man/io_uring_prep_openat.3 +0 -117
- data/vendor/liburing/man/io_uring_prep_openat2.3 +0 -117
- data/vendor/liburing/man/io_uring_prep_openat2_direct.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_openat_direct.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_poll_add.3 +0 -72
- data/vendor/liburing/man/io_uring_prep_poll_multishot.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_poll_remove.3 +0 -55
- data/vendor/liburing/man/io_uring_prep_poll_update.3 +0 -89
- data/vendor/liburing/man/io_uring_prep_provide_buffers.3 +0 -140
- data/vendor/liburing/man/io_uring_prep_read.3 +0 -69
- data/vendor/liburing/man/io_uring_prep_read_fixed.3 +0 -72
- data/vendor/liburing/man/io_uring_prep_readv.3 +0 -85
- data/vendor/liburing/man/io_uring_prep_readv2.3 +0 -111
- data/vendor/liburing/man/io_uring_prep_recv.3 +0 -105
- data/vendor/liburing/man/io_uring_prep_recv_multishot.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_recvmsg.3 +0 -124
- data/vendor/liburing/man/io_uring_prep_recvmsg_multishot.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_remove_buffers.3 +0 -52
- data/vendor/liburing/man/io_uring_prep_rename.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_renameat.3 +0 -96
- data/vendor/liburing/man/io_uring_prep_send.3 +0 -66
- data/vendor/liburing/man/io_uring_prep_send_set_addr.3 +0 -38
- data/vendor/liburing/man/io_uring_prep_send_zc.3 +0 -96
- data/vendor/liburing/man/io_uring_prep_send_zc_fixed.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_sendmsg.3 +0 -89
- data/vendor/liburing/man/io_uring_prep_sendmsg_zc.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_setxattr.3 +0 -64
- data/vendor/liburing/man/io_uring_prep_shutdown.3 +0 -53
- data/vendor/liburing/man/io_uring_prep_socket.3 +0 -118
- data/vendor/liburing/man/io_uring_prep_socket_direct.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_socket_direct_alloc.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_splice.3 +0 -120
- data/vendor/liburing/man/io_uring_prep_statx.3 +0 -74
- data/vendor/liburing/man/io_uring_prep_symlink.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_symlinkat.3 +0 -85
- data/vendor/liburing/man/io_uring_prep_sync_file_range.3 +0 -59
- data/vendor/liburing/man/io_uring_prep_tee.3 +0 -74
- data/vendor/liburing/man/io_uring_prep_timeout.3 +0 -95
- data/vendor/liburing/man/io_uring_prep_timeout_remove.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_timeout_update.3 +0 -98
- data/vendor/liburing/man/io_uring_prep_unlink.3 +0 -1
- data/vendor/liburing/man/io_uring_prep_unlinkat.3 +0 -82
- data/vendor/liburing/man/io_uring_prep_write.3 +0 -67
- data/vendor/liburing/man/io_uring_prep_write_fixed.3 +0 -72
- data/vendor/liburing/man/io_uring_prep_writev.3 +0 -85
- data/vendor/liburing/man/io_uring_prep_writev2.3 +0 -111
- data/vendor/liburing/man/io_uring_queue_exit.3 +0 -26
- data/vendor/liburing/man/io_uring_queue_init.3 +0 -89
- data/vendor/liburing/man/io_uring_queue_init_params.3 +0 -1
- data/vendor/liburing/man/io_uring_recvmsg_cmsg_firsthdr.3 +0 -1
- data/vendor/liburing/man/io_uring_recvmsg_cmsg_nexthdr.3 +0 -1
- data/vendor/liburing/man/io_uring_recvmsg_name.3 +0 -1
- data/vendor/liburing/man/io_uring_recvmsg_out.3 +0 -82
- data/vendor/liburing/man/io_uring_recvmsg_payload.3 +0 -1
- data/vendor/liburing/man/io_uring_recvmsg_payload_length.3 +0 -1
- data/vendor/liburing/man/io_uring_recvmsg_validate.3 +0 -1
- data/vendor/liburing/man/io_uring_register.2 +0 -834
- data/vendor/liburing/man/io_uring_register_buf_ring.3 +0 -140
- data/vendor/liburing/man/io_uring_register_buffers.3 +0 -104
- data/vendor/liburing/man/io_uring_register_buffers_sparse.3 +0 -1
- data/vendor/liburing/man/io_uring_register_buffers_tags.3 +0 -1
- data/vendor/liburing/man/io_uring_register_buffers_update_tag.3 +0 -1
- data/vendor/liburing/man/io_uring_register_eventfd.3 +0 -51
- data/vendor/liburing/man/io_uring_register_eventfd_async.3 +0 -1
- data/vendor/liburing/man/io_uring_register_file_alloc_range.3 +0 -52
- data/vendor/liburing/man/io_uring_register_files.3 +0 -112
- data/vendor/liburing/man/io_uring_register_files_sparse.3 +0 -1
- data/vendor/liburing/man/io_uring_register_files_tags.3 +0 -1
- data/vendor/liburing/man/io_uring_register_files_update.3 +0 -1
- data/vendor/liburing/man/io_uring_register_files_update_tag.3 +0 -1
- data/vendor/liburing/man/io_uring_register_iowq_aff.3 +0 -61
- data/vendor/liburing/man/io_uring_register_iowq_max_workers.3 +0 -71
- data/vendor/liburing/man/io_uring_register_ring_fd.3 +0 -49
- data/vendor/liburing/man/io_uring_register_sync_cancel.3 +0 -71
- data/vendor/liburing/man/io_uring_setup.2 +0 -669
- data/vendor/liburing/man/io_uring_sq_ready.3 +0 -31
- data/vendor/liburing/man/io_uring_sq_space_left.3 +0 -25
- data/vendor/liburing/man/io_uring_sqe_set_data.3 +0 -48
- data/vendor/liburing/man/io_uring_sqe_set_data64.3 +0 -1
- data/vendor/liburing/man/io_uring_sqe_set_flags.3 +0 -87
- data/vendor/liburing/man/io_uring_sqring_wait.3 +0 -34
- data/vendor/liburing/man/io_uring_submit.3 +0 -46
- data/vendor/liburing/man/io_uring_submit_and_get_events.3 +0 -31
- data/vendor/liburing/man/io_uring_submit_and_wait.3 +0 -38
- data/vendor/liburing/man/io_uring_submit_and_wait_timeout.3 +0 -56
- data/vendor/liburing/man/io_uring_unregister_buf_ring.3 +0 -30
- data/vendor/liburing/man/io_uring_unregister_buffers.3 +0 -27
- data/vendor/liburing/man/io_uring_unregister_eventfd.3 +0 -1
- data/vendor/liburing/man/io_uring_unregister_files.3 +0 -27
- data/vendor/liburing/man/io_uring_unregister_iowq_aff.3 +0 -1
- data/vendor/liburing/man/io_uring_unregister_ring_fd.3 +0 -32
- data/vendor/liburing/man/io_uring_wait_cqe.3 +0 -40
- data/vendor/liburing/man/io_uring_wait_cqe_nr.3 +0 -43
- data/vendor/liburing/man/io_uring_wait_cqe_timeout.3 +0 -53
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61fa840f595a0e75da1d6e1b761506b18fa90d5a94b25ce5832d22aae2e08fbd
|
|
4
|
+
data.tar.gz: 7d3a79225f68bb889ff0491ced1249c2679a222a3f79a5c4cf48f9571865ebc4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32d61cf7e0858e704fc39fe317048e3e531afcf97da70b9b3d1e4b59a078e2f5c8c65a3f72c656b61285df9e5c99d7430938403560ca6e8e7aa5d920dada274e
|
|
7
|
+
data.tar.gz: c5c1488b1cec55810ffccf8116d70e8312ccb7d93875c1047ab7608595eb81fda9601f44ef308b3e1f35319d683b6ceda423284c9cda76c6f44882a7341e681a
|
data/.github/workflows/test.yml
CHANGED
data/.yardopts
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
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.
|
|
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
|
|
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
|
|
data/docs/advanced-io.md
ADDED
|
@@ -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,
|
|
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
|
@@ -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
|
+
|
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
31
|
+
#define io_unset_nonblock(io, fd) io_verify_blocking_mode(io, fd, Qtrue)
|
|
32
32
|
#else
|
|
33
|
-
#define io_unset_nonblock(
|
|
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
|
-
|
|
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(
|
|
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);
|