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
@@ -0,0 +1,13 @@
1
+ module ::Kernel
2
+ def trace(*args)
3
+ STDOUT.orig_write(format_trace(args))
4
+ end
5
+
6
+ def format_trace(args)
7
+ if args.size > 1 && args.first.is_a?(String)
8
+ format("%s: %p\n", args.shift, args.size == 1 ? args.first : args)
9
+ else
10
+ format("%p\n", args.size == 1 ? args.first : args)
11
+ end
12
+ end
13
+ end
@@ -175,9 +175,9 @@ module Polyphony
175
175
  f = Fiber.new do
176
176
  block.call
177
177
  rescue Exception => e
178
- Thread.current.break_out_of_ev_loop(Thread.main.main_fiber, e)
178
+ Thread.current.schedule_and_wakeup(Thread.main.main_fiber, e)
179
179
  end
180
- Thread.current.break_out_of_ev_loop(f, nil)
180
+ Thread.current.schedule_and_wakeup(f, nil)
181
181
  end
182
182
  end
183
183
 
@@ -187,9 +187,9 @@ module Polyphony
187
187
  (@children ||= {}).keys
188
188
  end
189
189
 
190
- def spin(tag = nil, orig_caller = Kernel.caller, do_schedule: true, &block)
190
+ def spin(tag = nil, orig_caller = Kernel.caller, &block)
191
191
  f = Fiber.new { |v| f.run(v) }
192
- f.prepare(tag, block, orig_caller, self, do_schedule: do_schedule)
192
+ f.prepare(tag, block, orig_caller, self)
193
193
  (@children ||= {})[f] = true
194
194
  f
195
195
  end
@@ -227,14 +227,14 @@ module Polyphony
227
227
 
228
228
  # Fiber life cycle methods
229
229
  module FiberLifeCycle
230
- def prepare(tag, block, caller, parent, do_schedule: true)
230
+ def prepare(tag, block, caller, parent)
231
231
  @thread = Thread.current
232
232
  @tag = tag
233
233
  @parent = parent
234
234
  @caller = caller
235
235
  @block = block
236
236
  __fiber_trace__(:fiber_create, self)
237
- schedule if do_schedule
237
+ schedule
238
238
  end
239
239
 
240
240
  def run(first_value)
@@ -308,7 +308,7 @@ module Polyphony
308
308
  @waiting_fibers&.each_key { |f| f.schedule(result) }
309
309
 
310
310
  # propagate unaught exception to parent
311
- @parent&.schedule(result) if uncaught_exception && !@waiting_fibers
311
+ @parent&.schedule_with_priority(result) if uncaught_exception && !@waiting_fibers
312
312
  end
313
313
 
314
314
  def when_done(&block)
@@ -328,14 +328,18 @@ class ::Fiber
328
328
  extend Polyphony::FiberControlClassMethods
329
329
 
330
330
  attr_accessor :tag, :thread, :parent
331
- attr_reader :result, :mailbox
331
+ attr_reader :result
332
332
 
333
333
  def running?
334
334
  @running
335
335
  end
336
336
 
337
337
  def inspect
338
- "#<Fiber:#{object_id} #{location} (#{state})>"
338
+ if @tag
339
+ "#<Fiber #{tag}:#{object_id} #{location} (#{state})>"
340
+ else
341
+ "#<Fiber:#{object_id} #{location} (#{state})>"
342
+ end
339
343
  end
340
344
  alias_method :to_s, :inspect
341
345
 
@@ -36,6 +36,12 @@ class ::OpenSSL::SSL::SSLSocket
36
36
  end
37
37
  end
38
38
 
39
+ def accept_loop
40
+ loop do
41
+ yield accept
42
+ end
43
+ end
44
+
39
45
  alias_method :orig_sysread, :sysread
40
46
  def sysread(maxlen, buf = +'')
41
47
  loop do
