polyphony 0.45.4 → 0.47.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -0
  3. data/.gitmodules +0 -0
  4. data/CHANGELOG.md +32 -0
  5. data/Gemfile.lock +1 -1
  6. data/README.md +3 -3
  7. data/Rakefile +1 -1
  8. data/TODO.md +20 -34
  9. data/bin/test +4 -0
  10. data/examples/core/enumerable.rb +64 -0
  11. data/examples/performance/fiber_resume.rb +43 -0
  12. data/examples/performance/fiber_transfer.rb +13 -4
  13. data/examples/performance/multi_snooze.rb +0 -1
  14. data/examples/performance/thread-vs-fiber/compare.rb +59 -0
  15. data/examples/performance/thread-vs-fiber/em_server.rb +33 -0
  16. data/examples/performance/thread-vs-fiber/polyphony_server.rb +10 -21
  17. data/examples/performance/thread-vs-fiber/threaded_server.rb +22 -15
  18. data/examples/performance/thread_switch.rb +44 -0
  19. data/ext/liburing/liburing.h +585 -0
  20. data/ext/liburing/liburing/README.md +4 -0
  21. data/ext/liburing/liburing/barrier.h +73 -0
  22. data/ext/liburing/liburing/compat.h +15 -0
  23. data/ext/liburing/liburing/io_uring.h +343 -0
  24. data/ext/liburing/queue.c +333 -0
  25. data/ext/liburing/register.c +187 -0
  26. data/ext/liburing/setup.c +210 -0
  27. data/ext/liburing/syscall.c +54 -0
  28. data/ext/liburing/syscall.h +18 -0
  29. data/ext/polyphony/backend.h +1 -15
  30. data/ext/polyphony/backend_common.h +129 -0
  31. data/ext/polyphony/backend_io_uring.c +995 -0
  32. data/ext/polyphony/backend_io_uring_context.c +74 -0
  33. data/ext/polyphony/backend_io_uring_context.h +53 -0
  34. data/ext/polyphony/{libev_backend.c → backend_libev.c} +308 -297
  35. data/ext/polyphony/event.c +1 -1
  36. data/ext/polyphony/extconf.rb +31 -13
  37. data/ext/polyphony/fiber.c +60 -32
  38. data/ext/polyphony/libev.c +4 -0
  39. data/ext/polyphony/libev.h +8 -2
  40. data/ext/polyphony/liburing.c +8 -0
  41. data/ext/polyphony/playground.c +51 -0
  42. data/ext/polyphony/polyphony.c +9 -6
  43. data/ext/polyphony/polyphony.h +35 -19
  44. data/ext/polyphony/polyphony_ext.c +12 -4
  45. data/ext/polyphony/queue.c +100 -35
  46. data/ext/polyphony/runqueue.c +102 -0
  47. data/ext/polyphony/runqueue_ring_buffer.c +85 -0
  48. data/ext/polyphony/runqueue_ring_buffer.h +31 -0
  49. data/ext/polyphony/thread.c +42 -90
  50. data/lib/polyphony/adapters/trace.rb +2 -2
  51. data/lib/polyphony/core/exceptions.rb +0 -4
  52. data/lib/polyphony/core/global_api.rb +47 -23
  53. data/lib/polyphony/core/resource_pool.rb +12 -1
  54. data/lib/polyphony/core/sync.rb +7 -5
  55. data/lib/polyphony/extensions/core.rb +9 -15
  56. data/lib/polyphony/extensions/debug.rb +13 -0
  57. data/lib/polyphony/extensions/fiber.rb +13 -9
  58. data/lib/polyphony/extensions/openssl.rb +6 -0
  59. data/lib/polyphony/extensions/socket.rb +68 -10
  60. data/lib/polyphony/version.rb +1 -1
  61. data/test/helper.rb +36 -4
  62. data/test/io_uring_test.rb +55 -0
  63. data/test/stress.rb +4 -1
  64. data/test/test_backend.rb +63 -6
  65. data/test/test_ext.rb +1 -2
  66. data/test/test_fiber.rb +55 -20
  67. data/test/test_global_api.rb +132 -31
  68. data/test/test_queue.rb +117 -0
  69. data/test/test_resource_pool.rb +21 -0
  70. data/test/test_socket.rb +2 -2
  71. data/test/test_sync.rb +21 -0
  72. data/test/test_throttler.rb +3 -6
  73. data/test/test_trace.rb +7 -5
  74. metadata +32 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52f8ebf1104d921c9e3b0afa0b4605ee8f912eabe8b6264898f23905d2cb6c6f
