benchmark_driver_monotonic_raw 0.14.13
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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +357 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +386 -0
- data/Rakefile +9 -0
- data/benchmark-driver/.gitignore +12 -0
- data/benchmark-driver/CODE_OF_CONDUCT.md +74 -0
- data/benchmark-driver/Gemfile +6 -0
- data/benchmark-driver/LICENSE.txt +21 -0
- data/benchmark-driver/README.md +8 -0
- data/benchmark-driver/Rakefile +1 -0
- data/benchmark-driver/benchmark-driver.gemspec +21 -0
- data/benchmark-driver/bin/console +14 -0
- data/benchmark-driver/bin/setup +8 -0
- data/benchmark-driver/lib/benchmark-driver.rb +1 -0
- data/benchmark-driver/lib/benchmark/driver.rb +1 -0
- data/benchmark_driver.gemspec +28 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/exe/benchmark-driver +118 -0
- data/images/optcarrot.png +0 -0
- data/lib/benchmark_driver.rb +14 -0
- data/lib/benchmark_driver/bulk_output.rb +59 -0
- data/lib/benchmark_driver/config.rb +59 -0
- data/lib/benchmark_driver/default_job.rb +29 -0
- data/lib/benchmark_driver/default_job_parser.rb +91 -0
- data/lib/benchmark_driver/job_parser.rb +55 -0
- data/lib/benchmark_driver/metric.rb +79 -0
- data/lib/benchmark_driver/output.rb +88 -0
- data/lib/benchmark_driver/output/compare.rb +216 -0
- data/lib/benchmark_driver/output/markdown.rb +107 -0
- data/lib/benchmark_driver/output/record.rb +61 -0
- data/lib/benchmark_driver/output/simple.rb +103 -0
- data/lib/benchmark_driver/rbenv.rb +25 -0
- data/lib/benchmark_driver/repeater.rb +52 -0
- data/lib/benchmark_driver/ruby_interface.rb +83 -0
- data/lib/benchmark_driver/runner.rb +103 -0
- data/lib/benchmark_driver/runner/command_stdout.rb +118 -0
- data/lib/benchmark_driver/runner/ips.rb +259 -0
- data/lib/benchmark_driver/runner/memory.rb +150 -0
- data/lib/benchmark_driver/runner/once.rb +118 -0
- data/lib/benchmark_driver/runner/recorded.rb +73 -0
- data/lib/benchmark_driver/runner/ruby_stdout.rb +146 -0
- data/lib/benchmark_driver/runner/time.rb +20 -0
- data/lib/benchmark_driver/struct.rb +98 -0
- data/lib/benchmark_driver/version.rb +3 -0
- metadata +150 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'benchmark_driver/struct'
|
2
|
+
require 'benchmark_driver/metric'
|
3
|
+
require 'benchmark_driver/default_job'
|
4
|
+
require 'benchmark_driver/default_job_parser'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'shellwords'
|
7
|
+
|
8
|
+
# Run only once, for testing
|
9
|
+
class BenchmarkDriver::Runner::Once
|
10
|
+
METRIC = BenchmarkDriver::Metric.new(name: 'Iteration per second', unit: 'i/s')
|
11
|
+
|
12
|
+
# JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job"
|
13
|
+
Job = Class.new(BenchmarkDriver::DefaultJob)
|
14
|
+
# Dynamically fetched and used by `BenchmarkDriver::JobParser.parse`
|
15
|
+
JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
|
16
|
+
|
17
|
+
# @param [BenchmarkDriver::Config::RunnerConfig] config
|
18
|
+
# @param [BenchmarkDriver::Output] output
|
19
|
+
# @param [BenchmarkDriver::Context] contexts
|
20
|
+
def initialize(config:, output:, contexts:)
|
21
|
+
@config = config
|
22
|
+
@output = output
|
23
|
+
@contexts = contexts
|
24
|
+
end
|
25
|
+
|
26
|
+
# This method is dynamically called by `BenchmarkDriver::JobRunner.run`
|
27
|
+
# @param [Array<BenchmarkDriver::Default::Job>] jobs
|
28
|
+
def run(jobs)
|
29
|
+
jobs = jobs.map do |job|
|
30
|
+
Job.new(job.to_h.merge(loop_count: 1)) # to show this on output
|
31
|
+
end
|
32
|
+
|
33
|
+
@output.with_benchmark do
|
34
|
+
jobs.each do |job|
|
35
|
+
@output.with_job(name: job.name) do
|
36
|
+
job.runnable_contexts(@contexts).each do |context|
|
37
|
+
duration = run_benchmark(job, context: context) # no repeat support
|
38
|
+
if BenchmarkDriver::Result::ERROR.equal?(duration)
|
39
|
+
value = BenchmarkDriver::Result::ERROR
|
40
|
+
else
|
41
|
+
value = 1.0 / duration
|
42
|
+
end
|
43
|
+
|
44
|
+
@output.with_context(name: context.name, executable: context.executable, gems: context.gems, prelude: context.prelude) do
|
45
|
+
@output.report(values: { METRIC => value }, duration: duration, loop_count: 1)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# @param [BenchmarkDriver::Runner::Ips::Job] job - loop_count is not nil
|
56
|
+
# @param [BenchmarkDriver::Context] context
|
57
|
+
# @return [Float] duration
|
58
|
+
def run_benchmark(job, context:)
|
59
|
+
benchmark = BenchmarkScript.new(
|
60
|
+
preludes: [context.prelude, job.prelude],
|
61
|
+
script: job.script,
|
62
|
+
teardown: job.teardown,
|
63
|
+
loop_count: job.loop_count,
|
64
|
+
)
|
65
|
+
|
66
|
+
Tempfile.open(['benchmark_driver-', '.rb']) do |f|
|
67
|
+
with_script(benchmark.render(result: f.path)) do |path|
|
68
|
+
IO.popen([*context.executable.command, path], &:read) # TODO: print stdout if verbose=2
|
69
|
+
if $?.success?
|
70
|
+
Float(f.read)
|
71
|
+
else
|
72
|
+
BenchmarkDriver::Result::ERROR
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def with_script(script)
|
79
|
+
if @config.verbose >= 2
|
80
|
+
sep = '-' * 30
|
81
|
+
$stdout.puts "\n\n#{sep}[Script begin]#{sep}\n#{script}#{sep}[Script end]#{sep}\n\n"
|
82
|
+
end
|
83
|
+
|
84
|
+
Tempfile.open(['benchmark_driver-', '.rb']) do |f|
|
85
|
+
f.puts script
|
86
|
+
f.close
|
87
|
+
return yield(f.path)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def execute(*args)
|
92
|
+
output = IO.popen(args, err: [:child, :out], &:read) # handle stdout?
|
93
|
+
unless $?.success?
|
94
|
+
raise "Failed to execute: #{args.shelljoin} (status: #{$?.exitstatus})"
|
95
|
+
end
|
96
|
+
output
|
97
|
+
end
|
98
|
+
|
99
|
+
# @param [String] prelude
|
100
|
+
# @param [String] script
|
101
|
+
# @param [String] teardown
|
102
|
+
# @param [Integer] loop_count
|
103
|
+
BenchmarkScript = ::BenchmarkDriver::Struct.new(:preludes, :script, :teardown, :loop_count) do
|
104
|
+
# @param [String] result - A file to write result
|
105
|
+
def render(result:)
|
106
|
+
prelude = preludes.reject(&:nil?).reject(&:empty?).join("\n")
|
107
|
+
<<-RUBY
|
108
|
+
#{prelude}
|
109
|
+
__bmdv_before = Time.now
|
110
|
+
#{script}
|
111
|
+
__bmdv_after = Time.now
|
112
|
+
File.write(#{result.dump}, (__bmdv_after - __bmdv_before).inspect)
|
113
|
+
#{teardown}
|
114
|
+
RUBY
|
115
|
+
end
|
116
|
+
end
|
117
|
+
private_constant :BenchmarkScript
|
118
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'benchmark_driver/struct'
|
2
|
+
require 'benchmark_driver/metric'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
# Run only once, for testing
|
7
|
+
class BenchmarkDriver::Runner::Recorded
|
8
|
+
# JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job"
|
9
|
+
Job = ::BenchmarkDriver::Struct.new(
|
10
|
+
:name, # @param [String] name - This is mandatory for all runner
|
11
|
+
:metrics, # @param [Array<BenchmarkDriver::Metric>]
|
12
|
+
:warmup_results, # @param [Hash{ BenchmarkDriver::Context => Array<BenchmarkDriver::Metric> } }]
|
13
|
+
:benchmark_results, # @param [Hash{ BenchmarkDriver::Context => Array<BenchmarkDriver::Metric> } }]
|
14
|
+
:contexts, # @param [Array<BenchmarkDriver::Context>]
|
15
|
+
)
|
16
|
+
# Dynamically fetched and used by `BenchmarkDriver::JobParser.parse`
|
17
|
+
class << JobParser = Module.new
|
18
|
+
# @param [Hash{ BenchmarkDriver::Job => Hash{ TrueClass,FalseClass => Hash{ BenchmarkDriver::Context => BenchmarkDriver::Result } } }] job_warmup_context_result
|
19
|
+
# @param [BenchmarkDriver::Metrics::Type] metrics
|
20
|
+
def parse(job_warmup_context_result:, metrics:)
|
21
|
+
job_warmup_context_result.map do |job, warmup_context_result|
|
22
|
+
Job.new(
|
23
|
+
name: job.name,
|
24
|
+
warmup_results: warmup_context_result.fetch(true, {}),
|
25
|
+
benchmark_results: warmup_context_result.fetch(false, {}),
|
26
|
+
metrics: metrics,
|
27
|
+
contexts: warmup_context_result.values.map(&:keys).flatten!.tap(&:uniq!),
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param [BenchmarkDriver::Config::RunnerConfig] config
|
34
|
+
# @param [BenchmarkDriver::Output] output
|
35
|
+
# @param [BenchmarkDriver::Context] contexts
|
36
|
+
def initialize(config:, output:, contexts:)
|
37
|
+
@config = config
|
38
|
+
@output = output
|
39
|
+
@contexts = contexts
|
40
|
+
end
|
41
|
+
|
42
|
+
# This method is dynamically called by `BenchmarkDriver::JobRunner.run`
|
43
|
+
# @param [Array<BenchmarkDriver::Runner::Recorded::Job>] record
|
44
|
+
def run(records)
|
45
|
+
records.each do |record|
|
46
|
+
unless record.warmup_results.empty?
|
47
|
+
# TODO:
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@output.with_benchmark do
|
52
|
+
records.each do |record|
|
53
|
+
@output.with_job(name: record.name) do
|
54
|
+
record.benchmark_results.each do |context, result|
|
55
|
+
@output.with_context(
|
56
|
+
name: context.name,
|
57
|
+
executable: context.executable,
|
58
|
+
gems: context.gems,
|
59
|
+
prelude: context.prelude,
|
60
|
+
) do
|
61
|
+
@output.report(
|
62
|
+
values: result.values,
|
63
|
+
duration: result.duration,
|
64
|
+
loop_count: result.loop_count,
|
65
|
+
environment: result.environment,
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'benchmark_driver/struct'
|
2
|
+
require 'benchmark_driver/metric'
|
3
|
+
require 'tempfile'
|
4
|
+
require 'shellwords'
|
5
|
+
require 'open3'
|
6
|
+
|
7
|
+
# Use stdout of ruby command
|
8
|
+
class BenchmarkDriver::Runner::RubyStdout
|
9
|
+
# JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job"
|
10
|
+
Job = ::BenchmarkDriver::Struct.new(
|
11
|
+
:name, # @param [String] name - This is mandatory for all runner
|
12
|
+
:metrics, # @param [Array<BenchmarkDriver::Metric>]
|
13
|
+
:command, # @param [Array<String>]
|
14
|
+
:working_directory, # @param [String,NilClass]
|
15
|
+
:value_from_stdout, # @param [String]
|
16
|
+
:environment_from_stdout # @param [Hash{ String => String }]
|
17
|
+
)
|
18
|
+
# Dynamically fetched and used by `BenchmarkDriver::JobParser.parse`
|
19
|
+
class << JobParser = Module.new
|
20
|
+
# @param [String] name
|
21
|
+
# @param [String] command
|
22
|
+
# @param [String,NilClass] working_directory
|
23
|
+
# @param [Hash] metrics_type
|
24
|
+
# @param [String] stdout_to_metrics
|
25
|
+
def parse(name:, command:, working_directory: nil, metrics:, environment: {})
|
26
|
+
unless metrics.is_a?(Hash)
|
27
|
+
raise ArgumentError.new("metrics must be Hash, but got #{metrics.class}")
|
28
|
+
end
|
29
|
+
if metrics.size == 0
|
30
|
+
raise ArgumentError.new('At least one metric must be specified"')
|
31
|
+
elsif metrics.size != 1
|
32
|
+
raise NotImplementedError.new('Having multiple metrics is not supported yet')
|
33
|
+
end
|
34
|
+
|
35
|
+
metric, value_from_stdout = parse_metric(*metrics.first)
|
36
|
+
environment_from_stdout = Hash[environment.map { |k, v| [k, parse_environment(v)] }]
|
37
|
+
|
38
|
+
Job.new(
|
39
|
+
name: name,
|
40
|
+
command: command.shellsplit,
|
41
|
+
working_directory: working_directory,
|
42
|
+
metrics: [metric],
|
43
|
+
value_from_stdout: value_from_stdout,
|
44
|
+
environment_from_stdout: environment_from_stdout,
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def parse_metric(name, unit:, from_stdout:, larger_better: true, worse_word: 'slower')
|
51
|
+
metric = BenchmarkDriver::Metric.new(
|
52
|
+
name: name,
|
53
|
+
unit: unit,
|
54
|
+
larger_better: larger_better,
|
55
|
+
worse_word: worse_word,
|
56
|
+
)
|
57
|
+
[metric, from_stdout]
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_environment(from_stdout:)
|
61
|
+
from_stdout
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param [BenchmarkDriver::Config::RunnerConfig] config
|
66
|
+
# @param [BenchmarkDriver::Output] output
|
67
|
+
# @param [BenchmarkDriver::Context] contexts
|
68
|
+
def initialize(config:, output:, contexts:)
|
69
|
+
@config = config
|
70
|
+
@output = output
|
71
|
+
@contexts = contexts
|
72
|
+
end
|
73
|
+
|
74
|
+
# This method is dynamically called by `BenchmarkDriver::JobRunner.run`
|
75
|
+
# @param [Array<BenchmarkDriver::Default::Job>] jobs
|
76
|
+
def run(jobs)
|
77
|
+
metric = jobs.first.metrics.first
|
78
|
+
|
79
|
+
@output.with_benchmark do
|
80
|
+
jobs.each do |job|
|
81
|
+
@output.with_job(name: job.name) do
|
82
|
+
@contexts.each do |context|
|
83
|
+
exec = context.executable
|
84
|
+
repeat_params = { config: @config, larger_better: metric.larger_better }
|
85
|
+
value, environment = BenchmarkDriver::Repeater.with_repeat(repeat_params) do
|
86
|
+
stdout = with_chdir(job.working_directory) do
|
87
|
+
with_ruby_prefix(exec) { execute(*exec.command, *job.command) }
|
88
|
+
end
|
89
|
+
script = StdoutToMetrics.new(
|
90
|
+
stdout: stdout,
|
91
|
+
value_from_stdout: job.value_from_stdout,
|
92
|
+
environment_from_stdout: job.environment_from_stdout,
|
93
|
+
)
|
94
|
+
[script.value, script.environment]
|
95
|
+
end
|
96
|
+
|
97
|
+
@output.with_context(name: exec.name, executable: exec) do
|
98
|
+
@output.report(values: { metric => value }, environment: environment)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def with_ruby_prefix(executable, &block)
|
109
|
+
env = ENV.to_h.dup
|
110
|
+
ENV['PATH'] = "#{File.dirname(executable.command.first)}:#{ENV['PATH']}"
|
111
|
+
block.call
|
112
|
+
ensure
|
113
|
+
ENV.replace(env)
|
114
|
+
end
|
115
|
+
|
116
|
+
def with_chdir(working_directory, &block)
|
117
|
+
if working_directory
|
118
|
+
Dir.chdir(working_directory) { block.call }
|
119
|
+
else
|
120
|
+
block.call
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def execute(*args)
|
125
|
+
stdout, stderr, status = Open3.capture3(*args)
|
126
|
+
unless status.success?
|
127
|
+
raise "Failed to execute: #{args.shelljoin} (status: #{$?.exitstatus}):\n[stdout]:\n#{stdout}\n[stderr]:\n#{stderr}"
|
128
|
+
end
|
129
|
+
stdout
|
130
|
+
end
|
131
|
+
|
132
|
+
StdoutToMetrics = ::BenchmarkDriver::Struct.new(:stdout, :value_from_stdout, :environment_from_stdout) do
|
133
|
+
def value
|
134
|
+
eval(value_from_stdout, binding)
|
135
|
+
end
|
136
|
+
|
137
|
+
def environment
|
138
|
+
ret = {}
|
139
|
+
environment_from_stdout.each do |name, from_stdout|
|
140
|
+
ret[name] = eval(from_stdout, binding)
|
141
|
+
end
|
142
|
+
ret
|
143
|
+
end
|
144
|
+
end
|
145
|
+
private_constant :StdoutToMetrics
|
146
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'benchmark_driver/runner/ips'
|
2
|
+
|
3
|
+
class BenchmarkDriver::Runner::Time < BenchmarkDriver::Runner::Ips
|
4
|
+
METRIC = BenchmarkDriver::Metric.new(name: 'Execution time', unit: 's', larger_better: false)
|
5
|
+
|
6
|
+
# JobParser returns this, `BenchmarkDriver::Runner.runner_for` searches "*::Job"
|
7
|
+
Job = Class.new(BenchmarkDriver::DefaultJob)
|
8
|
+
# Dynamically fetched and used by `BenchmarkDriver::JobParser.parse`
|
9
|
+
JobParser = BenchmarkDriver::DefaultJobParser.for(klass: Job, metrics: [METRIC])
|
10
|
+
|
11
|
+
# Overriding BenchmarkDriver::Runner::Ips#metric
|
12
|
+
def metric
|
13
|
+
METRIC
|
14
|
+
end
|
15
|
+
|
16
|
+
# Overriding BenchmarkDriver::Runner::Ips#value_duration
|
17
|
+
def value_duration(duration:, loop_count:)
|
18
|
+
[duration, duration]
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Extended Struct with:
|
2
|
+
# * Polyfilled `keyword_init: true`
|
3
|
+
# * Default value configuration
|
4
|
+
# * Deeply freezing members
|
5
|
+
module BenchmarkDriver
|
6
|
+
class ::Struct
|
7
|
+
SUPPORT_KEYWORD_P = begin
|
8
|
+
::Struct.new(:a, keyword_init: true)
|
9
|
+
true
|
10
|
+
rescue TypeError
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class << Struct = Module.new
|
16
|
+
# @param [Array<Symbol>] args
|
17
|
+
# @param [Hash{ Symbol => Object }] defaults
|
18
|
+
def new(*args, defaults: {}, &block)
|
19
|
+
# Polyfill `keyword_init: true`
|
20
|
+
if ::Struct::SUPPORT_KEYWORD_P
|
21
|
+
klass = ::Struct.new(*args, keyword_init: true, &block)
|
22
|
+
else
|
23
|
+
klass = keyword_init_struct(*args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Default value config
|
27
|
+
configure_defaults(klass, defaults)
|
28
|
+
|
29
|
+
# Force deeply freezing members
|
30
|
+
force_deep_freeze(klass)
|
31
|
+
|
32
|
+
klass
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# Polyfill for Ruby < 2.5.0
|
38
|
+
def keyword_init_struct(*args, &block)
|
39
|
+
::Struct.new(*args).tap do |klass|
|
40
|
+
klass.prepend(Module.new {
|
41
|
+
# @param [Hash{ Symbol => Object }] args
|
42
|
+
def initialize(**args)
|
43
|
+
args.each do |key, value|
|
44
|
+
unless members.include?(key)
|
45
|
+
raise ArgumentError.new("unknown keywords: #{key}")
|
46
|
+
next
|
47
|
+
end
|
48
|
+
|
49
|
+
public_send("#{key}=", value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
})
|
53
|
+
klass.prepend(Module.new(&block))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def configure_defaults(klass, defaults)
|
58
|
+
class << klass
|
59
|
+
attr_accessor :defaults
|
60
|
+
end
|
61
|
+
klass.defaults = defaults
|
62
|
+
|
63
|
+
klass.prepend(Module.new {
|
64
|
+
def initialize(**)
|
65
|
+
super
|
66
|
+
self.class.defaults.each do |key, value|
|
67
|
+
if public_send(key).nil?
|
68
|
+
begin
|
69
|
+
value = value.dup
|
70
|
+
rescue TypeError # for Ruby <= 2.3, like `true.dup`
|
71
|
+
end
|
72
|
+
public_send("#{key}=", value)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
})
|
77
|
+
|
78
|
+
def klass.inherited(child)
|
79
|
+
child.defaults = self.defaults
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def force_deep_freeze(klass)
|
84
|
+
klass.class_eval do
|
85
|
+
def freeze
|
86
|
+
members.each do |member|
|
87
|
+
value = public_send(member)
|
88
|
+
if value.is_a?(Array)
|
89
|
+
value.each(&:freeze)
|
90
|
+
end
|
91
|
+
value.freeze
|
92
|
+
end
|
93
|
+
super
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|