about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2011-05-13 15:55:50 -0700
committerEric Wong <normalperson@yhbt.net>2011-05-13 15:55:50 -0700
commit6cefcff5889cceaa001f76f4be1a1c5e513b241d (patch)
tree1824896fe69e92bbe541b14f233d7269af6c33bd
parentab732113e13f1690fd2c1a18d1c66beb7864d847 (diff)
downloadkgio-6cefcff5889cceaa001f76f4be1a1c5e513b241d.tar.gz
For the case where a file is readable, it's faster to
just call open() instead of stat()-ing and then calling
open().

open() failures are still relatively cheap, too, they're still
more expensive than a failed stat() but cheaper than raising
an exception.
-rw-r--r--ext/kgio/kgio.h1
-rw-r--r--ext/kgio/kgio_ext.c1
-rw-r--r--ext/kgio/tryopen.c134
-rw-r--r--test/test_tryopen.rb71
4 files changed, 207 insertions, 0 deletions
diff --git a/ext/kgio/kgio.h b/ext/kgio/kgio.h
index 9056cef..0ecfde0 100644
--- a/ext/kgio/kgio.h
+++ b/ext/kgio/kgio.h
@@ -36,6 +36,7 @@ void init_kgio_accept(void);
 void init_kgio_connect(void);
 void init_kgio_autopush(void);
 void init_kgio_poll(void);
+void init_kgio_tryopen(void);
 
 void kgio_autopush_accept(VALUE, VALUE);
 void kgio_autopush_recv(VALUE);
diff --git a/ext/kgio/kgio_ext.c b/ext/kgio/kgio_ext.c
index f50b3c8..2365fd0 100644
--- a/ext/kgio/kgio_ext.c
+++ b/ext/kgio/kgio_ext.c
@@ -8,4 +8,5 @@ void Init_kgio_ext(void)
         init_kgio_accept();
         init_kgio_autopush();
         init_kgio_poll();
+        init_kgio_tryopen();
 }
