polyphony 0.43.3 → 0.43.9

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +1 -1
  3. data/CHANGELOG.md +44 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +21 -4
  6. data/TODO.md +1 -2
  7. data/bin/stress.rb +28 -0
  8. data/docs/_includes/head.html +40 -0
  9. data/docs/_includes/title.html +1 -0
  10. data/docs/_user-guide/web-server.md +11 -11
  11. data/docs/getting-started/overview.md +4 -4
  12. data/docs/index.md +4 -3
  13. data/docs/main-concepts/design-principles.md +23 -34
  14. data/docs/main-concepts/fiber-scheduling.md +1 -1
  15. data/docs/polyphony-logo.png +0 -0
  16. data/examples/core/xx-channels.rb +4 -2
  17. data/examples/core/xx-using-a-mutex.rb +2 -1
  18. data/examples/io/xx-happy-eyeballs.rb +21 -22
  19. data/examples/io/xx-zip.rb +19 -0
  20. data/examples/performance/fiber_transfer.rb +47 -0
  21. data/examples/performance/mem-usage.rb +34 -28
  22. data/examples/performance/messaging.rb +29 -0
  23. data/examples/performance/multi_snooze.rb +11 -9
  24. data/examples/xx-spin.rb +32 -0
  25. data/ext/polyphony/event.c +86 -0
  26. data/ext/polyphony/fiber.c +0 -5
  27. data/ext/polyphony/libev_agent.c +181 -24
  28. data/ext/polyphony/polyphony.c +0 -2
  29. data/ext/polyphony/polyphony.h +14 -7
  30. data/ext/polyphony/polyphony_ext.c +4 -2
  31. data/ext/polyphony/queue.c +187 -0
  32. data/ext/polyphony/ring_buffer.c +96 -0
  33. data/ext/polyphony/ring_buffer.h +28 -0
  34. data/ext/polyphony/thread.c +18 -12
  35. data/lib/polyphony.rb +5 -14
  36. data/lib/polyphony/core/channel.rb +3 -34
  37. data/lib/polyphony/core/global_api.rb +1 -1
  38. data/lib/polyphony/core/resource_pool.rb +13 -75
  39. data/lib/polyphony/core/sync.rb +12 -9
  40. data/lib/polyphony/core/thread_pool.rb +1 -1
  41. data/lib/polyphony/extensions/core.rb +34 -0
  42. data/lib/polyphony/extensions/fiber.rb +9 -2
  43. data/lib/polyphony/extensions/io.rb +17 -16
  44. data/lib/polyphony/extensions/openssl.rb +8 -0
  45. data/lib/polyphony/extensions/socket.rb +12 -0
  46. data/lib/polyphony/version.rb +1 -1
  47. data/test/helper.rb +1 -1
  48. data/test/q.rb +24 -0
  49. data/test/test_agent.rb +1 -1
  50. data/test/test_event.rb +12 -0
  51. data/test/test_global_api.rb +2 -2
  52. data/test/test_io.rb +24 -2
  53. data/test/test_queue.rb +59 -1
  54. data/test/test_resource_pool.rb +0 -43
  55. data/test/test_trace.rb +18 -17
  56. metadata +15 -5
  57. data/ext/polyphony/libev_queue.c +0 -217
  58. data/lib/polyphony/event.rb +0 -27
Binary file
@@ -2,10 +2,12 @@
2
2
 
3
3
  require 'bundler/setup'
4
4
  require 'polyphony'
5
+ require 'polyphony/core/channel'
5
6
 
6
7
  def echo(cin, cout)
7
8
  puts 'start echoer'
8
9
  while (msg = cin.receive)
10
+ puts "echoer received #{msg}"
9
11
  cout << "you said: #{msg}"
10
12
  end
11
13
  ensure
@@ -20,7 +22,7 @@ spin do
20
22
  puts 'start receiver'
21
23
  while (msg = chan2.receive)
22
24
  puts msg
23
- $main.resume if msg =~ /world/
25
+ $main.schedule if msg =~ /world/
24
26
  end
25
27
  ensure
26
28
  puts 'receiver stopped'
@@ -42,4 +44,4 @@ $main = spin do
42
44
  puts "done #{Time.now - t0}"
43
45
  end
44
46
 
45
- suspend
47
+ $main.await
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bundler/setup'
4
4
  require 'polyphony'
5
+ require 'polyphony/core/sync'
5
6
 
6
7
  def loop_it(number, lock)
7
8
  loop do
@@ -13,7 +14,7 @@ def loop_it(number, lock)
13
14
  end
14
15
  end
15
16
 
16
- lock = Polyphony::Sync::Mutex.new
17
+ lock = Polyphony::Mutex.new
17
18
  spin { loop_it(1, lock) }
