concurrently 1.0.1 → 1.1.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +8 -3
  4. data/README.md +70 -60
  5. data/RELEASE_NOTES.md +16 -1
  6. data/Rakefile +98 -14
  7. data/concurrently.gemspec +16 -12
  8. data/ext/mruby/io.rb +1 -1
  9. data/guides/Overview.md +191 -66
  10. data/guides/Performance.md +300 -102
  11. data/guides/Troubleshooting.md +28 -28
  12. data/lib/Ruby/concurrently/proc/evaluation/error.rb +10 -0
  13. data/lib/all/concurrently/error.rb +0 -3
  14. data/lib/all/concurrently/evaluation.rb +8 -12
  15. data/lib/all/concurrently/event_loop.rb +1 -1
  16. data/lib/all/concurrently/event_loop/fiber.rb +3 -3
  17. data/lib/all/concurrently/event_loop/io_selector.rb +1 -1
  18. data/lib/all/concurrently/event_loop/run_queue.rb +29 -17
  19. data/lib/all/concurrently/proc.rb +13 -13
  20. data/lib/all/concurrently/proc/evaluation.rb +29 -29
  21. data/lib/all/concurrently/proc/evaluation/error.rb +13 -0
  22. data/lib/all/concurrently/proc/fiber.rb +3 -6
  23. data/lib/all/concurrently/version.rb +1 -1
  24. data/lib/all/io.rb +118 -41
  25. data/lib/all/kernel.rb +82 -29
  26. data/lib/mruby/concurrently/event_loop/io_selector.rb +46 -0
  27. data/lib/mruby/kernel.rb +1 -1
  28. data/mrbgem.rake +28 -17
  29. data/mruby_builds/build_config.rb +67 -0
  30. data/perf/Ruby/stage.rb +23 -0
  31. data/perf/benchmark_call_methods.rb +32 -0
  32. data/perf/benchmark_call_methods_waiting.rb +52 -0
  33. data/perf/benchmark_wait_methods.rb +38 -0
  34. data/perf/mruby/stage.rb +8 -0
  35. data/perf/profile_await_readable.rb +10 -0
  36. data/perf/{concurrent_proc_call.rb → profile_call.rb} +1 -5
  37. data/perf/{concurrent_proc_call_and_forget.rb → profile_call_and_forget.rb} +1 -5
  38. data/perf/{concurrent_proc_call_detached.rb → profile_call_detached.rb} +1 -5
  39. data/perf/{concurrent_proc_call_nonblock.rb → profile_call_nonblock.rb} +1 -5
  40. data/perf/profile_wait.rb +7 -0
  41. data/perf/stage.rb +47 -0
  42. data/perf/stage/benchmark.rb +47 -0
  43. data/perf/stage/benchmark/code_gen.rb +29 -0
  44. data/perf/stage/benchmark/code_gen/batch.rb +41 -0
  45. data/perf/stage/benchmark/code_gen/single.rb +38 -0
  46. metadata +27 -23
  47. data/ext/mruby/array.rb +0 -19
  48. data/lib/Ruby/concurrently/error.rb +0 -4
  49. data/perf/_shared/stage.rb +0 -33
  50. data/perf/concurrent_proc_calls.rb +0 -49
  51. data/perf/concurrent_proc_calls_awaiting.rb +0 -48
