polyphony 0.45.4 → 0.47.1

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