18
19
  spin { loop_it(2, lock) }
19
20
  spin { loop_it(3, lock) }
@@ -1,37 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # idea taken from the example given in trio:
4
- # https://www.youtube.com/watch?v=oLkfnc_UMcE
5
-
6
- require 'bundler/setup'
7
3
  require 'polyphony'
8
4
 
9
- def try_connect(target, supervisor)
10
- puts "trying #{target[2]}"
11
- socket = Polyphony::Net.tcp_connect(target[2], 80)
12
- # connection successful
13
- supervisor.stop([target[2], socket])
5
+ def try_connect(ip_address, port, supervisor)
6
+ puts "trying #{ip_address}"
7
+ sleep rand * 0.2
8
+ socket = TCPSocket.new(ip_address, port)
9
+ puts "connected to #{ip_address}"
10
+ supervisor.schedule [ip_address, socket]
14
11
  rescue IOError, SystemCallError
15
12
  # ignore error
16
13
  end
17
14
 
18
- def happy_eyeballs(hostname, port, max_wait_time: 0.025)
15
+ def happy_eyeballs(hostname, port, max_wait_time: 0.010)
19
16
  targets = Socket.getaddrinfo(hostname, port, :INET, :STREAM)
20
17
  t0 = Time.now
21
- cancel_after(5) do
22
- success = supervise do |supervisor|
23
- targets.each_with_index do |t, idx|
24
- sleep(max_wait_time) if idx > 0
25
- supervisor.spin { try_connect(t, supervisor) }
26
- end
27
- end
28
- if success
29
- puts format('success: %s (%.3fs)', success[0], Time.now - t0)
30
- else
31
- puts "timed out (#{Time.now - t0}s)"
18
+ fibers = []
19
+ supervisor = Fiber.current
20
+ spin do
21
+ targets.each do |t|
22
+ spin { try_connect(t[2], t[1], supervisor) }
23
+ sleep(max_wait_time)
32
24
  end
25
+ suspend
26
+ end
27
+ target, socket = move_on_after(5) { suspend }
28
+ supervisor.shutdown_all_children
29
+ if target
30
+ puts format('success: %s (%.3fs)', target, Time.now - t0)
31
+ else
32
+ puts 'timed out'
33
33
  end
34
34
  end
35
35
 
36
- # Let's try it out:
37
36
  happy_eyeballs('debian.org', 'https')
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+ require 'zlib'
6
+
7
+ i, o = IO.pipe
8
+
9
+ w = Zlib::GzipWriter.new(o)
10
+
11
+ s = (1..1000).map { (65 + rand(26)).chr }.join
12
+ puts "full length: #{s.bytesize}"
13
+ w << s
14
+ w.close
15
+ o.close
16
+
17
+
18
+ z = i.read
19
+ puts "zipped length: #{z.bytesize}"
@@ -0,0 +1,47 @@
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
10
+
11
+ def run(num_fibers)
12
+ count = 0
13
+
14
+ GC.disable
15
+
16
+ first = nil
17
+ last = nil
18
+ supervisor = Fiber.current
19
+ num_fibers.times do
20
+ fiber = Fiber.new do
21
+ loop do
22
+ count += 1
23
+ if count == 1_000_000
24
+ supervisor.transfer
25
+ else
26
+ Fiber.current.next.transfer
27
+ end
28
+ end
29
+ end
30
+ first ||= fiber
31
+ last.next = fiber if last
32
+ last = fiber
33
+ end
34
+
35
+ last.next = first
36
+
37
+ t0 = Time.now
38
+ first.transfer
39
+ elapsed = Time.now - t0
40
+
41
+ puts "fibers: #{num_fibers} count: #{count} rate: #{count / elapsed}"
42
+ GC.start
43
+ end
44
+
45
+ run(100)
46
+ run(1000)
47
+ run(10000)
@@ -4,47 +4,53 @@ def mem_usage
4
4
  `ps -o rss #{$$}`.split.last.to_i
5
5
  end
6
6
 
7
- def calculate_fiber_memory_cost(count)
7
+ def calculate_memory_cost(name, count, &block)
8
+ GC.enable
9
+ ObjectSpace.garbage_collect
10
+ sleep 0.5
8
11
  GC.disable
9
12
  rss0 = mem_usage
10
- count.times { Fiber.new { sleep 1 } }
13
+ count0 = ObjectSpace.count_objects[:TOTAL] - ObjectSpace.count_objects[:FREE]
14
+ a = []
15
+ count.times { a << block.call }
11
16
  rss1 = mem_usage
12
- GC.start
17
+ count1 = ObjectSpace.count_objects[:TOTAL] - ObjectSpace.count_objects[:FREE]
18
+ p [count0, count1]
19
+ # sleep 0.5
13
20
  cost = (rss1 - rss0).to_f / count