@@ -0,0 +1,46 @@
1
+ module Concurrently
2
+ # @private
3
+ class EventLoop::IOSelector
4
+ def initialize(event_loop)
5
+ @run_queue = event_loop.run_queue
6
+ @poll = Poll.new
7
+ @fds = {}
8
+ @evaluations = {}
9
+ end
10
+
11
+ def awaiting?
12
+ @evaluations.size > 0
13
+ end
14
+
15
+ def await_reader(io, evaluation)
16
+ fd = @poll.add(io, Poll::In)
17
+ @fds.store io, fd
18
+ @evaluations.store fd, evaluation
19
+ end
20
+
21
+ def await_writer(io, evaluation)
22
+ fd = @poll.add(io, Poll::Out)
23
+ @fds.store io, fd
24
+ @evaluations.store fd, evaluation
25
+ end
26
+
27
+ def cancel_reader(io)
28
+ fd = @fds.delete io
29
+ @poll.remove fd
30
+ @evaluations.delete fd
31
+ end
32
+
33
+ def cancel_writer(io)
34
+ fd = @fds.delete io
35
+ @poll.remove fd
36
+ @evaluations.delete fd
37
+ end
38
+
39
+ def process_ready_in(waiting_time)
40
+ waiting_time = -1 if waiting_time == Float::INFINITY
41
+ if ready = @poll.wait(waiting_time)
42
+ ready.each{ |fd| @run_queue.resume_evaluation! @evaluations[fd], true }
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/mruby/kernel.rb CHANGED
@@ -4,7 +4,7 @@ module Kernel
4
4
  # Reimplements Kernel#concurrently. mruby does not support Proc.new without
5
5
  # a block.
6
6
  private def concurrently(*args, &block)
7
- Concurrently::Proc.new(&block).call_and_forget *args
7
+ Concurrently::Proc.new(&block).call_detached *args
8
8
  end
9
9
 
10
10
  # Reimplements Kernel#concurrent_proc. mruby does not support Proc.new without
data/mrbgem.rake CHANGED
@@ -3,19 +3,23 @@ require_relative 'lib/all/concurrently/version'
3
3
  MRuby::Gem::Specification.new('mruby-concurrently') do |spec|
4
4
  spec.version = Concurrently::VERSION
5
5
  spec.summary = %q{A concurrency framework based on fibers}
6
- spec.description = <<-DESC
7
- Concurrently is a concurrency framework for Ruby and mruby. With it, concurrent
8
- code can be written sequentially similar to async/await.
6
+ spec.description = <<'DESC'
7
+ Concurrently is a concurrency framework for Ruby and mruby based on
8
+ fibers. With it code can be evaluated independently in its own execution
9
+ context similar to a thread:
9
10
 
10
- The concurrency primitive of Concurrently is the concurrent proc. It is very
11
- similar to a regular proc. Calling a concurrent proc creates a concurrent
12
- evaluation which is kind of a lightweight thread: It can wait for stuff without
13
- blocking other concurrent evaluations.
14
-
15
- Under the hood, concurrent procs are evaluated inside fibers. They can wait for
16
- readiness of I/O or a period of time (or the result of other concurrent
17
- evaluations).
18
- DESC
11
+ hello = concurrently do
12
+ wait 0.2 # seconds
13
+ "hello"
14
+ end
15
+
16
+ world = concurrently do
17
+ wait 0.1 # seconds
18
+ "world"
19
+ end
20
+
21
+ puts "#{hello.await_result} #{world.await_result}"
22
+ DESC
19
23
 
20
24
  spec.homepage = "https://github.com/christopheraue/m-ruby-concurrently"
21
25
  spec.license = 'Apache-2.0'
@@ -32,11 +36,18 @@ evaluations).
32
36
  Dir["#{spec.dir}/lib/mruby/**/*.rb"].sort
33
37
  spec.test_rbfiles = Dir["#{spec.dir}/test/mruby/*.rb"]
34
38
 
35
- spec.add_dependency 'mruby-array-ext'
36
- spec.add_dependency 'mruby-numeric-ext'
37
- spec.add_dependency 'mruby-enumerator'
38
- spec.add_dependency 'mruby-fiber'
39
- spec.add_dependency 'mruby-time'
39
+ spec.add_dependency 'mruby-array-ext', :core => 'mruby-array-ext'
40
+ spec.add_dependency 'mruby-numeric-ext', :core => 'mruby-numeric-ext'
41
+ spec.add_dependency 'mruby-enumerator', :core => 'mruby-enumerator'
42
+ spec.add_dependency 'mruby-fiber', :core => 'mruby-fiber'
43
+ spec.add_dependency 'mruby-time', :core => 'mruby-time'
40
44
  spec.add_dependency 'mruby-io'
41
45
  spec.add_dependency 'mruby-callbacks_attachable', '~> 2.2', github: 'christopheraue/m-ruby-callbacks_attachable'
