From d100025759450dd1cbeccd1a3e44c46921bba26b Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Tue, 4 Jan 2011 17:50:51 -0800 Subject: http_response: implement httpdate in C 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. --- GNUmakefile | 2 +- ext/unicorn_http/extconf.rb | 1 + ext/unicorn_http/httpdate.c | 82 ++++++++++++++++++++++++++++++++++++++++ ext/unicorn_http/unicorn_http.rl | 3 ++ lib/unicorn/http_response.rb | 4 +- test/unit/test_response.rb | 11 ++++++ 6 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 ext/unicorn_http/httpdate.c 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 +#include +#include + +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"]]) -- cgit v1.2.3-24-ge0c7