Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Initial stab at a Promise extension

  • Loading branch information...
commit 8405e37f0e10565b39b87760fe129d9e116ebf88 0 parents
Lourens Naudé authored
6 .gitignore
... ... @@ -0,0 +1,6 @@
  1 +*.bundle
  2 +Makefile
  3 +mkmf.log
  4 +.DS_Store
  5 +*.o
  6 +pkg
88 README
... ... @@ -0,0 +1,88 @@
  1 +Lightweight Ruby MRI promise extension
  2 + (c) 2010 Lourens Naudé (methodmissing), James Tucker (raggi), tmm1 && ice799 for pointers (pun intended)
  3 +
  4 + http://github.com/methodmissing/promise
  5 +
  6 +This library works with Ruby 1.8 (1.9 pending) and is a more efficient
  7 +implementation of the following Ruby code :
  8 +
  9 + class Promise
  10 + def initialize(&blk)
  11 + @thread = Thread.new(&blk)
  12 + @result = nil
  13 + end
  14 +
  15 + def method_missing(meth, *args)
  16 + @result ||= @thread.value
  17 + end
  18 + end
  19 +
  20 +Examples :
  21 +
  22 + promise = Promise.new{ 1000.times{ IO.read(__FILE__) } }
  23 + assert_equal 1000, promise.id # blocks only if we don't have an async result yet
  24 +
  25 +Challenges :
  26 +
  27 + * Avoiding method_missing overhead, which bogs down even 1.9s BasicObject for this use case
  28 + * rb_define_method(rb_cPromise, "__send__", rb_promise_result_argc_any, -1);
  29 + * We attempt to negate some of that by overloading common Object members at compile time
  30 + * Ideally one should overload rb_call (block the current thread) and thus a Promise
  31 + implementation is best served as a MRI patch.
  32 +
  33 +Todo :
  34 +
  35 + * ruby 1.9 support
  36 + * Fiber support
  37 + * reduce further method call overhead
  38 + * look into a symbol table for caching
  39 +
  40 +Performance :
  41 +
  42 + methodmissing:promise lourens$ rake bench
  43 + (in /Users/lourens/projects/promise)
  44 + /Users/lourens/.rvm/rubies/ruby-1.8.7-p249/bin/ruby bench/promise.rb
  45 + Rehearsal --------------------------------------------------------------------
  46 + Promise 0.010000 0.000000 0.010000 ( 0.018255)
  47 + RubyPromise 0.030000 0.000000 0.030000 ( 0.024190)
  48 + Promise#== 0.010000 0.000000 0.010000 ( 0.017622)
  49 + RubyPromise#== 0.020000 0.010000 0.030000 ( 0.020867)
  50 + Promise#object_id 0.010000 0.000000 0.010000 ( 0.016699)
  51 + RubyPromise#object_id 0.020000 0.000000 0.020000 ( 0.021970)
  52 + Promise#__send__ 0.020000 0.000000 0.020000 ( 0.017142)
  53 + RubyPromise#__send__ 0.020000 0.000000 0.020000 ( 0.021984)
  54 + Promise (blocking) 0.040000 0.030000 0.070000 ( 0.072591)
  55 + RubyPromise (blocking) 0.050000 0.030000 0.080000 ( 0.077348)
  56 + Promise#== (blocking) 0.050000 0.030000 0.080000 ( 0.068011)
  57 + RubyPromise#== (blocking) 0.050000 0.020000 0.070000 ( 0.084727)
  58 + Promise#object_id (blocking) 0.050000 0.030000 0.080000 ( 0.069918)
  59 + RubyPromise#object_id (blocking) 0.050000 0.030000 0.080000 ( 0.082319)
  60 + Promise#__send__ (blocking) 0.040000 0.020000 0.060000 ( 0.071780)
  61 + RubyPromise#__send__ (blocking) 0.060000 0.030000 0.090000 ( 0.088574)
  62 + ----------------------------------------------------------- total: 0.760000sec
  63 +
  64 + user system total real
  65 + Promise 0.010000 0.000000 0.010000 ( 0.014602)
  66 + RubyPromise 0.020000 0.010000 0.030000 ( 0.022575)
  67 + Promise#== 0.010000 0.000000 0.010000 ( 0.015162)
  68 + RubyPromise#== 0.020000 0.000000 0.020000 ( 0.019323)
  69 + Promise#object_id 0.010000 0.000000 0.010000 ( 0.015996)
  70 + RubyPromise#object_id 0.010000 0.000000 0.010000 ( 0.019146)
  71 + Promise#__send__ 0.010000 0.010000 0.020000 ( 0.015637)
  72 + RubyPromise#__send__ 0.020000 0.000000 0.020000 ( 0.019562)
  73 + Promise (blocking) 0.040000 0.020000 0.060000 ( 0.069022)
  74 + RubyPromise (blocking) 0.050000 0.030000 0.080000 ( 0.080459)
  75 + Promise#== (blocking) 0.050000 0.030000 0.080000 ( 0.068640)
  76 + RubyPromise#== (blocking) 0.050000 0.020000 0.070000 ( 0.084409)
  77 + Promise#object_id (blocking) 0.050000 0.030000 0.080000 ( 0.067952)
  78 + RubyPromise#object_id (blocking) 0.060000 0.030000 0.090000 ( 0.085177)
  79 + Promise#__send__ (blocking) 0.040000 0.020000 0.060000 ( 0.067793)
  80 + RubyPromise#__send__ (blocking) 0.060000 0.030000 0.090000 ( 0.086539)
  81 +
  82 +To run the test suite:
  83 +
  84 + rake
  85 +
  86 +To run the benchmarks:
  87 +
  88 + rake bench