46
+
47
+ # use mruby-poll only on unix-like OSes
48
+ if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
49
+ spec.rbfiles.delete "#{spec.dir}/lib/mruby/concurrently/event_loop/io_selector.rb"
50
+ else
51
+ spec.add_dependency 'mruby-poll'
52
+ end
42
53
  end
@@ -0,0 +1,67 @@
1
+ MRuby::Build.new do
2
+ if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
3
+ toolchain :visualcpp
4
+ else
5
+ toolchain :gcc
6
+ end
7
+
8
+ enable_debug
9
+ end
10
+
11
+ MRuby::Build.new 'test', File.dirname(__FILE__) do
12
+ self.gem_clone_dir = "#{MRUBY_ROOT}/build/mrbgems"
13
+
14
+ if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
15
+ toolchain :visualcpp
16
+ else
17
+ toolchain :gcc
18
+ end
19
+
20
+ enable_test
21
+ gem File.expand_path File.dirname File.dirname __FILE__
22
+ end
23
+
24
+ MRuby::Build.new 'benchmark', File.dirname(__FILE__) do
25
+ self.gem_clone_dir = "#{MRUBY_ROOT}/build/mrbgems"
26
+
27
+ if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
28
+ toolchain :visualcpp
29
+ else
30
+ toolchain :gcc
31
+ cc.flags << '-O3'
32
+ end
33
+
34
+ gem core: 'mruby-bin-mruby'
35
+ gem core: 'mruby-bin-mirb'
36
+ gem core: 'mruby-proc-ext'
37
+ gem core: 'mruby-sprintf'
38
+ gem core: 'mruby-eval'
39
+
40
+ gem_dir = File.expand_path File.dirname File.dirname __FILE__
41
+ gem gem_dir do |gem|
42
+ gem.rbfiles += Dir["#{gem_dir}/perf/{stage.rb,stage/**/*.rb}"]
43
+ end
44
+ end
45
+
46
+ MRuby::Build.new 'profile', File.dirname(__FILE__) do
47
+ self.gem_clone_dir = "#{MRUBY_ROOT}/build/mrbgems"
48
+
49
+ if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR']
50
+ toolchain :visualcpp
51
+ else
52
+ toolchain :gcc
53
+ end
54
+
55
+ enable_debug
56
+ cc.defines = %w(MRB_ENABLE_DEBUG_HOOK)
57
+
58
+ gem github: 'miura1729/mruby-profiler'
59
+ gem core: 'mruby-bin-mruby'
60
+ gem core: 'mruby-proc-ext'
61
+ gem core: 'mruby-sprintf'
62
+
63
+ gem_dir = File.expand_path File.dirname File.dirname __FILE__
64
+ gem gem_dir do |gem|
65
+ gem.rbfiles << "#{gem_dir}/perf/stage.rb" << "#{gem_dir}/perf/mruby/stage.rb"
66
+ end
67
+ end
@@ -0,0 +1,23 @@
1
+ require 'bundler'
2
+
3
+ Bundler.require :default
4
+ Bundler.require :perf
5
+
6
+ perf = File.dirname File.dirname __FILE__
7
+ Dir["#{perf}/{stage.rb,stage/**/*.rb}"].sort.each{ |f| require f }
8
+
9
+ class Stage
10
+ def profile(seconds: 1, printer: 'flat')
11
+ gc_disabled do
12
+ profile = RubyProf::Profile.new(merge_fibers: true).tap(&:start)
13
+
14
+ result = execute(seconds: seconds){ yield }
15
+
16
+ printer[0] = printer[0].capitalize
17
+ RubyProf.const_get("#{printer}Printer").new(profile.stop).print(STDOUT, sort_method: :self_time)
18
+
19
+ result
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,32 @@
1
+ stage = Stage.new
2
+ batch_size = ARGV.fetch(0, 1).to_i
3
+ print_results_only = ARGV[1] == 'skip_header'
4
+
5
+ stage.benchmark 'proc.call',
6
+ batch_size: batch_size,
7
+ proc: "proc{}",
8
+ call: :call
9
+
10
+ stage.benchmark 'conproc.call',
11
+ batch_size: batch_size,
12
+ proc: "concurrent_proc{}",
13
+ call: :call
14
+
15
+ stage.benchmark 'conproc.call_nonblock',
16
+ batch_size: batch_size,
17
+ proc: "concurrent_proc{}",
18
+ call: :call_nonblock
19
+
20
+ stage.benchmark 'conproc.call_detached',
21
+ batch_size: batch_size,
22
+ proc: "concurrent_proc{}",
23
+ call: :call_detached,
24
+ sync: :wait
25
+
26
+ stage.benchmark 'conproc.call_and_forget',
27
+ batch_size: batch_size,
28
+ proc: "concurrent_proc{}",
29
+ call: :call_and_forget,
30
+ sync: :wait
31
+
32
+ stage.perform print_results_only
@@ -0,0 +1,52 @@
1
+ stage = Stage.new
2
+ batch_size = ARGV.fetch(0, 1).to_i
3
+ print_results_only = ARGV[1] == 'skip_header'
4
+
5
+ stage.benchmark 'call',
6
+ batch_size: batch_size,
7
+ proc: "concurrent_proc{}",
8
+ call: :call,
9
+ sync: true
10
+
11
+ stage.benchmark 'call_nonblock',
12
+ batch_size: batch_size,
13
+ proc: "concurrent_proc{}",
14
+ call: :call_nonblock
15
+
16
+ stage.benchmark 'call_detached',
17
+ batch_size: batch_size,
18
+ proc: "concurrent_proc{}",
19
+ call: :call_detached,
20
+ sync: true
21
+
22
+ stage.benchmark 'call_and_forget',
23
+ batch_size: batch_size,
24
+ proc: "concurrent_proc{}",
25
+ call: :call_and_forget,
26
+ sync: true
27
+
28
+ stage.benchmark 'waiting call',
29
+ batch_size: batch_size,
30
+ proc: "concurrent_proc{ wait 0 }",
31
+ call: :call,
32
+ sync: true
33
+
34
+ stage.benchmark 'waiting call_nonblock',
35
+ batch_size: batch_size,
36
+ proc: "concurrent_proc{ wait 0 }",
37
+ call: :call_nonblock,
38
+ sync: true
39
+
40
+ stage.benchmark 'waiting call_detached',
41
+ batch_size: batch_size,
42
+ proc: "concurrent_proc{ wait 0 }",
43
+ call: :call_detached,
44
+ sync: true
45
+
46
+ stage.benchmark 'waiting call_and_forget',
47
+ batch_size: batch_size,
48
+ proc: "concurrent_proc{ wait 0 }",
49
+ call: :call_and_forget,
50
+ sync: true
51
+
52
+ stage.perform print_results_only
@@ -0,0 +1,38 @@
1
+ stage = Stage.new
2
+ batch_size = ARGV.fetch(0, 1).to_i
3
+ print_results_only = ARGV[1] == 'skip_header'
4
+
5
+ stage.benchmark :wait,
6
+ batch_size: batch_size,
7
+ proc: <<RUBY, call: :call
8
+ proc do
9
+ wait 0 # schedule the proc be resumed ASAP
10
+ end
11
+ RUBY
12
+
13
+ stage.benchmark "await_readable",
14
+ batch_size: batch_size,
15
+ proc: <<RUBY, call: :call, args: <<RUBY
16
+ proc do |r,w|
17
+ r.await_readable
18
+ end
19
+ RUBY
20
+ IO.pipe.tap{ |r,w| w.write '0' }
21
+ RUBY
22
+
23
+ stage.benchmark "await_writable",
24
+ batch_size: batch_size,
25
+ proc: <<RUBY, call: :call, args: <<RUBY
26
+ proc do |r,w|
27
+ w.await_writable
28
+ end
29
+ RUBY
30
+ IO.pipe
31
+ RUBY
32
+
33
+ # Warm up
34
+ wait_proc = concurrent_proc{ wait 0 }
35
+ stage.execute{ wait_proc.call }
36
+
37
+ # Performance
38
+ stage.perform print_results_only
@@ -0,0 +1,8 @@
1
+ class Stage
2
+ def profile(opts = {})
3
+ gc_disabled do
4
+ execute(opts){ yield }
5
+ end
6
+ end
7
+ end
8
+
@@ -0,0 +1,10 @@
1
+ stage = Stage.new
2
+
3
+ r,w = IO.pipe
4
+ w.write '0'
5
+
6
+ result = stage.profile(seconds: 1) do
7
+ r.await_readable
8
+ end
9
+
10
+ puts "#{result[:iterations]} executions in #{result[:time].round 4} seconds"
@@ -1,12 +1,8 @@
1
- #!/bin/env ruby
2
-
3
- require_relative "_shared/stage"
4
-
5
1
  stage = Stage.new
