$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
require 'bson'
require 'json'
require 'stringio'
require 'test/unit'
require 'benchmark'
require 'ruby-prof' unless RUBY_PLATFORM =~ /java/
class BenchTest < Test::Unit::TestCase
RESET = 'reset'
def setup
puts
@count = 10_000
@label_width = 30
end
def teardown
puts
end
def gc_allocated
gc_stat = []
GC.start
gc_stat << GC.stat
yield
GC.start
gc_stat << GC.stat
gc_stat[1][:total_allocated_object] - gc_stat[0][:total_allocated_object]
end
def print_gain_and_freed_objects(measurement, gc_stat, i)
h = Hash[*[:label, :utime, :stime, :cutime, :cstime, :real].zip(measurement[i].to_a).flatten]
h[:allocated] = gc_stat[i+1][:total_allocated_object] - gc_stat[i][:total_allocated_object]
h[:freed] = gc_stat[i+1][:total_freed_object] - gc_stat[i][:total_freed_object]
h[:base] = measurement[0].utime if i > 0
h[:gain] = 1.0 - h[:utime]/h[:base] if i > 0
[
[ "label: \"%s\"", :label ],
[ ", allocated: %d", :allocated ],
[ ", freed: %d", :freed ],
[ ", user: %.1f", :utime ],
[ ", base: %.1f", :base ],
[ ", gain: %.2f", :gain ]
].each do |format, key|
print (format % h[key]) if h[key]
end
puts
end
def benchmark_with_gc(count, method_label_pairs)
measurement = []
gc_stat = []
GC.start
gc_stat << GC.stat
method_label_pairs.each_with_index do |method_label_pair, i|
meth, label = method_label_pair
meth.call
measurement << Benchmark.measure(label) do
count.times.each_with_index {|j| yield j }
end
GC.start
gc_stat << GC.stat
print_gain_and_freed_objects(measurement, gc_stat, i)
end
reset_method = method_label_pairs.find(method_label_pairs.first){|ml| ml[2] && ml[2] == RESET}.first
reset_method.call
end
# Optimization committed --------------------------------------------------------------------------------------------
def old_array_index
Array.class_eval <<-EVAL
def to_bson(encoded = ''.force_encoding(BSON::BINARY))
encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded|
each_with_index do |value, index|
encoded << value.bson_type
index.to_s.to_bson_cstring(encoded)
value.to_bson(encoded)
end
end
end
EVAL
end
def new_array_index_optimize
Array.class_eval <<-EVAL
@@_BSON_INDEX_SIZE = 1024
@@_BSON_INDEX_ARRAY = ::Array.new(@@_BSON_INDEX_SIZE){|i| (i.to_s.force_encoding(BSON::BINARY) << BSON::NULL_BYTE).freeze}.freeze
def to_bson(encoded = ''.force_encoding(BSON::BINARY))
encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded|
each_with_index do |value, index|
encoded << value.bson_type
if index < @@_BSON_INDEX_SIZE
encoded << @@_BSON_INDEX_ARRAY[index]
else
index.to_s.to_bson_cstring(encoded)
end
value.to_bson(encoded)
end
end
end
EVAL
end
def test_array_index_optimization
size = 1024
array = Array.new(size){|i| i}
method_label_pairs = [
[ method(:old_array_index), 'Array index optimize none' ],
[ method(:new_array_index_optimize), 'Array index optimize 1024', RESET ] # Xeon user: 20.3, base: 33.2, gain: 0.39
]
benchmark_with_gc(@count, method_label_pairs) { array.to_bson }
end
def old_encode_bson_with_placeholder
BSON::Encodable.module_eval <<-EVAL
def encode_with_placeholder_and_null(adjust, encoded = ''.force_encoding(BSON::BINARY))
pos = encoded.bytesize
encoded << PLACEHOLDER
yield(encoded)
encoded << BSON::NULL_BYTE
encoded[pos, 4] = (encoded.bytesize - pos + adjust).to_bson
encoded
end
EVAL
end
def new_encode_bson_with_placeholder_v0
BSON::Encodable.module_eval <<-EVAL
def encode_with_placeholder_and_null(adjust, encoded = ''.force_encoding(BINARY))
pos = encoded.bytesize
encoded << PLACEHOLDER
yield(encoded)
encoded << BSON::NULL_BYTE
encoded[pos, 4] = (encoded.bytesize - pos + adjust).to_bson_int32('') # [ encoded.bytesize - pos ].pack('l<') #
encoded
end
EVAL
end
def new_encode_bson_with_placeholder_v1
BSON::Encodable.module_eval <<-EVAL
def encode_with_placeholder_and_null(adjust, encoded = ''.force_encoding(BINARY))
pos = encoded.bytesize
encoded << PLACEHOLDER
yield(encoded)
encoded << BSON::NULL_BYTE
encoded.setint32(pos, encoded.bytesize - pos + adjust) # [ encoded.bytesize - pos ].pack('l<') #
encoded
end
EVAL
end
def test_encode_bson_with_placeholder
size = 1
hash = Hash[*(0..size).to_a.collect{|i| [ ('a' + i.to_s), i.to_s]}.flatten]
@count = 2_000_000
method_label_pairs = [
[ method(:old_encode_bson_with_placeholder), 'Encode bson optimize to_bson' ],
[ method(:new_encode_bson_with_placeholder_v0), 'Encode bson optimize to_bson_int32' ], # user: 22.2, base: 28.5, gain: 0.22
[ method(:new_encode_bson_with_placeholder_v1), 'Encode bson optimize setint32', RESET ] # user: 22.2, base: 28.5, gain: 0.22
]
benchmark_with_gc(@count, method_label_pairs) { hash.to_bson }
end
def old_encode_string_with_placeholder
BSON::Encodable.module_eval <<-EVAL
def encode_with_placeholder_and_null(adjust, encoded = ''.force_encoding(BSON::BINARY))
pos = encoded.bytesize
encoded << PLACEHOLDER
yield(encoded)
encoded << BSON::NULL_BYTE
encoded[pos, 4] = (encoded.bytesize - pos + adjust).to_bson
encoded
end
EVAL
end
def new_encode_string_with_placeholder_v0
BSON::Encodable.module_eval <<-EVAL
def encode_with_placeholder_and_null(adjust, encoded = ''.force_encoding(BINARY))
pos = encoded.bytesize
encoded << PLACEHOLDER
yield(encoded)
encoded << BSON::NULL_BYTE
encoded[pos, 4] = (encoded.bytesize - pos + adjust).to_bson_int32('') # [ encoded.bytesize - pos - 4 ].pack('l<') #
encoded
end
EVAL
end
def new_encode_string_with_placeholder_v1
BSON::Encodable.module_eval <<-EVAL
def encode_with_placeholder_and_null(adjust, encoded = ''.force_encoding(BINARY))
pos = encoded.bytesize
encoded << PLACEHOLDER
yield(encoded)
encoded << BSON::NULL_BYTE
encoded.setint32(pos, encoded.bytesize - pos + adjust) # [ encoded.bytesize - pos - 4 ].pack('l<') #
encoded
end
EVAL
end
def test_encode_string_with_placeholder
size = 1
hash = Hash[*(0..size).to_a.collect{|i| [ ('a' + i.to_s), i.to_s]}.flatten]
@count = 2_000_000
method_label_pairs = [
[ method(:old_encode_string_with_placeholder), 'Encode string optimize to_bson' ],
[ method(:new_encode_string_with_placeholder_v0), 'Encode string optimize to_bson_int32' ], # Xeon user: 22.2, base: 27.7, gain: 0.20
[ method(:new_encode_string_with_placeholder_v1), 'Encode string optimize setint32', RESET ] # Xeon user: 22.2, base: 27.7, gain: 0.20
]
benchmark_with_gc(@count, method_label_pairs) { hash.to_bson }
end
def old_integer_to_bson
Integer.class_eval <<-EVAL
def to_bson(encoded = ''.force_encoding(BINARY))
unless bson_int64?
out_of_range!
else
bson_int32? ? to_bson_int32(encoded) : to_bson_int64(encoded)
end
end
EVAL
end
def new_integer_to_bson
Integer.class_eval <<-EVAL
def to_bson(encoded = ''.force_encoding(BINARY))
if bson_int32?
to_bson_int32(encoded)
elsif bson_int64?
to_bson_int64(encoded)
else
out_of_range!
end
end
EVAL
end
def test_integer_to_bson_optimization
size = 1024
hash = Hash[*(0..size).to_a.collect{|i| [ ('a' + i.to_s).to_sym, i]}.flatten]
method_label_pairs = [
[ method(:old_integer_to_bson), 'Integer to_bson optimize none' ],
[ method(:new_integer_to_bson), 'Integer to_bson optimize test order', RESET ]
]
benchmark_with_gc(@count, method_label_pairs) { hash.to_bson }
end
# Optimization NOT committed ----------------------------------------------------------------------------------------
def old_hash_to_bson
Hash.class_eval <<-EVAL
def to_bson(encoded = ''.force_encoding(BSON::BINARY))
encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded|
each do |field, value|
encoded << value.bson_type
field.to_bson_cstring(encoded)
value.to_bson(encoded)
end
end
end
EVAL
end
def new_hash_to_bson_v0
# if-else seems to work better than setting a variable to method
# pending - mutex
Hash.class_eval <<-EVAL
@@_memo_threshold = 65535
@@_memo_hash = Hash.new
@@_memo_mutex = Mutex.new
def _memo_set(field)
@@_memo_mutex.synchronize do
@@_memo_hash[field] = @@_memo_hash.fetch(field) { yield }
end
end
def _memo_fetch(field)
@@_memo_mutex.synchronize do
@@_memo_hash.fetch(field) { yield }
end
end
def to_bson(encoded = ''.force_encoding(BSON::BINARY))
if size < @@_memo_threshold
encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded|
each do |field, value|
encoded << value.bson_type
encoded << _memo_set(field) { field.to_bson_cstring }
value.to_bson(encoded)
end
end
else
encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded|
each do |field, value|
encoded << value.bson_type
encoded << _memo_fetch(field) { field.to_bson_cstring }
value.to_bson(encoded)
end
end
end
end
EVAL
end
def new_hash_to_bson_v1
Hash.class_eval <<-EVAL
@@_memo_hash = Hash.new
def _memo(field)
@@_memo_hash[field] = @@_memo_hash.fetch(field) { yield }
end
def to_bson(encoded = ''.force_encoding(BSON::BINARY))
encode_with_placeholder_and_null(BSON_ADJUST, encoded) do |encoded|
each do |field, value|
encoded << value.bson_type
encoded << _memo(field) { field.to_bson_cstring }
value.to_bson(encoded)
end
end
end
EVAL
end
def test_symbol_key_optimization
size = 1024
hash = Hash[*(0..size).to_a.collect{|i| [ ('a' + i.to_s).to_sym, i]}.flatten]
method_label_pairs = [
[ method(:old_hash_to_bson), 'Symbol key optimize none', RESET ],
[ method(:new_hash_to_bson_v0), 'Symbol key optimize hash key v0' ], # Xeon user: 33.4, base: 35.9, gain: 0.07
[ method(:new_hash_to_bson_v1), 'Symbol key optimize hash key v1' ] # Xeon user: 26.4, base: 35.9, gain: 0.26
]
benchmark_with_gc(@count, method_label_pairs) { hash.to_bson }
end
def test_string_key_optimization
size = 1024
hash = Hash[*(0..size).to_a.collect{|i| [ ('a' + i.to_s), i]}.flatten]
method_label_pairs = [
[ method(:old_hash_to_bson), 'Symbol key optimize none', RESET ],
[ method(:new_hash_to_bson_v0), 'Symbol key optimize hash key v0' ], # Xeon user: 34.5, base: 32.6, gain: -0.06
[ method(:new_hash_to_bson_v1), 'Symbol key optimize hash key v1' ] # Xeon user: 27.5, base: 32.6, gain: 0.15
]
benchmark_with_gc(@count, method_label_pairs) { hash.to_bson }
end
# Discarded as not worthy -------------------------------------------------------------------------------------------
def old_hash_from_bson
BSON::Hash.class_eval <<-EVAL
def from_bson(bson)
hash = new
bson.read(4) # Swallow the first four bytes.
while (type = bson.readbyte.chr) != NULL_BYTE
field = bson.gets(NULL_BYTE).from_bson_string.chop!
hash[field] = BSON::Registry.get(type).from_bson(bson)
end
hash
end
EVAL
end
def new_hash_from_bson
BSON::Hash.class_eval <<-EVAL
def from_bson(bson)
hash = new
bson.seek(4, IO::SEEK_CUR) # Swallow the first four bytes.
while (type = bson.readbyte.chr) != NULL_BYTE
field = bson.gets(NULL_BYTE).from_bson_string.chop!
hash[field] = BSON::Registry.get(type).from_bson(bson)
end
hash
end
EVAL
end
def test_seek
size = 1
hash = Hash[*(0..size).to_a.collect{|i| [ ('a' + i.to_s), i.to_s]}.flatten]
@count = 2_000_000
method_label_pairs = [
[ method(:old_hash_from_bson), 'Encode bson optimize none', RESET ],
[ method(:new_hash_from_bson), 'Encode bson optimize seek' ] # Xeon user: 28.2, base: 28.3, gain: 0.00
]
benchmark_with_gc(@count, method_label_pairs) { hash.to_bson }
end
def old_integer_bson_int32?
Integer.class_eval <<-EVAL
def bson_int32?
(MIN_32BIT <= self) && (self <= MAX_32BIT)
end
EVAL
end
def new_integer_bson_int32?
Integer.class_eval <<-EVAL
@@FIXNUM_HIGHBITS32 = (-1 << 32)
def bson_int32?
(self & @@FIXNUM_HIGHBITS32) == 0
end
EVAL
end
def test_bson_int32?
count = 100_000_000
method_label_pairs = [
[ method(:integer_bson_int32?), 'Integer#bson_int32? old', RESET ],
[ method(:new_integer_bson_int32?), 'Integer#bson_int32? new' ] # user: 34.9, base: 21.4, gain: -0.63
]
benchmark_with_gc(count, method_label_pairs) {|i| i.bson_int32? }
end
# Ruby-prof profiling -----------------------------------------------------------------------------------------------
def doc_stats(tally, obj)
tally[obj.class.name] += 1
case obj.class.name
when 'Array'; obj.each {|elem| doc_stats(tally, elem) }
when 'FalseClass'; return
when 'Fixnum'; return
when 'Float'; return
when 'Hash'; obj.each {|elem| doc_stats(tally, elem) }
when 'NilClass'; return
when 'String'; return
when 'TrueClass'; return
else p obj.class; exit
end
end
def test_doc_stats
json_filename = '../../../training/data/sampledata/twitter.json'
line_limit = 10_000
twitter = nil
File.open(json_filename, 'r') do |f|
twitter = line_limit.times.collect { JSON.parse(f.gets) }
end
tally = Hash.new(0)
doc_stats(tally, twitter)
tally = tally.to_a.sort{|a,b| b[1] <=> a[1]}
pp tally
obj_count = tally.inject(0){|sum, elem| sum + elem[1]}
puts "objects: #{obj_count}"
puts "objects/doc: #{obj_count/line_limit}"
end
def test_encode_ruby_prof
json_filename = '../../../training/data/sampledata/twitter.json'
line_limit = 10_000
twitter = nil
File.open(json_filename, 'r') do |f|
twitter = line_limit.times.collect { JSON.parse(f.gets) }
end
profile = nil
allocated = nil
Benchmark.bm(@label_width) do |bench|
bench.report('test encode ruby prof') do
allocated = gc_allocated do
RubyProf.start
twitter.each {|doc| doc.to_bson }
profile = RubyProf.stop
end
end
end
puts "allocated: #{allocated} allocated/line: #{allocated/line_limit}"
File.open('encode-ruby-prof.out', 'w') do |f|
RubyProf::FlatPrinter.new(profile).print(f)
RubyProf::GraphPrinter.new(profile).print(f, {})
end
end
def test_decode_ruby_prof
json_filename = '../../../training/data/sampledata/twitter.json'
line_limit = 10_000
twitter = nil
File.open(json_filename, 'r') do |f|
twitter = line_limit.times.collect { StringIO.new(JSON.parse(f.gets).to_bson) }
end
profile = nil
allocated = nil
Benchmark.bm(@label_width) do |bench|
bench.report('test decode ruby prof') do
allocated = gc_allocated do
RubyProf.start
twitter.each {|io| io.rewind; Hash.from_bson(io) }
profile = RubyProf.stop
end
end
end
puts "allocated: #{allocated} allocated/line: #{allocated/line_limit}"
File.open('decode-ruby-prof.out', 'w') do |f|
RubyProf::FlatPrinter.new(profile).print(f)
RubyProf::GraphPrinter.new(profile).print(f, {})
end
end
end