abprof 0.2.0

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.
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "abprof"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ STDERR.puts "ABProf example: optcarrot with alt Ruby dir"
6
+
7
+ ABProf::ABWorker.iteration do
8
+ `cd ../alt_build && ./tool/runruby.rb ../optcarrot/bin/optcarrot --benchmark ../optcarrot/examples/Lan_Master.nes`
9
+ end
10
+
11
+ ABProf::ABWorker.start
@@ -0,0 +1,13 @@
1
+ require "abprof/benchmark_dsl"
2
+
3
+ ABProf.compare do
4
+ warmup 10
5
+ max_trials 5
6
+ min_trials 3
7
+ p_value 0.01
8
+ iters_per_trial 2
9
+
10
+ report_command "ruby examples/for_loop_10k.rb"
11
+ report_command "ruby examples/sleep.rb"
12
+
13
+ end
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ puts "ABProf example: 10,000 empty iterations"
6
+
7
+ ABProf::ABWorker.iteration do
8
+ 10_000.times {}
9
+ end
10
+
11
+ ABProf::ABWorker.start
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ STDERR.puts "ABProf example: optcarrot with profiling Ruby dir"
6
+
7
+ ABProf::ABWorker.iteration do
8
+ `cd ../inline_ruby_1800 && ./tool/runruby.rb ../optcarrot/bin/optcarrot --benchmark ../optcarrot/examples/Lan_Master.nes`
9
+ end
10
+
11
+ ABProf::ABWorker.start
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ STDERR.puts "ABProf example: optcarrot with profiling Ruby dir"
6
+
7
+ ABProf::ABWorker.iteration do
8
+ `cd ../inline_ruby_2500 && ./tool/runruby.rb ../optcarrot/bin/optcarrot --benchmark ../optcarrot/examples/Lan_Master.nes`
9
+ end
10
+
11
+ ABProf::ABWorker.start
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ STDERR.puts "ABProf example: optcarrot with with-inlined-funcs Ruby dir"
6
+
7
+ ABProf::ABWorker.iteration do
8
+ `cd ../inline_func_ruby && ./tool/runruby.rb ../optcarrot/bin/optcarrot --benchmark ../optcarrot/examples/Lan_Master.nes`
9
+ end
10
+
11
+ ABProf::ABWorker.start
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ STDERR.puts "ABProf example: optcarrot with profiling Ruby dir"
6
+
7
+ ABProf::ABWorker.iteration do
8
+ `cd ../profiling_ruby && ./tool/runruby.rb ../optcarrot/bin/optcarrot --benchmark ../optcarrot/examples/Lan_Master.nes`
9
+ end
10
+
11
+ ABProf::ABWorker.start
@@ -0,0 +1,20 @@
1
+ require "abprof/benchmark_dsl"
2
+
3
+ ABProf.compare do
4
+ warmup 10
5
+ max_trials 5
6
+ min_trials 3
7
+ p_value 0.01
8
+ iters_per_trial 2
9
+ fail_on_divergence true # For testing, usually
10
+ bare true
11
+
12
+ report do
13
+ 10_000.times {}
14
+ end
15
+
16
+ report do
17
+ sleep 0.1
18
+ end
19
+
20
+ end
data/examples/sleep.rb ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ puts "ABProf example: sleep 0.1 seconds"
6
+
7
+ ABProf::ABWorker.iteration do
8
+ sleep 0.001
9
+ end
10
+
11
+ ABProf::ABWorker.start
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ ABProf::ABWorker.iteration do
6
+ sleep 0.0015
7
+ end
8
+
9
+ ABProf::ABWorker.start
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "abprof"
4
+
5
+ STDERR.puts "ABProf example: optcarrot with vanilla Ruby dir"
6
+
7
+ ABProf::ABWorker.iteration do
8
+ `cd ../vanilla_build && ./tool/runruby.rb ../optcarrot/bin/optcarrot --benchmark ../optcarrot/examples/Lan_Master.nes`
9
+ end
10
+
11
+ ABProf::ABWorker.start
data/exe/abcompare ADDED
@@ -0,0 +1 @@
1
+ abprof
data/exe/abprof ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "trollop"
4
+ require "abprof"
5
+ require "abprof/benchmark_dsl"
6
+
7
+ OPTS = Trollop::options do
8
+ banner <<BANNER
9
+ Specify a first and second command line, and (often) a p-value or other
10
+ parameters.
11
+
12
+ Example: #{$0} examples/sleep.rb examples/sleep_longer.rb
13
+
14
+ The first and second commands are the first two arguments. You'll need to
15
+ quote multi-word commands, as is normal in bash.
16
+
17
+ Specifying lots of iterations and trials, high burn-in and a low P value
18
+ is accurate, but slow.
19
+
20
+ Specifying low iterations, trials and burn-in and a high P value gives
21
+ quick, rough results early on.
22
+
23
+ Specifying more iterations per trial is good for highly variable iteration
24
+ timing.
25
+
26
+ Specifying a lower max number of trials keeps the test from running *too*
27
+ long when the two are identical.
28
+
29
+ Specifying a high burn-in is necessary when cache behavior changes timing
30
+ significantly.
31
+
32
+ Vast numbers of trials can nearly always occasionally show differences
33
+ *somewhere* along the line, just by random chance. To avoid this, pick how
34
+ many samples first, run them all in one go, and then just check the p value
35
+ once.
36
+
37
+ A p value is often interpreted as the probability we got a wrong answer.
38
+ That's an oversimplification, but not (usually) a terrible one.
39
+ BANNER
40
+ opt :debug1, "Print first-process output to console"
41
+ opt :debug2, "Print second-process output to console"
42
+ opt :bare, "Use bare command-line commands, no Ruby harness", :default => ($0["compare"])
43
+ opt :pvalue, "P value (certainty) for Welch's T test", :default => 0.05
44
+ opt :burnin, "'Burn in' repetitions before real trials", :default => 10
45
+ opt :min_trials, "Minimum number of sample sets from each process", :default => 1
46
+ opt :max_trials, "Maximum number of sample sets from each process", :default => 20
47
+ opt :iters_per_trial, "Iterations per sample set", :default => 10
48
+ opt :print_samples, "Print all sample values for later analysis.", :default => false
49
+ opt :fail_on_divergence, "Return a non-zero code if pvalue is greater than specified."
50
+ end
51
+
52
+ if ARGV.length != 2
53
+ puts "Must specify both commands as normal arguments!"
54
+ exit -1
55
+ end
56
+
57
+ command1, command2 = ARGV
58
+
59
+ # Create DSL configuration for known properties,
60
+ # but don't actually run the sampling yet.
61
+ bm_inst = ABProf.compare(:no_at_exit => true) do
62
+ pvalue OPTS[:pvalue]
63
+ burnin OPTS[:burnin]
64
+ min_trials OPTS[:min_trials]
65
+ max_trials OPTS[:max_trials]
66
+ iters_per_trial OPTS[:iters_per_trial]
67
+ bare OPTS[:bare]
68
+ # No fail_on_divergence - we do this manually for the CLI utilities
69
+
70
+ report_command command1
71
+ report_command command2
72
+ end
73
+
74
+ state = bm_inst.run_sampling
75
+ 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
+
113
+ if OPTS[:print_samples]
114
+ puts "Samples for P1: #{state[:samples][0].inspect}"
115
+ puts "Samples for P2: #{state[:samples][1].inspect}"
116
+ end
117
+
118
+ exit 2 if diverged && OPTS[:fail_on_divergence]
119
+
120
+ # Otherwise, return success
data/lib/abprof.rb ADDED
@@ -0,0 +1,280 @@
1
+ require "abprof/version"
2
+
3
+ require "multi_json"
4
+
5
+ # Protocol:
6
+ # Controller sends "ITERS [integer]\n"
7
+ # Controller sends "QUIT\n" when done
8
+ # Test process responds with "NOT OK\n" or crashes for bad results
9
+ # Test process responds with "VALUE 27.23432" to explicitly return a single value
10
+ # Test process responds with "VALUES [1.4, 2.714, 39.4, -71.4]" to explicitly return many values
11
+ # QUIT requires no response.
12
+
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
+ # These are primarily for DSL use.
22
+ PROPERTIES = [ :debug, :pvalue, :iters_per_trial, :min_trials, :max_trials, :burnin, :bare, :fail_on_divergence ]
23
+
24
+ # This class is used by programs that are *being* profiled.
25
+ # It's necessarily a singleton since it needs to control STDIN.
26
+ # The bare mode can do without it, but it's needed for harness
27
+ # processes.
28
+ class ABWorker
29
+ def debug string
30
+ STDERR.puts(string) if ABProf.debug
31
+ end
32
+ def self.debug string
33
+ STDERR.puts(string) if ABProf.debug
34
+ end
35
+
36
+ def self.iteration(&block)
37
+ @iter_block = block
38
+ @return = :none
39
+ end
40
+
41
+ def self.iteration_with_return_value(&block)
42
+ @iter_block = block
43
+ @return = :per_iteration
44
+ end
45
+
46
+ def self.n_interations_with_return_value(&block)
47
+ @iter_block = block
48
+ @return = :per_n_iterations
49
+ end
50
+
51
+ def self.run_n(n)
52
+ debug "WORKER #{Process.pid}: running #{n} times"
53
+
54
+ case @return
55
+ when :none
56
+ n.times do
57
+ @iter_block.call
58
+ end
59
+ STDOUT.write "OK\n"
60
+ when :per_iteration
61
+ values = (0..(n-1)).map { |i| @iter_block.call.to_f }
62
+ STDOUT.write "VALUES #{values.inspect}"
63
+ when :per_n_iterations
64
+ value = @iter_block.call(n)
65
+ if value.respond_to?(:each)
66
+ # Return array of numbers
67
+ STDOUT.write "VALUES #{value.to_a.inspect}"
68
+ else
69
+ # Return single number
70
+ STDOUT.write "VALUE #{value.to_f}"
71
+ end
72
+ else
73
+ raise "Unknown @return value #{@return.inspect} inside abprof!"
74
+ end
75
+ end
76
+
77
+ def self.read_once
78
+ debug "WORKER #{Process.pid}: read loop"
79
+ @input ||= ""
80
+ @input += (STDIN.gets || "")
81
+ debug "WORKER #{Process.pid}: Input #{@input.inspect}"
82
+ if @input["\n"]
83
+ command, @input = @input.split("\n", 2)
84
+ debug "WORKER #{Process.pid}: command: #{command.inspect}"
85
+ if command == "QUIT"
86
+ exit 0
87
+ elsif command["ITERS"]
88
+ iters = command[5..-1].to_i
89
+ values = run_n iters
90
+ STDOUT.flush # Why does this synchronous file descriptor not flush when given a string with a newline? Ugh!
91
+ debug "WORKER #{Process.pid}: finished command ITERS: OK"
92
+ else
93
+ STDERR.puts "Unrecognized ABProf command: #{command.inspect}!"
94
+ exit -1
95
+ end
96
+ end
97
+ end
98
+
99
+ def self.start
100
+ loop do
101
+ read_once
102
+ end
103
+ end
104
+ end
105
+
106
+ SUMMARY_TYPES = {
107
+ "mean" => proc { |samples|
108
+ samples.inject(0.0, &:+) / samples.size
109
+ },
110
+ "median" => proc { |samples|
111
+ sz = samples.size
112
+ sorted = samples.sort
113
+ if sz % 2 == 1
114
+ # For odd-length, take middle element
115
+ sorted[ samples.size / 2 ]
116
+ else
117
+ # For even length, mean of two middle elements
118
+ (sorted[ sz / 2 ] + sorted[ sz / 2 + 1 ]) / 2.0
119
+ end
120
+ },
121
+ }
122
+ SUMMARY_METHODS = SUMMARY_TYPES.keys
123
+ def self.summarize(method, samples)
124
+ raise "Unknown summary method #{method.inspect}!" unless SUMMARY_METHODS.include?(method.to_s)
125
+ method_proc = SUMMARY_TYPES[method.to_s]
126
+ method_proc.call(samples)
127
+ end
128
+
129
+ class ABBareProcess
130
+ attr_reader :last_run
131
+ attr_reader :last_iters
132
+
133
+ def debug string
134
+ STDERR.puts(string) if @debug && ABProf.debug
135
+ end
136
+
137
+ def initialize command_line, opts = {}
138
+ @command = command_line
139
+ @debug = opts[:debug]
140
+ end
141
+
142
+ def quit
143
+ # No-op
144
+ end
145
+
146
+ def kill
147
+ # No-op
148
+ end
149
+
150
+ def run_iters(n)
151
+ t_start = t_end = nil
152
+ debug "Controller of #{@pid}: #{n} ITERS"
153
+
154
+ state = :succeeded
155
+ n.times do
156
+ if @command.respond_to?(:call)
157
+ t_start = Time.now
158
+ @command.call
159
+ t_end = Time.now
160
+ elsif @command.respond_to?(:to_s)
161
+ t_start = Time.now
162
+ system(@command.to_s)
163
+ t_end = Time.now
164
+ unless $?.success?
165
+ STDERR.puts "Failing process #{@pid} after failed iteration(s), error code #{state.inspect}"
166
+ # How to handle error with no self.kill?
167
+ raise "Failure from command #{@command.inspect}, dying!"
168
+ end
169
+ else
170
+ raise "Don't know how to execute bare object: #{@command.inspect}!"
171
+ end
172
+ end
173
+ @last_run = [(t_end - t_start).to_f]
174
+ @last_iters = n
175
+
176
+ @last_run
177
+ end
178
+ end
179
+
180
+ class ABHarnessProcess
181
+ attr_reader :last_run
182
+ attr_reader :last_iters
183
+
184
+ def debug string
185
+ STDERR.puts(string) if @debug && ABProf.debug
186
+ end
187
+
188
+ def initialize command_line, opts = {}
189
+ debug "Controller of nobody yet: SPAWN"
190
+ @in_reader, @in_writer = IO.pipe
191
+ @out_reader, @out_writer = IO.pipe
192
+ @in_writer.sync = true
193
+ @out_writer.sync = true
194
+
195
+ @pid = fork do
196
+ STDOUT.reopen(@out_writer)
197
+ STDIN.reopen(@in_reader)
198
+ @out_reader.close
199
+ @in_writer.close
200
+ 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!"
202
+ command_line.call
203
+ elsif command_line.respond_to?(:to_s)
204
+ exec command_line.to_s
205
+ else
206
+ raise "Don't know how to execute benchmark code: #{command_line.inspect}!"
207
+ end
208
+ exit! 0
209
+ end
210
+ @out_writer.close
211
+ @in_reader.close
212
+
213
+ @debug = opts[:debug]
214
+ debug "Controller spawned #{@pid} (debug: #{@debug.inspect})"
215
+ end
216
+
217
+ def quit
218
+ debug "Controller of #{@pid}: QUIT"
219
+ @in_writer.write "QUIT\n"
220
+ end
221
+
222
+ def kill
223
+ debug "Controller of #{@pid}: DIE"
224
+ ::Process.detach @pid
225
+ ::Process.kill "TERM", @pid
226
+ end
227
+
228
+ def run_iters(n)
229
+ debug "Controller of #{@pid}: #{n} ITERS"
230
+ @in_writer.write "ITERS #{n.to_i}\n"
231
+
232
+ ignored_out = 0
233
+ state = :failed
234
+ t_start = Time.now
235
+ loop do
236
+ # Read and block
237
+ output = @out_reader.gets
238
+ ignored_out += output.length
239
+ puts "Controller of #{@pid} out: #{output.inspect}" if @debug
240
+ debug "Controller of #{@pid} out: #{output.inspect}"
241
+ if output =~ /^VALUES/ # These anchors match newlines, too
242
+ state = :succeeded
243
+ vals = MultiJson.load output[7..-1]
244
+ raise "Must return an array value from iterations!" unless vals.is_a?(Array)
245
+ raise "Must return an array of numbers from iterations!" unless vals[0].is_a?(Numeric)
246
+ @last_run = vals
247
+ elsif output =~ /^VALUE/ # These anchors match newlines, too
248
+ state = :succeeded
249
+ val = output[6..-1].to_f
250
+ raise "Must return a number from iterations!" unless val.is_a?(Numeric)
251
+ @last_run = [ val ]
252
+ elsif output =~ /^OK$/ # These anchors match newlines, too
253
+ state = :succeeded_get_time
254
+ break
255
+ end
256
+ if output =~ /^NOT OK$/ # These anchors match newlines, too
257
+ # Failed, break
258
+ state = :explicit_not_ok
259
+ break
260
+ end
261
+ if ignored_out > 10_000
262
+ # 10k of output and no OK? Bail with failed state.
263
+ state = :too_much_output_without_status
264
+ break
265
+ end
266
+ end
267
+ t_end = Time.now
268
+ unless [:succeeded, :succeeded_get_time].include?(state)
269
+ self.kill
270
+ STDERR.puts "Killing process #{@pid} after failed iterations, error code #{state.inspect}"
271
+ end
272
+
273
+ @last_run = [ (t_end - t_start).to_f ] if state == :succeeded_get_time
274
+ @last_iters = n
275
+
276
+ @last_run
277
+ end
278
+
279
+ end
280
+ end