@@ -19,18 +19,24 @@ class ::Socket
19
19
  end
20
20
 
21
21
  def recv(maxlen, flags = 0, outbuf = nil)
22
- outbuf ||= +''
23
- loop do
24
- result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
25
- case result
26
- when nil then raise IOError
27
- when :wait_readable then Thread.current.backend.wait_io(self, false)
28
- else
29
- return result
30
- end
31
- end
22
+ Thread.current.backend.recv(self, buf || +'', maxlen)
23
+ # outbuf ||= +''
24
+ # loop do
25
+ # result = recv_nonblock(maxlen, flags, outbuf, **NO_EXCEPTION)
26
+ # case result
27
+ # when nil then raise IOError
28
+ # when :wait_readable then Thread.current.backend.wait_io(self, false)
29
+ # else
30
+ # return result
31
+ # end
32
+ # end
32
33
  end
33
34
 
35
+ def recv_loop(&block)
36
+ Thread.current.backend.recv_loop(self, &block)
37
+ end
38
+ alias_method :read_loop, :recv_loop
39
+
34
40
  def recvfrom(maxlen, flags = 0)
35
41
  @read_buffer ||= +''
36
42
  loop do
@@ -44,6 +50,23 @@ class ::Socket
44
50
  end
45
51
  end
46
52
 
53
+ def send(mesg, flags = 0)
54
+ Thread.current.backend.send(self, mesg)
55
+ end
56
+
57
+ def write(str, *args)
58
+ if args.empty?
59
+ Thread.current.backend.send(self, str)
60
+ else
61
+ Thread.current.backend.send(self, str + args.join)
62
+ end
63
+ end
64
+ alias_method :<<, :write
65
+
66
+ def readpartial(maxlen, str = +'')
67
+ Thread.current.backend.recv(self, str, maxlen)
68
+ end
69
+
47
70
  ZERO_LINGER = [0, 0].pack('ii').freeze
48
71
 
49
72
  def dont_linger
@@ -120,6 +143,37 @@ class ::TCPSocket
120
143
  setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
121
144
  end
122
145
 
146
+ def recv(maxlen, flags = 0, outbuf = nil)
147
+ Thread.current.backend.recv(self, buf || +'', maxlen)
148
+ end
149
+
150
+ def recv_loop(&block)
151
+ Thread.current.backend.recv_loop(self, &block)
152
+ end
153
+
154
+ def send(mesg, flags = 0)
155
+ Thread.current.backend.send(self, mesg)
156
+ end
157
+
158
+ def write(str)
159
+ Thread.current.backend.send(self, str)
160
+ end
161
+ alias_method :<<, :write
162
+
163
+ def readpartial(maxlen, str = nil)
164
+ @read_buffer ||= +''
165
+ result = Thread.current.backend.recv(self, @read_buffer, maxlen)
166
+ raise EOFError unless result
167
+
168
+ if str
169
+ str << @read_buffer
170
+ else
171
+ str = @read_buffer
172
+ end
173
+ @read_buffer = +''
174
+ str
175
+ end
176
+
123
177
  def read_nonblock(len, str = nil, exception: true)
124
178
  @io.read_nonblock(len, str, exception: exception)
125
179
  end
@@ -142,6 +196,10 @@ class ::TCPServer
142
196
  @io.accept
143
197
  end
144
198
 
199
+ def accept_loop(&block)
200
+ Thread.current.backend.accept_loop(@io, &block)
201
+ end
202
+
145
203
  alias_method :orig_close, :close
146
204
  def close
147
205
  @io.close
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Polyphony
4
- VERSION = '0.45.4'
4
+ VERSION = '0.47.1'
5
5
  end
@@ -22,29 +22,52 @@ class ::Fiber
22
22
  attr_writer :auto_watcher
23
23
  end
24
24
 
