abprof 0.2.1 → 0.2.2
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.
- 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
|