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 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