#!/usr/bin/env ruby $: << File.join(File.dirname(__FILE__), '..', 'lib') require 'benchmark' require 'moneta' require 'fileutils' class String def random(n) (1..n).map { self[rand(size),1] }.join end end class Array def sum inject(0, &:+) end def mean sum / size end def stddev m = mean Math.sqrt(map {|s| (s - m) ** 2 }.mean) end end class MonetaBenchmarks DIR = __FILE__ + '.tmp' STORES = { # SDBM accepts only very short key/value pairs (1k for both) # SDBM: { file: "#{DIR}/sdbm" }, # YAML is too slow # YAML: { file: "#{DIR}/yaml" }, ActiveRecord: { table: 'activerecord', connection: { adapter: (defined?(JRUBY_VERSION) ? 'jdbcmysql' : 'mysql2'), username: 'root', database: 'moneta' } }, Cassandra: {}, Client: {}, Couch: {}, DBM: { file: "#{DIR}/dbm" }, DataMapper: { setup: 'mysql://root:@localhost/moneta', table: 'datamapper' }, Daybreak: { file: "#{DIR}/daybreak" }, File: { dir: "#{DIR}/file" }, GDBM: { file: "#{DIR}/gdbm" }, HBase: {}, HashFile: { dir: "#{DIR}/hashfile" }, KyotoCabinet: { file: "#{DIR}/kyotocabinet.kch" }, LRUHash: {}, LevelDB: { dir: "#{DIR}/leveldb" }, LocalMemCache: { file: "#{DIR}/lmc" }, LMDB: { dir: "#{DIR}/lmdb" }, MemcachedDalli: {}, MemcachedNative: {}, Memory: {}, MongoMoped: {}, MongoOfficial: {}, PStore: { file: "#{DIR}/pstore" }, Redis: {}, RestClient: { url: 'http://localhost:8808/' }, Riak: {}, Sequel: { table: 'sequel', db: (defined?(JRUBY_VERSION) ? 'jdbc:mysql://localhost/moneta?user=root' : 'mysql2://root:@localhost/moneta') }, Sqlite: { file: ':memory:' }, TDB: { file: "#{DIR}/tdb" }, TokyoCabinet: { file: "#{DIR}/tokyocabinet" }, TokyoTyrant: {}, } CONFIGS = { uniform_small: { runs: 3, keys: 1000, min_key_len: 1, max_key_len: 32, key_dist: :uniform, min_val_len: 0, max_val_len: 256, val_dist: :uniform }, uniform_medium: { runs: 3, keys: 1000, min_key_len: 3, max_key_len: 128, key_dist: :uniform, min_val_len: 0, max_val_len: 1024, val_dist: :uniform }, uniform_large: { runs: 3, keys: 100, min_key_len: 3, max_key_len: 128, key_dist: :uniform, min_val_len: 0, max_val_len: 10240, val_dist: :uniform }, normal_small: { runs: 3, keys: 1000, min_key_len: 1, max_key_len: 32, key_dist: :normal, min_val_len: 0, max_val_len: 256, val_dist: :normal }, normal_medium: { runs: 3, keys: 1000, min_key_len: 3, max_key_len: 128, key_dist: :normal, min_val_len: 0, max_val_len: 1024, val_dist: :normal }, normal_large: { runs: 3, keys: 100, min_key_len: 3, max_key_len: 128, key_dist: :normal, min_val_len: 0, max_val_len: 10240, val_dist: :normal }, } DICT = 'ABCDEFGHIJKLNOPQRSTUVWXYZabcdefghijklnopqrstuvwxyz123456789'.freeze HEADER = "\n Minimum Maximum Total Mean Stddev Ops/s" SEPARATOR = '=' * 77 module Rand extend self def normal_rand(mean, stddev) # Box-Muller transform theta = 2 * Math::PI * (rand(1e10) / 1e10) scale = stddev * Math.sqrt(-2 * Math.log(1 - (rand(1e10) / 1e10))) [mean + scale * Math.cos(theta), mean + scale * Math.sin(theta)] end def uniform(min, max) rand(max - min) + min end def normal(min, max) mean = (min + max) / 2 stddev = (max - min) / 4 loop do val = normal_rand(mean, stddev) return val.first if val.first >= min && val.first <= max return val.last if val.last >= min && val.last <= max end end end def parallel(&block) if defined?(JRUBY_VERSION) Thread.new(&block) else Process.fork(&block) end end def write_histogram(file, sizes) min = sizes.min delta = sizes.max - min histogram = [] sizes.each do |s| s = 10 * (s - min) / delta histogram[s] ||= 0 histogram[s] += 1 end File.open(file, 'w') do |f| histogram.each_with_index { |n,i| f.puts "#{i*delta/10+min} #{n}" } end end def start_servers parallel do begin Moneta::Server.new(Moneta.new(:Memory)).run rescue Exception => ex puts "\e[31mFailed to start Moneta server - #{ex.message}\e[0m" end end parallel do begin require 'rack' require 'webrick' require 'rack/moneta_rest' # Keep webrick quiet ::WEBrick::HTTPServer.class_eval do def access_log(config, req, res); end end ::WEBrick::BasicLog.class_eval do def log(level, data); end end Rack::Server.start(app: Rack::Builder.app do use Rack::Lint run Rack::MonetaRest.new(store: :Memory) end, environment: :none, server: :webrick, Port: 8808) rescue Exception => ex puts "\e[31mFailed to start Rack server - #{ex.message}\e[0m" end end sleep 1 # Wait for servers end def test_stores STORES.each do |name, options| begin if name == :DataMapper begin require 'dm-core' DataMapper.setup(:default, adapter: :in_memory) rescue LoadError => ex puts "\e[31mFailed to load DataMapper - #{ex.message}\e[0m" end elsif name == :Riak require 'riak' Riak.disable_list_keys_warnings = true end cache = Moneta.new(name, options.dup) cache['test'] = 'test' rescue Exception => ex puts "\e[31m#{name} not benchmarked - #{ex.message}\e[0m" STORES.delete(name) ensure (cache.close rescue nil) if cache end end end def generate_data until @data.size == @config[:keys] key = DICT.random(Rand.send(@config[:key_dist], @config[:min_key_len], @config[:max_key_len])) @data[key] = DICT.random(Rand.send(@config[:val_dist], @config[:min_val_len], @config[:max_val_len])) end key_lens, val_lens = @data.keys.map(&:size), @data.values.map(&:size) @data = @data.to_a write_histogram("#{DIR}/key.histogram", key_lens) write_histogram("#{DIR}/value.histogram", val_lens) puts "\n\e[1m\e[34m#{SEPARATOR}\n\e[34mComputing keys and values...\n\e[34m#{SEPARATOR}\e[0m" puts %{ Minimum Maximum Total Mean Stddev} puts 'Key Length % 8d % 8d % 8d % 8d % 8d' % [key_lens.min, key_lens.max, key_lens.sum, key_lens.mean, key_lens.stddev] puts 'Value Length % 8d % 8d % 8d % 8d % 8d' % [val_lens.min, val_lens.max, val_lens.sum, val_lens.mean, val_lens.stddev] end def print_config puts "\e[1m\e[36m#{SEPARATOR}\n\e[36mConfig #{@config_name}\n\e[36m#{SEPARATOR}\e[0m" @config.each do |k,v| puts '%-16s = %-10s' % [k,v] end end def print_store_stats(name) puts HEADER [:write, :read, :sum].each do |i| ops = (1000 * @config[:runs] * @data.size) / @stats[name][i].sum line = '%-17.17s %-5s % 8d % 8d % 8d % 8d % 8d % 8d' % [name, i, @stats[name][i].min, @stats[name][i].max, @stats[name][i].sum, @stats[name][i].mean, @stats[name][i].stddev, ops] @summary << [-ops, line << "\n"] if i == :sum puts line end errors = @stats[name][:error].sum if errors > 0 puts "\e[31m%-23.23s % 8d % 8d % 8d % 8d\e[0m" % ['Read errors', @stats[name][:error].min, @stats[name][:error].max, errors, errors / @config[:runs]] else puts "\e[32mNo read errors" end end def benchmark_store(name, options) puts "\n\e[1m\e[34m#{SEPARATOR}\n\e[34m#{name}\n\e[34m#{SEPARATOR}\e[0m" store = Moneta.new(name, options.dup) @stats[name] = { write: [], read: [], sum: [], error: [] } %w(Rehearse Measure).each do |type| state = '' print "%s [%#{2 * @config[:runs]}s] " % [type, state] @config[:runs].times do |run| store.clear @data.shuffle! m1 = Benchmark.measure do @data.each {|k,v| store[k] = v } end print "%s[%-#{2 * @config[:runs]}s] " % ["\b" * (2 * @config[:runs] + 3), state << 'W'] @data.shuffle! error = 0 m2 = Benchmark.measure do @data.each do |k, v| error += 1 if v != store[k] end end print "%s[%-#{2 * @config[:runs]}s] " % ["\b" * (2 * @config[:runs] + 3), state << 'R'] if type == 'Measure' @stats[name][:write] << m1.real * 1000 @stats[name][:error] << error @stats[name][:read] << m2.real * 1000 @stats[name][:sum] << (m1.real + m2.real) * 1000 end end end print_store_stats(name) rescue StandardError => ex puts "\n\e[31mFailed to benchmark #{name} - #{ex.message}\e[0m\n" ensure store.close if store end def run_benchmarks STORES.each do |name, options| benchmark_store(name, options) sleep 1 end end def print_summary puts "\n\e[1m\e[36m#{SEPARATOR}\n\e[36mSummary #{@config_name}: #{@config[:runs]} runs, #{@data.size} keys\n\e[36m#{SEPARATOR}\e[0m#{HEADER}\n" @summary.sort_by(&:first).each do |entry| puts entry.last end end def initialize(args) @config_name = args.size == 1 ? args.first.to_sym : :uniform_medium unless @config = CONFIGS[@config_name] puts "Configuration #{@config_name} not found" exit end # Disable jruby stdout pollution by memcached if defined?(JRUBY_VERSION) require 'java' properties = java.lang.System.getProperties(); properties.put('net.spy.log.LoggerImpl', 'net.spy.memcached.compat.log.SunLogger'); java.lang.System.setProperties(properties); java.util.logging.Logger.getLogger('').setLevel(java.util.logging.Level::OFF) end @stats, @data, @summary = {}, {}, [] end def run FileUtils.rm_rf(DIR) FileUtils.mkpath(DIR) start_servers test_stores print_config generate_data run_benchmarks print_summary FileUtils.rm_rf(DIR) end end MonetaBenchmarks.new(ARGV).run