abprof 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +49 -15
- data/examples/measured_sleep.rb +13 -0
- data/examples/multi_for_loop.rb +15 -0
- data/examples/multi_sleep.rb +15 -0
- data/exe/abprof +6 -40
- data/lib/abprof.rb +22 -24
- data/lib/abprof/benchmark_dsl.rb +20 -8
- data/lib/abprof/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae747acd5bc96fca3eafd6e0a1a8b0ba2d4ef62b
|
4
|
+
data.tar.gz: ae5cfee49428221a97f8c45935ef967e3072473a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 201b109cc93df7c18fe8d219ef6e3966abd8236bd950ca6a080a11c1c54d590506de62bb012e1b70cb540847ed50c7779ce7c3ac075328e3459a2b35501e049c
|
7
|
+
data.tar.gz: e79d72f6a0e8c5d439f250bd90755ca770b55c979229b983d9f3c64ce3c43500aaf724d4362ad3cc0107642c1775d8699b40cc4ab802a442c721bb24b5cdbc98
|
data/README.md
CHANGED
@@ -234,23 +234,57 @@ Of course, if your test is *really* slow, or you're trying to detect a
|
|
234
234
|
very small difference, it can just take a really long time. Like A/B
|
235
235
|
testing, this method has its pitfalls.
|
236
236
|
|
237
|
-
### More Control
|
237
|
+
### More Control of Sampling
|
238
238
|
|
239
239
|
Would you like to explicitly return the value(s) to compare? You can
|
240
|
-
replace the "iteration" block above with
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
240
|
+
replace the "iteration" block above with
|
241
|
+
"iteration\_with\_return\_value" to return a measurement of your
|
242
|
+
choice. That allows you to do setup or teardown inside the block that
|
243
|
+
isn't necessarily counted in the total time. You can also use a custom
|
244
|
+
counter or timer rather than Ruby's Time.now, which is the default for
|
245
|
+
ABProf.
|
246
|
+
|
247
|
+
If you return a higher-is-better value like a counter rather than a
|
248
|
+
lower-is-better value like time, you'll find that ABProf keeps telling
|
249
|
+
you the *lower*-valued process, which may be slower rather than
|
250
|
+
faster. ABProf can tell which one gets lower numbers, but it doesn't
|
251
|
+
know whether that means better or worse.
|
252
|
+
|
253
|
+
That's why the console output shows the word "faster?" with a question
|
254
|
+
mark. It knows it's giving you lower. It hopes that means faster.
|
255
|
+
|
256
|
+
### More Samples Per Trial
|
257
|
+
|
258
|
+
Would you like to control how the N iterations (default 10) per trial
|
259
|
+
get run? Want to do setup or teardown before or after them as a group,
|
260
|
+
not individuall?
|
261
|
+
|
262
|
+
Replace the "iteration" block above with
|
263
|
+
"n\_iterations\_with\_return\_value". Your block will take a single
|
264
|
+
parameter N for the number of iterations - run the code that many
|
265
|
+
times and return either a single measured speed or time, or an array
|
266
|
+
of speeds or times, which will be your samples.
|
267
|
+
|
268
|
+
Note: this technique has some subtleties -- you're better off *not*
|
269
|
+
doing this to rapidly collect many, many samples of very small
|
270
|
+
performance differences. If you do, transient conditions like
|
271
|
+
background processes can skew the results a *lot* when many T-test
|
272
|
+
samples are collected in a short time. You're much better off running
|
273
|
+
the same operation many times and returning the cumulative value in
|
274
|
+
those cases, or otherwise controlling for transient conditions that
|
275
|
+
drift over time.
|
276
|
+
|
277
|
+
In those cases, either set the iters-per-trial very low (likely to 1)
|
278
|
+
so that both processes are getting the benefit/penalty from transient
|
279
|
+
background conditions, or set the number of iterations per trial very
|
280
|
+
high so that each trial takes several seconds or longer, to allow
|
281
|
+
transient conditions to pass.
|
282
|
+
|
283
|
+
ABProf also runs the two processes' iterations in a random order by
|
284
|
+
default, starting from one process or the other based on a per-trial
|
285
|
+
random number. This helps a little, but only a little. If you *don't*
|
286
|
+
want ABProf to do that for some reason, turn on the static_order
|
287
|
+
option to get simple "process1 then process2" order for every trial.
|
254
288
|
|
255
289
|
## Development
|
256
290
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "abprof"
|
4
|
+
|
5
|
+
puts "ABProf example: sleep 0.1 seconds (multiple measurements per trial)"
|
6
|
+
|
7
|
+
ABProf::ABWorker.n_iterations_with_return_value do |n|
|
8
|
+
(1..n).map do
|
9
|
+
t1 = Time.now
|
10
|
+
100_000.times {}
|
11
|
+
(Time.now - t1) # Return array of measurements
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ABProf::ABWorker.start
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "abprof"
|
4
|
+
|
5
|
+
puts "ABProf example: sleep 0.1 seconds (multiple measurements per trial)"
|
6
|
+
|
7
|
+
ABProf::ABWorker.n_iterations_with_return_value do |n|
|
8
|
+
(1..n).map do
|
9
|
+
t1 = Time.now
|
10
|
+
sleep 0.01
|
11
|
+
(Time.now - t1) # Return array of measurements
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ABProf::ABWorker.start
|
data/exe/abprof
CHANGED
@@ -37,8 +37,7 @@ once.
|
|
37
37
|
A p value is often interpreted as the probability we got a wrong answer.
|
38
38
|
That's an oversimplification, but not (usually) a terrible one.
|
39
39
|
BANNER
|
40
|
-
opt :
|
41
|
-
opt :debug2, "Print second-process output to console"
|
40
|
+
opt :debug, "Print more output to console"
|
42
41
|
opt :bare, "Use bare command-line commands, no Ruby harness", :default => ($0["compare"])
|
43
42
|
opt :pvalue, "P value (certainty) for Welch's T test", :default => 0.05
|
44
43
|
opt :burnin, "'Burn in' repetitions before real trials", :default => 10
|
@@ -47,6 +46,7 @@ BANNER
|
|
47
46
|
opt :iters_per_trial, "Iterations per sample set", :default => 10
|
48
47
|
opt :print_samples, "Print all sample values for later analysis.", :default => false
|
49
48
|
opt :fail_on_divergence, "Return a non-zero code if pvalue is greater than specified."
|
49
|
+
opt :static_order, "Don't randomize the order of sampled processes per trial."
|
50
50
|
end
|
51
51
|
|
52
52
|
if ARGV.length != 2
|
@@ -65,56 +65,22 @@ bm_inst = ABProf.compare(:no_at_exit => true) do
|
|
65
65
|
max_trials OPTS[:max_trials]
|
66
66
|
iters_per_trial OPTS[:iters_per_trial]
|
67
67
|
bare OPTS[:bare]
|
68
|
+
debug OPTS[:debug]
|
69
|
+
static_order OPTS[:static_order]
|
68
70
|
# No fail_on_divergence - we do this manually for the CLI utilities
|
69
71
|
|
70
72
|
report_command command1
|
71
73
|
report_command command2
|
72
74
|
end
|
73
75
|
|
74
|
-
state = bm_inst.run_sampling
|
76
|
+
state = bm_inst.run_sampling(:print_output => true)
|
75
77
|
p_val = state[:p_tests][-1]
|
76
|
-
diverged = false
|
77
|
-
if p_val < bm_inst.p_value
|
78
|
-
puts "Based on measured P value #{p_val}, we believe there is a speed difference."
|
79
|
-
puts "As of end of run, p value is #{p_val}. Now run more times to check, or with lower p."
|
80
|
-
|
81
|
-
summary11 = ABProf.summarize("mean", state[:samples][0])
|
82
|
-
summary12 = ABProf.summarize("median", state[:samples][0])
|
83
|
-
summary21 = ABProf.summarize("mean", state[:samples][1])
|
84
|
-
summary22 = ABProf.summarize("median", state[:samples][1])
|
85
|
-
|
86
|
-
fastest = "1"
|
87
|
-
command = bm_inst.reports[0]
|
88
|
-
mean_times = summary21 / summary11
|
89
|
-
median_times = summary22 / summary12
|
90
|
-
if summary11 > summary21
|
91
|
-
fastest = "2"
|
92
|
-
command = bm_inst.reports[1]
|
93
|
-
mean_times = summary11 / summary21
|
94
|
-
median_times = summary12 / summary22
|
95
|
-
end
|
96
|
-
|
97
|
-
puts "Lower (faster?) process is #{fastest}, command line: #{command.inspect}"
|
98
|
-
puts "Lower command is (very) roughly #{median_times} times lower (faster?) -- assuming linear sampling."
|
99
|
-
|
100
|
-
print "\n"
|
101
|
-
puts "Process 1 mean result: #{summary11}"
|
102
|
-
puts "Process 1 median result: #{summary12}"
|
103
|
-
puts "Process 2 mean result: #{summary21}"
|
104
|
-
puts "Process 2 median result: #{summary22}"
|
105
|
-
else
|
106
|
-
puts "Based on measured P value #{p_val} and threshold #{bm_inst.pvalue}, we believe there is"
|
107
|
-
puts "no significant difference detectable with this set of trials."
|
108
|
-
puts "If you believe there is a small difference that wasn't detected, try raising the number"
|
109
|
-
puts "of iterations per trial, or the maximum number of trials."
|
110
|
-
diverged = true
|
111
|
-
end
|
112
78
|
|
113
79
|
if OPTS[:print_samples]
|
114
80
|
puts "Samples for P1: #{state[:samples][0].inspect}"
|
115
81
|
puts "Samples for P2: #{state[:samples][1].inspect}"
|
116
82
|
end
|
117
83
|
|
118
|
-
exit 2 if
|
84
|
+
exit 2 if (p_val >= bm_inst.p_value) && OPTS[:fail_on_divergence]
|
119
85
|
|
120
86
|
# Otherwise, return success
|
data/lib/abprof.rb
CHANGED
@@ -11,15 +11,8 @@ require "multi_json"
|
|
11
11
|
# QUIT requires no response.
|
12
12
|
|
13
13
|
module ABProf
|
14
|
-
def self.debug
|
15
|
-
@debug
|
16
|
-
end
|
17
|
-
def self.debug=(new_val)
|
18
|
-
@debug = new_val
|
19
|
-
end
|
20
|
-
|
21
14
|
# These are primarily for DSL use.
|
22
|
-
PROPERTIES = [ :debug, :pvalue, :iters_per_trial, :min_trials, :max_trials, :burnin, :bare, :fail_on_divergence ]
|
15
|
+
PROPERTIES = [ :debug, :pvalue, :iters_per_trial, :min_trials, :max_trials, :burnin, :bare, :fail_on_divergence, :static_order ]
|
23
16
|
|
24
17
|
# This class is used by programs that are *being* profiled.
|
25
18
|
# It's necessarily a singleton since it needs to control STDIN.
|
@@ -27,10 +20,10 @@ module ABProf
|
|
27
20
|
# processes.
|
28
21
|
class ABWorker
|
29
22
|
def debug string
|
30
|
-
STDERR.puts(string) if
|
23
|
+
STDERR.puts(string) if ENV['ABDEBUG'] == "true"
|
31
24
|
end
|
32
25
|
def self.debug string
|
33
|
-
STDERR.puts(string) if
|
26
|
+
STDERR.puts(string) if ENV['ABDEBUG'] == "true"
|
34
27
|
end
|
35
28
|
|
36
29
|
def self.iteration(&block)
|
@@ -43,13 +36,13 @@ module ABProf
|
|
43
36
|
@return = :per_iteration
|
44
37
|
end
|
45
38
|
|
46
|
-
def self.
|
39
|
+
def self.n_iterations_with_return_value(&block)
|
47
40
|
@iter_block = block
|
48
41
|
@return = :per_n_iterations
|
49
42
|
end
|
50
43
|
|
51
44
|
def self.run_n(n)
|
52
|
-
debug "WORKER #{Process.pid}: running #{n} times"
|
45
|
+
debug "WORKER #{Process.pid}: running #{n} times [#{@return.inspect}]"
|
53
46
|
|
54
47
|
case @return
|
55
48
|
when :none
|
@@ -59,15 +52,17 @@ module ABProf
|
|
59
52
|
STDOUT.write "OK\n"
|
60
53
|
when :per_iteration
|
61
54
|
values = (0..(n-1)).map { |i| @iter_block.call.to_f }
|
62
|
-
STDOUT.write "VALUES #{values.inspect}"
|
55
|
+
STDOUT.write "VALUES #{values.inspect}\n"
|
63
56
|
when :per_n_iterations
|
64
57
|
value = @iter_block.call(n)
|
65
58
|
if value.respond_to?(:each)
|
66
59
|
# Return array of numbers
|
67
|
-
|
60
|
+
debug "WORKER #{Process.pid}: Sent to controller: VALUES #{value.to_a.inspect}"
|
61
|
+
STDOUT.write "VALUES #{value.to_a.inspect}\n"
|
68
62
|
else
|
69
63
|
# Return single number
|
70
|
-
|
64
|
+
debug "WORKER #{Process.pid}: Sent to controller: VALUE #{value.to_f}"
|
65
|
+
STDOUT.write "VALUE #{value.to_f}\n"
|
71
66
|
end
|
72
67
|
else
|
73
68
|
raise "Unknown @return value #{@return.inspect} inside abprof!"
|
@@ -131,7 +126,7 @@ module ABProf
|
|
131
126
|
attr_reader :last_iters
|
132
127
|
|
133
128
|
def debug string
|
134
|
-
STDERR.puts(string) if @debug
|
129
|
+
STDERR.puts(string) if @debug
|
135
130
|
end
|
136
131
|
|
137
132
|
def initialize command_line, opts = {}
|
@@ -182,7 +177,7 @@ module ABProf
|
|
182
177
|
attr_reader :last_iters
|
183
178
|
|
184
179
|
def debug string
|
185
|
-
STDERR.puts(string) if @debug
|
180
|
+
STDERR.puts(string) if @debug
|
186
181
|
end
|
187
182
|
|
188
183
|
def initialize command_line, opts = {}
|
@@ -191,14 +186,18 @@ module ABProf
|
|
191
186
|
@out_reader, @out_writer = IO.pipe
|
192
187
|
@in_writer.sync = true
|
193
188
|
@out_writer.sync = true
|
189
|
+
@debug = opts[:debug]
|
194
190
|
|
195
191
|
@pid = fork do
|
196
192
|
STDOUT.reopen(@out_writer)
|
197
193
|
STDIN.reopen(@in_reader)
|
198
194
|
@out_reader.close
|
199
195
|
@in_writer.close
|
196
|
+
|
197
|
+
ENV['ABDEBUG'] = @debug.inspect
|
198
|
+
|
200
199
|
if command_line.respond_to?(:call)
|
201
|
-
puts "Caution! An ABProf Harness process (non-bare) is being used with a block. This is almost never what you want!"
|
200
|
+
STDERR.puts "Caution! An ABProf Harness process (non-bare) is being used with a block. This is almost never what you want!"
|
202
201
|
command_line.call
|
203
202
|
elsif command_line.respond_to?(:to_s)
|
204
203
|
exec command_line.to_s
|
@@ -210,7 +209,6 @@ module ABProf
|
|
210
209
|
@out_writer.close
|
211
210
|
@in_reader.close
|
212
211
|
|
213
|
-
@debug = opts[:debug]
|
214
212
|
debug "Controller spawned #{@pid} (debug: #{@debug.inspect})"
|
215
213
|
end
|
216
214
|
|
@@ -236,7 +234,6 @@ module ABProf
|
|
236
234
|
# Read and block
|
237
235
|
output = @out_reader.gets
|
238
236
|
ignored_out += output.length
|
239
|
-
puts "Controller of #{@pid} out: #{output.inspect}" if @debug
|
240
237
|
debug "Controller of #{@pid} out: #{output.inspect}"
|
241
238
|
if output =~ /^VALUES/ # These anchors match newlines, too
|
242
239
|
state = :succeeded
|
@@ -244,25 +241,26 @@ module ABProf
|
|
244
241
|
raise "Must return an array value from iterations!" unless vals.is_a?(Array)
|
245
242
|
raise "Must return an array of numbers from iterations!" unless vals[0].is_a?(Numeric)
|
246
243
|
@last_run = vals
|
244
|
+
break
|
247
245
|
elsif output =~ /^VALUE/ # These anchors match newlines, too
|
248
246
|
state = :succeeded
|
249
247
|
val = output[6..-1].to_f
|
250
248
|
raise "Must return a number from iterations!" unless val.is_a?(Numeric)
|
251
249
|
@last_run = [ val ]
|
250
|
+
break
|
252
251
|
elsif output =~ /^OK$/ # These anchors match newlines, too
|
253
252
|
state = :succeeded_get_time
|
254
253
|
break
|
255
|
-
|
256
|
-
if output =~ /^NOT OK$/ # These anchors match newlines, too
|
254
|
+
elsif output =~ /^NOT OK$/ # These anchors match newlines, too
|
257
255
|
# Failed, break
|
258
256
|
state = :explicit_not_ok
|
259
257
|
break
|
260
|
-
|
261
|
-
if ignored_out > 10_000
|
258
|
+
elsif ignored_out > 10_000
|
262
259
|
# 10k of output and no OK? Bail with failed state.
|
263
260
|
state = :too_much_output_without_status
|
264
261
|
break
|
265
262
|
end
|
263
|
+
# None of these? Loop again.
|
266
264
|
end
|
267
265
|
t_end = Time.now
|
268
266
|
unless [:succeeded, :succeeded_get_time].include?(state)
|
data/lib/abprof/benchmark_dsl.rb
CHANGED
@@ -20,6 +20,7 @@ module ABProf
|
|
20
20
|
@max_trials = 20
|
21
21
|
@iters_per_trial = 10
|
22
22
|
@bare = false
|
23
|
+
@static_order = false
|
23
24
|
|
24
25
|
@state = {
|
25
26
|
:samples => [[], []],
|
@@ -49,9 +50,16 @@ module ABProf
|
|
49
50
|
@process2.run_iters @burnin
|
50
51
|
end
|
51
52
|
|
52
|
-
def
|
53
|
-
|
54
|
-
@
|
53
|
+
def run_one_trial(pts = {})
|
54
|
+
order_rand = (rand() * 2.0).to_i
|
55
|
+
if @static_order || order_rand == 0
|
56
|
+
@state[:samples][0] += @process1.run_iters @iters_per_trial
|
57
|
+
@state[:samples][1] += @process2.run_iters @iters_per_trial
|
58
|
+
else
|
59
|
+
# Same thing, but do process2 first
|
60
|
+
@state[:samples][1] += @process2.run_iters @iters_per_trial
|
61
|
+
@state[:samples][0] += @process1.run_iters @iters_per_trial
|
62
|
+
end
|
55
63
|
@state[:iter] += 1
|
56
64
|
end
|
57
65
|
|
@@ -63,7 +71,6 @@ module ABProf
|
|
63
71
|
@process1 = process_type.new command1, :debug => @debug
|
64
72
|
@process2 = process_type.new command2, :debug => @debug
|
65
73
|
|
66
|
-
puts "Beginning #{@burnin} iterations of burn-in for each process." if opts[:print_output]
|
67
74
|
run_burnin opts
|
68
75
|
|
69
76
|
puts "Beginning sampling from processes." if opts[:print_output]
|
@@ -71,7 +78,7 @@ module ABProf
|
|
71
78
|
# Sampling
|
72
79
|
p_val = 1.0
|
73
80
|
@max_trials.times do
|
74
|
-
|
81
|
+
run_one_trial opts
|
75
82
|
|
76
83
|
# No t-test without 3+ samples
|
77
84
|
if @state[:samples][0].size > 2
|
@@ -79,7 +86,11 @@ module ABProf
|
|
79
86
|
t = Statsample::Test.t_two_samples_independent(@state[:samples][0].to_vector, @state[:samples][1].to_vector)
|
80
87
|
p_val = t.probability_not_equal_variance
|
81
88
|
@state[:p_tests].push p_val
|
82
|
-
|
89
|
+
avg_1 = @state[:samples][0].inject(0.0, &:+) / @state[:samples][0].length
|
90
|
+
avg_2 = @state[:samples][1].inject(0.0, &:+) / @state[:samples][1].length
|
91
|
+
smaller = "1"
|
92
|
+
smaller = "2" if avg_1 > avg_2
|
93
|
+
puts "Trial #{@state[:iter]}, Welch's T-test p value: #{p_val.inspect} (Guessed smaller: #{smaller})" if opts[:print_output]
|
83
94
|
end
|
84
95
|
|
85
96
|
# Just finished trial number i+1. So we can exit only if i+1 was at least
|
@@ -118,7 +129,7 @@ module ABProf
|
|
118
129
|
command = @reports[0]
|
119
130
|
mean_times = summary21 / summary11
|
120
131
|
median_times = summary22 / summary12
|
121
|
-
if
|
132
|
+
if summary12 > summary22
|
122
133
|
fastest = "2"
|
123
134
|
command = @reports[1]
|
124
135
|
mean_times = summary11 / summary21
|
@@ -126,7 +137,8 @@ module ABProf
|
|
126
137
|
end
|
127
138
|
|
128
139
|
puts "Lower (faster?) process is #{fastest}, command line: #{command.inspect}"
|
129
|
-
puts "Lower command is (very) roughly #{median_times} times lower (faster?) -- assuming linear sampling."
|
140
|
+
puts "Lower command is (very) roughly #{median_times} times lower (faster?) -- assuming linear sampling, checking at median."
|
141
|
+
puts " Checking at mean, it would be #{mean_times} lower (faster?)."
|
130
142
|
|
131
143
|
print "\n"
|
132
144
|
puts "Process 1 mean result: #{summary11}"
|
data/lib/abprof/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abprof
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Noah Gibbs
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -138,6 +138,9 @@ files:
|
|
138
138
|
- examples/inline_ruby_1800.rb
|
139
139
|
- examples/inline_ruby_2500.rb
|
140
140
|
- examples/inlined_ruby.rb
|
141
|
+
- examples/measured_sleep.rb
|
142
|
+
- examples/multi_for_loop.rb
|
143
|
+
- examples/multi_sleep.rb
|
141
144
|
- examples/profiling_ruby.rb
|
142
145
|
- examples/simple_dsl.rb
|
143
146
|
- examples/sleep.rb
|