polyphony 0.45.2 → 0.47.0
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 +39 -0
- data/Gemfile.lock +3 -3
- data/README.md +3 -3
- data/Rakefile +1 -1
- data/TODO.md +20 -28
- data/bin/test +4 -0
- data/examples/core/enumerable.rb +64 -0
- data/examples/io/raw.rb +14 -0
- data/examples/io/reline.rb +18 -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.rb +2 -2
- data/lib/polyphony/adapters/process.rb +0 -3
- 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/sync.rb +7 -5
- data/lib/polyphony/extensions/core.rb +14 -33
- data/lib/polyphony/extensions/debug.rb +13 -0
- data/lib/polyphony/extensions/fiber.rb +21 -3
- data/lib/polyphony/extensions/io.rb +15 -4
- data/lib/polyphony/extensions/openssl.rb +6 -0
- data/lib/polyphony/extensions/socket.rb +63 -10
- data/lib/polyphony/version.rb +1 -1
- data/polyphony.gemspec +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_io.rb +42 -0
- data/test/test_queue.rb +117 -0
- data/test/test_signal.rb +11 -8
- 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 +36 -6
@@ -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
|
@@ -103,7 +103,7 @@ module Polyphony
|
|
103
103
|
suspend
|
104
104
|
fibers.map(&:result)
|
105
105
|
ensure
|
106
|
-
await_select_cleanup(state)
|
106
|
+
await_select_cleanup(state) if state
|
107
107
|
end
|
108
108
|
alias_method :join, :await
|
109
109
|
|
@@ -165,6 +165,20 @@ module Polyphony
|
|
165
165
|
state[:selected] = true
|
166
166
|
end
|
167
167
|
end
|
168
|
+
|
169
|
+
# Creates and schedules with priority an out-of-band fiber that runs the
|
170
|
+
# supplied block. If any uncaught exception is raised while the fiber is
|
171
|
+
# running, it will bubble up to the main thread's main fiber, which will
|
172
|
+
# also be scheduled with priority. This method is mainly used trapping
|
173
|
+
# signals (see also the patched `Kernel#trap`)
|
174
|
+
def schedule_priority_oob_fiber(&block)
|
175
|
+
f = Fiber.new do
|
176
|
+
block.call
|
177
|
+
rescue Exception => e
|
178
|
+
Thread.current.schedule_and_wakeup(Thread.main.main_fiber, e)
|
179
|
+
end
|
180
|
+
Thread.current.schedule_and_wakeup(f, nil)
|
181
|
+
end
|
168
182
|
end
|
169
183
|
|
170
184
|
# Methods for controlling child fibers
|
@@ -294,7 +308,7 @@ module Polyphony
|
|
294
308
|
@waiting_fibers&.each_key { |f| f.schedule(result) }
|
295
309
|
|
296
310
|
# propagate unaught exception to parent
|
297
|
-
@parent&.
|
311
|
+
@parent&.schedule_with_priority(result) if uncaught_exception && !@waiting_fibers
|
298
312
|
end
|
299
313
|
|
300
314
|
def when_done(&block)
|
@@ -321,7 +335,11 @@ class ::Fiber
|
|
321
335
|
end
|
322
336
|
|
323
337
|
def inspect
|
324
|
-
|
338
|
+
if @tag
|
339
|
+
"#<Fiber #{tag}:#{object_id} #{location} (#{state})>"
|
340
|
+
else
|
341
|
+
"#<Fiber:#{object_id} #{location} (#{state})>"
|
342
|
+
end
|
325
343
|
end
|
326
344
|
alias_method :to_s, :inspect
|
327
345
|
|
@@ -90,11 +90,22 @@ class ::IO
|
|
90
90
|
# def each_codepoint
|
91
91
|
# end
|
92
92
|
|
93
|
-
|
94
|
-
|
93
|
+
alias_method :orig_getbyte, :getbyte
|
94
|
+
def getbyte
|
95
|
+
char = getc
|
96
|
+
char ? char.getbyte(0) : nil
|
97
|
+
end
|
95
98
|
|
96
|
-
|
97
|
-
|
99
|
+
alias_method :orig_getc, :getc
|
100
|
+
def getc
|
101
|
+
return @read_buffer.slice!(0) if @read_buffer && !@read_buffer.empty?
|
102
|
+
|
103
|
+
@read_buffer ||= +''
|
104
|
+
Thread.current.backend.read(self, @read_buffer, 8192, false)
|
105
|
+
return @read_buffer.slice!(0) if !@read_buffer.empty?
|
106
|
+
|
107
|
+
nil
|
108
|
+
end
|
98
109
|
|
99
110
|
alias_method :orig_read, :read
|
100
111
|
def read(len = nil)
|
@@ -19,16 +19,21 @@ 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
|
33
|
+
end
|
34
|
+
|
35
|
+
def recv_loop(&block)
|
36
|
+
Thread.current.backend.recv_loop(self, &block)
|
32
37
|
end
|
33
38
|
|
34
39
|
def recvfrom(maxlen, flags = 0)
|
@@ -44,6 +49,19 @@ class ::Socket
|
|
44
49
|
end
|
45
50
|
end
|
46
51
|
|
52
|
+
def send(mesg, flags = 0)
|
53
|
+
Thread.current.backend.send(self, mesg)
|
54
|
+
end
|
55
|
+
|
56
|
+
def write(str)
|
57
|
+
Thread.current.backend.send(self, str)
|
58
|
+
end
|
59
|
+
alias_method :<<, :write
|
60
|
+
|
61
|
+
def readpartial(maxlen, str = +'')
|
62
|
+
Thread.current.backend.recv(self, str, maxlen)
|
63
|
+
end
|
64
|
+
|
47
65
|
ZERO_LINGER = [0, 0].pack('ii').freeze
|
48
66
|
|
49
67
|
def dont_linger
|
@@ -120,6 +138,37 @@ class ::TCPSocket
|
|
120
138
|
setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_REUSEPORT, 1)
|
121
139
|
end
|
122
140
|
|
141
|
+
def recv(maxlen, flags = 0, outbuf = nil)
|
142
|
+
Thread.current.backend.recv(self, buf || +'', maxlen)
|
143
|
+
end
|
144
|
+
|
145
|
+
def recv_loop(&block)
|
146
|
+
Thread.current.backend.recv_loop(self, &block)
|
147
|
+
end
|
148
|
+
|
149
|
+
def send(mesg, flags = 0)
|
150
|
+
Thread.current.backend.send(self, mesg)
|
151
|
+
end
|
152
|
+
|
153
|
+
def write(str)
|
154
|
+
Thread.current.backend.send(self, str)
|
155
|
+
end
|
156
|
+
alias_method :<<, :write
|
157
|
+
|
158
|
+
def readpartial(maxlen, str = nil)
|
159
|
+
@read_buffer ||= +''
|
160
|
+
result = Thread.current.backend.recv(self, @read_buffer, maxlen)
|
161
|
+
raise EOFError unless result
|
162
|
+
|
163
|
+
if str
|
164
|
+
str << @read_buffer
|
165
|
+
else
|
166
|
+
str = @read_buffer
|
167
|
+
end
|
168
|
+
@read_buffer = +''
|
169
|
+
str
|
170
|
+
end
|
171
|
+
|
123
172
|
def read_nonblock(len, str = nil, exception: true)
|
124
173
|
@io.read_nonblock(len, str, exception: exception)
|
125
174
|
end
|
@@ -142,6 +191,10 @@ class ::TCPServer
|
|
142
191
|
@io.accept
|
143
192
|
end
|
144
193
|
|
194
|
+
def accept_loop(&block)
|
195
|
+
Thread.current.backend.accept_loop(@io, &block)
|
196
|
+
end
|
197
|
+
|
145
198
|
alias_method :orig_close, :close
|
146
199
|
def close
|
147
200
|
@io.close
|
data/lib/polyphony/version.rb
CHANGED
data/polyphony.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.require_paths = ["lib"]
|
22
22
|
s.required_ruby_version = '>= 2.6'
|
23
23
|
|
24
|
-
s.add_development_dependency 'rake-compiler', '1.
|
24
|
+
s.add_development_dependency 'rake-compiler', '1.1.1'
|
25
25
|
s.add_development_dependency 'minitest', '5.13.0'
|
26
26
|
s.add_development_dependency 'minitest-reporters', '1.4.2'
|
27
27
|
s.add_development_dependency 'simplecov', '0.17.1'
|
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
|