21
+ count_delta = (count1 - count0) / count
14
22
 
15
- puts "fiber memory cost: #{cost}KB"
23
+ puts "#{name} rss cost: #{cost}KB object count: #{count_delta}"
16
24
  end
17
25
 
18
- calculate_fiber_memory_cost(10000)
19
-
20
- def calculate_thread_memory_cost(count)
21
- GC.disable
22
- rss0 = mem_usage
23
- count.times { Thread.new { sleep 1 } }
24
- sleep 0.5
25
- rss1 = mem_usage
26
- sleep 0.5
27
- GC.start
28
- cost = (rss1 - rss0).to_f / count
26
+ f = Fiber.new { |f| f.transfer }
27
+ f.transfer Fiber.current
29
28
 
30
- puts "thread memory cost: #{cost}KB"
29
+ calculate_memory_cost('fiber', 10000) do
30
+ f = Fiber.new { |f| f.transfer :foo }
31
+ f.transfer Fiber.current
32
+ f
31
33
  end
32
34
 
33
- calculate_thread_memory_cost(500)
35
+ t = Thread.new { sleep 1}
36
+ t.kill
37
+ t.join
38
+
39
+ calculate_memory_cost('thread', 500) do
40
+ t = Thread.new { sleep 1 }
41
+ sleep 0.001
42
+ t
43
+ end
44
+ (Thread.list - [Thread.current]).each(&:kill).each(&:join)
34
45
 
35
46
  require 'bundler/setup'
36
47
  require 'polyphony'
37
48
 
38
- def calculate_extended_fiber_memory_cost(count)
39
- GC.disable
40
- rss0 = mem_usage
41
- count.times { spin { :foo } }
42
- snooze
43
- rss1 = mem_usage
44
- GC.start
45
- cost = (rss1 - rss0).to_f / count
49
+ f = spin { sleep 0.1 }
50
+ f.await
46
51
 
47
- puts "extended fiber memory cost: #{cost}KB"
52
+ calculate_memory_cost('polyphony fiber', 10000) do
53
+ f = spin { :foo }
54
+ f.await
55
+ f
48
56
  end
49
-
50
- calculate_extended_fiber_memory_cost(10000)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ X = 1_000_000
7
+
8
+ GC.disable
9
+
10
+ count = 0
11
+
12
+ pong = spin_loop do
13
+ msg, ping = receive
14
+ count += 1
15
+ ping << 'pong'
16
+ end
17
+
18
+ ping = spin do
19
+ X.times do
20
+ pong << ['ping', Fiber.current]
21
+ msg = receive
22
+ count += 1
23
+ end
24
+ end
25
+
26
+ t0 = Time.now
27
+ ping.await
28
+ dt = Time.now - t0
29
+ puts format('message rate: %d/s', (X / dt))
@@ -5,19 +5,20 @@ require 'polyphony'
5
5
 
6
6
  def bm(fibers, iterations)
7
7
  count = 0
8
- t0 = Time.now
9
- supervise do |s|
10
- fibers.times do
11
- s.spin do
12
- iterations.times do
13
- snooze
14
- count += 1
15
- end
8
+ t_pre = Time.now
9
+ fibers.times do
10
+ spin do
11
+ iterations.times do
12
+ snooze
13
+ count += 1
16
14
  end
17
15
  end
18
16
  end
17
+ t0 = Time.now
18
+ Fiber.current.await_all_children
19
19
  dt = Time.now - t0
20
- puts "#{[fibers, iterations].inspect} count: #{count} #{count / dt.to_f}/s"
20
+ puts "#{[fibers, iterations].inspect} setup: #{t0 - t_pre}s count: #{count} #{count / dt.to_f}/s"
21
+ Thread.current.run_queue_trace
21
22
  end
22
23
 
23
24
  GC.disable
@@ -27,5 +28,6 @@ bm(10, 100_000)
27
28
  bm(100, 10_000)
28
29
  bm(1_000, 1_000)
29
30
  bm(10_000, 100)
31
+
30
32
  # bm(100_000, 10)
