benchmark_driver 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +0 -4
- data/.travis.yml +10 -6
- data/Gemfile +7 -2
- data/Gemfile.lock +30 -0
- data/README.md +125 -117
- data/Rakefile +14 -7
- data/benchmark_driver.gemspec +2 -4
- data/bin/console +1 -1
- data/examples/call.rb +12 -0
- data/examples/call_blank.rb +13 -0
- data/examples/call_erb.rb +33 -0
- data/examples/call_interpolation.rb +13 -0
- data/examples/exec_blank.rb +14 -0
- data/examples/exec_interpolation.rb +15 -0
- data/examples/yaml/array_duration_time.yml +3 -0
- data/examples/yaml/array_loop.yml +3 -0
- data/examples/yaml/array_loop_memory.yml +6 -0
- data/examples/yaml/array_loop_time.yml +4 -0
- data/examples/yaml/blank_hash.yml +8 -0
- data/examples/yaml/blank_hash_array.yml +10 -0
- data/examples/yaml/blank_loop.yml +9 -0
- data/examples/yaml/blank_loop_time.yml +10 -0
- data/examples/yaml/blank_string.yml +6 -0
- data/examples/yaml/blank_string_array.yml +8 -0
- data/examples/yaml/example_multi.yml +6 -0
- data/{benchmarks → examples/yaml}/example_single.yml +0 -0
- data/exe/benchmark-driver +44 -18
- data/lib/benchmark/driver.rb +52 -257
- data/lib/benchmark/driver/benchmark_result.rb +21 -0
- data/lib/benchmark/driver/configuration.rb +65 -0
- data/lib/benchmark/driver/duration_runner.rb +24 -0
- data/lib/benchmark/driver/error.rb +16 -0
- data/lib/benchmark/driver/repeatable_runner.rb +18 -0
- data/lib/benchmark/driver/ruby_dsl_parser.rb +57 -0
- data/lib/benchmark/driver/time.rb +12 -0
- data/lib/benchmark/driver/version.rb +2 -2
- data/lib/benchmark/driver/yaml_parser.rb +103 -0
- data/lib/benchmark/output.rb +16 -0
- data/lib/benchmark/output/ips.rb +114 -0
- data/lib/benchmark/output/memory.rb +57 -0
- data/lib/benchmark/output/time.rb +57 -0
- data/lib/benchmark/runner.rb +13 -0
- data/lib/benchmark/runner/call.rb +97 -0
- data/lib/benchmark/runner/exec.rb +190 -0
- metadata +40 -10
- data/benchmarks/core/array.yml +0 -4
- data/benchmarks/example_multi.yml +0 -10
- data/benchmarks/lib/erb.yml +0 -30
@@ -0,0 +1,21 @@
|
|
1
|
+
# @param [Benchmark::Driver::Configuration::Job] job
|
2
|
+
# @param [Integer] iterations - Executed iterations of benchmark script in the job
|
3
|
+
# @param [Float] real - Real time taken by the job
|
4
|
+
# @param [Integer] max_rss - Maximum resident set size of the process during its lifetime, in Kilobytes.
|
5
|
+
class Benchmark::Driver::BenchmarkResult < Struct.new(:job, :iterations, :real, :max_rss)
|
6
|
+
alias :duration :real
|
7
|
+
|
8
|
+
def ips
|
9
|
+
iterations / real
|
10
|
+
end
|
11
|
+
|
12
|
+
def ip100ms
|
13
|
+
ips / 10
|
14
|
+
end
|
15
|
+
|
16
|
+
def iterations
|
17
|
+
# runner's warmup uses `result.ips` to calculate `job.loop_count`, and thus
|
18
|
+
# at that moment `job.loop_count` isn't available and we need to use `super`.
|
19
|
+
super || job.loop_count
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# @param [Array<Benchmark::Driver::Configuration::Job>] jobs
|
2
|
+
# @param [Benchmark::Driver::Configuration::RunnerOptions] runner_options
|
3
|
+
# @param [Benchmark::Driver::Configuration::OutputOptions] output_options
|
4
|
+
class Benchmark::Driver::Configuration < Struct.new(:jobs, :runner_options, :output_options)
|
5
|
+
# @param [String,nil] name
|
6
|
+
# @param [String,Proc] sctipt
|
7
|
+
# @param [String,nil] prelude
|
8
|
+
# @param [Integer,nil] loop_count - If this is nil, loop count is automatically estimated by warmup.
|
9
|
+
class Job < Struct.new(:name, :script, :prelude, :loop_count)
|
10
|
+
# @param [Integer,nil] guessed_count - Set by runner only when loop_count is nil. This is not configuration.
|
11
|
+
attr_accessor :guessed_count
|
12
|
+
|
13
|
+
def warmup_needed?
|
14
|
+
# This needs to check original configuration
|
15
|
+
self[:loop_count].nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def loop_count
|
19
|
+
super || guessed_count
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [String] name
|
24
|
+
# @param [String] path
|
25
|
+
Executable = Struct.new(:name, :path)
|
26
|
+
|
27
|
+
DEFAULT_EXECUTABLES = [Executable.new(RUBY_VERSION, RbConfig.ruby)]
|
28
|
+
|
29
|
+
# @param [Symbol] type - Type of runner
|
30
|
+
# @param [Array<Benchmark::Driver::Configuration::Executable>] executables
|
31
|
+
# @param [Integer,nil] repeat_count - Times to repeat benchmarks. When this is not nil, benchmark_driver will use the best result.
|
32
|
+
class RunnerOptions < Struct.new(:type, :executables, :repeat_count)
|
33
|
+
def initialize(*)
|
34
|
+
super
|
35
|
+
self.executables = DEFAULT_EXECUTABLES
|
36
|
+
end
|
37
|
+
|
38
|
+
def executables_specified?
|
39
|
+
executables != DEFAULT_EXECUTABLES
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param [Symbol] type - Type of output
|
44
|
+
# @param [TrueClass,FalseClass] compare
|
45
|
+
OutputOptions = Struct.new(:type, :compare)
|
46
|
+
|
47
|
+
# @param [Object] config
|
48
|
+
def self.symbolize_keys!(config)
|
49
|
+
case config
|
50
|
+
when Hash
|
51
|
+
config.keys.each do |key|
|
52
|
+
config[key.to_sym] = symbolize_keys!(config.delete(key))
|
53
|
+
end
|
54
|
+
when Array
|
55
|
+
config.map! { |c| symbolize_keys!(c) }
|
56
|
+
end
|
57
|
+
config
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param [String] str
|
61
|
+
def self.camelize(str)
|
62
|
+
return str if str !~ /_/ && str =~ /[A-Z]+.*/
|
63
|
+
str.split('_').map { |e| e.capitalize }.join
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Benchmark::Driver::DurationRunner
|
2
|
+
# @param [Benchmark::Driver::Configuration::Job] job
|
3
|
+
def initialize(job)
|
4
|
+
@job = job
|
5
|
+
end
|
6
|
+
|
7
|
+
# @param [Integer,Float] seconds
|
8
|
+
# @param [Integer] unit_iters
|
9
|
+
# @param [Proc] runner - should take (job, unit_iters) and return duration.
|
10
|
+
# @return [Benchmark::Driver::BenchmarkResult]
|
11
|
+
def run(seconds:, unit_iters:, runner:)
|
12
|
+
real_time = 0.0
|
13
|
+
iterations = 0
|
14
|
+
unit_iters = unit_iters.to_i
|
15
|
+
|
16
|
+
benchmark_until = Benchmark::Driver::Time.now + seconds
|
17
|
+
while Benchmark::Driver::Time.now < benchmark_until
|
18
|
+
real_time += runner.call(@job, unit_iters)
|
19
|
+
iterations += unit_iters
|
20
|
+
end
|
21
|
+
|
22
|
+
Benchmark::Driver::BenchmarkResult.new(@job, iterations, real_time)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Benchmark::Driver
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class ExecutionTimeTooShort < Error
|
6
|
+
def initialize(job, iterations)
|
7
|
+
@job = job
|
8
|
+
@iterations = iterations
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
"Execution time of job #{@job.name.dump} was too short in #{@iterations} executions;\n "\
|
13
|
+
'Please retry, try slower script or increase loop_count.'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Benchmark::Driver::RepeatableRunner
|
2
|
+
# @param [Benchmark::Driver::Configuration::Job] job
|
3
|
+
def initialize(job)
|
4
|
+
@job = job
|
5
|
+
end
|
6
|
+
|
7
|
+
# @param [Integer] repeat_count
|
8
|
+
# @param [Proc] runner - should take (job, unit_iters) and return duration.
|
9
|
+
# @return [Benchmark::Driver::BenchmarkResult]
|
10
|
+
def run(repeat_count:, runner:)
|
11
|
+
real_times = (repeat_count || 1).times.map do
|
12
|
+
runner.call(@job, @job.loop_count)
|
13
|
+
end
|
14
|
+
Benchmark::Driver::BenchmarkResult.new(@job).tap do |result|
|
15
|
+
result.real = real_times.select { |d| d > 0 }.min || real_times.max
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'benchmark/driver/configuration'
|
2
|
+
|
3
|
+
class Benchmark::Driver::RubyDslParser
|
4
|
+
def initialize(runner: :call, output: :ips)
|
5
|
+
@prelude = nil
|
6
|
+
@jobs = []
|
7
|
+
@runner_options = Benchmark::Driver::Configuration::RunnerOptions.new(runner)
|
8
|
+
@output_options = Benchmark::Driver::Configuration::OutputOptions.new(output)
|
9
|
+
end
|
10
|
+
|
11
|
+
# API to fetch configuration parsed from DSL
|
12
|
+
# @return [Benchmark::Driver::Configuration]
|
13
|
+
def configuration
|
14
|
+
@jobs.each do |job|
|
15
|
+
job.prelude = @prelude
|
16
|
+
end
|
17
|
+
Benchmark::Driver::Configuration.new(@jobs).tap do |c|
|
18
|
+
c.runner_options = @runner_options
|
19
|
+
c.output_options = @output_options
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [String] prelude - Script required for benchmark whose execution time is not measured.
|
24
|
+
def prelude=(prelude)
|
25
|
+
unless prelude.is_a?(String)
|
26
|
+
raise ArgumentError.new("prelude must be String but got #{prelude.inspect}")
|
27
|
+
end
|
28
|
+
unless @prelude.nil?
|
29
|
+
raise ArgumentError.new("prelude is already set:\n#{@prelude}")
|
30
|
+
end
|
31
|
+
|
32
|
+
@prelude = prelude
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [String,nil] name - Name shown on result output. This must be provided if block is given.
|
36
|
+
# @param [String,nil] script - Benchmarked script in String. Only either of script or block must be provided.
|
37
|
+
# @param [Proc,nil] block - Benchmarked Proc object.
|
38
|
+
def report(name = nil, script: nil, &block)
|
39
|
+
if script.nil? && !block_given?
|
40
|
+
raise ArgumentError.new('script or block must be provided')
|
41
|
+
elsif !script.nil? && block_given?
|
42
|
+
raise ArgumentError.new('script and block cannot be specified at the same time')
|
43
|
+
elsif name.nil? && block_given?
|
44
|
+
raise ArgumentError.new('name must be specified if block is given')
|
45
|
+
elsif !name.nil? && !name.is_a?(String)
|
46
|
+
raise ArgumentError.new("name must be String but got #{name.inspect}")
|
47
|
+
elsif !script.nil? && !script.is_a?(String)
|
48
|
+
raise ArgumentError.new("script must be String but got #{script.inspect}")
|
49
|
+
end
|
50
|
+
|
51
|
+
@jobs << Benchmark::Driver::Configuration::Job.new(name || script, script || block)
|
52
|
+
end
|
53
|
+
|
54
|
+
def compare!
|
55
|
+
@output_options.compare = true
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Benchmark::Driver::Time
|
2
|
+
if defined?(Process::CLOCK_MONOTONIC)
|
3
|
+
def self.now
|
4
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
5
|
+
end
|
6
|
+
else
|
7
|
+
$stderr.puts "Process::CLOCK_MONOTONIC was unavailable. Using Time."
|
8
|
+
def self.now
|
9
|
+
::Time.now
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Benchmark::Driver::YamlParser
|
2
|
+
DEFAULT_RUNNER = :exec # In YamlParser, we can't use :call.
|
3
|
+
DEFAULT_OUTPUT = :ips
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# @param [String] prelude
|
7
|
+
# @param [Integer,nil] loop_count
|
8
|
+
# @param [String,Array<String,Hash{ Symbol => String }>,Hash{ Symbol => String }] benchmark
|
9
|
+
# @param [String,Symbol,Hash{ Symbol => Integer,TrueClass,FalseClass }] runner
|
10
|
+
# @param [String,Symbol,Hash{ Symbol => Integer,TrueClass,FalseClass }] output
|
11
|
+
# @return [Benchmark::Driver::Configuration]
|
12
|
+
def parse(prelude: '', loop_count: nil, benchmark:, runner: {}, output: {})
|
13
|
+
jobs = parse_benchmark(benchmark)
|
14
|
+
jobs.each do |job|
|
15
|
+
job.prelude = prelude
|
16
|
+
job.loop_count ||= loop_count
|
17
|
+
end
|
18
|
+
|
19
|
+
config = Benchmark::Driver::Configuration.new(jobs)
|
20
|
+
config.runner_options = parse_runner(runner)
|
21
|
+
config.output_options = parse_output(output)
|
22
|
+
config
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# @param [String,Symbol,Hash{ Symbol => Integer,TrueClass,FalseClass }] runner
|
28
|
+
def parse_runner(runner)
|
29
|
+
case runner
|
30
|
+
when String, Symbol
|
31
|
+
Benchmark::Driver::Configuration::RunnerOptions.new(runner.to_sym)
|
32
|
+
when Hash
|
33
|
+
parse_runner_options(runner)
|
34
|
+
else
|
35
|
+
raise ArgumentError.new("Expected String, Symbol or Hash in runner, but got: #{runner.inspect}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_runner_options(type: DEFAULT_RUNNER, repeat_count: nil)
|
40
|
+
Benchmark::Driver::Configuration::RunnerOptions.new.tap do |r|
|
41
|
+
r.type = type.to_sym
|
42
|
+
r.repeat_count = Integer(repeat_count) if repeat_count
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @param [String,Symbol,Hash{ Symbol => Integer,TrueClass,FalseClass }] output
|
47
|
+
def parse_output(output)
|
48
|
+
case output
|
49
|
+
when String, Symbol
|
50
|
+
Benchmark::Driver::Configuration::OutputOptions.new(output.to_sym)
|
51
|
+
when Hash
|
52
|
+
parse_output_options(output)
|
53
|
+
else
|
54
|
+
raise ArgumentError.new("Expected String, Symbol or Hash in output, but got: #{output.inspect}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_output_options(type: DEFAULT_OUTPUT, compare: false)
|
59
|
+
Benchmark::Driver::Configuration::OutputOptions.new.tap do |r|
|
60
|
+
r.type = type.to_sym
|
61
|
+
r.compare = compare
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Parse "benchmark" declarative. This may have multiple benchmarks.
|
66
|
+
# @param [String,Array<String,Hash{ Symbol => String }>,Hash{ Symbol => String }] benchmark
|
67
|
+
def parse_benchmark(benchmark)
|
68
|
+
case benchmark
|
69
|
+
when String
|
70
|
+
[parse_each_benchmark(benchmark)]
|
71
|
+
when Array
|
72
|
+
benchmark.map { |b| parse_each_benchmark(b) }
|
73
|
+
when Hash
|
74
|
+
benchmark.map do |key, value|
|
75
|
+
Benchmark::Driver::Configuration::Job.new(key.to_s, value)
|
76
|
+
end
|
77
|
+
else
|
78
|
+
raise ArgumentError.new("benchmark must be String, Array or Hash, but got: #{benchmark.inspect}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Parse one benchmark specified in "benchmark" declarative.
|
83
|
+
# @param [String,Hash{ Symbol => String }>] job
|
84
|
+
def parse_each_benchmark(benchmark)
|
85
|
+
case benchmark
|
86
|
+
when String
|
87
|
+
Benchmark::Driver::Configuration::Job.new(benchmark, benchmark)
|
88
|
+
when Hash
|
89
|
+
parse_job(benchmark)
|
90
|
+
else
|
91
|
+
raise ArgumentError.new("Expected String or Hash in element of benchmark, but got: #{benchmark.inspect}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param [String,nil] name
|
96
|
+
# @param [String] script
|
97
|
+
# TODO: support benchmark-specific prelude
|
98
|
+
def parse_job(name: nil, script:)
|
99
|
+
name = script if name.nil?
|
100
|
+
Benchmark::Driver::Configuration::Job.new(name, script)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Benchmark::Output
|
2
|
+
class << self
|
3
|
+
# Benchmark::Output is pluggable.
|
4
|
+
# Create `Benchmark::Output::FooBar` as benchmark-output-foo_bar.gem and specify `output: foo_bar`.
|
5
|
+
#
|
6
|
+
# @param [Symbol] name
|
7
|
+
def find(name)
|
8
|
+
class_name = Benchmark::Driver::Configuration.camelize(name.to_s)
|
9
|
+
Benchmark::Output.const_get(class_name, false)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'benchmark/output/ips'
|
15
|
+
require 'benchmark/output/memory'
|
16
|
+
require 'benchmark/output/time'
|
@@ -0,0 +1,114 @@
|
|
1
|
+
class Benchmark::Output::Ips
|
2
|
+
# This class requires runner to measure following fields in `Benchmark::Driver::BenchmarkResult` to show output.
|
3
|
+
REQUIRED_FIELDS = [:real]
|
4
|
+
|
5
|
+
NAME_LENGTH = 20
|
6
|
+
|
7
|
+
# @param [Array<Benchmark::Driver::Configuration::Job>] jobs
|
8
|
+
# @param [Array<Benchmark::Driver::Configuration::Executable>] executables
|
9
|
+
# @param [Benchmark::Driver::Configuration::OutputOptions] options
|
10
|
+
def initialize(jobs:, executables:, options:)
|
11
|
+
@jobs = jobs
|
12
|
+
@executables = executables
|
13
|
+
@options = options
|
14
|
+
@results = []
|
15
|
+
@name_by_result = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def start_warming
|
19
|
+
$stdout.puts 'Warming up --------------------------------------'
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [String] name
|
23
|
+
def warming(name)
|
24
|
+
if name.length > NAME_LENGTH
|
25
|
+
$stdout.puts(name)
|
26
|
+
else
|
27
|
+
$stdout.print("%#{NAME_LENGTH}s" % name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param [Benchmark::Driver::BenchmarkResult] result
|
32
|
+
def warmup_stats(result)
|
33
|
+
$stdout.puts "#{humanize(result.ip100ms)} i/100ms"
|
34
|
+
end
|
35
|
+
|
36
|
+
def start_running
|
37
|
+
$stdout.puts 'Calculating -------------------------------------'
|
38
|
+
if @executables.size > 1
|
39
|
+
$stdout.print(' ' * NAME_LENGTH)
|
40
|
+
@executables.each do |executable|
|
41
|
+
$stdout.print(" %10s " % executable.name)
|
42
|
+
end
|
43
|
+
$stdout.puts
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def running(name)
|
48
|
+
warming(name)
|
49
|
+
@row_results = []
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param [Benchmark::Driver::BenchmarkResult] result
|
53
|
+
def benchmark_stats(result)
|
54
|
+
executable = @executables[@row_results.size]
|
55
|
+
$stdout.print("#{humanize(result.ips, [10, executable.name.length].max)} ")
|
56
|
+
|
57
|
+
@results << result
|
58
|
+
@row_results << result
|
59
|
+
if @row_results.size == @executables.size
|
60
|
+
$stdout.print("i/s - #{humanize(result.iterations)} in")
|
61
|
+
@row_results.each do |r|
|
62
|
+
$stdout.print(" %3.6fs" % r.real)
|
63
|
+
end
|
64
|
+
$stdout.puts
|
65
|
+
end
|
66
|
+
|
67
|
+
@name_by_result[result] = executable.name
|
68
|
+
end
|
69
|
+
|
70
|
+
def finish
|
71
|
+
if @results.size > 1 && @options.compare
|
72
|
+
compare
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def humanize(value, width = 10)
|
79
|
+
scale = (Math.log10(value) / 3).to_i
|
80
|
+
suffix =
|
81
|
+
case scale
|
82
|
+
when 1; 'k'
|
83
|
+
when 2; 'M'
|
84
|
+
when 3; 'B'
|
85
|
+
when 4; 'T'
|
86
|
+
when 5; 'Q'
|
87
|
+
else # < 1000 or > 10^15, no scale or suffix
|
88
|
+
scale = 0
|
89
|
+
' '
|
90
|
+
end
|
91
|
+
"%#{width}.3f#{suffix}" % (value.to_f / (1000 ** scale))
|
92
|
+
end
|
93
|
+
|
94
|
+
def compare
|
95
|
+
$stdout.puts("\nComparison:")
|
96
|
+
results = @results.sort_by { |r| -r.ips }
|
97
|
+
first = results.first
|
98
|
+
|
99
|
+
results.each do |result|
|
100
|
+
if result == first
|
101
|
+
slower = ''
|
102
|
+
else
|
103
|
+
slower = '- %.2fx slower' % (first.ips / result.ips)
|
104
|
+
end
|
105
|
+
|
106
|
+
name = result.job.name
|
107
|
+
if @executables.size > 1
|
108
|
+
name = "#{name} (#{@name_by_result.fetch(result)})"
|
109
|
+
end
|
110
|
+
$stdout.puts("%#{NAME_LENGTH}s: %11.1f i/s #{slower}" % [name, result.ips])
|
111
|
+
end
|
112
|
+
$stdout.puts
|
113
|
+
end
|
114
|
+
end
|