4
- data.tar.gz: 38f0f7cd62997ba5681185c3c4859f57d86de875d8292faf67fffa53c7160181
3
+ metadata.gz: e9b7b3885b4f6d88b63d29814e7ce7883927c58fefaacab3ef980dafb3ef380f
4
+ data.tar.gz: 14ee82740f415364b675db77f9246482937a2650df437ecf42c8d590bbc3d9fb
5
5
  SHA512:
6
- metadata.gz: 2429259a2e79757ec879c4c00db8fd8b87302b9e0a61c6ae2ddd5fdb6e1a3a06ac63d551ed33a4c77480e156eb1247a0fab4a263179a60fd2e9a2d3173831a58
7
- data.tar.gz: 04f58dafb5faf1e21b08fd85d508ceb6e9869b0a69d63c133661a834f169eb750b599639ffdbff474abf7f92bc6bfee3a43b9442c2885a6dd88328657b0e1d36
6
+ metadata.gz: 98d245d285e39383c2fad4f3045b2b994922f9c0c1100aec7b264c731cba9a433e8e0677a173bbbb3cfc11199e9f5f29d98c55ebf7224a5bb3358db93ba25bd3
7
+ data.tar.gz: 6bfa625ff66461d4a23b8e5c6c8d9c1818b6030950da28e7712d3207ee11654527b36205ddab904bfed58a85144b3f73e7892479992eb775bfe7fcc1d4e54ac0
@@ -23,6 +23,8 @@ jobs:
23
23
  run: |
24
24
  gem install bundler
25
25
  bundle install
26
+ - name: Show Linux kernel version
27
+ run: uname -r
26
28
  - name: Compile C-extension
27
29
  run: bundle exec rake compile
28
30
  - name: Run tests
