about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2011-01-04 17:50:51 -0800
committerEric Wong <normalperson@yhbt.net>2011-01-04 17:51:59 -0800
commitd100025759450dd1cbeccd1a3e44c46921bba26b (patch)
tree8f623be43ae96cc9246a43b01fb5650751ca3769
parent6183611108c571dbed29dfe2854b9f06757fd27f (diff)
downloadunicorn-d100025759450dd1cbeccd1a3e44c46921bba26b.tar.gz
This can return a static string and be significantly
faster as it reduces object allocations and Ruby method
calls for the fastest websites that serve thousands of
requests a second.

It assumes the Ruby runtime is single-threaded, but that
is the case of Ruby 1.8 and 1.9 and also what Unicorn
is all about.  This change is safe for Rainbows! under 1.8
and 1.9.
-rw-r--r--GNUmakefile2
-rw-r--r--ext/unicorn_http/extconf.rb1
-rw-r--r--ext/unicorn_http/httpdate.c82
-rw-r--r--ext/unicorn_http/unicorn_http.rl3
-rw-r--r--lib/unicorn/http_response.rb4
-rw-r--r--test/unit/test_response.rb11
6 files changed, 99 insertions, 4 deletions
diff --git a/GNUmakefile b/GNUmakefile
index 8fe7336..2799077 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -45,7 +45,7 @@ T_r_log := $(subst .r,$(log_suffix),$(T_r))
 test_prefix = $(CURDIR)/test/$(RUBY_ENGINE)-$(RUBY_VERSION)
 
 ext := ext/unicorn_http
-c_files := $(ext)/unicorn_http.c $(wildcard $(ext)/*.h)
+c_files := $(ext)/unicorn_http.c $(ext)/httpdate.c $(wildcard $(ext)/*.h)
 rl_files := $(wildcard $(ext)/*.rl)
 base_bins := unicorn unicorn_rails
 bins := $(addprefix bin/, $(base_bins))
diff --git a/ext/unicorn_http/extconf.rb b/ext/unicorn_http/extconf.rb
index a2c8442..7da82e7 100644
--- a/ext/unicorn_http/extconf.rb
+++ b/ext/unicorn_http/extconf.rb
@@ -4,5 +4,6 @@ require 'mkmf'
 have_macro("SIZEOF_OFF_T", "ruby.h") or check_sizeof("off_t", "sys/types.h")
 have_macro("SIZEOF_LONG", "ruby.h") or check_sizeof("long", "sys/types.h")
 have_func("rb_str_set_len", "ruby.h")
+have_func("gmtime_r", "time.h")
 
 create_makefile("unicorn_http")
diff --git a/ext/unicorn_http/httpdate.c b/ext/unicorn_http/httpdate.c
new file mode 100644
index 0000000..bfa11ca
--- /dev/null
+++ b/ext/unicorn_http/httpdate.c
@@ -0,0 +1,82 @@
+#include <ruby.h>
+#include <time.h>
+#include <stdio.h>
+
+static const size_t buf_capa = sizeof("Thu, 01 Jan 1970 00:00:00 GMT");
+static VALUE buf;
+static char *buf_ptr;
+static const char *const week[] = {
+        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+static const char *const months[] = {
+        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+/* for people on wonky systems only */
+#ifndef HAVE_GMTIME_R
+static struct tm * my_gmtime_r(time_t *now, struct tm *tm)
+{
+        struct tm *global = gmtime(now);
+        if (global)
+                *tm = *global;
+        return tm;
+}
+#  define gmtime_r my_gmtime_r
+#endif
+
+
+/*
+ * Returns a string which represents the time as rfc1123-date of HTTP-date
+ * defined by RFC 2616:
+ *
+ *   day-of-week, DD month-name CCYY hh:mm:ss GMT
+ *
+ * Note that the result is always GMT.
+ *
+ * This method is identical to Time#httpdate in the Ruby standard library,
+ * except it is implemented in C for performance.  We always saw
+ * Time#httpdate at or near the top of the profiler output so we
+ * decided to rewrite this in C.
+ *
+ * Caveats: it relies on a Ruby implementation with the global VM lock,
+ * a thread-safe version will be provided when a Unix-only, GVL-free Ruby
+ * implementation becomes viable.
+ */
+static VALUE httpdate(VALUE self)
+{
+        static time_t last;
+        time_t now = time(NULL); /* not a syscall on modern 64-bit systems */
+        struct tm tm;
+
+        if (last == now)
+                return buf;
+        last = now;
+        gmtime_r(&now, &tm);
+
+        /* we can make this thread-safe later if our Ruby loses the GVL */
+        snprintf(buf_ptr, buf_capa,
+                 "%s, %02d %s %4d %02d:%02d:%02d GMT",
+                 week[tm.tm_wday],
+                 tm.tm_mday,
+                 months[tm.tm_mon],
+                 tm.tm_year + 1900,
+                 tm.tm_hour,
+                 tm.tm_min,
+                 tm.tm_sec);
+
+        return buf;
+}
+
+void init_unicorn_httpdate(void)
+{
+        VALUE mod = rb_const_get(rb_cObject, rb_intern("Unicorn"));
+        mod = rb_define_module_under(mod, "HttpResponse");
+
+        buf = rb_str_new(0, buf_capa - 1);
+        rb_global_variable(&buf);
+        buf_ptr = RSTRING_PTR(buf);
+        httpdate(Qnil);
+
+        rb_define_method(mod, "httpdate", httpdate, 0);
+}
diff --git a/ext/unicorn_http/unicorn_http.rl b/ext/unicorn_http/unicorn_http.rl
index 9b33e31..fd259b6 100644
--- a/ext/unicorn_http/unicorn_http.rl
+++ b/ext/unicorn_http/unicorn_http.rl
@@ -12,6 +12,8 @@
 #include "global_variables.h"
 #include "c_util.h"
 
+void init_unicorn_httpdate(void);
+
 #define UH_FL_CHUNKED  0x1
 #define UH_FL_HASBODY  0x2
 #define UH_FL_INBODY   0x4
@@ -897,5 +899,6 @@ void Init_unicorn_http(void)
   SET_GLOBAL(g_content_length, "CONTENT_LENGTH");
   SET_GLOBAL(g_http_connection, "CONNECTION");
   id_clear = rb_intern("clear");
+  init_unicorn_httpdate();
 }
 #undef SET_GLOBAL