31
33
  # bm(1_000_000, 1)
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ puts "pid: #{Process.pid}"
7
+ GC.disable
8
+
9
+ def mem_usage
10
+ # orig_backtick('ps -o rss #{$$}').split.last.to_i
11
+ `ps -o rss #{$$}`.split.last.to_i
12
+ end
13
+
14
+ f = File.open('spin.log', 'w+')
15
+
16
+ m0 = mem_usage
17
+
18
+ X = ARGV[0] ? ARGV[0].to_i : 10
19
+ STDOUT.orig_write "Starting #{X} fibers...\n"
20
+ t0 = Time.now
21
+ x = nil
22
+ X.times do |i|
23
+ spin { p i; suspend }
24
+ end
25
+
26
+ suspend
27
+ f.close
28
+ t1 = Time.now
29
+ m1 = mem_usage
30
+ rate = X / (t1 - t0)
31
+ mem_cost = (m1 - m0) / X.to_f
32
+ STDOUT.orig_write("#{ { time: t1 - t0, spin_rate: rate, fiber_mem_cost: mem_cost }.inspect }\n")
@@ -0,0 +1,86 @@
1
+ #include "polyphony.h"
2
+ #include "ring_buffer.h"
3
+
4
+ typedef struct event {
5
+ VALUE waiting_fiber;
6
+ } Event_t;
7
+
8
+ VALUE cEvent = Qnil;
9
+
10
+ static void Event_mark(void *ptr) {
11
+ Event_t *event = ptr;
12
+ rb_gc_mark(event->waiting_fiber);
13
+ }
14
+
15
+ static void Event_free(void *ptr) {
16
+ xfree(ptr);
17
+ }
18
+
19
+ static size_t Event_size(const void *ptr) {
20
+ return sizeof(Event_t);
21
+ }
22
+
23
+ static const rb_data_type_t Event_type = {
24
+ "Event",
25
+ {Event_mark, Event_free, Event_size,},
26
+ 0, 0, 0
27
+ };
28
+
29
+ static VALUE Event_allocate(VALUE klass) {
30
+ Event_t *event;
31
+
32
+ event = ALLOC(Event_t);
33
+ return TypedData_Wrap_Struct(klass, &Event_type, event);
34
+ }
35
+
36
+ #define GetEvent(obj, event) \
37
+ TypedData_Get_Struct((obj), Event_t, &Event_type, (event))
38
+
39
+ static VALUE Event_initialize(VALUE self) {
40
+ Event_t *event;
41
+ GetEvent(self, event);
42
+
43
+ event->waiting_fiber = Qnil;
44
+
45
+ return self;
46
+ }
47
+
48
+ VALUE Event_signal(int argc, VALUE *argv, VALUE self) {
49
+ VALUE value = argc > 0 ? argv[0] : Qnil;
50
+ Event_t *event;
51
+ GetEvent(self, event);
52
+
53
+ if (event->waiting_fiber != Qnil) {
54
+ Fiber_make_runnable(event->waiting_fiber, value);
55
+ event->waiting_fiber = Qnil;
56
+ }
57
+ return self;
58
+ }
59
+
60
+ VALUE Event_await(VALUE self) {
61
+ Event_t *event;
62
+ GetEvent(self, event);
63
+
64
+ if (event->waiting_fiber != Qnil)
65
+ rb_raise(rb_eRuntimeError, "Event is already awaited by another fiber");
66
+
67
+ VALUE agent = rb_ivar_get(rb_thread_current(), ID_ivar_agent);
68
+ event->waiting_fiber = rb_fiber_current();
69
+ VALUE switchpoint_result = LibevAgent_wait_event(agent, Qnil);
70
+ event->waiting_fiber = Qnil;
71
+
72
+ TEST_RESUME_EXCEPTION(switchpoint_result);
73
+ RB_GC_GUARD(agent);
74
+ RB_GC_GUARD(switchpoint_result);
75
+
76
+ return switchpoint_result;
77
+ }
78
+
79
+ void Init_Event() {
80
+ cEvent = rb_define_class_under(mPolyphony, "Event", rb_cData);
81
+ rb_define_alloc_func(cEvent, Event_allocate);
82
+
83
+ rb_define_method(cEvent, "initialize", Event_initialize, 0);
84
+ rb_define_method(cEvent, "await", Event_await, 0);
85
+ rb_define_method(cEvent, "signal", Event_signal, -1);
86
+ }
@@ -9,8 +9,6 @@ ID ID_trace_runnable;
9
9
  ID ID_trace_terminate;
10
10
  ID ID_trace_wait;
11
11
 
12
- VALUE cEvent = Qnil;
13
-
14
12
  VALUE SYM_dead;
15
13
  VALUE SYM_running;
16
14
  VALUE SYM_runnable;
@@ -35,9 +33,6 @@ static VALUE Fiber_safe_transfer(int argc, VALUE *argv, VALUE self) {
35
33
 
36
34
  inline VALUE Fiber_auto_watcher(VALUE self) {
37
35
  VALUE watcher;
38
- if (cEvent == Qnil) {
39
- cEvent = rb_const_get(mPolyphony, rb_intern("Event"));
40
- }
41
36
 
42
37
  watcher = rb_ivar_get(self, ID_ivar_auto_watcher);
43
38
  if (watcher == Qnil) {