Permalink
Browse files

merged date branch - all tests pass

  • Loading branch information...
2 parents b2e1807 + c48ec0d commit 2a3db2ff01a821840deb176ad7d51773fbcd78e9 @deepfryed deepfryed committed Mar 13, 2012
View
@@ -24,7 +24,10 @@ A rational rudimentary object relational mapper.
* IdentityMap.
* Migrations.
-## Synopsis
+## Performance notes
+
+The current version creates DateTime objects for timestamp fields and this is roughly 80% slower on
+rubies older than 1.9.3.
### DB
@@ -74,7 +77,7 @@ primitive Ruby type conversion.
attribute :id, Swift::Type::Integer, serial: true, key: true
attribute :name, Swift::Type::String
attribute :email, Swift::Type::String
- attribute :updated_at, Swift::Type::Time
+ attribute :updated_at, Swift::Type::DateTime
end # User
Swift.db do |db|
@@ -276,8 +279,12 @@ http://github.com/shanna/swift/tree/master/benchmarks
#### ORM
-The following bechmarks were run on a machine with 4G ram, 5200rpm sata drive,
-Intel Core2Duo P8700 2.53GHz and stock PostgreSQL 8.4.1.
+The test environment:
+
+* ruby 1.9.3p0
+* Intel Core2Duo P8700 2.53GHz, 4G RAM and Kingston SATA2 SSD
+
+The test setup:
* 10,000 rows are created once.
* All the rows are selected once.
@@ -288,55 +295,27 @@ Intel Core2Duo P8700 2.53GHz and stock PostgreSQL 8.4.1.
the actual memory consumption might be much lower than the numbers below.
```
- ./simple.rb -n1 -r10000 -s ar -s dm -s sequel -s swift
-
- benchmark sys user total real rss
- ar #create 1.01 7.91 8.92 11.426 406.22m
- ar #select 0.02 0.31 0.33 0.378 40.69m
- ar #update 0.88 9.64 10.52 13.908 504.93m
+ ./simple.rb -n1 -r10000 -s ar -s dm -s sequel -s swift
- dm #create 0.23 3.52 3.75 5.405 211.00m
- dm #select 0.11 1.67 1.78 1.912 114.57m
- dm #update 0.54 7.34 7.88 9.453 531.30m
-
- sequel #create 0.77 4.61 5.38 8.194 235.50m
- sequel #select 0.01 0.13 0.14 0.180 12.73m
- sequel #update 0.64 4.76 5.40 7.790 229.69m
-
- swift #create 0.13 0.66 0.79 1.463 85.77m
- swift #select 0.01 0.10 0.11 0.135 8.92m
- swift #update 0.14 0.75 0.89 1.585 59.56m
-
- -- bulk insert api --
- swift #write 0.00 0.10 0.10 0.180 14.79m
-```
+ benchmark sys user total real rss
+ ar #create 0.75 7.18 7.93 10.5043 366.95m
+ ar #select 0.07 0.26 0.33 0.3680 40.71m
+ ar #update 0.96 7.92 8.88 11.7537 436.38m
+ dm #create 0.33 3.73 4.06 5.0908 245.68m
+ dm #select 0.08 1.51 1.59 1.6154 87.95m
+ dm #update 0.44 7.09 7.53 8.8685 502.77m
-#### Adapter
+ sequel #create 0.60 5.07 5.67 7.9804 236.69m
+ sequel #select 0.02 0.12 0.14 0.1778 12.75m
+ sequel #update 0.82 4.95 5.77 8.2062 230.00m
-The adapter level SELECT benchmarks without using ORM.
+ swift #create 0.27 0.59 0.86 1.5085 84.85m
+ swift #select 0.03 0.06 0.09 0.1037 11.24m
+ swift #update 0.20 0.69 0.89 1.5867 62.19m
-* Same dataset as above.
-* All rows are selected 5 times.
-* The pg benchmark uses pg_typecast gem to provide typecasting support
- for pg gem and also makes the benchmarks more fair.
-
-##### PostgreSQL
-
-```
- benchmark sys user total real rss
- do #select 0.020000 1.250000 1.270000 1.441281 71.98m
- pg #select 0.000000 0.580000 0.580000 0.769186 42.93m
- swift #select 0.040000 0.510000 0.550000 0.627581 43.23m
-```
-
-##### MySQL
-
-```
- benchmark sys user total real rss
- do #select 0.030000 1.130000 1.160000 1.172205 71.86m
- mysql2 #select 0.040000 0.660000 0.700000 0.704414 72.72m
- swift #select 0.010000 0.480000 0.490000 0.499643 42.03m
+ -- bulk insert api --
+ swift #write 0.04 0.06 0.10 0.1699 14.05m
```
## TODO
View
@@ -24,6 +24,7 @@ gem 'i18n', '0.4.1'
gem 'builder', '2.1.2'
gem 'sqlite3-ruby'
-gem 'sequel', '3.21.0'
-gem 'sequel_pg', '1.0.2'
+gem 'sequel', '3.30.0'
+gem 'sequel_pg', '1.2.0'
gem 'pg_typecast', '0.1.1'
+gem 'home_run'
View
@@ -38,15 +38,16 @@ GEM
data_objects (= 0.10.3)
do_sqlite3 (0.10.3)
data_objects (= 0.10.3)
+ home_run (1.0.6)
i18n (0.4.1)
mysql2 (0.2.6)
pg (0.10.1)
pg_typecast (0.1.1)
pg (>= 0.9.0)
- sequel (3.21.0)
- sequel_pg (1.0.2)
+ sequel (3.30.0)
+ sequel_pg (1.2.0)
pg (>= 0.8.0)
- sequel (>= 3.6.0)
+ sequel (>= 3.29.0)
sqlite3 (1.3.3)
sqlite3-ruby (1.3.3)
sqlite3 (>= 1.3.3)
@@ -69,10 +70,11 @@ DEPENDENCIES
do_mysql (>= 0.10.3)
do_postgres (>= 0.10.3)
do_sqlite3 (>= 0.10.3)
+ home_run
i18n (= 0.4.1)
mysql2 (= 0.2.6)
pg (= 0.10.1)
pg_typecast (= 0.1.1)
- sequel (= 3.21.0)
- sequel_pg (= 1.0.2)
+ sequel (= 3.30.0)
+ sequel_pg (= 1.2.0)
sqlite3-ruby
View
@@ -0,0 +1,18 @@
+#!/usr/bin/env ruby
+
+require 'swift'
+require 'benchmark'
+
+values = DATA.read.split(/\n+/) * 2000
+
+Benchmark.bm(30) do |bm|
+ bm.report("datetime - native") { 10.times { values.each {|text| DateTime.strptime(text, "%F %T.%N %z")} }}
+ bm.report("datetime - swift") { 10.times { values.each {|text| Swift::DateTime.parse(text)} }}
+end
+
+__END__
+2011-12-27 08:45:33.012 +1100
+2011-12-27 09:18:00.345 +1100
+2011-12-27 09:18:34.678 +1100
+2011-12-27 09:35:03.901 +1100
+2011-12-27 10:50:08.234 +1100
View
@@ -10,7 +10,7 @@ class User < Swift::Scheme
attribute :id, Swift::Type::Integer, serial: true, key: true
attribute :name, Swift::Type::String
attribute :email, Swift::Type::String
- attribute :updated_at, Swift::Type::Time
+ attribute :updated_at, Swift::Type::DateTime
end # User
class Runner
@@ -61,7 +61,7 @@ def run_updates
end
def run_writes
- Swift.db.execute('truncate users')
+ Swift.db.execute('truncate users') rescue Swift.db.execute('delete from users') # sqlite3
Benchmark.run('swift #write') do
stream = StringIO.new rows.times.map {|n| "test #{n}\ttest@example.com\t#{Time.now}\n" }.join('')
Swift.db.write(User, %w{name email updated_at}, stream)
View
@@ -221,7 +221,6 @@ static VALUE adapter_reconnect(VALUE self) {
@option options [String] :password ('')
@option options [String] :host ('localhost')
@option options [Integer] :port (DB default)
- @option options [String] :timezone (*nix TZ format) See http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
@return [Swift::Adapter]
@see Swift::DB
@@ -244,7 +243,6 @@ static VALUE adapter_initialize(VALUE self, VALUE options) {
rb_hash_delete(extra, ID2SYM(rb_intern("password")));
rb_hash_delete(extra, ID2SYM(rb_intern("host")));
rb_hash_delete(extra, ID2SYM(rb_intern("port")));
- rb_hash_delete(extra, ID2SYM(rb_intern("timezone")));
std::string extra_options_string = parse_extra_options(extra);
@@ -262,7 +260,6 @@ static VALUE adapter_initialize(VALUE self, VALUE options) {
CATCH_DBI_EXCEPTIONS();
rb_iv_set(self, "@options", options);
- rb_iv_set(self, "@timezone", rb_hash_aref(options, ID2SYM(rb_intern("timezone"))));
return Qnil;
}
View
@@ -0,0 +1,96 @@
+#include "datetime.h"
+
+extern VALUE dtformat;
+
+VALUE cSwiftDateTime, day_seconds;
+ID fcivil, fparse, fstrptime;
+
+// NOTE: only parses '%F %T.%N %z' format and falls back to the built-in DateTime#parse
+// and is almost 2x faster than doing:
+//
+// rb_funcall(klass, fstrptime, 2, rb_str_new(data, size), dtformat);
+//
+VALUE datetime_parse(VALUE klass, const char *data, uint64_t size) {
+ struct tm tm;
+ double seconds;
+ const char *ptr;
+ char tzsign = 0, fraction[32];
+ int tzhour = 0, tzmin = 0, lastmatch = -1, offset = 0, idx;
+
+ memset(&tm, 0, sizeof(struct tm));
+ sscanf(data, "%04d-%02d-%02d %02d:%02d:%02d%n",
+ &tm.tm_year, &tm.tm_mon, &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &lastmatch);
+
+ // fallback to default datetime parser, this is more expensive.
+ if (tm.tm_mday == 0)
+ return Qnil;
+
+ seconds = tm.tm_sec;
+
+ // parse millisecs if any -- tad faster than using %lf in sscanf above.
+ if (lastmatch > 0 && lastmatch < size && *(data + lastmatch) == '.') {
+ idx = 0;
+ ptr = data + ++lastmatch;
+ while (*ptr && isdigit(*ptr) && idx < 31) {
+ lastmatch++;
+ fraction[idx++] = *ptr++;
+ }
+
+ fraction[idx] = 0;
+ seconds += (double)atoll(fraction) / pow(10, idx);
+ }
+
+ // parse timezone offsets if any - matches +HH:MM +HH MM +HHMM
+ if (lastmatch > 0 && lastmatch < size) {
+ const char *ptr = data + lastmatch;
+ while(*ptr && *ptr != '+' && *ptr != '-') ptr++;
+ tzsign = *ptr++;
+ if (*ptr && isdigit(*ptr)) {
+ tzhour = *ptr++ - '0';
+ if (*ptr && isdigit(*ptr)) tzhour = tzhour * 10 + *ptr++ - '0';
+ while(*ptr && !isdigit(*ptr)) ptr++;
+ if (*ptr) {
+ tzmin = *ptr++ - '0';
+ if (*ptr && isdigit(*ptr)) tzmin = tzmin * 10 + *ptr++ - '0';
+ }
+ }
+ }
+
+ if (tzsign) {
+ offset = tzsign == '+'
+ ? (time_t)tzhour * 3600 + (time_t)tzmin * 60
+ : (time_t)tzhour * -3600 + (time_t)tzmin * -60;
+ }
+
+ return rb_funcall(klass, fcivil, 7,
+ INT2FIX(tm.tm_year), INT2FIX(tm.tm_mon), INT2FIX(tm.tm_mday),
+ INT2FIX(tm.tm_hour), INT2FIX(tm.tm_min), DBL2NUM(seconds),
+ offset == 0 ? INT2FIX(0) : rb_Rational(INT2FIX(offset), day_seconds)
+ );
+}
+
+VALUE rb_datetime_parse(VALUE self, VALUE string) {
+ const char *data = CSTRING(string);
+ int size = TYPE(string) == T_STRING ? RSTRING_LEN(string) : strlen(data);
+
+ if (NIL_P(string))
+ return Qnil;
+
+ VALUE datetime = datetime_parse(self, data, size);
+ return NIL_P(datetime) ? rb_call_super(1, &string) : datetime;
+}
+
+void init_swift_datetime() {
+ rb_require("date");
+
+ VALUE mSwift = rb_define_module("Swift");
+ VALUE cDateTime = CONST_GET(rb_mKernel, "DateTime");
+ cSwiftDateTime = rb_define_class_under(mSwift, "DateTime", cDateTime);
+ fcivil = rb_intern("civil");
+ fparse = rb_intern("parse");
+ fstrptime = rb_intern("strptime");
+ day_seconds = INT2FIX(86400);
+
+ rb_global_variable(&day_seconds);
+ rb_define_singleton_method(cSwiftDateTime, "parse", RUBY_METHOD_FUNC(rb_datetime_parse), 1);
+}
View
@@ -0,0 +1,12 @@
+#ifndef SWIFT_DATETIME_H
+#define SWIFT_DATETIME_H
+
+#include "swift.h"
+#include <math.h>
+
+void init_swift_datetime();
+VALUE datetime_parse(VALUE klass, const char *data, uint64_t size);
+
+extern VALUE cSwiftDateTime;
+
+#endif
View
@@ -1,8 +1,10 @@
#!/usr/bin/env ruby
require 'mkmf'
-Config::CONFIG['CC'] = 'g++'
-Config::CONFIG['CPP'] = 'g++'
+ConfigClass = defined?(RbConfig) ? RbConfig : Config
+
+ConfigClass::CONFIG['CC'] = 'g++'
+ConfigClass::CONFIG['CPP'] = 'g++'
$CFLAGS = '-fPIC -Os -I/usr/include -I/opt/local/include -I/usr/local/include'
@@ -56,5 +58,4 @@ def assert_dbicpp_version ver
exit 1 unless library_installed? 'dbic++', apt_install_hint('dbic++-dev')
assert_dbicpp_version '0.6.0'
-
create_makefile 'swift'
View
@@ -1,7 +1,9 @@
#include "query.h"
+#include <math.h>
-ID fstrftime, fto_s, fusec;
-VALUE dtformat, tzformat, utf8;
+ID fstrftime;
+VALUE dtformat, utf8;
+VALUE cDateTime;
VALUE query_execute(Query *query) {
try {
@@ -76,13 +78,8 @@ void query_bind_values(Query *query, VALUE bind_values) {
bind_value = rb_funcall(bind_value, rb_intern("read"), 0);
query->bind.push_back(dbi::PARAM_BINARY((unsigned char*)RSTRING_PTR(bind_value), RSTRING_LEN(bind_value)));
}
- // TODO convert timestamps to server timezone if @timezone is set in adapter.
- else if (rb_obj_is_kind_of(bind_value, rb_cTime)) {
+ else if (rb_obj_is_kind_of(bind_value, rb_cTime) || rb_obj_is_kind_of(bind_value, cDateTime)) {
std::string timestamp = RSTRING_PTR(rb_funcall(bind_value, fstrftime, 1, dtformat));
-
- timestamp += RSTRING_PTR(rb_funcall(rb_funcall(bind_value, fusec, 0), fto_s, 0));
- timestamp += RSTRING_PTR(rb_funcall(bind_value, fstrftime, 1, tzformat));
-
query->bind.push_back(dbi::PARAM(timestamp));
}
else {
@@ -95,14 +92,13 @@ void query_bind_values(Query *query, VALUE bind_values) {
}
void init_swift_query() {
- fstrftime = rb_intern("strftime");
- fto_s = rb_intern("to_s");
- fusec = rb_intern("usec");
- dtformat = rb_str_new2("%F %T.");
- tzformat = rb_str_new2("%z");
+ rb_require("date");
+
utf8 = rb_str_new2("UTF-8");
+ fstrftime = rb_intern("strftime");
+ dtformat = rb_str_new2("%F %T.%N %z");
+ cDateTime = CONST_GET(rb_mKernel, "DateTime");
rb_global_variable(&utf8);
- rb_global_variable(&tzformat);
rb_global_variable(&dtformat);
}
Oops, something went wrong.

0 comments on commit 2a3db2f

Please sign in to comment.