25
+ module ::Kernel
26
+ def trace(*args)
27
+ STDOUT.orig_write(format_trace(args))
28
+ end
29
+
30
+ def format_trace(args)
31
+ if args.first.is_a?(String)
32
+ if args.size > 1
33
+ format("%s: %p\n", args.shift, args)
34
+ else
35
+ format("%s\n", args.first)
36
+ end
37
+ else
38
+ format("%p\n", args.size == 1 ? args.first : args)
39
+ end
40
+ end
41
+ end
42
+
25
43
  class MiniTest::Test
26
44
  def setup
27
- # puts "* setup #{self.name}"
45
+ # trace "* setup #{self.name}"
28
46
  if Fiber.current.children.size > 0
29
47
  puts "Children left: #{Fiber.current.children.inspect}"
30
48
  exit!
31
49
  end
32
50
  Fiber.current.setup_main_fiber
33
51
  Fiber.current.instance_variable_set(:@auto_watcher, nil)
52
+ Thread.current.backend.finalize
34
53
  Thread.current.backend = Polyphony::Backend.new
35
- sleep 0 # apparently this helps with timer accuracy
54
+ sleep 0
36
55
  end
37
56
 
38
57
  def teardown
39
- # puts "* teardown #{self.name.inspect} Fiber.current: #{Fiber.current.inspect}"
58
+ # trace "* teardown #{self.name}"
40
59
  Fiber.current.terminate_all_children
41
60
  Fiber.current.await_all_children
42
- Fiber.current.auto_watcher = nil
61
+ Fiber.current.instance_variable_set(:@auto_watcher, nil)
43
62
  rescue => e
44
63
  puts e
45
64
  puts e.backtrace.join("\n")
46
65
  exit!
47
66
  end
67
+
68
+ def fiber_tree(fiber)
69
+ { fiber: fiber, children: fiber.children.map { |f| fiber_tree(f) } }
70
+ end
48
71
  end
49
72
 
50
73
  module Kernel
@@ -54,3 +77,12 @@ module Kernel
54
77
  e
55
78
  end
56
79
  end
80
+
81
+ module Minitest::Assertions
82
+ def assert_in_range exp_range, act
83
+ msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
84
+ assert exp_range.include?(act), msg
85
+ end
86
+ end
87
+
88
+ puts "Polyphony backend: #{Thread.current.backend.kind}"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ ::Exception.__disable_sanitized_backtrace__ = true
6
+
7
+ module ::Kernel
8
+ def trace(*args)
9
+ STDOUT.orig_write(format_trace(args))
10
+ end
11
+
12
+ def format_trace(args)
13
+ if args.first.is_a?(String)
14
+ if args.size > 1
15
+ format("%s: %p\n", args.shift, args)
16
+ else
17
+ format("%s\n", args.first)
18
+ end
19
+ else
20
+ format("%p\n", args.size == 1 ? args.first : args)
21
+ end
22
+ end
23
+ end
24
+
25
+ i, o = IO.pipe
26
+
27
+ buf = []
28
+ f = spin do
29
+ buf << :ready
30
+ # loop do
31
+ # s = Thread.current.backend.read(i, +'', 6, false)
32
+ # trace read_result: s
33
+ # break if s.nil?
34
+ # buf << s
35
+ # rescue Exception => e
36
+ # trace exception: e
37
+ # raise e
38
+ # end
39
+ Thread.current.backend.read_loop(i) { |d| buf << d }
40
+ buf << :done
41
+ end
42
+
43
+ # writing always causes snoozing
44
+ o << 'foo'
45
+ o << 'bar'
46
+ trace '...closing'
47
+ o.close
48
+ trace '...closed'
49
+
50
+ f.await
51
+
52
+ raise "Bad result: #{buf.inspect}" unless buf == [:ready, 'foo', 'bar', :done]
53
+
54
+ puts '-' * 80
55
+ p buf
@@ -6,6 +6,7 @@ TEST_CMD = 'ruby test/run.rb'
6
6
 
