worker_roulette 0.0.10 → 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/worker_roulette/a_foreman.rb +32 -14
- data/lib/worker_roulette/a_tradesman.rb +27 -21
- data/lib/worker_roulette/lua.rb +31 -0
- data/lib/worker_roulette/version.rb +1 -1
- data/lib/worker_roulette.rb +1 -0
- data/spec/benchmark/perf_test.rb +70 -24
- data/spec/integration/evented_worker_roulette_spec.rb +65 -35
- data/spec/spec_helper.rb +2 -0
- metadata +3 -2
@@ -2,21 +2,39 @@ require_relative './foreman'
|
|
2
2
|
module WorkerRoulette
|
3
3
|
class AForeman < Foreman
|
4
4
|
def enqueue_work_order_without_headers(work_order, &callback)
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
5
|
+
Lua.call(self.class.lua_enqueue_work_orders, [COUNTER_KEY, job_board_key, sender_key, @channel],
|
6
|
+
[@sender, Oj.dump(work_order), WorkerRoulette::JOB_NOTIFICATIONS], &callback)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def self.lua_enqueue_work_orders
|
11
|
+
<<-HERE
|
12
|
+
local counter_key = KEYS[1]
|
13
|
+
local job_board_key = KEYS[2]
|
14
|
+
local sender_key = KEYS[3]
|
15
|
+
local channel = KEYS[4]
|
16
|
+
|
17
|
+
local sender = ARGV[1]
|
18
|
+
local work_order = ARGV[2]
|
19
|
+
local job_notification = ARGV[3]
|
20
|
+
|
21
|
+
local function enqueue_work_orders(sender, work_order, job_notification)
|
22
|
+
local result = sender .. ' updated'
|
23
|
+
local sender_on_job_board = redis.call('ZSCORE', job_board_key, sender_key)
|
24
|
+
|
25
|
+
if (sender_on_job_board == false) then
|
26
|
+
local count = redis.call('INCR', counter_key)
|
27
|
+
local job_added = redis.call('ZADD',job_board_key, count, sender_key)
|
28
|
+
result = sender .. ' added'
|
29
|
+
end
|
30
|
+
|
31
|
+
local work_added = redis.call('RPUSH',sender_key, work_order)
|
32
|
+
local job_board_update = redis.call('PUBLISH', channel, job_notification)
|
33
|
+
return result
|
18
34
|
end
|
19
|
-
|
35
|
+
|
36
|
+
return enqueue_work_orders(sender, work_order, job_notification)
|
37
|
+
HERE
|
20
38
|
end
|
21
39
|
end
|
22
40
|
end
|
@@ -9,27 +9,12 @@ module WorkerRoulette
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def work_orders!(&callback)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
redis.multi
|
16
|
-
redis.lrange(sender_key, 0, -1)
|
17
|
-
redis.del(sender_key)
|
18
|
-
redis.zrem(job_board_key, sender_key)
|
19
|
-
redis.exec do |work_orders|
|
20
|
-
callback.call ((work_orders || []).first || []).map {|work_order| Oj.load(work_order)} if callback
|
21
|
-
end
|
22
|
-
end
|
12
|
+
Lua.call(self.class.lua_drain_work_orders, [job_board_key, nil], [@namespace]) do |results|
|
13
|
+
@sender = (results.first || '').split(':').first
|
14
|
+
callback.call (results[1] || []).map {|work_order| Oj.load(work_order)} if callback
|
23
15
|
end
|
24
16
|
end
|
25
17
|
|
26
|
-
def get_lock(redis, sender, timeout, on_failure = nil, &on_success)
|
27
|
-
@lock = EM::Hiredis::Lock.new(redis, sender, timeout)
|
28
|
-
@lock.callback &on_success
|
29
|
-
@lock.errback &(on_failure || proc {})
|
30
|
-
@lock
|
31
|
-
end
|
32
|
-
|
33
18
|
def unsubscribe(&callback)
|
34
19
|
deferable = @redis_pubsub.unsubscribe(@channel)
|
35
20
|
deferable.callback do
|
@@ -44,9 +29,30 @@ module WorkerRoulette
|
|
44
29
|
end
|
45
30
|
end
|
46
31
|
|
47
|
-
|
48
|
-
def
|
49
|
-
|
32
|
+
private
|
33
|
+
def self.lua_drain_work_orders
|
34
|
+
<<-HERE
|
35
|
+
local job_board_key = KEYS[1]
|
36
|
+
local empty = KEYS[2]
|
37
|
+
local namespace = ARGV[1]
|
38
|
+
|
39
|
+
local function drain_work_orders(job_board_key, namespace)
|
40
|
+
local sender_key = redis.call('ZRANGE', job_board_key, 0, 0)[1]
|
41
|
+
|
42
|
+
if sender_key == false then
|
43
|
+
return {}
|
44
|
+
end
|
45
|
+
|
46
|
+
local results = {}
|
47
|
+
results[1] = sender_key
|
48
|
+
results[2] = redis.call('LRANGE', sender_key, 0, -1)
|
49
|
+
results[3] = redis.call('DEL', sender_key)
|
50
|
+
results[4] = redis.call('ZREM', job_board_key, sender_key)
|
51
|
+
return results
|
52
|
+
end
|
53
|
+
|
54
|
+
return drain_work_orders(job_board_key, namespace)
|
55
|
+
HERE
|
50
56
|
end
|
51
57
|
end
|
52
58
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module WorkerRoulette
|
2
|
+
module Lua
|
3
|
+
@cache = Hash.new
|
4
|
+
|
5
|
+
def self.call(lua_script, keys_accessed = [], args = [], &callback)
|
6
|
+
WorkerRoulette.tradesman_connection_pool.with do |redis|
|
7
|
+
results = redis.evalsha(sha(lua_script), keys_accessed.length, *keys_accessed, *args)
|
8
|
+
results.callback &callback
|
9
|
+
results.errback {self.eval(redis, lua_script, keys_accessed, args, &callback)}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.sha(lua_script)
|
14
|
+
@cache[lua_script] ||= Digest::SHA1.hexdigest(lua_script)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.cache
|
18
|
+
@cache.dup
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.clear_cache!
|
22
|
+
@cache = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.eval(redis, lua_script, keys_accessed, args, &callback)
|
26
|
+
results = redis.eval(lua_script, keys_accessed.size, *keys_accessed, *args)
|
27
|
+
results.callback &callback
|
28
|
+
results.errback {|err_msg| raise EM::Hiredis::RedisError.new(err_msg)}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/worker_roulette.rb
CHANGED
data/spec/benchmark/perf_test.rb
CHANGED
@@ -3,48 +3,94 @@ require 'benchmark'
|
|
3
3
|
require 'eventmachine'
|
4
4
|
|
5
5
|
REDIS_CONNECTION_POOL_SIZE = 100
|
6
|
-
ITERATIONS =
|
6
|
+
ITERATIONS = 100_000
|
7
7
|
|
8
8
|
work_order = {'ding dong' => "hello_foreman_" * 100}
|
9
9
|
|
10
|
-
WorkerRoulette.start(size: REDIS_CONNECTION_POOL_SIZE)#{driver: :synchrony}
|
11
|
-
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
10
|
+
# WorkerRoulette.start(size: REDIS_CONNECTION_POOL_SIZE, evented: true)#{driver: :synchrony}
|
11
|
+
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
12
12
|
|
13
|
-
puts "Redis Connection Pool Size: #{REDIS_CONNECTION_POOL_SIZE}"
|
13
|
+
# puts "Redis Connection Pool Size: #{REDIS_CONNECTION_POOL_SIZE}"
|
14
|
+
|
15
|
+
# Benchmark.bmbm do |x|
|
16
|
+
# x.report "Time to insert and read #{ITERATIONS} large work_orders" do # ~2500 work_orders / second round trip; 50-50 read-write time; CPU and IO bound
|
17
|
+
# ITERATIONS.times do |iteration|
|
18
|
+
# sender = 'sender_' + iteration.to_s
|
19
|
+
# foreman = WorkerRoulette.foreman(sender)
|
20
|
+
# foreman.enqueue_work_order(work_order)
|
21
|
+
# end
|
22
|
+
|
23
|
+
# ITERATIONS.times do |iteration|
|
24
|
+
# sender = 'sender_' + iteration.to_s
|
25
|
+
# tradesman = WorkerRoulette.tradesman
|
26
|
+
# tradesman.work_orders!
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
|
31
|
+
EM::Hiredis.reconnect_timeout = 0.01
|
14
32
|
|
15
33
|
Benchmark.bmbm do |x|
|
16
|
-
x.report "Time to insert and read #{ITERATIONS} large work_orders" do # ~2500 work_orders / second round trip; 50-50 read-write time; CPU and IO bound
|
17
|
-
|
18
|
-
sender = 'sender_' + iteration.to_s
|
19
|
-
foreman = WorkerRoulette.foreman(sender)
|
20
|
-
foreman.enqueue_work_order(work_order)
|
21
|
-
end
|
34
|
+
x.report "Time to evently insert and read #{ITERATIONS} large work_orders" do # ~2500 work_orders / second round trip; 50-50 read-write time; CPU and IO bound
|
35
|
+
EM.run do
|
22
36
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
tradesman.
|
37
|
+
WorkerRoulette.start(evented: true)#{driver: :synchrony}
|
38
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
39
|
+
@total = 0
|
40
|
+
@tradesman = WorkerRoulette.a_tradesman
|
41
|
+
|
42
|
+
ITERATIONS.times do |iteration|
|
43
|
+
sender = 'sender_' + iteration.to_s
|
44
|
+
foreman = WorkerRoulette.a_foreman(sender)
|
45
|
+
foreman.enqueue_work_order(work_order) do
|
46
|
+
@tradesman.work_orders! do
|
47
|
+
@total += 1
|
48
|
+
EM.stop if @total == (ITERATIONS - 1)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
27
52
|
end
|
28
53
|
end
|
29
54
|
end
|
30
55
|
|
31
|
-
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
32
|
-
|
33
56
|
Benchmark.bmbm do |x|
|
34
|
-
x.report "Time
|
35
|
-
|
36
|
-
|
57
|
+
x.report "Time to evently pubsub insert and read #{ITERATIONS} large work_orders" do # ~2500 work_orders / second round trip; 50-50 read-write time; CPU and IO bound
|
58
|
+
EM.run do
|
59
|
+
@processed = 0
|
60
|
+
@total = 0
|
61
|
+
WorkerRoulette.start(evented: true)#{driver: :synchrony}
|
62
|
+
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
63
|
+
@total = 0
|
64
|
+
@tradesman = WorkerRoulette.a_tradesman
|
65
|
+
on_subscribe = ->(*args) do
|
66
|
+
ITERATIONS.times do |iteration|
|
37
67
|
sender = 'sender_' + iteration.to_s
|
38
|
-
foreman = WorkerRoulette.
|
68
|
+
foreman = WorkerRoulette.a_foreman(sender)
|
39
69
|
foreman.enqueue_work_order(work_order)
|
40
70
|
end
|
41
|
-
|
42
|
-
tradesman.wait_for_work_orders(
|
71
|
+
end
|
72
|
+
@tradesman.wait_for_work_orders(on_subscribe) {@processed += 1; EM.stop if @processed == (ITERATIONS - 1)}
|
43
73
|
end
|
44
74
|
end
|
45
75
|
end
|
46
76
|
|
47
|
-
WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
77
|
+
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
78
|
+
|
79
|
+
# Benchmark.bmbm do |x|
|
80
|
+
# x.report "Time for tradesmans to enqueue_work_order and read #{ITERATIONS} large work_orders via pubsub" do # ~1800 work_orders / second round trip
|
81
|
+
# ITERATIONS.times do |iteration|
|
82
|
+
# p = -> do
|
83
|
+
# sender = 'sender_' + iteration.to_s
|
84
|
+
# foreman = WorkerRoulette.foreman(sender)
|
85
|
+
# foreman.enqueue_work_order(work_order)
|
86
|
+
# end
|
87
|
+
# tradesman = WorkerRoulette.tradesman
|
88
|
+
# tradesman.wait_for_work_orders(p) {|m| m; tradesman.unsubscribe}
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
|
93
|
+
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
48
94
|
|
49
95
|
# EM.run do
|
50
96
|
# EM.add_timer(6) {puts "em off";EM.stop}
|
@@ -63,4 +109,4 @@ WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
|
63
109
|
# end
|
64
110
|
|
65
111
|
# puts @end - @start
|
66
|
-
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
112
|
+
# WorkerRoulette.tradesman_connection_pool.with {|r| r.flushdb}
|
@@ -23,9 +23,9 @@ describe WorkerRoulette do
|
|
23
23
|
it "should enqueue work" do
|
24
24
|
called = false
|
25
25
|
foreman = WorkerRoulette.a_foreman('foreman')
|
26
|
-
foreman.enqueue_work_order('some old fashion work') do |redis_response|
|
26
|
+
foreman.enqueue_work_order('some old fashion work') do |redis_response, stuff|
|
27
27
|
called = true
|
28
|
-
redis_response.should ==
|
28
|
+
redis_response.should == 'foreman added'
|
29
29
|
end
|
30
30
|
done(0.1) {called.should == true}
|
31
31
|
end
|
@@ -63,28 +63,28 @@ describe WorkerRoulette do
|
|
63
63
|
end
|
64
64
|
|
65
65
|
it "should post the sender's id to the job board with an order number" do
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
66
|
+
first_foreman = WorkerRoulette.a_foreman('first_foreman')
|
67
|
+
first_foreman.enqueue_work_order('foo') do
|
68
|
+
subject.enqueue_work_order(work_orders.first) do
|
69
|
+
subject.enqueue_work_order(work_orders.last) do
|
70
|
+
redis.zrange(subject.job_board_key, 0, -1, with_scores: true).should == [["first_foreman", 1.0], ["katie_80", 2.0]]
|
71
|
+
done
|
72
|
+
end
|
70
73
|
end
|
71
74
|
end
|
72
75
|
end
|
73
76
|
|
74
|
-
it "should
|
75
|
-
|
76
|
-
subject.enqueue_work_order(work_orders.first) do
|
77
|
-
done
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
it "should generate sequential order numbers" do
|
77
|
+
it "should generate a monotically increasing score for senders not on the job board, but not for senders already there" do
|
78
|
+
first_foreman = WorkerRoulette.a_foreman('first_foreman')
|
82
79
|
redis.get(subject.counter_key).should == nil
|
83
|
-
|
80
|
+
first_foreman.enqueue_work_order(work_orders.first) do
|
84
81
|
redis.get(subject.counter_key).should == "1"
|
85
|
-
|
86
|
-
redis.get(subject.counter_key).should == "
|
87
|
-
|
82
|
+
first_foreman.enqueue_work_order(work_orders.last) do
|
83
|
+
redis.get(subject.counter_key).should == "1"
|
84
|
+
subject.enqueue_work_order(work_orders.first) do
|
85
|
+
redis.get(subject.counter_key).should == "2"
|
86
|
+
done
|
87
|
+
end
|
88
88
|
end
|
89
89
|
end
|
90
90
|
end
|
@@ -100,13 +100,52 @@ describe WorkerRoulette do
|
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
103
|
+
context Lua do
|
104
|
+
before do
|
105
|
+
Lua.clear_cache!
|
106
|
+
redis.script(:flush)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should load and call a lua script" do
|
110
|
+
lua_script = 'return redis.call("SET", KEYS[1], ARGV[1])'
|
111
|
+
Lua.call(lua_script, ['foo'], ['daddy']) do |result|
|
112
|
+
Lua.cache.keys.first.should == lua_script
|
113
|
+
Lua.cache.values.first.should == Digest::SHA1.hexdigest(lua_script)
|
114
|
+
result.should == "OK"
|
115
|
+
done
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should send a sha instead of a script once the script has been cached" do
|
120
|
+
lua_script = 'return KEYS'
|
121
|
+
Lua.should_receive(:eval).and_call_original
|
122
|
+
|
123
|
+
Lua.call(lua_script) do |result|
|
124
|
+
|
125
|
+
Lua.should_not_receive(:eval)
|
126
|
+
Lua.call(lua_script) do |result|
|
127
|
+
result.should == []
|
128
|
+
done
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should raise an error to the caller if the script fails in redis" do
|
134
|
+
lua_script = 'this is junk'
|
135
|
+
# Lua.call(lua_script)
|
136
|
+
# rspec cannot test this bc of the callbacks, but if you have doubts,
|
137
|
+
# uncomment the line above and watch it fail
|
138
|
+
done
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
103
142
|
context Tradesman do
|
104
143
|
let(:foreman) {WorkerRoulette.a_foreman(sender)}
|
105
144
|
let(:subject) {WorkerRoulette.a_tradesman}
|
106
145
|
|
107
146
|
it "should be working on behalf of a sender" do
|
108
147
|
foreman.enqueue_work_order(work_orders) do
|
109
|
-
subject.work_orders! do
|
148
|
+
subject.work_orders! do |r|
|
110
149
|
subject.sender.should == sender
|
111
150
|
done
|
112
151
|
end
|
@@ -137,21 +176,16 @@ describe WorkerRoulette do
|
|
137
176
|
end
|
138
177
|
end
|
139
178
|
|
140
|
-
it "should get the sender and work_order list transactionally" do
|
141
|
-
EM::Hiredis::Client.any_instance.should_receive(:multi).and_call_original
|
142
|
-
subject.work_orders! {done}
|
143
|
-
end
|
144
|
-
|
145
179
|
it "should get the work_orders from the next queue when a new job is ready" do
|
146
180
|
subject.should_receive(:work_orders!).and_call_original
|
181
|
+
publish = proc {foreman.enqueue_work_order(work_orders)}
|
147
182
|
|
148
|
-
subject.wait_for_work_orders do |redis_work_orders, message, channel|
|
183
|
+
subject.wait_for_work_orders(publish) do |redis_work_orders, message, channel|
|
149
184
|
subject.sender.should == "katie_80"
|
150
185
|
redis_work_orders.should == [work_orders_with_headers]
|
151
186
|
done
|
152
187
|
end
|
153
188
|
|
154
|
-
foreman.enqueue_work_order(work_orders)
|
155
189
|
end
|
156
190
|
|
157
191
|
it "should publish and subscribe on custom channels" do
|
@@ -164,8 +198,8 @@ describe WorkerRoulette do
|
|
164
198
|
good_foreman = WorkerRoulette.a_foreman('foreman', 'good_channel')
|
165
199
|
bad_foreman = WorkerRoulette.a_foreman('foreman', 'bad_channel')
|
166
200
|
|
167
|
-
good_publish =
|
168
|
-
bad_publish =
|
201
|
+
good_publish = proc {good_foreman.enqueue_work_order('some old fashion work')}
|
202
|
+
bad_publish = proc {bad_foreman.enqueue_work_order('evil biddings you should not carry out')}
|
169
203
|
|
170
204
|
tradesman.should_receive(:work_orders!).and_call_original
|
171
205
|
evil_tradesman.should_receive(:work_orders!).and_call_original
|
@@ -175,32 +209,28 @@ describe WorkerRoulette do
|
|
175
209
|
tradesman.wait_for_work_orders(good_publish) do |good_work|
|
176
210
|
good_work.to_s.should match("old fashion")
|
177
211
|
good_work.to_s.should_not match("evil")
|
178
|
-
good_subscribed.should == true
|
179
212
|
end
|
180
213
|
|
181
214
|
evil_tradesman.wait_for_work_orders(bad_publish) do |bad_work|
|
182
215
|
bad_work.to_s.should_not match("old fashion")
|
183
216
|
bad_work.to_s.should match("evil")
|
184
|
-
bad_subscribed.should == true
|
185
217
|
end
|
186
218
|
|
187
|
-
good_foreman.enqueue_work_order('some old fashion work')
|
188
|
-
bad_foreman.enqueue_work_order('evil biddings you should not carry out')
|
189
219
|
done(0.2)
|
190
220
|
end
|
191
221
|
|
192
222
|
it "should unsubscribe from the job board" do
|
193
|
-
|
223
|
+
publish = proc {foreman.enqueue_work_order(work_orders)}
|
224
|
+
subject.wait_for_work_orders(publish) do |redis_work_orders, message, channel|
|
194
225
|
subject.unsubscribe {done}
|
195
226
|
end
|
196
227
|
EM::Hiredis::PubsubClient.any_instance.should_receive(:close_connection).and_call_original
|
197
|
-
foreman.enqueue_work_order(work_orders)
|
198
228
|
end
|
199
229
|
|
200
230
|
it "should periodically (random time between 10 and 15 seconds?) poll the job board for new work, in case it missed a notification"
|
201
231
|
it "should not delete the messages from the queue until they have been processed succcesfully"
|
202
232
|
it "should not cache the sender our counter keys"
|
203
|
-
it "should pull off more than one
|
233
|
+
it "should pull off work orders for more than one sender"
|
204
234
|
end
|
205
235
|
|
206
236
|
context "Read Lock" do
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: worker_roulette
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.11
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-02-
|
12
|
+
date: 2014-02-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: oj
|
@@ -253,6 +253,7 @@ files:
|
|
253
253
|
- lib/worker_roulette/a_foreman.rb
|
254
254
|
- lib/worker_roulette/a_tradesman.rb
|
255
255
|
- lib/worker_roulette/foreman.rb
|
256
|
+
- lib/worker_roulette/lua.rb
|
256
257
|
- lib/worker_roulette/tradesman.rb
|
257
258
|
- lib/worker_roulette/version.rb
|
258
259
|
- spec/benchmark/perf_test.rb
|