Posted by Bryan
on February 20, 2008
It took me a while to figure out a good way to approach this, so thought I’d share. I’ve been wondering how to best write test code for a Rails plugin that deals with ActiveRecord models, and after trying a few different approaches, I decided to do the thing I should have done in the first place: look at the test code for the plugins in Rails core. The acts_as_tree test code is what I ended up using as a guide for my tests for MultiFieldDate. This approach uses SQLite as an in-memory database, so you do have to have SQLite3 and the sqlite3-ruby gem installed for this to work. Alternatively, this should work with other database engines, but it would probably be a good idea to use table prefixes in those cases to avoid conflicts with existing tables.
Here’s a snippet of the MultiFieldDate test code:
require 'test/unit'
require 'rubygems'
require 'active_record'
$:.unshift File.dirname(__FILE__) + '/../lib'
require File.dirname(__FILE__) + '/../init'
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
# AR keeps printing annoying schema statements
$stdout = StringIO.new
def setup_db
ActiveRecord::Base.logger
ActiveRecord::Schema.define(:version => 1) do
create_table :people do |t|
t.column :name, :string
t.column :birth_date, :date
t.column :birth_year, :integer
t.column :birth_month, :integer
t.column :birth_day, :integer
end
end
end
def teardown_db
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.drop_table(table)
end
end
class Person < ActiveRecord::Base
multi_field_date :birth, :date_field => 'birth_date', :year_field => 'birth_year',
:month_field => 'birth_month', :day_field => 'birth_day'
end
class PersonTest < Test::Unit::TestCase
def setup
setup_db
end
def teardown
teardown_db
end
def test_create_without_birth_date
person = Person.new(:name => 'Joe Jones')
person.save
assert(person.valid?, true)
end
#...
end
Posted by Bryan
on February 16, 2008
Here’s a couple one-liner scripts to help convert your Rails app to 2.0. These scripts are by no means perfect, but should help if you have a lot of files with “start_form_tag”, “end_form_tag”, or “:post => true”. Back your files up first, of course.
find . |grep -i "\.erb$" | xargs ruby -p -i -e "gsub(/<%=.*start_form_tag(.*)%>/i, '<% form_tag \1 do -%>')"
find . |grep -i "\.erb$" | xargs ruby -p -i -e "gsub(/<%.*end_form_tag.*%>/i, '<% end #form_tag-%>')"
find . |grep -i "\.erb$" | xargs ruby -p -i -e 'gsub(/:post\s=>\strue/i, ":method\s=>\s:post")'
Replace “erb” with “rtml” if you haven’t converted your views to .erb yet.
Posted by Bryan
on February 06, 2008
A couple people posted responses to the benchmarks on my Sun blog, so here they are.
From Susan Potter:
Bryan,
I wrote a benchmarking blog post in October last year relating to the difference in code-level optimizations between Ruby 1.8.6 and Ruby 1.9.0 and how the roles were reversed in these two versions: (link)
And her direct response:
This is a response to Bryan Donovan’s blog post called ways to pass options.
Bryan’s original benchmark was run on Ruby 1.8.4. I will be running the benchmarks on Ruby 1.8.6 and Ruby 1.9 to demonstrate performance reversal of code level optimizations between Ruby versions (as I did in Ruby performance reversal benchmark 1.8 to 1.9).
I will use Bryan’s Ruby benchmark code to run for both Ruby 1.8.6 and Ruby 1.9.0 (official).
Ruby 1.8.6 Results
Below are the results of running the options passing benchmark in Ruby 1.8.6:
With no option values passed
Rehearsal -------------------------------------------
delete: 0.290000 0.050000 0.340000 ( 0.428564)
merge: 0.700000 0.190000 0.890000 ( 1.017415)
or_nil: 0.370000 0.050000 0.420000 ( 0.509209)
---------------------------------- total: 1.650000sec
user system total real
delete: 0.300000 0.050000 0.350000 ( 0.428401)
merge: 0.760000 0.130000 0.890000 ( 1.010474)
or_nil: 0.360000 0.050000 0.410000 ( 0.490170)
With some option values passed
Rehearsal -------------------------------------------
delete: 0.360000 0.060000 0.420000 ( 0.505094)
merge: 0.890000 0.130000 1.020000 ( 1.150165)
or_nil: 0.390000 0.070000 0.460000 ( 0.563713)
---------------------------------- total: 1.900000sec
user system total real
delete: 0.340000 0.060000 0.400000 ( 0.489191)
merge: 0.920000 0.110000 1.030000 ( 1.149708)
or_nil: 0.350000 0.090000 0.440000 ( 0.528287)
With all option values passed
Rehearsal -------------------------------------------
delete: 0.420000 0.050000 0.470000 ( 0.547446)
merge: 0.970000 0.130000 1.100000 ( 1.228093)
or_nil: 0.420000 0.050000 0.470000 ( 0.574593)
---------------------------------- total: 2.040000sec
user system total real
delete: 0.400000 0.050000 0.450000 ( 0.485888)
merge: 0.960000 0.140000 1.100000 ( 1.237509)
or_nil: 0.400000 0.060000 0.460000 ( 0.536063)
Ruby 1.9.0 Results
Below are the results of running the options passing benchmark in Ruby 1.9.0:
With no option values passed
Rehearsal -------------------------------------------
delete: 0.150000 0.000000 0.150000 ( 0.230391)
merge: 0.540000 0.010000 0.550000 ( 0.645735)
or_nil: 0.120000 0.000000 0.120000 ( 0.188468)
---------------------------------- total: 0.820000sec
user system total real
delete: 0.160000 0.000000 0.160000 ( 0.238828)
merge: 0.520000 0.000000 0.520000 ( 0.610940)
or_nil: 0.120000 0.000000 0.120000 ( 0.180744)
With some option values passed
Rehearsal -------------------------------------------
delete: 0.220000 0.000000 0.220000 ( 0.294110)
merge: 0.690000 0.010000 0.700000 ( 0.803382)
or_nil: 0.220000 0.000000 0.220000 ( 0.297869)
---------------------------------- total: 1.140000sec
user system total real
delete: 0.210000 0.000000 0.210000 ( 0.283983)
merge: 0.700000 0.000000 0.700000 ( 0.802766)
or_nil: 0.230000 0.000000 0.230000 ( 0.295433)
With all option values passed
Rehearsal -------------------------------------------
delete: 0.270000 0.010000 0.280000 ( 0.348052)
merge: 0.770000 0.040000 0.810000 ( 0.947252)
or_nil: 0.240000 0.010000 0.250000 ( 0.336790)
---------------------------------- total: 1.340000sec
user system total real
delete: 0.280000 0.000000 0.280000 ( 0.660810)
merge: 0.750000 0.000000 0.750000 ( 1.618925)
or_nil: 0.240000 0.000000 0.240000 ( 0.478159)
In Ruby 1.8.6 the delete code is consistently more efficient, whereas in Ruby 1.9.0 the or_nil code is mostly more efficient, except for the With some option values passed scenario.
This once again shows that when optimizing performance on the code level, you need to be careful that you justify your micro-optimizations before you create code spaghetti just “because….”. Readability and maintainability is most important and when needed, only then should you optimize.
Charles Oliver Nutter’s Response
Charles Oliver Nutter (of JRuby) responded as well:
Stumbled on your blog post and ran the same numbers in JRuby on soylatte Java 6 on basically the same machine. My numbers for Ruby 1.8.6p111 were roughly the same as yours.
Here’s Ruby:
user system total real
delete: 0.200000 0.000000 0.200000 ( 0.207049)
merge: 0.710000 0.010000 0.720000 ( 0.720514)
or_nil: 0.260000 0.000000 0.260000 ( 0.256742)
With some option values passed
user system total real
delete: 0.250000 0.000000 0.250000 ( 0.260502)
merge: 0.810000 0.010000 0.820000 ( 0.818434)
or_nil: 0.290000 0.000000 0.290000 ( 0.294220)
With all option values passed
user system total real
delete: 0.320000 0.000000 0.320000 ( 0.319335)
merge: 0.890000 0.010000 0.900000 ( 0.896330)
or_nil: 0.330000 0.000000 0.330000 ( 0.336673)
And here’s JRuby:
With no option values passed
user system total real
delete: 0.133000 0.000000 0.133000 ( 0.133000)
merge: 0.318000 0.000000 0.318000 ( 0.319000)
or_nil: 0.474000 0.000000 0.474000 ( 0.474000)
With some option values passed
user system total real
delete: 0.170000 0.000000 0.170000 ( 0.169000)
merge: 0.295000 0.000000 0.295000 ( 0.295000)
or_nil: 0.145000 0.000000 0.145000 ( 0.146000)
With all option values passed
user system total real
delete: 0.154000 0.000000 0.154000 ( 0.154000)
merge: 0.289000 0.000000 0.289000 ( 0.288000)
or_nil: 0.101000 0.000000 0.101000 ( 0.100000)
Looks like something’s artificially slowing down the “or_nil” case in the first scenario, but the rest of the numbers look pretty solid.
Interesting results all around. I’m not going to lose sleep over the milliseconds I’m saving/losing with various approaches, so I’ll likely use the ops = ops[:foo] || ‘default’ approach for readability.
Posted by Bryan
on February 05, 2008
(reposting from my Sun blog)
I’ve wondered before what the fastest way is to pass a hash of options to a method in Ruby.. so today I benchmarked a few methods I’ve used in the past.
I’ve seen three main ways of passing an options hash to a method and extracting the options or use default values if they weren’t passed in:
- Merge the options with a hash of defaults, then assign values to local variables (or just use the options hash directly within the method)
- Use something like var = options[:var] || ‘default’
- Use the delete method on the hash, e.g., var = options.delete(:var) || ‘default’
From what I can tell, using delete is the fastest:
require 'benchmark'
def ops_delete(ops={})
a = ops.delete(:a) || 11
b = ops.delete(:b) || 22
c = ops.delete(:c) || 33
d = ops.delete(:d) || 44
end
def ops_merge(ops={})
ops = {:a => 11, :b => 22, :c => 33, :d => 44}.merge(ops)
a = ops[:a]
b = ops[:b]
c = ops[:c]
d = ops[:d]
end
def ops_or(ops={})
a = ops[:a] || 11
b = ops[:b] || 22
c = ops[:c] || 33
d = ops[:d] || 44
end
n = 100000
puts "With no option values passed"
Benchmark.bmbm(7) do |x|
x.report("delete:") { n.times do ops_delete; end }
x.report("merge:") { n.times do ops_merge; end }
x.report("ops_or:") { n.times do ops_or; end }
end
puts
puts "With some option values passed"
Benchmark.bmbm(7) do |x|
x.report("delete:") { n.times do ops_delete(:a => 5, :c => 2); end }
x.report("merge:") { n.times do ops_merge(:a => 5, :c => 2); end }
x.report("ops_or:") { n.times do ops_or(:a => 5, :c => 2); end }
end
puts
puts "With all option values passed"
Benchmark.bmbm(7) do |x|
x.report("delete:") { n.times do ops_delete(:a => 5, :b => 1, :c => 2, :d => 4); end }
x.report("merge:") { n.times do ops_merge(:a => 5, :b => 1, :c => 2, :d => 4); end }
x.report("ops_or:") { n.times do ops_or(:a => 5, :b => 1, :c => 2, :d => 4); end }
end
Results (rehearsals omitted):
With no option values passed
user system total real
delete: 0.210000 0.000000 0.210000 ( 0.209877)
merge: 0.720000 0.000000 0.720000 ( 0.750394)
ops_or: 0.260000 0.000000 0.260000 ( 0.258416)
With some option values passed
user system total real
delete: 0.250000 0.000000 0.250000 ( 0.255536)
merge: 0.830000 0.000000 0.830000 ( 0.831358)
ops_or: 0.310000 0.000000 0.310000 ( 0.314737)
With all option values passed
user system total real
delete: 0.310000 0.000000 0.310000 ( 0.306889)
merge: 0.900000 0.000000 0.900000 ( 0.905261)
ops_or: 0.340000 0.000000 0.340000 ( 0.348061)
This was done in Ruby 1.8.4 on a 2GHz MacBook Intel Core Duo with 1GB 667 MHz DDR2 SDRAM.