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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7d20589cbf53709f04f0c72d9c172cde437f0338
4
- data.tar.gz: 7e4324a28723597c74202b156736abeada7894c1
3
+ metadata.gz: ae747acd5bc96fca3eafd6e0a1a8b0ba2d4ef62b
4
+ data.tar.gz: ae5cfee49428221a97f8c45935ef967e3072473a
5
5
  SHA512:
6
- metadata.gz: 568bce937feeedeccefbf7f4c922d2acf4a746551009310f64974e1c8f98148efd9f9bcf6d29bfb552818339df93b79255e3c747af49142d5c38d239958d49b5
7
- data.tar.gz: 4e1bb9d16b3c6ab2fda879ebe986ae78f7f4a8564b40e277156dc9a1e64400e8aa38507e3102e627cb773fe79e8eaec024e3a0395fe8cd02e80c16147da23fd6
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 "iteration\_with\_return\_value"
241
- or "n\_iterations\_with\_return\_value". In the former case, return a
242
- single number at then end of the block, which is the measured value
243
- specifically for that time through the loop. In the latter case, your
244
- block will take a single parameter N for the number of iterations -
245
- run the code that many times and return either a single measured speed
246
- or time, or an array of speeds or times, which will be your samples.
247
-
248
- This can be useful when running N iterations doesn't necessarily
249
- generate exactly N results, or when the time the whole chunk of code
250
- takes to run isn't the most representative number for performance. The
251
- statistical test will help filter out random test-setup noise
252
- somewhat, but sometimes it's best to not count the noise in your
253
- measurement at all, for many good reasons.
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,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ puts "ABProf example: sleep 0.1 seconds, manual return value"
6
+
7
+ ABProf::ABWorker.iteration_with_return_value do
8
+ t1 = Time.now
9
+ sleep 0.01
10
+ (Time.now - t1)
11
+ end
12
+
13
+ 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
+ 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 :debug1, "Print first-process output to console"
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 diverged && OPTS[:fail_on_divergence]
84
+ exit 2 if (p_val >= bm_inst.p_value) && OPTS[:fail_on_divergence]
119
85
 
120
86
  # Otherwise, return success
@@ -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 ABProf.debug
23
+ STDERR.puts(string) if ENV['ABDEBUG'] == "true"
31
24
  end
32
25
  def self.debug string
33
- STDERR.puts(string) if ABProf.debug
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.n_interations_with_return_value(&block)
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
- STDOUT.write "VALUES #{value.to_a.inspect}"
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
- STDOUT.write "VALUE #{value.to_f}"
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 && ABProf.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 && ABProf.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
- end
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
- end
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)
@@ -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 run_one_iteration(pts = {})
53
- @state[:samples][0] += @process1.run_iters @iters_per_trial
54
- @state[:samples][1] += @process2.run_iters @iters_per_trial
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
- run_one_iteration opts
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
- puts "Trial #{@state[:iter]}, Welch's T-test p value: #{p_val.inspect}" if opts[:print_output]
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 summary11 > summary21
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}"
@@ -1,3 +1,3 @@
1
1
  module Abprof
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
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.1
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-05 00:00:00.000000000 Z
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