6
2
 
7
3
  conproc = concurrent_proc{}
8
4
 
9
- result = stage.measure(seconds: 1) do
5
+ result = stage.profile(seconds: 1) do
10
6
  conproc.call
11
7
  end
12
8
 
@@ -1,13 +1,9 @@
1
- #!/bin/env ruby
2
-
3
- require_relative "_shared/stage"
4
-
5
1
  stage = Stage.new
6
2
 
7
3
  evaluation = Concurrently::Evaluation.current
8
4
  conproc = concurrent_proc{ evaluation.resume! }
9
5
 
10
- result = stage.measure(seconds: 1) do
6
+ result = stage.profile(seconds: 1) do
11
7
  conproc.call_and_forget
12
8
  await_resume!
13
9
  end
@@ -1,13 +1,9 @@
1
- #!/bin/env ruby
2
-
3
- require_relative "_shared/stage"
4
-
5
1
  stage = Stage.new
6
2
 
7
3
  evaluation = Concurrently::Evaluation.current
8
4
  conproc = concurrent_proc{ evaluation.resume! }
9
5
 
10
- result = stage.measure(seconds: 1) do
6
+ result = stage.profile(seconds: 1) do
11
7
  conproc.call_detached
12
8
  await_resume!
13
9
  end
@@ -1,12 +1,8 @@
1
- #!/bin/env ruby
2
-
3
- require_relative "_shared/stage"
4
-
5
1
  stage = Stage.new