diff --git a/ext/kgio/tryopen.c b/ext/kgio/tryopen.c
new file mode 100644
index 0000000..c9d7bb6
--- /dev/null
+++ b/ext/kgio/tryopen.c
@@ -0,0 +1,134 @@
+#include <ruby.h>
+#ifdef HAVE_RUBY_IO_H
+#  include <ruby/io.h>
+#else
+#  include <rubyio.h>
+#endif
+
+#ifdef HAVE_RUBY_ST_H
+#  include <ruby/st.h>
+#else
+#  include <st.h>
+#endif
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+static ID id_for_fd, id_to_path;
+static st_table *errno2sym;
+
+struct open_args {
+        const char *pathname;
+        int flags;
+        mode_t mode;
+};
+
+static VALUE nogvl_open(void *ptr)
+{
+        struct open_args *o = ptr;
+
+        return (VALUE)open(o->pathname, o->flags, o->mode);
+}
+
+#ifndef HAVE_RB_THREAD_BLOCKING_REGION
+#  define RUBY_UBF_IO (-1)
+typedef void rb_unblock_function_t(void *);
+typedef VALUE rb_blocking_function_t(void *);
+static VALUE rb_thread_blocking_region(
+        rb_blocking_function_t *fn, void *data1,
+        rb_unblock_function_t *ubf, void *data2)
+{
+        TRAP_BEG; /* for FIFO */
+        rv = fn(data1);
+        TRAP_END;
+
+        return rv;
+}
+#endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */
+
+static VALUE s_tryopen(int argc, VALUE *argv, VALUE klass)
+{
+        int fd;
+        VALUE pathname, flags, mode;
+        struct open_args o;
+        int retried = 0;
+
+        rb_scan_args(argc, argv, "12", &pathname, &flags, &mode);
+        o.pathname = StringValueCStr(pathname);
+
+        switch (TYPE(flags)) {
+        case T_NIL: o.flags = O_RDONLY; break;
+        case T_FIXNUM: o.flags = FIX2INT(flags); break;
+        case T_BIGNUM: o.flags = NUM2INT(flags); break;
+        default: rb_raise(rb_eArgError, "flags must be an Integer");
+        }
+        switch (TYPE(mode)) {
+        case T_NIL: o.mode = 0666; break;
+        case T_FIXNUM: o.mode = FIX2INT(mode); break;
+        case T_BIGNUM: o.mode = NUM2INT(mode); break;
+        default: rb_raise(rb_eArgError, "mode must be an Integer");
+        }
+
+retry:
+        fd = (int)rb_thread_blocking_region(nogvl_open, &o, RUBY_UBF_IO, 0);
+        if (fd == -1) {
+                if (errno == EMFILE || errno == ENFILE) {
+                        rb_gc();
+                        if (retried)
+                                rb_sys_fail(o.pathname);
+                        retried = 1;
+                        goto retry;
+                }
+                if (fd == -1) {
+                        int saved_errno = errno;
+                        VALUE rv;
+
+                        if (!st_lookup(errno2sym, (st_data_t)errno, &rv)) {
+                                errno = saved_errno;
+                                rb_sys_fail(o.pathname);
+                        }
+                        return rv;
+                }
+        }
+        return rb_funcall(rb_cFile, id_for_fd, 1, INT2FIX(fd));
+}
+
+void init_kgio_tryopen(void)
+{
+        VALUE mKgio = rb_define_module("Kgio");
+        VALUE tmp;
+        VALUE *ptr;
+        long len;
+
+        rb_define_singleton_method(mKgio, "tryopen", s_tryopen, -1);
+        id_for_fd = rb_intern("for_fd");
+        id_to_path = rb_intern("to_path");
+
+        errno2sym = st_init_numtable();
+        tmp = rb_funcall(rb_mErrno, rb_intern("constants"), 0);
+        ptr = RARRAY_PTR(tmp);
+        len = RARRAY_LEN(tmp);
+        for (; --len >= 0; ptr++) {
+                VALUE error;
+                ID const_id;
+
+                switch (TYPE(*ptr)) {
+                case T_SYMBOL: const_id = SYM2ID(*ptr); break;
+                case T_STRING: const_id = rb_intern(RSTRING_PTR(*ptr)); break;
+                default: rb_bug("constant not a symbol or string");
+                }
+
+                error = rb_const_get(rb_mErrno, const_id);
+                if (!rb_const_defined(error, rb_intern("Errno")))
+                        continue;
+
+                error = rb_const_get(error, rb_intern("Errno"));
+                switch (TYPE(error)) {
+                case T_FIXNUM:
+                case T_BIGNUM:
+                        st_insert(errno2sym, (st_data_t)NUM2INT(error),
+                                  ID2SYM(const_id));
+                }
+        }
+}
diff --git a/test/test_tryopen.rb b/test/test_tryopen.rb
new file mode 100644
index 0000000..c885ca0
--- /dev/null
+++ b/test/test_tryopen.rb
@@ -0,0 +1,71 @@
+require 'tempfile'
+require 'test/unit'
+$-w = true
+require 'kgio'
+
+class TestTryopen < Test::Unit::TestCase
+
+  def test_tryopen_success
+    tmp = Kgio.tryopen(__FILE__)
+    assert_instance_of File, tmp
+    assert_equal File.read(__FILE__), tmp.read
+    assert_nothing_raised { tmp.close }
+  end
+
+  def test_tryopen_ENOENT
+    tmp = Tempfile.new "tryopen"
+    path = tmp.path
+    tmp.close!
+    tmp = Kgio.tryopen(path)
+    assert_equal :ENOENT, tmp
+  end
+
+  def test_tryopen_EPERM
+    tmp = Tempfile.new "tryopen"
+    File.chmod 0000, tmp.path
+    tmp = Kgio.tryopen(tmp.path)
+    assert_equal :EACCES, tmp
+  end
+
+  def test_tryopen_readwrite
+    tmp = Tempfile.new "tryopen"
+    file = Kgio.tryopen(tmp.path, IO::RDWR)
+    file.syswrite "FOO"
+    assert_equal "FOO", tmp.sysread(3)
+  end
+
+  def test_tryopen_mode
+    tmp = Tempfile.new "tryopen"
+    path = tmp.path
+    tmp.close!
+    file = Kgio.tryopen(path, IO::RDWR|IO::CREAT, 0000)
+    assert_equal 0100000, File.stat(path).mode
+    ensure
+      File.unlink path
+  end
+
+  require "benchmark"
+  def test_benchmark
+    nr = 1000000
+    tmp = Tempfile.new('tryopen')
+    file = tmp.path
+    Benchmark.bmbm do |x|
+      x.report("tryopen (OK)") do
+        nr.times { Kgio.tryopen(file).close }
+      end
+      x.report("open (OK)") do
+        nr.times { File.readable?(file) && File.open(file).close }
+      end
+    end
+    tmp.close!
+    assert_equal :ENOENT, Kgio.tryopen(file)
+    Benchmark.bmbm do |x|
+      x.report("tryopen (ENOENT)") do
+        nr.times { Kgio.tryopen(file) }
+      end
+      x.report("open (ENOENT)") do
+        nr.times { File.readable?(file) && File.open(file) }
+      end
+    end
+  end if ENV["BENCHMARK"]
+end