modesty 0.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.
Files changed (130) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +18 -0
  3. data/LICENSE +21 -0
  4. data/README.md +121 -0
  5. data/Rakefile +29 -0
  6. data/VERSION +1 -0
  7. data/init.rb +1 -0
  8. data/lib/modesty.rb +26 -0
  9. data/lib/modesty/api.rb +14 -0
  10. data/lib/modesty/core_ext.rb +5 -0
  11. data/lib/modesty/core_ext/array.rb +21 -0
  12. data/lib/modesty/core_ext/fixnum.rb +5 -0
  13. data/lib/modesty/core_ext/hash.rb +39 -0
  14. data/lib/modesty/core_ext/string.rb +9 -0
  15. data/lib/modesty/core_ext/symbol.rb +33 -0
  16. data/lib/modesty/datastore.rb +51 -0
  17. data/lib/modesty/datastore/redis.rb +180 -0
  18. data/lib/modesty/experiment.rb +87 -0
  19. data/lib/modesty/experiment/base.rb +47 -0
  20. data/lib/modesty/experiment/builder.rb +48 -0
  21. data/lib/modesty/experiment/console.rb +4 -0
  22. data/lib/modesty/experiment/data.rb +75 -0
  23. data/lib/modesty/experiment/interface.rb +29 -0
  24. data/lib/modesty/experiment/significance.rb +376 -0
  25. data/lib/modesty/experiment/stats.rb +163 -0
  26. data/lib/modesty/frameworks/rails.rb +27 -0
  27. data/lib/modesty/identity.rb +32 -0
  28. data/lib/modesty/load.rb +80 -0
  29. data/lib/modesty/load/load_experiments.rb +14 -0
  30. data/lib/modesty/load/load_metrics.rb +17 -0
  31. data/lib/modesty/metric.rb +56 -0
  32. data/lib/modesty/metric/base.rb +38 -0
  33. data/lib/modesty/metric/builder.rb +23 -0
  34. data/lib/modesty/metric/data.rb +133 -0
  35. data/modesty.gemspec +192 -0
  36. data/spec/core_ext_spec.rb +17 -0
  37. data/spec/experiment_spec.rb +239 -0
  38. data/spec/identity_spec.rb +161 -0
  39. data/spec/load_spec.rb +87 -0
  40. data/spec/metric_spec.rb +176 -0
  41. data/spec/rails_spec.rb +48 -0
  42. data/spec/redis_spec.rb +29 -0
  43. data/spec/significance_spec.rb +147 -0
  44. data/spec/spec.opts +1 -0
  45. data/test/myapp/config/modesty.yml +9 -0
  46. data/test/myapp/modesty/experiments/cookbook.rb +4 -0
  47. data/test/myapp/modesty/metrics/kitchen_metrics.rb +9 -0
  48. data/test/myapp/modesty/metrics/stove/burner_metrics.rb +2 -0
  49. data/vendor/.piston.yml +8 -0
  50. data/vendor/mock_redis/.gitignore +2 -0
  51. data/vendor/mock_redis/README +8 -0
  52. data/vendor/mock_redis/lib/mock_redis.rb +10 -0
  53. data/vendor/mock_redis/lib/mock_redis/hash.rb +61 -0
  54. data/vendor/mock_redis/lib/mock_redis/list.rb +6 -0
  55. data/vendor/mock_redis/lib/mock_redis/misc.rb +69 -0
  56. data/vendor/mock_redis/lib/mock_redis/set.rb +108 -0
  57. data/vendor/mock_redis/lib/mock_redis/string.rb +32 -0
  58. data/vendor/redis-rb/.gitignore +8 -0
  59. data/vendor/redis-rb/LICENSE +20 -0
  60. data/vendor/redis-rb/README.markdown +129 -0
  61. data/vendor/redis-rb/Rakefile +155 -0
  62. data/vendor/redis-rb/benchmarking/logging.rb +62 -0
  63. data/vendor/redis-rb/benchmarking/pipeline.rb +51 -0
  64. data/vendor/redis-rb/benchmarking/speed.rb +21 -0
  65. data/vendor/redis-rb/benchmarking/suite.rb +24 -0
  66. data/vendor/redis-rb/benchmarking/thread_safety.rb +38 -0
  67. data/vendor/redis-rb/benchmarking/worker.rb +71 -0
  68. data/vendor/redis-rb/examples/basic.rb +15 -0
  69. data/vendor/redis-rb/examples/dist_redis.rb +43 -0
  70. data/vendor/redis-rb/examples/incr-decr.rb +17 -0
  71. data/vendor/redis-rb/examples/list.rb +26 -0
  72. data/vendor/redis-rb/examples/pubsub.rb +31 -0
  73. data/vendor/redis-rb/examples/sets.rb +36 -0
  74. data/vendor/redis-rb/examples/unicorn/config.ru +3 -0
  75. data/vendor/redis-rb/examples/unicorn/unicorn.rb +20 -0
  76. data/vendor/redis-rb/lib/redis.rb +676 -0
  77. data/vendor/redis-rb/lib/redis/client.rb +201 -0
  78. data/vendor/redis-rb/lib/redis/compat.rb +21 -0
  79. data/vendor/redis-rb/lib/redis/connection.rb +134 -0
  80. data/vendor/redis-rb/lib/redis/distributed.rb +526 -0
  81. data/vendor/redis-rb/lib/redis/hash_ring.rb +131 -0
  82. data/vendor/redis-rb/lib/redis/pipeline.rb +13 -0
  83. data/vendor/redis-rb/lib/redis/subscribe.rb +79 -0
  84. data/vendor/redis-rb/redis.gemspec +29 -0
  85. data/vendor/redis-rb/test/commands_on_hashes_test.rb +46 -0
  86. data/vendor/redis-rb/test/commands_on_lists_test.rb +50 -0
  87. data/vendor/redis-rb/test/commands_on_sets_test.rb +78 -0
  88. data/vendor/redis-rb/test/commands_on_sorted_sets_test.rb +109 -0
  89. data/vendor/redis-rb/test/commands_on_strings_test.rb +70 -0
  90. data/vendor/redis-rb/test/commands_on_value_types_test.rb +88 -0
  91. data/vendor/redis-rb/test/connection_handling_test.rb +87 -0
  92. data/vendor/redis-rb/test/db/.gitignore +1 -0
  93. data/vendor/redis-rb/test/distributd_key_tags_test.rb +53 -0
  94. data/vendor/redis-rb/test/distributed_blocking_commands_test.rb +54 -0
  95. data/vendor/redis-rb/test/distributed_commands_on_hashes_test.rb +12 -0
  96. data/vendor/redis-rb/test/distributed_commands_on_lists_test.rb +18 -0
  97. data/vendor/redis-rb/test/distributed_commands_on_sets_test.rb +85 -0
  98. data/vendor/redis-rb/test/distributed_commands_on_strings_test.rb +50 -0
  99. data/vendor/redis-rb/test/distributed_commands_on_value_types_test.rb +73 -0
  100. data/vendor/redis-rb/test/distributed_commands_requiring_clustering_test.rb +141 -0
  101. data/vendor/redis-rb/test/distributed_connection_handling_test.rb +25 -0
  102. data/vendor/redis-rb/test/distributed_internals_test.rb +18 -0
  103. data/vendor/redis-rb/test/distributed_persistence_control_commands_test.rb +24 -0
  104. data/vendor/redis-rb/test/distributed_publish_subscribe_test.rb +90 -0
  105. data/vendor/redis-rb/test/distributed_remote_server_control_commands_test.rb +31 -0
  106. data/vendor/redis-rb/test/distributed_sorting_test.rb +21 -0
  107. data/vendor/redis-rb/test/distributed_test.rb +60 -0
  108. data/vendor/redis-rb/test/distributed_transactions_test.rb +34 -0
  109. data/vendor/redis-rb/test/encoding_test.rb +16 -0
  110. data/vendor/redis-rb/test/helper.rb +86 -0
  111. data/vendor/redis-rb/test/internals_test.rb +27 -0
  112. data/vendor/redis-rb/test/lint/hashes.rb +90 -0
  113. data/vendor/redis-rb/test/lint/internals.rb +53 -0
  114. data/vendor/redis-rb/test/lint/lists.rb +93 -0
  115. data/vendor/redis-rb/test/lint/sets.rb +66 -0
  116. data/vendor/redis-rb/test/lint/sorted_sets.rb +132 -0
  117. data/vendor/redis-rb/test/lint/strings.rb +98 -0
  118. data/vendor/redis-rb/test/lint/value_types.rb +84 -0
  119. data/vendor/redis-rb/test/persistence_control_commands_test.rb +22 -0
  120. data/vendor/redis-rb/test/pipelining_commands_test.rb +78 -0
  121. data/vendor/redis-rb/test/publish_subscribe_test.rb +151 -0
  122. data/vendor/redis-rb/test/redis_mock.rb +64 -0
  123. data/vendor/redis-rb/test/remote_server_control_commands_test.rb +56 -0
  124. data/vendor/redis-rb/test/sorting_test.rb +44 -0
  125. data/vendor/redis-rb/test/test.conf +8 -0
  126. data/vendor/redis-rb/test/thread_safety_test.rb +34 -0
  127. data/vendor/redis-rb/test/transactions_test.rb +91 -0
  128. data/vendor/redis-rb/test/unknown_commands_test.rb +14 -0
  129. data/vendor/redis-rb/test/url_param_test.rb +52 -0
  130. metadata +277 -0
