polyphony 0.45.4 → 0.47.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +2 -0
- data/.gitmodules +0 -0
- data/CHANGELOG.md +32 -0
- data/Gemfile.lock +1 -1
- data/README.md +3 -3
- data/Rakefile +1 -1
- data/TODO.md +20 -34
- data/bin/test +4 -0
- data/examples/core/enumerable.rb +64 -0
- data/examples/performance/fiber_resume.rb +43 -0
- data/examples/performance/fiber_transfer.rb +13 -4
- data/examples/performance/multi_snooze.rb +0 -1
- data/examples/performance/thread-vs-fiber/compare.rb +59 -0
- data/examples/performance/thread-vs-fiber/em_server.rb +33 -0
- data/examples/performance/thread-vs-fiber/polyphony_server.rb +10 -21
- data/examples/performance/thread-vs-fiber/threaded_server.rb +22 -15
- data/examples/performance/thread_switch.rb +44 -0
- data/ext/liburing/liburing.h +585 -0
- data/ext/liburing/liburing/README.md +4 -0
- data/ext/liburing/liburing/barrier.h +73 -0
- data/ext/liburing/liburing/compat.h +15 -0
- data/ext/liburing/liburing/io_uring.h +343 -0
- data/ext/liburing/queue.c +333 -0
- data/ext/liburing/register.c +187 -0
- data/ext/liburing/setup.c +210 -0
- data/ext/liburing/syscall.c +54 -0
- data/ext/liburing/syscall.h +18 -0
- data/ext/polyphony/backend.h +1 -15
- data/ext/polyphony/backend_common.h +129 -0
- data/ext/polyphony/backend_io_uring.c +995 -0
- data/ext/polyphony/backend_io_uring_context.c +74 -0
- data/ext/polyphony/backend_io_uring_context.h +53 -0
- data/ext/polyphony/{libev_backend.c → backend_libev.c} +308 -297
- data/ext/polyphony/event.c +1 -1
- data/ext/polyphony/extconf.rb +31 -13
- data/ext/polyphony/fiber.c +60 -32
- data/ext/polyphony/libev.c +4 -0
- data/ext/polyphony/libev.h +8 -2
- data/ext/polyphony/liburing.c +8 -0
- data/ext/polyphony/playground.c +51 -0
- data/ext/polyphony/polyphony.c +9 -6
- data/ext/polyphony/polyphony.h +35 -19
- data/ext/polyphony/polyphony_ext.c +12 -4
- data/ext/polyphony/queue.c +100 -35
- data/ext/polyphony/runqueue.c +102 -0
- data/ext/polyphony/runqueue_ring_buffer.c +85 -0
- data/ext/polyphony/runqueue_ring_buffer.h +31 -0
- data/ext/polyphony/thread.c +42 -90
- data/lib/polyphony/adapters/trace.rb +2 -2
- data/lib/polyphony/core/exceptions.rb +0 -4
- data/lib/polyphony/core/global_api.rb +47 -23
- data/lib/polyphony/core/resource_pool.rb +12 -1
- data/lib/polyphony/core/sync.rb +7 -5
- data/lib/polyphony/extensions/core.rb +9 -15
- data/lib/polyphony/extensions/debug.rb +13 -0
- data/lib/polyphony/extensions/fiber.rb +13 -9
- data/lib/polyphony/extensions/openssl.rb +6 -0
- data/lib/polyphony/extensions/socket.rb +68 -10
- data/lib/polyphony/version.rb +1 -1
- data/test/helper.rb +36 -4
- data/test/io_uring_test.rb +55 -0
- data/test/stress.rb +4 -1
- data/test/test_backend.rb +63 -6
- data/test/test_ext.rb +1 -2
- data/test/test_fiber.rb +55 -20
- data/test/test_global_api.rb +132 -31
- data/test/test_queue.rb +117 -0
- data/test/test_resource_pool.rb +21 -0
- data/test/test_socket.rb +2 -2
- data/test/test_sync.rb +21 -0
- data/test/test_throttler.rb +3 -6
- data/test/test_trace.rb +7 -5
- 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.
|
178
|
+
Thread.current.schedule_and_wakeup(Thread.main.main_fiber, e)
|
179
179
|
end
|
180
|
-
Thread.current.
|
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,
|
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
|
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
|
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
|
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&.
|
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
|
331
|
+
attr_reader :result
|
332
332
|
|
333
333
|
def running?
|
334
334
|
@running
|
335
335
|
end
|
336
336
|
|
337
337
|
def inspect
|
338
|
-
|
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
|
|
@@ -19,18 +19,24 @@ class ::Socket
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def recv(maxlen, flags = 0, outbuf = nil)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
data/lib/polyphony/version.rb
CHANGED
data/test/helper.rb
CHANGED
@@ -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
|
-
#
|
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
|
54
|
+
sleep 0
|
36
55
|
end
|
37
56
|
|
38
57
|
def teardown
|
39
|
-
#
|
58
|
+
# trace "* teardown #{self.name}"
|
40
59
|
Fiber.current.terminate_all_children
|
41
60
|
Fiber.current.await_all_children
|
42
|
-
Fiber.current.auto_watcher
|
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
|
data/test/stress.rb
CHANGED
@@ -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
|
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)",
|
data/test/test_backend.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|