diff --git a/lib/unicorn/http_response.rb b/lib/unicorn/http_response.rb
index 2d13863..c59ce2c 100644
--- a/lib/unicorn/http_response.rb
+++ b/lib/unicorn/http_response.rb
@@ -1,6 +1,4 @@
 # -*- encoding: binary -*-
-require 'time'
-
 # Writes a Rack response to your client using the HTTP/1.1 specification.
 # You use it by simply doing:
 #
@@ -25,7 +23,7 @@ module Unicorn::HttpResponse
 
     if headers
       buf = "HTTP/1.1 #{status}\r\n" \
-            "Date: #{Time.now.httpdate}\r\n" \
+            "Date: #{httpdate}\r\n" \
             "Status: #{status}\r\n" \
             "Connection: close\r\n"
       headers.each do |key, value|
diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb
index 7dcf977..ac549bc 100644
--- a/test/unit/test_response.rb
+++ b/test/unit/test_response.rb
@@ -7,12 +7,23 @@
 # for more information.
 
 require 'test/test_helper'
+require 'time'
 
 include Unicorn
 
 class ResponseTest < Test::Unit::TestCase
   include Unicorn::HttpResponse
 
+  def test_httpdate
+    before = Time.now.to_i
+    str = httpdate
+    assert_kind_of(String, str)
+    middle = Time.parse(str).to_i
+    after = Time.now.to_i
+    assert before <= middle
+    assert middle <= after
+  end
+
   def test_response_headers
     out = StringIO.new
     http_response_write(out,[200, {"X-Whatever" => "stuff"}, ["cool"]])