@@ -0,0 +1,29 @@
1
+ require 'modesty'
2
+
3
+ describe "Real Redis" do
4
+ before :each do
5
+ Modesty.metrics.clear
6
+ Modesty.experiments.clear
7
+ end
8
+
9
+ it "can connect to redis" do
10
+ lambda { Modesty.set_store :redis }.should_not raise_error
11
+ Modesty.data.store.should be_an_instance_of Redis
12
+ lambda { Modesty.data.flushdb }.should_not raise_error
13
+ end
14
+
15
+ it "can track metrics in real redis" do
16
+ Modesty.new_metric :foo
17
+ lambda do
18
+ (1..100).each do |i|
19
+ Modesty.track! :foo, 2
20
+ Modesty.metrics[:foo].count.should == i*2
21
+ end
22
+ end.should_not raise_error
23
+ end
24
+
25
+ after :all do
26
+ Modesty.data.flushdb
27
+ Modesty.set_store :redis, :mock => true
28
+ end
29
+ end
@@ -0,0 +1,147 @@
1
+ require 'modesty'
2
+
3
+ describe "Significance" do
4
+ before :each do
5
+ Modesty.data.flushdb
6
+ Modesty.experiments.clear
7
+ Modesty.metrics.clear
8
+
9
+ @foo = Modesty.new_metric :foo
10
+ @bar = Modesty.new_metric :bar
11
+
12
+ @e = Modesty.new_experiment :baz do |e|
13
+ e.metrics :foo, :bar
14
+
15
+ e.conversion :foo_conv, :on => [:foo, :bar]
16
+ e.distribution :foo_dist, :on => :foo
17
+ end
18
+
19
+ @foo_dist = @e.stats[:foo_dist]
20
+ @foo_conv = @e.stats[:foo_conv]
21
+
22
+ Modesty.identify! 1
23
+ end
24
+
25
+ it "handles significant distribution data" do
26
+ 250.times do |uid|
27
+ @e.chooses :experiment, :for => uid
28
+ Modesty.track! :foo, rand(200), :with => {:user => uid}
29
+ end
30
+
31
+ 250.times do |uid|
32
+ uid = 251 + uid
33
+ @e.chooses :control, :for => uid
34
+ Modesty.track! :foo, rand(100), :with => {:user => uid}
35
+ end
36
+
37
+ @foo_dist.should be_a Modesty::Experiment::DistributionStat
38
+ an = @foo_dist.analysis
39
+ an.should be_a Hash
40
+ an[:control][:mean].should be_close(50, 10)
41
+ an[:experiment][:mean].should be_close(100, 20)
42
+ an[:control][:size].should == 250
43
+ an[:experiment][:size].should == 250
44
+
45
+ sig = @foo_dist.significance
46
+ sig.should be_a Float
47
+ sig.should be < 0.01
48
+ @foo_dist.should be_significant
49
+ end
50
+
51
+ it "handles insignificant distribution data" do
52
+ @e.chooses :experiment
53
+ 250.times do
54
+ Modesty.track! :foo, 1+rand(10)
55
+ end
56
+ @e.chooses :control
57
+ 250.times do
58
+ Modesty.track! :foo, 1+rand(10)
59
+ end
60
+
61
+ sig = @foo_dist.significance
62
+ sig.should be_nil
63
+ @foo_dist.should_not be_significant
64
+ end
65
+
66
+ it "handles significant conversion data" do
67
+ @e.chooses :experiment
68
+ 500.times do
69
+ Modesty.track! :foo, 1+rand(5)
70
+ Modesty.track! :bar, 100
71
+ end
72
+
73
+ @e.chooses :control
74
+ 500.times do
75
+ Modesty.track! :foo, 1+rand(100)
76
+ Modesty.track! :bar, 100
77
+ end
78
+
79
+ @foo_conv.should be_a Modesty::Experiment::ConversionStat
80
+ sig = @foo_conv.significance
81
+ sig.should_not be_nil
82
+ end
83
+ end
84
+
85
+ describe "Statistics with blocks" do
86
+ before :each do
87
+ Modesty.data.flushdb
88
+ Modesty.metrics.clear
89
+ Modesty.experiments.clear
90
+
91
+ Modesty.new_metric :foo
92
+ Modesty.new_metric :bar
93
+
94
+ @e = Modesty.new_experiment :baz do |e|
95
+ e.metrics :foo, :bar
96
+
97
+ e.distribution :special_dist do |metrics|
98
+ metrics[:foo].distribution + metrics[:bar].distribution
99
+ end
100
+
101
+ e.conversion :special_conv do |metrics|
102
+ [
103
+ metrics[:foo].unique(:users),
104
+ metrics[:foo].count
105
+ ]
106
+ end
107
+ end
108
+
109
+ (1..500).each do |i|
110
+ Modesty.identify!(i)
111
+ Modesty.group :baz
112
+ Modesty.track! :foo, 1+rand(i)
113
+ Modesty.track! :bar, 1+rand(501-i)
114
+ end
115
+
116
+ end
117
+
118
+ it "uses the blocks for distribution" do
119
+ three_days = @e.stats[:special_dist].data(3.days.ago..Date.today)
120
+ three_days.should == {
121
+ :control => (
122
+ @e.metrics(:control)[:foo].distribution(3.days.ago..Date.today).sum +
123
+ @e.metrics(:control)[:bar].distribution(3.days.ago, Date.today).sum
124
+ ),
125
+ :experiment => (
126
+ @e.metrics(:experiment)[:foo].distribution(3.days.ago, :today).sum +
127
+ @e.metrics(:experiment)[:bar].distribution(3.days.ago..Date.today).sum
128
+ )
129
+ }
130
+
131
+ three_days.should == @e.stats[:special_dist].data(3.days.ago, :today)
132
+ end
133
+
134
+ it "uses the blocks for conversion" do
135
+ three_days = @e.stats[:special_conv].data(3.days.ago..Date.today)
136
+ three_days.sort.should == [
137
+ [
138
+ @e.metrics(:experiment)[:foo].unique(:users, 3.days.ago, :today).sum,
139
+ @e.metrics(:experiment)[:foo].count(3.days.ago, Date.today).sum
140
+ ],
141
+ [
142
+ @e.metrics(:control)[:foo].unique(:users, 3.days.ago..Date.today).sum,
143
+ @e.metrics(:control)[:foo].count(3.days.ago, :today).sum
144
+ ],
145
+ ].sort
146
+ end
147
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ test:
2
+ datastore:
3
+ type: redis
4
+ port: 6379
5
+ host: localhost
6
+
7
+ paths:
8
+ experiments: experiments
9
+ metrics: metrics
@@ -0,0 +1,4 @@
1
+ Modesty.new_experiment :cookbook do |e|
2
+ e.alternatives :big, :medium, :small
3
+ e.metrics :baked_goods/:cookies, :baked_goods/:brownies, :baked_goods/:cake
4
+ end
@@ -0,0 +1,9 @@
1
+ Modesty.new_metric :baked_goods do |m|
2
+ m.description "Yummy baked things"
3
+ m.submetric :cookies
4
+ m.submetric :brownies
5
+ m.submetric :cake do |m|
6
+ m.submetric :chocolate
7
+ m.submetric :ice_cream
8
+ end
9
+ end
@@ -0,0 +1,2 @@
1
+ Modesty.new_metric :burner
2
+ Modesty.new_metric :griddle
@@ -0,0 +1,8 @@
1
+ ---
2
+ repository_url: git://github.com/ezmobius/redis-rb.git
3
+ lock: false
4
+ repository_class: Piston::Git::Repository
5
+ format: 1
6
+ handler:
7
+ commit: 283b58be1fbbed401dba36e1b9f4fa0d8be7904d
8
+ branch: master
@@ -0,0 +1,2 @@
1
+ .*.swo
2
+ .*.swp
@@ -0,0 +1,8 @@
1
+ Usage:
2
+
3
+ r = MockRedis.new(:some, :cool => :options)
4
+
5
+ now pretend r is a redis client. see github.com/ezmobius/redis-rb.
6
+ DO NOT USE IN PRODUCTION. PLZ. KTHX
7
+
8
+ This was based on a random file in assaf's "vanity", but as I extended it, it seemed like it needed its own repo.
@@ -0,0 +1,10 @@
1
+ class MockRedis
2
+ LIB_DIR = File.dirname(__FILE__)
3
+ end
4
+
5
+ $:.unshift MockRedis::LIB_DIR
6
+ require 'mock_redis/misc.rb'
7
+ require 'mock_redis/string.rb'
8
+ require 'mock_redis/set.rb'
9
+ require 'mock_redis/hash.rb'
10
+ $:.shift
@@ -0,0 +1,61 @@
1
+ class MockRedis
2
+ module HashMethods
3
+ def hset(key, hkey, val)
4
+ case h = self.hash[key]
5
+ when nil then self.hash[key] = {hkey => val.to_s}
6
+ when Hash then h[hkey] = val.to_s
7
+ else fail "Not a hash"
8
+ end
9
+ end
10
+
11
+ def hget(key, hkey)
12
+ case h = self.hash[key]
13
+ when nil then nil
14
+ when Hash then h[hkey]
15
+ else fail "Not a hash"
16
+ end
17
+ end
18
+
19
+ def hincrby(key, hkey, val)
20
+ case h = self.hash[key]
21
+ when nil then self.hash[key] = {hkey => val.to_s}
22
+ when Hash then h[hkey] = (h[hkey].to_i + val).to_s
23
+ else fail "Not a hash"
24
+ end
25
+ end
26
+
27
+ def hgetall(key)
28
+ case h = self.hash[key]
29
+ when nil then {}
30
+ when Hash then h
31
+ else fail "Not a hash"
32
+ end
33
+ end
34
+
35
+ def hkeys(key)
36
+ case h = self.hash[key]
37
+ when nil then []
38
+ when Hash then h.keys
39
+ else fail "Not a hash"
40
+ end
41
+ end
42
+
43
+ def hvals(key)
44
+ case h = self.hash[key]
45
+ when nil then []
46
+ when Hash then h.values
47
+ else fail "Not a hash"
48
+ end
49
+ end
50
+
51
+ def hlen(key)
52
+ case h = self.hash[key]
53
+ when nil then 0
54
+ when Hash then h.keys.count
55
+ else fail "Not a hash"
56
+ end
57
+ end
58
+ end
59
+
60
+ include HashMethods
61
+ end
@@ -0,0 +1,6 @@
1
+ class MockRedis
2
+ module ListMethods
3
+ end
4
+
5
+ include ListMethods
6
+ end
@@ -0,0 +1,69 @@
1
+ class MockRedis
2
+ module MiscMethods
3
+ def hash
4
+ @@hash ||= {}
5
+ end
6
+
7
+ def del(*keys)
8
+ keys.flatten.each do |key|
9
+ self.hash.delete key
10
+ end
11
+ "OK"
12
+ end
13
+
14
+ def exists(key)
15
+ self.hash.has_key?(key)
16
+ end
17
+
18
+ def type(key)
19
+ case thing = self.hash[key]
20
+ when nil then "none"
21
+ when String then "string"
22
+ when Array then "list"
23
+ when Set then "set"
24
+ when Hash then "hash"
25
+ end
26
+ end
27
+
28
+ def rename(old, new)
29
+ self.hash[new] = self.hash[old]
30
+ self.hash.delete(old)
31
+ end
32
+
33
+ def renamenx(old, new)
34
+ rename(old, new) unless exists(new)
35
+ end
36
+
37
+ def keys(pattern)
38
+ regexp = Regexp.new(pattern.split("*").map { |r| Regexp.escape(r) }.join(".*"))
39
+ self.hash.keys.select { |key| key =~ regexp }
40
+ end
41
+
42
+ def randomkey
43
+ self.hash.keys[rand(dbsize)]
44
+ end
45
+
46
+ def dbsize
47
+ self.hash.keys.count
48
+ end
49
+
50
+ def flushdb
51
+ self.hash.clear
52
+ end
53
+ alias flushall flushdb
54
+
55
+ def multi
56
+ yield if block_given?
57
+ end
58
+
59
+ def ping
60
+ "PONG"
61
+ end
62
+
63
+ def pipelined
64
+ yield
65
+ end
66
+ end
67
+
68
+ include MiscMethods
69
+ end
@@ -0,0 +1,108 @@
1
+ class MockRedis
2
+ require 'set'
3
+
4
+ module SetMethods
5
+ def sadd(key, value)
6
+ fail_unless_set(key)
7
+ value = value.to_s
8
+ case set = self.hash[key]
9
+ when nil then self.hash[key] = Set.new([value])
10
+ when Set then set.add value
11
+ end
12
+ end
13
+
14
+ def srem(key, value)
15
+ value = value.to_s
16
+ fail_unless_set(key)
17
+ case set = self.hash[key]
18
+ when nil then return
19
+ when Set then set.delete(value)
20
+ end
21
+ end
22
+
23
+ def sismember(key, value)
24
+ fail_unless_set(key)
25
+ case set = self.hash[key]
26
+ when nil then return false ; puts "no set here"
27
+ when Set then set.include?(value.to_s)
28
+ end
29
+ end
30
+
31
+ def spop(key, val)
32
+ fail_unless_set(key)
33
+ case set = self.hash[key]
34
+ when nil then nil
35
+ when Set
36
+ el = set.to_a[rand(set.size)]
37
+ set.delete el
38
+ el
39
+ end
40
+ end
41
+
42
+ def smove(src_key, dst_key, member)
43
+ fail_unless_set(dst_key)
44
+ if el = self.srem(src_key, member)
45
+ self.sadd(dst_key, member)
46
+ end
47
+ end
48
+
49
+ def srandmember(key)
50
+ fail_unless_set(key)
51
+ case set = self.hash[key]
52
+ when nil then nil
53
+ when Set then set.to_a[rand(set.size)]
54
+ end
55
+ end
56
+
57
+ def smembers(key)
58
+ fail_unless_set(key)
59
+ case set = self.hash[key]
60
+ when nil then []
61
+ when Set then set.to_a
62
+ end
63
+ end
64
+
65
+ def scard(key)
66
+ fail_unless_set(key)
67
+ case set = self.hash[key]
68
+ when nil then 0
69
+ when Set then set.size
70
+ end
71
+ end
72
+
73
+ def sinter(*keys)
74
+ keys.each { |k| fail_unless_set(k) }
75
+ return Set.new if keys.any? { |k| self.hash[k].nil? }
76
+ keys.inject do |set, key|
77
+ set & self.hash[key]
78
+ end
79
+ end
80
+
81
+ def sunion(*keys)
82
+ keys.each { |k| fail_unless_set(k) }
83
+ keys.inject(Set.new) do |set, key|
84
+ return set if self.hash[key].nil?
85
+ set | self.hash[key]
86
+ end
87
+ end
88
+
89
+ def sdiff(first, *others)
90
+ [first, *others].each { |k| fail_unless_set(k) }
91
+ others = others.map { |k| self.hash[k] || Set.new }
92
+ others.inject(first) do |memo, set|
93
+ memo - self.hash[set]
94
+ end
95
+ end
96
+
97
+ private
98
+ def is_a_set?(key)
99
+ self.hash[key].is_a?(Set) || self.hash[key].nil?
100
+ end
101
+
102
+ def fail_unless_set(key)
103
+ fail "Not a set" unless is_a_set?(key)
104
+ end
105
+ end
106
+
107
+ include SetMethods
108
+ end