vanity 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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')
|