File without changes
@@ -1,3 +1,35 @@
1
+ ## 0.47.1
2
+
3
+ * Fix API compatibility between TCPSocket and IO
4
+
5
+ ## 0.47.0
6
+
7
+ * Implement `#spin_scope` used for creating blocking fiber scopes
8
+ * Reimplement `move_on_after`, `cancel_after`, `Timeout.timeout` using
9
+ `Backend#timeout` (avoids creating canceller fiber for most common use case)
10
+ * Implement `Backend#timeout` API
11
+ * Implemented capped queues
12
+
13
+ ## 0.46.1
14
+
15
+ * Add `TCPServer#accept_loop`, `OpenSSL::SSL::SSLSocket#accept_loop` method
16
+ * Fix compilation error on MacOS (#43)
17
+ * Fix backtrace for `Timeout.timeout`
18
+ * Add `Backend#timer_loop`
19
+
20
+ ## 0.46.0
21
+
22
+ * Implement [io_uring backend](https://github.com/digital-fabric/polyphony/pull/44)
23
+
24
+ ## 0.45.5
25
+
26
+ * Fix compilation error (#43)
27
+ * Add support for resetting move_on_after, cancel_after timeouts
28
+ * Optimize anti-event starvation polling
29
+ * Implement optimized runqueue for better performance
30
+ * Schedule parent with priority on uncaught exception
31
+ * Fix race condition in `Mutex#synchronize` (#41)
32
+
1
33
  ## 0.45.4
2
34
 
3
35
  * Improve signal trapping mechanism
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- polyphony (0.45.4)
4
+ polyphony (0.47.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -35,9 +35,9 @@
35
35
  Polyphony is a library for building concurrent applications in Ruby. Polyphony
36
36
  harnesses the power of [Ruby fibers](https://ruby-doc.org/core-2.5.1/Fiber.html)
37
37
  to provide a cooperative, sequential coroutine-based concurrency model. Under
38
- the hood, Polyphony uses [libev](https://github.com/enki/libev) as a
39
- high-performance event reactor that provides timers, I/O watchers and other
40
- asynchronous event primitives.
38
+ the hood, Polyphony uses
39
+ [io_uring](https://unixism.net/loti/what_is_io_uring.html) or
40
+ [libev](https://github.com/enki/libev) to maximize I/O performance.
41
41
 
42
42
  ## Features
43
43
 
data/Rakefile CHANGED
@@ -23,4 +23,4 @@ task :docs do
23
23
  exec 'RUBYOPT=-W0 jekyll serve -s docs -H ec2-18-156-117-172.eu-central-1.compute.amazonaws.com'
24
24
  end
25
25
 
26
- CLEAN.include "**/*.o", "**/*.so", "**/*.bundle", "**/*.jar", "pkg", "tmp"
26
+ CLEAN.include "**/*.o", "**/*.so", "**/*.so.*", "**/*.a", "**/*.bundle", "**/*.jar", "pkg", "tmp"
data/TODO.md CHANGED
@@ -1,21 +1,24 @@
1
- (
2
- io_uring: some work has been done on an io_uring based scheduler here:
3
- https://github.com/dsh0416/evt
4
-
5
- This can serve as a starting point for doing stuff with io_uring
6
- )
1
+ ## Roadmap for Polyphony 1.0
7
2
 
8
- 0.45.4
9
-
10
- - Adapter for io/console (what does `IO#raw` do?)
11
- - Adapter for Pry and IRB (Which fixes #5 and #6)
3
+ - Check why worker-thread example doesn't work.
4
+ - Add test that mimics the original design for Monocrono:
5
+ - 256 fibers each waiting for a message
6
+ - When message received do some blocking work using a `ThreadPool`
7
+ - Send messages, collect responses, check for correctness
12
8
  - Improve `#supervise`. It does not work as advertised, and seems to exhibit an
13
9
  inconsistent behaviour (see supervisor example).
14
- - Fix backtrace for `Timeout.timeout` API (see timeout example).
15
- - Check why worker-thread example doesn't work.
16
10
 
17
- 0.46.0
11
+ - io_uring
12
+ - Use playground.c to find out why we when submitting and waiting for
13
+ completion in single syscall signals seem to be blocked until the syscall
14
+ returns. Is this a bug in io_uring/liburing?
18
15
 
16
+ -----------------------------------------------------
17
+
18
+ - Add `Backend#splice(in, out, nbytes)` API
19
+ - Adapter for io/console (what does `IO#raw` do?)
20
+ - Adapter for Pry and IRB (Which fixes #5 and #6)
21
+ - allow backend selection at runtime
19
22
  - Debugging
20
23
  - Eat your own dogfood: need a good tool to check what's going on when some
21
24
  test fails
@@ -128,8 +131,6 @@
128
131
  - discuss using `snooze` for ensuring responsiveness when executing CPU-bound work
129
132
 
130
133
 
131
- ## 0.47
132
-
133
134
  ### Some more API work, more docs
134
135
 
135
136
  - sintra app with database access (postgresql)
@@ -141,14 +142,10 @@
141
142
  - proceed from there
142
143
 
143
144
 
144
- ## 0.48
145
-
146
145
  ### Sinatra / Sidekiq
147
146
 
148
147
  - Pull out redis/postgres code, put into new `polyphony-xxx` gems
149
148
 
150
- ## 0.49
151
-
152
149
  ### Testing && Docs
153
150
 
154
151
  - More tests
@@ -159,7 +156,10 @@
159
156
  - `IO.foreach`
160
157
  - `Process.waitpid`
161
158
 
162
- ## 0.50 DNS
159
+ ### Quic / HTTP/3
160
+
161
+ - Python impl: https://github.com/aiortc/aioquic/
162
+ - Go impl: https://github.com/lucas-clemente/quic-go
163
163
 
164
164
  ### DNS client
165
165
 
@@ -192,17 +192,3 @@ Prior art:
192
192
 
193
193
  - https://github.com/socketry/async-dns
194
194
 
195
- ## Work on API
196
-
197
- - Add option for setting the exception raised on cancelling using `#cancel_after`:
198
-
199
- ```ruby
200
- cancel_after(3, with_error: MyErrorClass) do
201
- do_my_thing
202
- end
203
- # or a RuntimeError with message
204
- cancel_after(3, with_error: 'Cancelled due to timeout') do
205
- do_my_thing
206
- end
207
- ```
208
-
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ clear && POLYPHONY_USE_LIBEV=1 rake recompile && ruby test/run.rb
4
+ clear && rake recompile && ruby test/run.rb
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ Exception.__disable_sanitized_backtrace__ = true
7
+
8
+ module Enumerable
9
+ def map_concurrently(&block)
10
+ spin do
11
+ results = []
12
+ each_with_index do |i, idx|
13
+ spin { results[idx] = block.(i) }
14
+ end
15
+ Fiber.current.await_all_children
16
+ results
17
+ end.await
18
+ end
19
+
20
+ def each_concurrently(max_fibers: nil, &block)
21
+ return each_concurrently_with_fiber_pool(max_fibers, &block) if max_fibers
22
+
23
+ spin do
24
+ results = []
25
+ each do |i|
26
+ spin(&block).schedule(i)
27
+ end
28
+ Fiber.current.await_all_children
29
+ end.await
30
+ self
31
+ end
32
+
33
+ def each_concurrently_with_fiber_pool(max_fibers, &block)
34
+ spin do
35
+ fiber_count = 0
36
+ workers = []
37
+ each do |i|
38
+ if fiber_count < max_fibers
39
+ workers << spin do
40
+ loop do
41
+ item = receive
42
+ break if item == :__stop__
43
+ block.(item)
44
+ end
45
+ end
46
+ end
47
+
48
+ fiber = workers.shift
49
+ fiber << i
50
+ workers << fiber
51
+ end
52
+ workers.each { |f| f << :__stop__ }
53
+ Fiber.current.await_all_children
54
+ end.await
55
+ self
56
+ end
57
+ end
58
+
59
+ o = 1..3
60
+ o.each_concurrently(max_fibers: 2) do |i|
61
+ puts "#{Fiber.current} sleep #{i}"
62
+ sleep(i)
63
+ puts "wakeup #{i}"
64
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiber'
4
+
5
+ class Fiber
6
+ attr_accessor :next
7
+ end
8
+
9
+ # This program shows how the performance of Fiber.transfer degrades as the fiber
10
+ # count increases
11
+
12
+ def run(num_fibers)
13
+ count = 0
14
+
15
+ GC.start
16
+ GC.disable
17
+
18
+ fibers = []
19
+ num_fibers.times do
20
+ fibers << Fiber.new { loop { Fiber.yield } }
21
+ end
22
+
23
+ t0 = Time.now
24
+
25
+ while count < 1000000
26
+ fibers.each do |f|
27
+ count += 1
28
+ f.resume
29
+ end
30
+ end
31
+
32
+ elapsed = Time.now - t0
33
+
34
+ puts "fibers: #{num_fibers} count: #{count} rate: #{count / elapsed}"
35
+ rescue Exception => e
36
+ puts "Stopped at #{count} fibers"
37
+ p e
38
+ end
39
+
40
+ run(100)
41
+ run(1000)
42
+ run(10000)
43
+ run(100000)
@@ -12,6 +12,7 @@ end
12
12
  def run(num_fibers)
13
13
  count = 0
14
14
 
15
+ GC.start
15
16
  GC.disable
16
17
 
17
18
  first = nil
@@ -36,13 +37,21 @@ def run(num_fibers)
36
37
  last.next = first
37
38
 
38
39
  t0 = Time.now
40
+ puts "start transfer..."
39
41
  first.transfer
40
42
  elapsed = Time.now - t0
41
43
 
42
- puts "fibers: #{num_fibers} count: #{count} rate: #{count / elapsed}"
43
- GC.start
44
+ rss = `ps -o rss= -p #{Process.pid}`.to_i
45
+
46
+ puts "fibers: #{num_fibers} rss: #{rss} count: #{count} rate: #{count / elapsed}"
47
+ rescue Exception => e
48
+ puts "Stopped at #{count} fibers"
49
+ p e
44
50
  end
45
51
 
52
+ puts "pid: #{Process.pid}"
46
53
  run(100)
47
- run(1000)
48
- run(10000)
54
+ # run(1000)
55
+ # run(10000)
56
+ # run(100000)
57
+ # run(400000)
@@ -18,7 +18,6 @@ def bm(fibers, iterations)
18
18
  Fiber.current.await_all_children
19
19
  dt = Time.now - t0
20
20
  puts "#{[fibers, iterations].inspect} setup: #{t0 - t_pre}s count: #{count} #{count / dt.to_f}/s"
21
- Thread.current.run_queue_trace
22
21
  end
23
22
 
24
23
  GC.disable
@@ -0,0 +1,59 @@
1
+ SERVERS = {
2
+ polyphony: {
3
+ port: 1234,
4
+ cmd: 'ruby examples/performance/thread-vs-fiber/polyphony_server.rb'
5
+ },
6
+ threaded: {
7
+ port: 1235,
8
+ cmd: 'ruby examples/performance/thread-vs-fiber/threaded_server.rb'
9
+ },
10
+ em: {
11
+ port: 1236,
12
+ cmd: 'ruby examples/performance/thread-vs-fiber/em_server.rb'
13
+ }
14
+ }
15
+ SETTINGS = [
16
+ '-t1 -c1',
17
+ '-t4 -c8',
18
+ '-t8 -c64',
19
+ '-t16 -c512',
20
+ '-t32 -c4096',
21
+ '-t64 -c8192',
22
+ '-t128 -c16384',
23
+ '-t256 -c32768'
24
+ ]
25
+
26
+ def run_test(name, port, cmd, setting)
27
+ puts "*" * 80
28
+ puts "Run #{name} (#{port}): #{setting}"
29
+ puts "*" * 80
30
+
31
+ pid = spawn("#{cmd} > /dev/null 2>&1")
32
+ sleep 1
33
+
34
+ output = `wrk -d60 #{setting} \"http://127.0.0.1:#{port}/\"`
35
+ puts output
36
+ (output =~ /Requests\/sec:\s+(\d+)/) && $1.to_i
37
+ ensure
38
+ Process.kill('KILL', pid)
39
+ Process.wait(pid)
40
+ 3.times { puts }
41
+ end
42
+
43
+ def perform_benchmark
44
+ results = []
45
+ SETTINGS.each do |s|
46
+ results << SERVERS.inject({}) do |h, (n, o)|
47
+ h[n] = run_test(n, o[:port], o[:cmd], s)
48
+ h
49
+ end
50
+ end
51
+ results
52
+ end
53
+
54
+ results = []
55
+ 3.times { results << perform_benchmark }
56
+
57
+ require 'pp'
58
+ puts "results:"
59
+ pp results
@@ -0,0 +1,33 @@
1
+ require 'eventmachine'
2
+ require 'http/parser'
3
+ require 'socket'
4
+
5
+ module HTTPServer
6
+ def post_init
7
+ @parser = Http::Parser.new
8
+ @pending_requests = []
9
+ @parser.on_message_complete = proc { @pending_requests << @parser }
10
+ end
11
+
12
+ def receive_data(data)
13
+ @parser << data
14
+ write_response while @pending_requests.shift
15
+ end
16
+
17
+ def write_response
18
+ status_code = "200 OK"
19
+ data = "Hello world!\n"
20
+ headers = "Content-Type: text/plain\r\nContent-Length: #{data.bytesize}\r\n"
21
+ send_data "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
22
+ end
23
+ end
24
+
25
+ EventMachine::run do
26
+ EventMachine::start_server(
27
+ '0.0.0.0',
28
+ 1236,
29
+ HTTPServer
30
+ )
31
+ puts "pid #{Process.pid} EventMachine listening on port 1236"
32
+
33
+ end
@@ -4,42 +4,31 @@ require 'bundler/setup'
4
4
  require 'polyphony'
5
5
  require 'http/parser'
6
6
 
7
- $connection_count = 0
8
-
9
7
  def handle_client(socket)
10
- $connection_count += 1
8
+ pending_requests = []
11
9
  parser = Http::Parser.new
12
- reqs = []
13
- parser.on_message_complete = proc do |env|
14
- reqs << Object.new # parser
15
- end
16
- socket.read_loop do |data|
10
+ parser.on_message_complete = proc { pending_requests << parser }
11
+
12
+ socket.recv_loop do |data|
17
13
  parser << data
18
- while (req = reqs.shift)
19
- handle_request(socket, req)
20
- req = nil
21
- snooze
22
- end
14
+ write_response(socket) while pending_requests.shift
23
15
  end
24
16
  rescue IOError, SystemCallError => e
25
17
  # do nothing
26
18
  ensure
27
- $connection_count -= 1
28
19
  socket&.close
29
20
  end
30
21
 
31
- def handle_request(client, parser)
22
+ def write_response(socket)
32
23
  status_code = "200 OK"
33
24
  data = "Hello world!\n"
34
25
  headers = "Content-Type: text/plain\r\nContent-Length: #{data.bytesize}\r\n"
35
- client.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
26
+ socket.write "HTTP/1.1 #{status_code}\r\n#{headers}\r\n#{data}"
36
27
  end
37
28
 
38
29
  server = TCPServer.open('0.0.0.0', 1234)
39
- puts "pid #{Process.pid}"
40
- puts "listening on port 1234"
30
+ puts "pid #{Process.pid} Polyphony (#{Thread.current.backend.kind}) listening on port 1234"
41
31
 
42
- loop do
43
- client = server.accept
44
- spin { handle_client(client) }
32
+ server.accept_loop do |c|
33
+ spin { handle_client(c) }
45
34
  end