7
7
  def run_test(count)
8
8
  puts "#{count}: running tests..."
9
+ # sleep 1
9
10
  system(TEST_CMD)
10
11
  return if $?.exitstatus == 0
11
12
 
@@ -15,7 +16,9 @@ end
15
16
 
16
17
  trap('INT') { exit! }
17
18
  t0 = Time.now
18
- count.times { |i| run_test(i + 1) }
19
+ count.times do |i|
20
+ run_test(i + 1)
21
+ end
19
22
  elapsed = Time.now - t0
20
23
  puts format(
21
24
  "Successfully ran %d tests in %f seconds (%f per test)",
@@ -85,7 +85,7 @@ class BackendTest < MiniTest::Test
85
85
  i, o = IO.pipe
86
86
 
87
87
  buf = []
88
- spin do
88
+ f = spin do
89
89
  buf << :ready
90
90
  @backend.read_loop(i) { |d| buf << d }
91
91
  buf << :done
@@ -96,9 +96,7 @@ class BackendTest < MiniTest::Test
96
96
  o << 'bar'
97
97
  o.close
98
98
 
99
- # read_loop will snooze after every read
100
- 6.times { snooze }
101
-
99
+ f.await
102
100
  assert_equal [:ready, 'foo', 'bar', :done], buf
103
101
  end
104
102
 
@@ -111,12 +109,12 @@ class BackendTest < MiniTest::Test
111
109
  end
112
110
 
113
111
  c1 = TCPSocket.new('127.0.0.1', 1234)
114
- 10.times { snooze }
112
+ sleep 0.01
115
113
 
116
114
  assert_equal 1, clients.size
117
115
 
118
116
  c2 = TCPSocket.new('127.0.0.1', 1234)
119
- 10.times { snooze }
117
+ sleep 0.01
120
118
 
121
119
  assert_equal 2, clients.size
122
120
 
@@ -127,4 +125,63 @@ class BackendTest < MiniTest::Test
127
125
  snooze
128
126
  server&.close
129
127
  end
128
+
129
+ def test_timer_loop
130
+ i = 0
131
+ f = spin do
132
+ @backend.timer_loop(0.01) { i += 1 }
133
+ end
134
+ @backend.sleep(0.05)
135
+ f.stop
136
+ f.await # TODO: check why this test sometimes segfaults if we don't a<wait fiber
137
+ assert_in_range 4..6, i
138
+ end
139
+
140
+ class MyTimeoutException < Exception
141
+ end
142
+
143
+ def test_timeout
144
+ buffer = []
145
+ assert_raises(Polyphony::TimeoutException) do
146
+ @backend.timeout(0.01, Polyphony::TimeoutException) do
147
+ buffer << 1
148
+ sleep 0.02
149
+ buffer << 2
150
+ end
151
+ end
152
+ assert_equal [1], buffer
153
+
154
+ buffer = []
155
+ assert_raises(MyTimeoutException) do
156
+ @backend.timeout(0.01, MyTimeoutException) do
157
+ buffer << 1
158
+ sleep 1
159
+ buffer << 2
160
+ end
161
+ end
162
+ assert_equal [1], buffer
163
+
164
+ buffer = []
165
+ result = @backend.timeout(0.01, nil, 42) do
166
+ buffer << 1
167
+ sleep 1
168
+ buffer << 2
169
+ end
170
+ assert_equal 42, result
171
+ assert_equal [1], buffer
172
+ end
173
+
174
+ def test_nested_timeout
175
+ buffer = []
176
+ assert_raises(MyTimeoutException) do
177
+ @backend.timeout(0.01, MyTimeoutException) do
178
+ @backend.timeout(0.02, nil) do
179
+ buffer << 1
180
+ sleep 1
181
+ buffer << 2
182
+ end
183
+ end
184
+ end
185
+ assert_equal [1], buffer
186
+ end
130
187
  end