benchmark-inputs 1.0.1 → 1.1.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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +13 -0
- data/README.md +46 -41
- data/benchmark-inputs.gemspec +2 -2
- data/lib/benchmark/inputs.rb +124 -48
- data/lib/benchmark/inputs/version.rb +1 -1
- metadata +8 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a6e909d515239a8262e68c3f13b3cdb6d3840be4999b647aa1267dbd3184f69b
|
4
|
+
data.tar.gz: 37c78630c9af1f254438db2cb427e073a2bc97e2b25f7cedf4a0f73deba34d00
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 184afc9a7110ad536dce0881db8c092958986e245e0a90af9040ac70372afac583811b29765c0098b5233e7e1c637c3c10188cce44e4e4c0d48930b96e5ccf8d
|
7
|
+
data.tar.gz: 9859889f725c6849d524d6fd08cb1a579c6598f1135b7000adc4cc77cb3910899a8700ee931f3679d36b36cbd11b7c19e08350b4c03b708dc8896b4f7af0c22e
|
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1,47 +1,51 @@
|
|
1
1
|
# benchmark-inputs
|
2
2
|
|
3
|
-
Input-focused benchmarking for Ruby. Given one or more blocks and
|
4
|
-
|
5
|
-
(in invocations per second) of each block. Blocks which
|
6
|
-
quickly, as in microbenchmarks, are automatically invoked
|
7
|
-
provide accurate measurements.
|
3
|
+
Input-focused benchmarking for Ruby. Given one or more blocks and an
|
4
|
+
array of inputs to yield to each of them, benchmark-inputs will measure
|
5
|
+
the speed (in invocations per second) of each block. Blocks which
|
6
|
+
execute very quickly, as in microbenchmarks, are automatically invoked
|
7
|
+
repeatedly to provide accurate measurements.
|
8
8
|
|
9
9
|
|
10
10
|
## Motivation
|
11
11
|
|
12
12
|
I <3 [Fast Ruby][fast-ruby]. By extension, I <3 [benchmark-ips]. But,
|
13
|
-
for
|
14
|
-
way I'd like.
|
13
|
+
for some use cases, benchmark-ips doesn't let me write benchmarks the
|
14
|
+
way I'd like. Consider the following example, *using benchmark-ips*:
|
15
15
|
|
16
16
|
```ruby
|
17
|
-
require
|
17
|
+
require "benchmark/ips" ### USING benchmark-ips (NOT benchmark-inputs)
|
18
18
|
|
19
|
-
STRINGS = [
|
19
|
+
STRINGS = ["abc", "aaa", "xyz", ""]
|
20
20
|
Benchmark.ips do |job|
|
21
|
-
job.report(
|
22
|
-
job.report(
|
21
|
+
job.report("String#tr"){ STRINGS.each{|s| s.tr("a", "A") } }
|
22
|
+
job.report("String#gsub"){ STRINGS.each{|s| s.gsub(/a/, "A") } }
|
23
23
|
job.compare!
|
24
24
|
end
|
25
25
|
```
|
26
26
|
|
27
|
-
The calls to `STRINGS.each` introduce performance overhead
|
27
|
+
The calls to `STRINGS.each` introduce performance overhead that skews
|
28
28
|
the time measurements. The less time the target function takes, the
|
29
|
-
more relative overhead, and thus
|
30
|
-
|
31
|
-
|
29
|
+
more relative overhead, and thus more skew. For a microbenchmark this
|
30
|
+
can be a problem. A possible workaround is to invoke the function on
|
31
|
+
each value individually, but that is more verbose and more error-prone:
|
32
32
|
|
33
33
|
```ruby
|
34
|
-
require
|
34
|
+
require "benchmark/ips" ### USING benchmark-ips (NOT benchmark-inputs)
|
35
35
|
|
36
|
-
s1
|
36
|
+
s1, s2, s3, s4 = ["abc", "aaa", "xyz", ""]
|
37
37
|
Benchmark.ips do |job|
|
38
|
-
job.report(
|
39
|
-
s1.tr(
|
40
|
-
|
38
|
+
job.report("String#tr") do
|
39
|
+
s1.tr("a", "A")
|
40
|
+
s2.tr("a", "A")
|
41
|
+
s3.tr("a", "A")
|
42
|
+
s4.tr("a", "A")
|
41
43
|
end
|
42
|
-
job.report(
|
43
|
-
s1.gsub(/a/,
|
44
|
-
|
44
|
+
job.report("String#gsub") do
|
45
|
+
s1.gsub(/a/, "A")
|
46
|
+
s2.gsub(/a/, "A")
|
47
|
+
s3.gsub(/a/, "A")
|
48
|
+
s4.gsub(/a/, "A")
|
45
49
|
end
|
46
50
|
job.compare!
|
47
51
|
end
|
@@ -51,11 +55,11 @@ end
|
|
51
55
|
this gem: <a name="example1"></a>
|
52
56
|
|
53
57
|
```ruby
|
54
|
-
require
|
58
|
+
require "benchmark/inputs" ### USING benchmark-inputs
|
55
59
|
|
56
|
-
Benchmark.inputs([
|
57
|
-
job.report(
|
58
|
-
job.report(
|
60
|
+
Benchmark.inputs(["abc", "aaa", "xyz", ""]) do |job|
|
61
|
+
job.report("String#tr"){|s| s.tr("a", "A") }
|
62
|
+
job.report("String#gsub"){|s| s.gsub(/a/, "A") }
|
59
63
|
job.compare!
|
60
64
|
end
|
61
65
|
```
|
@@ -80,18 +84,17 @@ Destructive operations also pose a challenge for microbenchmarks. Each
|
|
80
84
|
invocation needs to operate on the same data, but `dup`ing the data
|
81
85
|
introduces too much overhead and skew.
|
82
86
|
|
83
|
-
benchmark-inputs' solution is to estimate the overhead incurred by
|
84
|
-
`dup`, and exclude that from time measurements. Because the
|
85
|
-
job already controls the input data,
|
86
|
-
|
87
|
+
benchmark-inputs' solution is to estimate the overhead incurred by each
|
88
|
+
`dup`, and exclude that from the time measurements. Because the
|
89
|
+
benchmark job already controls the input data, everything can be handled
|
90
|
+
behind the scenes. To enable this, use the `dup_inputs` option:
|
87
91
|
|
88
92
|
```ruby
|
89
|
-
require
|
93
|
+
require "benchmark/inputs"
|
90
94
|
|
91
|
-
Benchmark.inputs([
|
92
|
-
job.
|
93
|
-
job.report(
|
94
|
-
job.report('String#gsub!'){|s| s.gsub!(/a/, 'A') }
|
95
|
+
Benchmark.inputs(["abc", "aaa", "xyz", ""], dup_inputs: true) do |job|
|
96
|
+
job.report("String#tr!"){|s| s.tr!("a", "A") }
|
97
|
+
job.report("String#gsub!"){|s| s.gsub!(/a/, "A") }
|
95
98
|
job.compare!
|
96
99
|
end
|
97
100
|
```
|
@@ -111,7 +114,7 @@ Comparison:
|
|
111
114
|
|
112
115
|
That shows a slightly larger performance gap than the previous
|
113
116
|
benchmark. This makes sense because the overhead of allocating new
|
114
|
-
strings--previously via a non-bang method, but now via `dup
|
117
|
+
strings -- previously via a non-bang method, but now via `dup` -- is now
|
115
118
|
excluded from the timings. Thus, the speed of `tr!` relative to `gsub!`
|
116
119
|
is further emphasized.
|
117
120
|
|
@@ -129,13 +132,15 @@ problem.
|
|
129
132
|
|
130
133
|
## Installation
|
131
134
|
|
132
|
-
|
135
|
+
```bash
|
136
|
+
$ gem install benchmark-inputs
|
137
|
+
```
|
133
138
|
|
134
139
|
|
135
140
|
## Usage
|
136
141
|
|
137
|
-
See the [example above](#example1), or check the
|
138
|
-
[documentation](http://www.rubydoc.info/gems/benchmark-inputs).
|
142
|
+
See the [example above](#example1), or check out the
|
143
|
+
[API documentation](http://www.rubydoc.info/gems/benchmark-inputs).
|
139
144
|
|
140
145
|
|
141
146
|
## License
|
@@ -146,4 +151,4 @@ See the [example above](#example1), or check the
|
|
146
151
|
|
147
152
|
|
148
153
|
[fast-ruby]: https://github.com/JuanitoFatas/fast-ruby
|
149
|
-
[benchmark-ips]: https://
|
154
|
+
[benchmark-ips]: https://rubygems.org/gems/benchmark-ips
|
data/benchmark-inputs.gemspec
CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_development_dependency "bundler", "~> 1.12"
|
22
|
-
spec.add_development_dependency "rake", "
|
22
|
+
spec.add_development_dependency "rake", ">= 10.0"
|
23
23
|
spec.add_development_dependency "minitest", "~> 5.0"
|
24
|
-
spec.add_development_dependency "yard", "~> 0.
|
24
|
+
spec.add_development_dependency "yard", "~> 0.9"
|
25
25
|
end
|
data/lib/benchmark/inputs.rb
CHANGED
@@ -1,23 +1,45 @@
|
|
1
|
-
require
|
1
|
+
require "benchmark/inputs/version"
|
2
2
|
|
3
3
|
module Benchmark
|
4
4
|
|
5
5
|
# Initializes a benchmark job with the given inputs and yields that
|
6
6
|
# job to the given block.
|
7
7
|
#
|
8
|
-
#
|
9
|
-
# Benchmark.inputs([
|
10
|
-
# job.report(
|
11
|
-
# job.report(
|
8
|
+
# @example Benchmarking non-destructive operations
|
9
|
+
# Benchmark.inputs(["abc", "aaa", "xyz", ""]) do |job|
|
10
|
+
# job.report("String#tr"){|s| s.tr("a", "A") }
|
11
|
+
# job.report("String#gsub"){|s| s.gsub(/a/, "A") }
|
12
12
|
# job.compare!
|
13
13
|
# end
|
14
14
|
#
|
15
|
-
# @
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
|
15
|
+
# @example Benchmarking destructive operations
|
16
|
+
# Benchmark.inputs(["abc", "aaa", "xyz", ""], dup_inputs: true) do |job|
|
17
|
+
# job.report("String#tr!"){|s| s.tr!("a", "A") }
|
18
|
+
# job.report("String#gsub!"){|s| s.gsub!(/a/, "A") }
|
19
|
+
# job.compare!
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @param values [Array]
|
23
|
+
# input values to yield to each benchmark action
|
24
|
+
# @param options [Hash]
|
25
|
+
# @option options :dup_inputs [Boolean]
|
26
|
+
# whether input values will be +dup+-ed before they are passed to a
|
27
|
+
# {Inputs::Job#report} block
|
28
|
+
# @option options :sample_n [Integer]
|
29
|
+
# number of samples to take when benchmarking
|
30
|
+
# @option options :sample_dt [Integer]
|
31
|
+
# approximate duration of time (in nanoseconds) each sample should
|
32
|
+
# take when benchmarking
|
33
|
+
# @yield [job]
|
34
|
+
# configures job and runs benchmarks
|
35
|
+
# @yieldparam job [Benchmark::Inputs::Job]
|
36
|
+
# benchmark runner
|
37
|
+
# @return [Benchmark::Inputs::Job]
|
38
|
+
# benchmark runner
|
39
|
+
# @raise [ArgumentError]
|
40
|
+
# if +values+ is empty
|
41
|
+
def self.inputs(values, **options)
|
42
|
+
job = Inputs::Job.new(values, options)
|
21
43
|
yield job
|
22
44
|
job
|
23
45
|
end
|
@@ -29,46 +51,78 @@ module Benchmark
|
|
29
51
|
NS_PER_MS = NS_PER_S / 1_000
|
30
52
|
|
31
53
|
class Job
|
32
|
-
attr_accessor :sample_n, :sample_dt
|
33
|
-
attr_reader :dup_inputs, :reports
|
34
54
|
|
35
|
-
|
55
|
+
# @param inputs [Array]
|
56
|
+
# input values to yield to each benchmark action
|
57
|
+
# @param dup_inputs [Boolean]
|
58
|
+
# whether input values will be +dup+-ed before they are passed
|
59
|
+
# to a {report} block
|
60
|
+
# @param sample_n [Integer]
|
61
|
+
# number of samples to take when benchmarking
|
62
|
+
# @param sample_dt [Integer]
|
63
|
+
# approximate duration of time (in nanoseconds) each sample
|
64
|
+
# should take when benchmarking
|
65
|
+
# @raise [ArgumentError]
|
66
|
+
# if +inputs+ is empty
|
67
|
+
def initialize(inputs, dup_inputs: false, sample_n: 10, sample_dt: NS_PER_MS * 200)
|
68
|
+
raise ArgumentError, "No inputs specified" if inputs.empty?
|
69
|
+
|
36
70
|
@inputs = inputs
|
37
|
-
@dup_inputs =
|
38
|
-
@sample_n =
|
39
|
-
@sample_dt =
|
71
|
+
@dup_inputs = dup_inputs
|
72
|
+
@sample_n = sample_n
|
73
|
+
@sample_dt = sample_dt
|
40
74
|
@reports = []
|
41
75
|
def_bench!
|
42
76
|
end
|
43
77
|
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
78
|
+
# Indicates whether input values will be +dup+-ed before they are
|
79
|
+
# passed to a {report} block. Defaults to +false+. This should
|
80
|
+
# be set to +true+ if {report} blocks destructively modify their
|
47
81
|
# arguments.
|
48
82
|
#
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
#
|
53
|
-
# job.report('String#gsub!'){|s| s.gsub!(/a/, 'A') }
|
54
|
-
# job.compare!
|
55
|
-
# end
|
83
|
+
# @return [Boolean]
|
84
|
+
attr_reader :dup_inputs
|
85
|
+
|
86
|
+
# See {dup_inputs}.
|
56
87
|
#
|
57
|
-
# @param [Boolean]
|
58
|
-
|
59
|
-
|
88
|
+
# @param flag [Boolean]
|
89
|
+
# @return [Boolean]
|
90
|
+
def dup_inputs=(flag)
|
91
|
+
@dup_inputs = flag
|
60
92
|
def_bench!
|
61
93
|
@dup_inputs
|
62
94
|
end
|
63
95
|
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
|
96
|
+
# The number of samples to take when benchmarking. Defaults to 10.
|
97
|
+
#
|
98
|
+
# @return [Integer]
|
99
|
+
attr_accessor :sample_n
|
100
|
+
|
101
|
+
# The approximate duration of time (in nanoseconds) each sample
|
102
|
+
# should take when benchmarking. Defaults to 200,000 nanoseconds.
|
103
|
+
#
|
104
|
+
# @return [Integer]
|
105
|
+
attr_accessor :sample_dt
|
106
|
+
|
107
|
+
# Array of benchmark reports. Each call to {report} adds an
|
108
|
+
# element to this array.
|
68
109
|
#
|
69
|
-
# @
|
70
|
-
|
71
|
-
|
110
|
+
# @return [Array<Benchmark::Inputs::Report>]
|
111
|
+
attr_reader :reports
|
112
|
+
|
113
|
+
# Benchmarks the given block using the previously provided input
|
114
|
+
# values. If {dup_inputs} is set to true, each input value is
|
115
|
+
# +dup+'d before being passed to the block. The block's
|
116
|
+
# iterations per second (i/s) is printed to +$stdout+, and a
|
117
|
+
# {Report} is added to {reports}.
|
118
|
+
#
|
119
|
+
# @param label [String]
|
120
|
+
# label for the benchmark
|
121
|
+
# @yield [input]
|
122
|
+
# action to benchmark
|
123
|
+
# @yieldparam input [Object]
|
124
|
+
# one of the initially provided input values
|
125
|
+
# @return [void]
|
72
126
|
def report(label)
|
73
127
|
# estimate repititions
|
74
128
|
reps = 1
|
@@ -91,13 +145,14 @@ module Benchmark
|
|
91
145
|
$stdout.puts(r.label)
|
92
146
|
$stdout.printf(" %.1f i/s (\u00B1%.2f%%)\n", r.ips, r.stddev / r.ips * 100)
|
93
147
|
@reports << r
|
94
|
-
r
|
95
148
|
end
|
96
149
|
|
97
150
|
# Prints the relative speeds (from fastest to slowest) of all
|
98
|
-
#
|
151
|
+
# {report}-ed benchmarks to +$stdout+.
|
152
|
+
#
|
153
|
+
# @return [void]
|
99
154
|
def compare!
|
100
|
-
return $stdout.puts(
|
155
|
+
return $stdout.puts("Nothing to compare!") if @reports.empty?
|
101
156
|
|
102
157
|
@reports.sort_by!{|r| -r.ips }
|
103
158
|
@reports.each{|r| r.slower_than!(@reports.first) }
|
@@ -109,9 +164,9 @@ module Benchmark
|
|
109
164
|
@reports.each_with_index do |r, i|
|
110
165
|
$stdout.printf(format, r.label, r.ips)
|
111
166
|
if r.ratio
|
112
|
-
$stdout.printf(
|
167
|
+
$stdout.printf(" - %.2fx slower", r.ratio)
|
113
168
|
elsif i > 0
|
114
|
-
$stdout.printf(
|
169
|
+
$stdout.printf(" - same-ish: difference falls within error")
|
115
170
|
end
|
116
171
|
$stdout.puts
|
117
172
|
end
|
@@ -123,12 +178,11 @@ module Benchmark
|
|
123
178
|
def def_bench!
|
124
179
|
assigns = @inputs.each_index.map do |i|
|
125
180
|
"x#{i} = @inputs[#{i}]"
|
126
|
-
end.join(
|
181
|
+
end.join(";")
|
127
182
|
|
128
|
-
yields = @inputs.
|
129
|
-
|
130
|
-
|
131
|
-
end.join(';')
|
183
|
+
yields = @inputs.each_index.map do |i|
|
184
|
+
dup_inputs ? "yield(x#{i}.dup)" : "yield(x#{i})"
|
185
|
+
end.join(";")
|
132
186
|
|
133
187
|
code = <<-CODE
|
134
188
|
def bench(reps)
|
@@ -151,8 +205,21 @@ module Benchmark
|
|
151
205
|
|
152
206
|
|
153
207
|
class Report
|
154
|
-
|
208
|
+
# The label for the report.
|
209
|
+
#
|
210
|
+
# @return [String]
|
211
|
+
attr_reader :label
|
212
|
+
|
213
|
+
# The ratio of iterations per second for this report compared to
|
214
|
+
# the fastest report. Will be +nil+ if the difference between the
|
215
|
+
# two falls within the combined measurement error.
|
216
|
+
#
|
217
|
+
# This value is set by {Benchmark::Inputs::Job#compare!}.
|
218
|
+
#
|
219
|
+
# @return [Float, nil]
|
220
|
+
attr_reader :ratio
|
155
221
|
|
222
|
+
# @!visibility private
|
156
223
|
def initialize(label, invocs_per_sample)
|
157
224
|
@label = label.to_s
|
158
225
|
@invocs_per_sample = invocs_per_sample.to_f
|
@@ -163,6 +230,7 @@ module Benchmark
|
|
163
230
|
@m2 = 0.0
|
164
231
|
end
|
165
232
|
|
233
|
+
# @!visibility private
|
166
234
|
def add_sample(time_ns)
|
167
235
|
sample_ips = @invocs_per_sample * NS_PER_S / time_ns
|
168
236
|
|
@@ -175,18 +243,26 @@ module Benchmark
|
|
175
243
|
@stddev = nil
|
176
244
|
end
|
177
245
|
|
246
|
+
# The estimated iterations per second for the report.
|
247
|
+
#
|
248
|
+
# @return [Float]
|
178
249
|
def ips
|
179
250
|
@mean
|
180
251
|
end
|
181
252
|
|
253
|
+
# The {ips} standard deviation.
|
254
|
+
#
|
255
|
+
# @return [Float]
|
182
256
|
def stddev
|
183
257
|
@stddev ||= @n < 2 ? 0.0 : Math.sqrt(@m2 / (@n - 1))
|
184
258
|
end
|
185
259
|
|
260
|
+
# @!visibility private
|
186
261
|
def slower_than!(faster)
|
187
262
|
@ratio = overlap?(faster) ? nil : (faster.ips / self.ips)
|
188
263
|
end
|
189
264
|
|
265
|
+
# @!visibility private
|
190
266
|
def overlap?(faster)
|
191
267
|
(faster.ips - faster.stddev) <= (self.ips + self.stddev)
|
192
268
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: benchmark-inputs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Hefner
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -28,14 +28,14 @@ dependencies:
|
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '10.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0.
|
61
|
+
version: '0.9'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '0.
|
68
|
+
version: '0.9'
|
69
69
|
description:
|
70
70
|
email:
|
71
71
|
- jonathan.hefner@gmail.com
|
@@ -75,6 +75,7 @@ extra_rdoc_files: []
|
|
75
75
|
files:
|
76
76
|
- ".gitignore"
|
77
77
|
- ".travis.yml"
|
78
|
+
- CHANGELOG.md
|
78
79
|
- Gemfile
|
79
80
|
- LICENSE.txt
|
80
81
|
- README.md
|
@@ -101,10 +102,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
102
|
- !ruby/object:Gem::Version
|
102
103
|
version: '0'
|
103
104
|
requirements: []
|
104
|
-
|
105
|
-
rubygems_version: 2.4.8
|
105
|
+
rubygems_version: 3.0.1
|
106
106
|
signing_key:
|
107
107
|
specification_version: 4
|
108
108
|
summary: Input-focused benchmarking
|
109
109
|
test_files: []
|
110
|
-
has_rdoc:
|