6
2
 
7
3
  conproc = concurrent_proc{}
8
4
 
9
- result = stage.measure(seconds: 1) do
5
+ result = stage.profile(seconds: 1) do
10
6
  conproc.call_nonblock
11
7
  end
12
8
 
@@ -0,0 +1,7 @@
1
+ stage = Stage.new
2
+
3
+ result = stage.profile(seconds: 1) do
4
+ wait 0
5
+ end
6
+
7
+ puts "#{result[:iterations]} executions in #{result[:time].round 4} seconds"
data/perf/stage.rb ADDED
@@ -0,0 +1,47 @@
1
+ class Stage
2
+ def initialize
3
+ @benchmarks = []
4
+ end
5
+
6
+ def gc_disabled
7
+ GC.start
8
+ GC.disable
9
+ yield
10
+ ensure
11
+ GC.enable
12
+ end
13
+
14
+ def execute(opts = {})
15
+ seconds = opts[:seconds] || 1
16
+ event_loop = Concurrently::EventLoop.current
17
+ event_loop.reinitialize!
18
+ iterations = 0
19
+ start_time = event_loop.lifetime
20
+ end_time = start_time + seconds
21
+ while event_loop.lifetime < end_time
22
+ yield
23
+ iterations += 1
24
+ end
25
+ stop_time = event_loop.lifetime
26
+ { iterations: iterations, time: (stop_time-start_time) }
27
+ end
28
+
29
+ def benchmark(*args)
30
+ @benchmarks << Benchmark.new(self, *args)
31
+ end
32
+
33
+ def perform(print_results_only = false)
34
+ if @benchmarks.size
35
+ unless print_results_only
36
+ puts Benchmark.header
37
+ @benchmarks.each do |b|
38
+ puts b.desc
39
+ end
40
+ end
41
+ puts Benchmark.result_header
42
+ @benchmarks.each{ |b| b.run }
43
+ puts
44
+ end
45
+ end
46
+ end
47
+