abprof 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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