vanity 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +35 -0
- data/README.rdoc +33 -6
- data/lib/vanity.rb +13 -7
- data/lib/vanity/backport.rb +43 -0
- data/lib/vanity/commands/report.rb +13 -3
- data/lib/vanity/experiment/ab_test.rb +98 -66
- data/lib/vanity/experiment/base.rb +51 -5
- data/lib/vanity/metric.rb +213 -0
- data/lib/vanity/mock_redis.rb +76 -0
- data/lib/vanity/playground.rb +78 -61
- data/lib/vanity/rails/dashboard.rb +11 -2
- data/lib/vanity/rails/helpers.rb +3 -3
- data/lib/vanity/templates/_ab_test.erb +3 -4
- data/lib/vanity/templates/_experiment.erb +4 -4
- data/lib/vanity/templates/_experiments.erb +2 -2
- data/lib/vanity/templates/_metric.erb +9 -0
- data/lib/vanity/templates/_metrics.erb +13 -0
- data/lib/vanity/templates/_report.erb +14 -3
- data/lib/vanity/templates/flot.min.js +1 -0
- data/lib/vanity/templates/jquery.min.js +19 -0
- data/lib/vanity/templates/vanity.css +16 -4
- data/lib/vanity/templates/vanity.js +96 -0
- data/test/ab_test_test.rb +159 -96
- data/test/experiment_test.rb +99 -18
- data/test/experiments/age_and_zipcode.rb +1 -0
- data/test/experiments/metrics/cheers.rb +3 -0
- data/test/experiments/metrics/signups.rb +2 -0
- data/test/experiments/metrics/yawns.rb +3 -0
- data/test/experiments/null_abc.rb +1 -0
- data/test/metric_test.rb +287 -0
- data/test/playground_test.rb +1 -80
- data/test/rails_test.rb +9 -6
- data/test/test_helper.rb +37 -6
- data/vanity.gemspec +1 -1
- data/vendor/{redis-0.1 → redis-rb}/LICENSE +0 -0
- data/vendor/{redis-0.1 → redis-rb}/README.markdown +0 -0
- data/vendor/{redis-0.1 → redis-rb}/Rakefile +0 -0
- data/vendor/redis-rb/bench.rb +44 -0
- data/vendor/redis-rb/benchmarking/suite.rb +24 -0
- data/vendor/redis-rb/benchmarking/worker.rb +71 -0
- data/vendor/redis-rb/bin/distredis +33 -0
- data/vendor/redis-rb/examples/basic.rb +16 -0
- data/vendor/redis-rb/examples/incr-decr.rb +18 -0
- data/vendor/redis-rb/examples/list.rb +26 -0
- data/vendor/redis-rb/examples/sets.rb +36 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/dist_redis.rb +0 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/hash_ring.rb +0 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/pipeline.rb +0 -2
- data/vendor/{redis-0.1 → redis-rb}/lib/redis.rb +25 -7
- data/vendor/{redis-0.1 → redis-rb}/lib/redis/raketasks.rb +0 -0
- data/vendor/redis-rb/profile.rb +22 -0
- data/vendor/redis-rb/redis-rb.gemspec +30 -0
- data/vendor/{redis-0.1 → redis-rb}/spec/redis_spec.rb +113 -0
- data/vendor/{redis-0.1 → redis-rb}/spec/spec_helper.rb +0 -0
- data/vendor/redis-rb/speed.rb +16 -0
- data/vendor/{redis-0.1 → redis-rb}/tasks/redis.tasks.rb +5 -1
- metadata +37 -14
data/test/playground_test.rb
CHANGED
@@ -1,89 +1,10 @@
|
|
1
1
|
require "test/test_helper"
|
2
2
|
|
3
|
-
class PlaygroundTest <
|
4
|
-
def setup
|
5
|
-
@namespace = "vanity:0"
|
6
|
-
end
|
3
|
+
class PlaygroundTest < Test::Unit::TestCase
|
7
4
|
|
8
5
|
def test_has_one_global_instance
|
9
6
|
assert instance = Vanity.playground
|
10
7
|
assert_equal instance, Vanity.playground
|
11
8
|
end
|
12
9
|
|
13
|
-
def test_playground_namespace
|
14
|
-
assert @namespace, Vanity.playground.namespace
|
15
|
-
end
|
16
|
-
|
17
|
-
|
18
|
-
# -- Loading experiments --
|
19
|
-
|
20
|
-
def test_fails_if_cannot_load_named_experiment
|
21
|
-
assert_raises LoadError do
|
22
|
-
experiment("Green button")
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def test_loading_experiment
|
27
|
-
Vanity.playground.load_path = Dir.tmpdir
|
28
|
-
File.open File.join(Dir.tmpdir, "green_button.rb"), "w" do |f|
|
29
|
-
f.write <<-RUBY
|
30
|
-
ab_test "Green Button" do
|
31
|
-
def xmts
|
32
|
-
"x"
|
33
|
-
end
|
34
|
-
end
|
35
|
-
RUBY
|
36
|
-
end
|
37
|
-
assert_equal "x", experiment(:green_button).xmts
|
38
|
-
end
|
39
|
-
|
40
|
-
def test_fails_if_error_loading_experiment
|
41
|
-
Vanity.playground.load_path = Dir.tmpdir
|
42
|
-
File.open File.join(Dir.tmpdir, "green_button.rb"), "w" do |f|
|
43
|
-
f.write "fail 'yawn!'"
|
44
|
-
end
|
45
|
-
assert_raises LoadError do
|
46
|
-
experiment(:green_button)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def test_complains_if_not_defined_where_expected
|
51
|
-
Vanity.playground.load_path = Dir.tmpdir
|
52
|
-
File.open File.join(Dir.tmpdir, "green_button.rb"), "w" do |f|
|
53
|
-
f.write ""
|
54
|
-
end
|
55
|
-
assert_raises LoadError do
|
56
|
-
experiment("Green button")
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def test_reloading_experiments
|
61
|
-
Vanity.playground.define(:ab, :ab_test) {}
|
62
|
-
Vanity.playground.define(:cd, :ab_test) {}
|
63
|
-
assert 2, Vanity.playground.experiments.count
|
64
|
-
Vanity.playground.reload!
|
65
|
-
assert_empty Vanity.playground.experiments
|
66
|
-
end
|
67
|
-
|
68
|
-
# -- Defining experiment --
|
69
|
-
|
70
|
-
def test_can_access_experiment_by_name_or_id
|
71
|
-
exp = Vanity.playground.define(:green_button, :ab_test) { }
|
72
|
-
assert_equal exp, experiment("Green Button")
|
73
|
-
assert_equal exp, experiment(:green_button)
|
74
|
-
end
|
75
|
-
|
76
|
-
def test_fail_when_defining_same_experiment_twice
|
77
|
-
Vanity.playground.define("Green Button", :ab_test) { }
|
78
|
-
assert_raises RuntimeError do
|
79
|
-
Vanity.playground.define("Green Button", :ab_test) { }
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def test_uses_playground_namespace_for_experiment
|
84
|
-
Vanity.playground.define(:green_button, :ab_test) { }
|
85
|
-
assert_equal "#{@namespace}:green_button", experiment(:green_button).send(:key)
|
86
|
-
assert_equal "#{@namespace}:green_button:participants", experiment(:green_button).send(:key, "participants")
|
87
|
-
end
|
88
|
-
|
89
10
|
end
|
data/test/rails_test.rb
CHANGED
@@ -4,7 +4,7 @@ class UseVanityController < ActionController::Base
|
|
4
4
|
attr_accessor :current_user
|
5
5
|
|
6
6
|
def index
|
7
|
-
render text
|
7
|
+
render :text=>ab_test(:pie_or_cake)
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
@@ -13,7 +13,10 @@ class UseVanityTest < ActionController::TestCase
|
|
13
13
|
tests UseVanityController
|
14
14
|
|
15
15
|
def setup
|
16
|
-
|
16
|
+
super
|
17
|
+
metric :sugar_high
|
18
|
+
Vanity.playground.define :pie_or_cake, :ab_test do
|
19
|
+
metrics :sugar_high
|
17
20
|
end
|
18
21
|
UseVanityController.class_eval do
|
19
22
|
use_vanity :current_user
|
@@ -29,7 +32,7 @@ class UseVanityTest < ActionController::TestCase
|
|
29
32
|
|
30
33
|
def test_vanity_cookie_default_id
|
31
34
|
get :index
|
32
|
-
|
35
|
+
assert cookies['vanity_id'] =~ /^[a-f0-9]{32}$/
|
33
36
|
end
|
34
37
|
|
35
38
|
def test_vanity_cookie_retains_id
|
@@ -45,7 +48,7 @@ class UseVanityTest < ActionController::TestCase
|
|
45
48
|
end
|
46
49
|
|
47
50
|
def test_vanity_identity_set_from_user
|
48
|
-
@controller.current_user = mock("user", id
|
51
|
+
@controller.current_user = mock("user", :id=>"user_id")
|
49
52
|
get :index
|
50
53
|
assert_equal "user_id", @controller.send(:vanity_identity)
|
51
54
|
end
|
@@ -56,7 +59,7 @@ class UseVanityTest < ActionController::TestCase
|
|
56
59
|
end
|
57
60
|
@controller.current_user = Object.new
|
58
61
|
get :index
|
59
|
-
|
62
|
+
assert cookies['vanity_id'] =~ /^[a-f0-9]{32}$/
|
60
63
|
end
|
61
64
|
|
62
65
|
def test_vanity_identity_set_with_block
|
@@ -70,7 +73,7 @@ class UseVanityTest < ActionController::TestCase
|
|
70
73
|
end
|
71
74
|
|
72
75
|
def teardown
|
76
|
+
super
|
73
77
|
UseVanityController.send(:filter_chain).clear
|
74
|
-
nuke_playground
|
75
78
|
end
|
76
79
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,35 +1,66 @@
|
|
1
1
|
$LOAD_PATH.delete_if { |path| path[/gems\/vanity-\d/] }
|
2
2
|
$LOAD_PATH.unshift File.expand_path("../lib", File.dirname(__FILE__))
|
3
3
|
RAILS_ROOT = File.expand_path("..")
|
4
|
-
require "
|
4
|
+
require "test/unit"
|
5
5
|
require "mocha"
|
6
6
|
require "action_controller"
|
7
7
|
require "action_controller/test_case"
|
8
8
|
require "initializer"
|
9
9
|
require "lib/vanity/rails"
|
10
|
-
|
10
|
+
require "timecop"
|
11
|
+
|
12
|
+
|
13
|
+
class Test::Unit::TestCase
|
14
|
+
|
15
|
+
def setup
|
16
|
+
FileUtils.mkpath "tmp/experiments/metrics"
|
17
|
+
new_playground
|
18
|
+
end
|
11
19
|
|
12
|
-
class MiniTest::Unit::TestCase
|
13
20
|
# Call this on teardown. It wipes put the playground and any state held in it
|
14
21
|
# (mostly experiments), resets vanity ID, and clears Redis of all experiments.
|
15
22
|
def nuke_playground
|
16
|
-
Vanity.playground.redis.flushdb
|
17
23
|
new_playground
|
24
|
+
Vanity.playground.redis.flushdb
|
18
25
|
end
|
19
26
|
|
20
27
|
# Call this if you need a new playground, e.g. to re-define the same experiment,
|
21
28
|
# or reload an experiment (saved by the previous playground).
|
22
29
|
def new_playground
|
23
|
-
|
30
|
+
logger = Logger.new("/dev/null") unless $VERBOSE
|
31
|
+
Vanity.playground = Vanity::Playground.new(:logger=>logger, :load_path=>"tmp/experiments", :db=>15)
|
32
|
+
Vanity.playground.mock! unless ENV["REDIS"]
|
24
33
|
end
|
25
34
|
|
35
|
+
# Defines the specified metrics (one or more names). Returns metric, or array
|
36
|
+
# of metric (if more than one argument).
|
37
|
+
def metric(*names)
|
38
|
+
metrics = names.map do |name|
|
39
|
+
id = name.to_s.downcase.gsub(/\W+/, '_').to_sym
|
40
|
+
Vanity.playground.metrics[id] ||= Vanity::Metric.new(Vanity.playground, name)
|
41
|
+
end
|
42
|
+
names.size == 1 ? metrics.first : metrics
|
43
|
+
end
|
44
|
+
|
26
45
|
def teardown
|
27
|
-
nuke_playground
|
28
46
|
Vanity.context = nil
|
47
|
+
FileUtils.rm_rf "tmp"
|
48
|
+
Vanity.playground.redis.flushdb
|
29
49
|
end
|
50
|
+
|
30
51
|
end
|
31
52
|
|
32
53
|
ActionController::Routing::Routes.draw do |map|
|
33
54
|
map.connect ':controller/:action/:id'
|
34
55
|
end
|
35
56
|
Rails.configuration = Rails::Configuration.new
|
57
|
+
|
58
|
+
class Array
|
59
|
+
# Not in Ruby 1.8.6.
|
60
|
+
unless method_defined?(:shuffle)
|
61
|
+
def shuffle
|
62
|
+
copy = clone
|
63
|
+
Array.new(size) { copy.delete_at(Kernel.rand(copy.size)) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/vanity.gemspec
CHANGED
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
$:.push File.join(File.dirname(__FILE__), 'lib')
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
times = 20000
|
6
|
+
|
7
|
+
@r = Redis.new#(:debug => true)
|
8
|
+
@r['foo'] = "The first line we sent to the server is some text"
|
9
|
+
|
10
|
+
Benchmark.bmbm do |x|
|
11
|
+
x.report("set") do
|
12
|
+
20000.times do |i|
|
13
|
+
@r["set#{i}"] = "The first line we sent to the server is some text"; @r["foo#{i}"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
x.report("set (pipelined)") do
|
18
|
+
@r.pipelined do |pipeline|
|
19
|
+
20000.times do |i|
|
20
|
+
pipeline["set_pipelined#{i}"] = "The first line we sent to the server is some text"; @r["foo#{i}"]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
x.report("push+trim") do
|
26
|
+
20000.times do |i|
|
27
|
+
@r.push_head "push_trim#{i}", i
|
28
|
+
@r.list_trim "push_trim#{i}", 0, 30
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
x.report("push+trim (pipelined)") do
|
33
|
+
@r.pipelined do |pipeline|
|
34
|
+
20000.times do |i|
|
35
|
+
pipeline.push_head "push_trim_pipelined#{i}", i
|
36
|
+
pipeline.list_trim "push_trim_pipelined#{i}", 0, 30
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
@r.keys('*').each do |k|
|
43
|
+
@r.delete k
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
def run_in_background(command)
|
4
|
+
fork { system command }
|
5
|
+
end
|
6
|
+
|
7
|
+
def with_all_segments(&block)
|
8
|
+
0.upto(9) do |segment_number|
|
9
|
+
block_size = 100000
|
10
|
+
start_index = segment_number * block_size
|
11
|
+
end_index = start_index + block_size - 1
|
12
|
+
block.call(start_index, end_index)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
#with_all_segments do |start_index, end_index|
|
17
|
+
# puts "Initializing keys from #{start_index} to #{end_index}"
|
18
|
+
# system "ruby worker.rb initialize #{start_index} #{end_index} 0"
|
19
|
+
#end
|
20
|
+
|
21
|
+
with_all_segments do |start_index, end_index|
|
22
|
+
run_in_background "ruby worker.rb write #{start_index} #{end_index} 10"
|
23
|
+
run_in_background "ruby worker.rb read #{start_index} #{end_index} 1"
|
24
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
BENCHMARK_ROOT = File.dirname(__FILE__)
|
2
|
+
REDIS_ROOT = File.join(BENCHMARK_ROOT, "..", "lib")
|
3
|
+
|
4
|
+
$: << REDIS_ROOT
|
5
|
+
require 'redis'
|
6
|
+
require 'benchmark'
|
7
|
+
|
8
|
+
def show_usage
|
9
|
+
puts <<-EOL
|
10
|
+
Usage: worker.rb [read:write] <start_index> <end_index> <sleep_msec>
|
11
|
+
EOL
|
12
|
+
end
|
13
|
+
|
14
|
+
def shift_from_argv
|
15
|
+
value = ARGV.shift
|
16
|
+
unless value
|
17
|
+
show_usage
|
18
|
+
exit -1
|
19
|
+
end
|
20
|
+
value
|
21
|
+
end
|
22
|
+
|
23
|
+
operation = shift_from_argv.to_sym
|
24
|
+
start_index = shift_from_argv.to_i
|
25
|
+
end_index = shift_from_argv.to_i
|
26
|
+
sleep_msec = shift_from_argv.to_i
|
27
|
+
sleep_duration = sleep_msec/1000.0
|
28
|
+
|
29
|
+
redis = Redis.new
|
30
|
+
|
31
|
+
case operation
|
32
|
+
when :initialize
|
33
|
+
|
34
|
+
start_index.upto(end_index) do |i|
|
35
|
+
redis[i] = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
when :clear
|
39
|
+
|
40
|
+
start_index.upto(end_index) do |i|
|
41
|
+
redis.delete(i)
|
42
|
+
end
|
43
|
+
|
44
|
+
when :read, :write
|
45
|
+
|
46
|
+
puts "Starting to #{operation} at segment #{end_index + 1}"
|
47
|
+
|
48
|
+
loop do
|
49
|
+
t1 = Time.now
|
50
|
+
start_index.upto(end_index) do |i|
|
51
|
+
case operation
|
52
|
+
when :read
|
53
|
+
redis.get(i)
|
54
|
+
when :write
|
55
|
+
redis.incr(i)
|
56
|
+
else
|
57
|
+
raise "Unknown operation: #{operation}"
|
58
|
+
end
|
59
|
+
sleep sleep_duration
|
60
|
+
end
|
61
|
+
t2 = Time.now
|
62
|
+
|
63
|
+
requests_processed = end_index - start_index
|
64
|
+
time = t2 - t1
|
65
|
+
puts "#{t2.strftime("%H:%M")} [segment #{end_index + 1}] : Processed #{requests_processed} requests in #{time} seconds - #{(requests_processed/time).round} requests/sec"
|
66
|
+
end
|
67
|
+
|
68
|
+
else
|
69
|
+
raise "Unknown operation: #{operation}"
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class RedisCluster
|
4
|
+
|
5
|
+
def initialize(opts={})
|
6
|
+
opts = {:port => 6379, :host => 'localhost', :basedir => "#{Dir.pwd}/rdsrv" }.merge(opts)
|
7
|
+
FileUtils.mkdir_p opts[:basedir]
|
8
|
+
opts[:size].times do |i|
|
9
|
+
port = opts[:port] + i
|
10
|
+
FileUtils.mkdir_p "#{opts[:basedir]}/#{port}"
|
11
|
+
File.open("#{opts[:basedir]}/#{port}.conf", 'w'){|f| f.write(make_config(port, "#{opts[:basedir]}/#{port}", "#{opts[:basedir]}/#{port}.log"))}
|
12
|
+
system(%Q{#{File.join(File.expand_path(File.dirname(__FILE__)), "../redis/redis-server #{opts[:basedir]}/#{port}.conf &" )}})
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def make_config(port=6379, data=port, logfile='stdout', loglevel='debug')
|
17
|
+
config = %Q{
|
18
|
+
timeout 300
|
19
|
+
save 900 1
|
20
|
+
save 300 10
|
21
|
+
save 60 10000
|
22
|
+
dir #{data}
|
23
|
+
loglevel #{loglevel}
|
24
|
+
logfile #{logfile}
|
25
|
+
databases 16
|
26
|
+
port #{port}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
RedisCluster.new :size => 4
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
r = Redis.new
|
5
|
+
|
6
|
+
puts
|
7
|
+
p 'incr'
|
8
|
+
r.delete 'counter'
|
9
|
+
|
10
|
+
p r.incr('counter')
|
11
|
+
p r.incr('counter')
|
12
|
+
p r.incr('counter')
|
13
|
+
|
14
|
+
puts
|
15
|
+
p 'decr'
|
16
|
+
p r.decr('counter')
|
17
|
+
p r.decr('counter')
|
18
|
+
p r.decr('counter')
|