118 Rakefile
... ... @@ -0,0 +1,118 @@
  1 +require 'rake'
  2 +require 'rake/testtask'
  3 +require 'rake/clean'
  4 +
  5 +PROMISE_ROOT = 'ext/promise'
  6 +
  7 +PROMISE_ARGC_MAP = { -1 => "rb_promise_result_argc_any",
  8 + 0 => "rb_promise_result_argc_none",
  9 + 1 => "rb_promise_result_argc_one",
  10 + 2 => "rb_promise_result_argc_two",}
  11 +
  12 +PROMISE_METHODS_18 = %w[method_missing inspect tap clone public_methods object_id __send__ instance_variable_defined? equal? freeze extend send methods hash dup to_enum instance_variables eql? instance_eval id singleton_methods taint frozen? instance_variable_get enum_for instance_of? to_a method type instance_exec protected_methods method_missing == === instance_variable_set kind_of? respond_to? to_s class __id__ tainted? =~ private_methods untaint nil? is_a?]
  13 +
  14 +PROMISE_ARGC_18 = { "==" => 1,
  15 + "equal?" => 1,
  16 + "===" => 1,
  17 + "=~" => 1,
  18 + "eql?" => 1,
  19 + "initialize_copy" => 1,
  20 + "method" => 1,
  21 + "instance_exec" => 1,
  22 + "instance_of?" => 1,
  23 + "kind_of?" => 1,
  24 + "is_a?" => 1,
  25 + "instance_variable_get" => 1,
  26 + "instance_variable_defined?" => 1,
  27 + "instance_variable_set" => 2,
  28 + "methods" => -1,
  29 + "singleton_methods" => -1,
  30 + "protected_methods" => -1,
  31 + "private_methods" => -1,
  32 + "public_methods" => -1,
  33 + "__send__" => -1,
  34 + "send" => -1,
  35 + "to_enum" => -1,
  36 + "instance_eval" => -1,
  37 + "enum_for" => -1,
  38 + "method_missing" => -1,
  39 + "respond_to?" => -1
  40 + }
  41 +
  42 +desc 'Default: test'
  43 +task :default => :test
  44 +
  45 +desc 'Run promise tests.'
  46 +Rake::TestTask.new(:test) do |t|
  47 + t.libs = [PROMISE_ROOT]
  48 + t.pattern = 'test/test_*.rb'
  49 + t.verbose = true
  50 +end
  51 +task :test => :build
  52 +
  53 +namespace :build do
  54 + file "#{PROMISE_ROOT}/promise.c"
  55 + file "#{PROMISE_ROOT}/extconf.rb"
  56 + file "#{PROMISE_ROOT}/Makefile" => %W(#{PROMISE_ROOT}/promise.c #{PROMISE_ROOT}/extconf.rb) do
  57 + Dir.chdir(PROMISE_ROOT) do
  58 + ruby 'extconf.rb'
  59 + end
  60 + end
  61 +
  62 + desc "generate makefile"
  63 + task :makefile => %W(#{PROMISE_ROOT}/Makefile #{PROMISE_ROOT}/promise.c)
  64 +
  65 + dlext = Config::CONFIG['DLEXT']
  66 + file "#{PROMISE_ROOT}/promise.#{dlext}" => %W(#{PROMISE_ROOT}/Makefile #{PROMISE_ROOT}/promise.c) do
  67 + Dir.chdir(PROMISE_ROOT) do
  68 + sh 'make' # TODO - is there a config for which make somewhere?
  69 + end
  70 + end
  71 +
  72 + desc "compile promise extension"
  73 + task :compile => "#{PROMISE_ROOT}/promise.#{dlext}"
  74 +
  75 + task :clean do
  76 + Dir.chdir(PROMISE_ROOT) do
  77 + sh 'make clean'
  78 + end if File.exists?("#{PROMISE_ROOT}/Makefile")
  79 + end
  80 +
  81 + File.open("#{PROMISE_ROOT}/promise.h", "w+"){|h|
  82 + def_method = lambda do |m|
  83 + argc = PROMISE_ARGC_18[m] || 0
  84 + h.write "rb_define_method(rb_cPromise, \"#{m}\", #{PROMISE_ARGC_MAP[argc]}, #{argc.to_s});\n"
  85 + end
  86 + h.write "#ifdef RUBY18\n"
  87 + PROMISE_METHODS_18.each{|m| def_method.call(m) }
  88 + h.write "#endif\n"
  89 + }
  90 +
  91 + CLEAN.include("#{PROMISE_ROOT}/Makefile")
  92 + CLEAN.include("#{PROMISE_ROOT}/promise.#{dlext}")
  93 +end
  94 +
  95 +task :clean => %w(build:clean)
  96 +
  97 +desc "compile"
  98 +task :build => %w(build:compile)
  99 +
  100 +task :install do |t|
  101 + Dir.chdir(PROMISE_ROOT) do
  102 + sh 'sudo make install'
  103 + end
  104 +end
  105 +
  106 +desc "clean build install"
  107 +task :setup => %w(clean build install)
  108 +
  109 +desc "run benchmarks"
  110 +task :bench do
  111 + ruby "bench/promise.rb"
  112 +end
  113 +task :bench => :build
  114 +
  115 +desc "build gem"
  116 +task :gem do
  117 + sh "gem build promise.gemspec"
  118 +end
33 bench/promise.rb
... ... @@ -0,0 +1,33 @@
  1 +require "benchmark"
  2 +require "ext/promise/promise"
  3 +
  4 +class RubyPromise
  5 + def initialize(&blk)
  6 + @thread = Thread.new(&blk)
  7 + @result = nil
  8 + end
  9 +
  10 + def method_missing(meth, *args)
  11 + @result ||= @thread.value
  12 + end
  13 +end
  14 +
  15 +TESTS = 1000
  16 +Benchmark.bmbm do |results|
  17 + results.report("Promise") { TESTS.times { Promise.new{ 1 }.undefined } }
  18 + results.report("RubyPromise") { TESTS.times { RubyPromise.new{ 1 }.undefined } }
  19 + results.report("Promise#==") { TESTS.times { Promise.new{ 1 } == 1 } }
  20 + results.report("RubyPromise#==") { TESTS.times { RubyPromise.new{ 1 } == 1 } }
  21 + results.report("Promise#object_id") { TESTS.times { Promise.new{ 1 }.object_id } }
  22 + results.report("RubyPromise#object_id") { TESTS.times { RubyPromise.new{ 1 }.object_id } }
  23 + results.report("Promise#__send__") { TESTS.times { Promise.new{ 1 }.__send__ :object_id } }
  24 + results.report("RubyPromise#__send__") { TESTS.times { RubyPromise.new{ 1 }.__send__ :object_id } }
  25 + results.report("Promise (blocking)") { TESTS.times { Promise.new{ IO.read(__FILE__) }.undefined } }
  26 + results.report("RubyPromise (blocking)") { TESTS.times { RubyPromise.new{ IO.read(__FILE__) }.undefined } }
  27 + results.report("Promise#== (blocking)") { TESTS.times { Promise.new{ IO.read(__FILE__) } == 1 } }
  28 + results.report("RubyPromise#== (blocking)") { TESTS.times { RubyPromise.new{ IO.read(__FILE__) } == 1 } }
  29 + results.report("Promise#object_id (blocking)") { TESTS.times { Promise.new{ IO.read(__FILE__) }.object_id } }
  30 + results.report("RubyPromise#object_id (blocking)") { TESTS.times { RubyPromise.new{ IO.read(__FILE__) }.object_id } }
  31 + results.report("Promise#__send__ (blocking)") { TESTS.times { Promise.new{ IO.read(__FILE__) }.__send__ :object_id } }
  32 + results.report("RubyPromise#__send__ (blocking)") { TESTS.times { RubyPromise.new{ IO.read(__FILE__) }.__send__ :object_id } }
  33 +end
12 ext/promise/extconf.rb
... ... @@ -0,0 +1,12 @@
  1 +require 'mkmf'
  2 +
  3 +def add_define(name)
  4 + $defs.push("-D#{name}")
  5 +end
  6 +
  7 +dir_config('promise')
  8 +
  9 +add_define 'RUBY19' if have_func('rb_thread_blocking_region') && have_macro('RUBY_UBF_IO', 'ruby.h')
  10 +add_define 'RUBY18' if have_var('rb_trap_immediate', ['ruby.h', 'rubysig.h'])
  11 +
  12 +create_makefile('promise')
100 ext/promise/promise.c
... ... @@ -0,0 +1,100 @@
  1 +#include "ruby.h"
  2 +
  3 +typedef struct {
  4 + VALUE thread;
  5 + VALUE computation;
  6 + VALUE result;
  7 +} RPromise;
  8 +
  9 +static ID s_value;
  10 +
  11 +#define GetPromiseStruct(obj) (Check_Type(obj, T_DATA), (RPromise*)DATA_PTR(obj))
  12 +#define PromiseResult(pr) \
  13 + RPromise* prs = GetPromiseStruct(pr); \
  14 + if (!prs->result) prs->result = rb_funcall(prs->thread, s_value, 0); \
  15 + return prs->result;
  16 +
  17 +VALUE rb_cPromise;
  18 +
  19 +static void mark_promise(RPromise* pr)
  20 +{
  21 + rb_gc_mark(pr->computation);
  22 + rb_gc_mark(pr->result);
  23 +}
  24 +
  25 +static void free_promise(RPromise* pr)
  26 +{
  27 + xfree(pr);
  28 +}
  29 +
  30 +static VALUE
  31 +promise_alloc(VALUE klass)
  32 +{
  33 + VALUE pr;
  34 + RPromise* prs;
  35 + pr = Data_Make_Struct(klass, RPromise, mark_promise, free_promise, prs);
  36 + prs->thread = 0;
  37 + prs->computation = Qnil;
  38 + prs->result = 0;
  39 + return pr;
  40 +}
  41 +
  42 +static VALUE
  43 +rb_promise_new()
  44 +{
  45 + return promise_alloc(rb_cPromise);
  46 +}
  47 +
  48 +static VALUE
  49 +rb_promise_compute_deferred(void *pr){
  50 + RPromise* prs = GetPromiseStruct(pr);
  51 + VALUE args[0];
  52 + return rb_proc_call(prs->computation, (VALUE)args);
  53 +}
  54 +
  55 +static VALUE
  56 +rb_promise_initialize(VALUE pr)
  57 +{
  58 + RPromise* prs = GetPromiseStruct(pr);
  59 + rb_need_block();
  60 + prs->computation = rb_block_proc();
  61 + prs->thread = rb_thread_create(rb_promise_compute_deferred, (void*)(VALUE)pr);
  62 + return pr;
  63 +}
  64 +
  65 +static VALUE
  66 +rb_promise_result_argc_any(int argc, VALUE *argv, VALUE pr)
  67 +{
  68 + PromiseResult(pr);
  69 +}
  70 +
  71 +static VALUE
  72 +rb_promise_result_argc_two(VALUE pr, VALUE arg1, VALUE arg2)
  73 +{
  74 + PromiseResult(pr);
  75 +}
  76 +
  77 +static VALUE
  78 +rb_promise_result_argc_one(VALUE pr, VALUE arg)
  79 +{
  80 + PromiseResult(pr);
  81 +}
  82 +
  83 +static VALUE
  84 +rb_promise_result_argc_none(VALUE pr)
  85 +{
  86 + PromiseResult(pr);
  87 +}
  88 +
  89 +void
  90 +Init_promise()
  91 +{
  92 + rb_cPromise = rb_define_class("Promise", rb_cObject);
  93 + rb_define_alloc_func(rb_cPromise, promise_alloc);
  94 +
  95 + rb_define_method(rb_cPromise, "initialize", rb_promise_initialize, 0);
  96 +
  97 + s_value = rb_intern("value");
  98 +
  99 +#include "promise.h"
  100 +}
48 ext/promise/promise.h
... ... @@ -0,0 +1,48 @@
  1 +#ifdef RUBY18
  2 +rb_define_method(rb_cPromise, "method_missing", rb_promise_result_argc_any, -1);
  3 +rb_define_method(rb_cPromise, "inspect", rb_promise_result_argc_none, 0);
  4 +rb_define_method(rb_cPromise, "tap", rb_promise_result_argc_none, 0);
  5 +rb_define_method(rb_cPromise, "clone", rb_promise_result_argc_none, 0);
  6 +rb_define_method(rb_cPromise, "public_methods", rb_promise_result_argc_any, -1);
  7 +rb_define_method(rb_cPromise, "object_id", rb_promise_result_argc_none, 0);
  8 +rb_define_method(rb_cPromise, "__send__", rb_promise_result_argc_any, -1);
  9 +rb_define_method(rb_cPromise, "instance_variable_defined?", rb_promise_result_argc_one, 1);
  10 +rb_define_method(rb_cPromise, "equal?", rb_promise_result_argc_one, 1);
  11 +rb_define_method(rb_cPromise, "freeze", rb_promise_result_argc_none, 0);
  12 +rb_define_method(rb_cPromise, "extend", rb_promise_result_argc_none, 0);
  13 +rb_define_method(rb_cPromise, "send", rb_promise_result_argc_any, -1);
  14 +rb_define_method(rb_cPromise, "methods", rb_promise_result_argc_any, -1);
  15 +rb_define_method(rb_cPromise, "hash", rb_promise_result_argc_none, 0);
  16 +rb_define_method(rb_cPromise, "dup", rb_promise_result_argc_none, 0);
  17 +rb_define_method(rb_cPromise, "to_enum", rb_promise_result_argc_any, -1);
  18 +rb_define_method(rb_cPromise, "instance_variables", rb_promise_result_argc_none, 0);
  19 +rb_define_method(rb_cPromise, "eql?", rb_promise_result_argc_one, 1);
  20 +rb_define_method(rb_cPromise, "instance_eval", rb_promise_result_argc_any, -1);
  21 +rb_define_method(rb_cPromise, "id", rb_promise_result_argc_none, 0);
  22 +rb_define_method(rb_cPromise, "singleton_methods", rb_promise_result_argc_any, -1);
  23 +rb_define_method(rb_cPromise, "taint", rb_promise_result_argc_none, 0);
  24 +rb_define_method(rb_cPromise, "frozen?", rb_promise_result_argc_none, 0);
  25 +rb_define_method(rb_cPromise, "instance_variable_get", rb_promise_result_argc_one, 1);
  26 +rb_define_method(rb_cPromise, "enum_for", rb_promise_result_argc_any, -1);
  27 +rb_define_method(rb_cPromise, "instance_of?", rb_promise_result_argc_one, 1);
  28 +rb_define_method(rb_cPromise, "to_a", rb_promise_result_argc_none, 0);
  29 +rb_define_method(rb_cPromise, "method", rb_promise_result_argc_one, 1);
  30 +rb_define_method(rb_cPromise, "type", rb_promise_result_argc_none, 0);
  31 +rb_define_method(rb_cPromise, "instance_exec", rb_promise_result_argc_one, 1);
  32 +rb_define_method(rb_cPromise, "protected_methods", rb_promise_result_argc_any, -1);
  33 +rb_define_method(rb_cPromise, "method_missing", rb_promise_result_argc_any, -1);
  34 +rb_define_method(rb_cPromise, "==", rb_promise_result_argc_one, 1);
  35 +rb_define_method(rb_cPromise, "===", rb_promise_result_argc_one, 1);
  36 +rb_define_method(rb_cPromise, "instance_variable_set", rb_promise_result_argc_two, 2);
  37 +rb_define_method(rb_cPromise, "kind_of?", rb_promise_result_argc_one, 1);
  38 +rb_define_method(rb_cPromise, "respond_to?", rb_promise_result_argc_any, -1);
  39 +rb_define_method(rb_cPromise, "to_s", rb_promise_result_argc_none, 0);
  40 +rb_define_method(rb_cPromise, "class", rb_promise_result_argc_none, 0);
  41 +rb_define_method(rb_cPromise, "__id__", rb_promise_result_argc_none, 0);
  42 +rb_define_method(rb_cPromise, "tainted?", rb_promise_result_argc_none, 0);
  43 +rb_define_method(rb_cPromise, "=~", rb_promise_result_argc_one, 1);
  44 +rb_define_method(rb_cPromise, "private_methods", rb_promise_result_argc_any, -1);
  45 +rb_define_method(rb_cPromise, "untaint", rb_promise_result_argc_none, 0);
  46 +rb_define_method(rb_cPromise, "nil?", rb_promise_result_argc_none, 0);
  47 +rb_define_method(rb_cPromise, "is_a?", rb_promise_result_argc_one, 1);
  48 +#endif
25 test/test_promise.rb
... ... @@ -0,0 +1,25 @@
  1 +require "test/unit"
  2 +require "promise"
  3 +
  4 +class TestPromise < Test::Unit::TestCase
  5 + def test_initialize
  6 + assert_instance_of Promise, Promise.new{ 1 * 1 }
  7 + end
  8 +
  9 + def test_initialize_without_computation
  10 + assert_raises(LocalJumpError) {
  11 + Promise.new
  12 + }
  13 + end
  14 +
  15 + def test_computation
  16 + promise = Promise.new{ 1 * 1 }
  17 + assert_equal 1, promise.a_value
  18 + assert_equal 1, promise.object_id
  19 + end
  20 +
  21 + def test_blocking_computation
  22 + promise = Promise.new{ 1000.times{ IO.read(__FILE__) } }
  23 + assert_equal 1000, promise.class
  24 + end
  25 +end

0 comments on commit 8405e37

Please sign in to comment.
Something went wrong with that request. Please try again.