winr 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.
Files changed (8) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +152 -0
  5. data/bin/winr +314 -0
  6. data/test/sum.rb +38 -0
  7. data/winr.gemspec +14 -0
  8. metadata +49 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b5b9f7f96814ddc74f0cf997f96355ca43be031355fa06c58de54421ec395ee9
4
+ data.tar.gz: 073a1747bac59f25a0eaa161fdff7ef491eca92eb917ef36094bd6cf12077f35
5
+ SHA512:
6
+ metadata.gz: 99584b189c83cc9bac6558a6e379b70f074c59f9e73f95425096d0ad557ef5b239eb2feaad9ad55c90e60b99ae03c9f23d523fb73edc0d0d37a2af2286ae6176
7
+ data.tar.gz: 4ee9ad210345e670ed3bcae76a5d08ced2dd47a1c1a61fb417d457960c73fc17e89b68a63d6f02ed71a5949b03339b6d62ac812801024b4f7290baa8fcf6528c
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Steve Shreeve
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # winr
2
+
3
+ A quick and lightweight benchmarking tool for Ruby
4
+
5
+ ## Goals
6
+
7
+ 1. Provide a simple way to benchmark code
8
+ 2. Easy to configure and compare results
9
+
10
+ ## Overview
11
+
12
+ There are several tools for benchmarking Ruby code. Unfortunately, it's hard
13
+ to remember the way to set up the benchmarks, what format to use to represent
14
+ what to test, and how to perform the testing. One of the most popular tools
15
+ used for benchmarking Ruby code is `benchmark-driver`, which was used heavily
16
+ by the Ruby 3x3 team. It uses three levels of abstraction and is powerful,
17
+ but there is a lot of inconsistency in naming conventions and using the tool.
18
+
19
+ The idea of `winr` is to offer an easy to configure benchmarking tool that is
20
+ easy to use. There are three levels of abstraction:
21
+
22
+ * environments - An environment is an optional top-level container for running
23
+ code. It can represent different versions Ruby, or different setup options
24
+ that will be shared by any nested contexts or tasks.
25
+ * contexts - A context represents an optional mid-level container for running
26
+ code. Any context configuration is shared by any contained tasks.
27
+ * tasks - A task represents a unit of work to be tested, within its optional
28
+ containing context and optional containing environment.
29
+
30
+ Within an environment, context, or task, the following can be defined:
31
+
32
+ * name - The name of the element, to use when displaying output
33
+ * begin - Code that should be run before any contained elements
34
+ * end - Code that should run after any contained elements
35
+ * script - Code that is contained within a task
36
+ * loops - The number of times that a task's script should be executed
37
+ * warmup - An optional top-level setting that is used when the number of
38
+ loops has not been defined. This value is the number of seconds to run
39
+ the task before determining the number of loops that have run. The
40
+ default value is 3 seconds, meaning that the number of loops is not
41
+ defined, the task will be allowed to run for 3 seconds and the number
42
+ of iterations during this time will be used.
43
+
44
+ ## Examples
45
+
46
+ Configuration information is stored in a Ruby file, like the following:
47
+
48
+ ```ruby
49
+ $ cat test/sum.rb
50
+
51
+ {
52
+ environments: [
53
+ {
54
+ name: "Shiny new Ruby",
55
+ command: "/opt/ruby-3.2.0/bin/ruby",
56
+ },
57
+ {
58
+ name: "Old trusted Ruby",
59
+ command: "/opt/ruby-2.7.6/bin/ruby",
60
+ },
61
+ ],
62
+ contexts: [
63
+ {
64
+ begin: <<~"|",
65
+ require "digest/md5"
66
+
67
+ max = 1e5.to_i
68
+ |
69
+ },
70
+ ],
71
+ tasks: [
72
+ {
73
+ name: "array splat",
74
+ script: <<~"|",
75
+ ary = [*1..max]
76
+ sum = ary.sum
77
+ |
78
+ },
79
+ {
80
+ name: "times",
81
+ script: <<~"|",
82
+ sum = 0
83
+ max.to_i.times {|n| sum += n }
84
+ |
85
+ },
86
+ ],
87
+ warmup: 3,
88
+ }
89
+ ```
90
+
91
+ The benchmark is run as follows:
92
+
93
+ ```
94
+ $ winr test/sum.rb
95
+
96
+ ┌──────────────────┬───────────────────────────────────────┐
97
+ │ Shiny new Ruby │ Results │
98
+ ├──────────────────┼───────────┬─────────────┬─────────────┤
99
+ │ array splat │ 2.99 s │ 768.19 i/s │ 1.30 ms/i │
100
+ │ times │ 7.86 s │ 292.33 i/s │ 3.42 ms/i │
101
+ └──────────────────┴───────────┴─────────────┴─────────────┘
102
+
103
+ ┌──────────────────┬───────────────────────────────────────┐
104
+ │ Old trusted Ruby │ Results │
105
+ ├──────────────────┼───────────┬─────────────┬─────────────┤
106
+ │ array splat │ 2.96 s │ 777.46 i/s │ 1.29 ms/i │
107
+ │ times │ 7.73 s │ 297.19 i/s │ 3.36 ms/i │
108
+ └──────────────────┴───────────┴─────────────┴─────────────┘
109
+
110
+ ┌──────────────────┬─────────────────────────────────────┐
111
+ │ Rank │ Performance │
112
+ ├──────────────────┼─────────────┬──────────────┬────────┤
113
+ │ array splat │ 777.46 i/s │ fastest │ 2/1/1 │
114
+ │ array splat │ 768.19 i/s │ 1.01x slower │ 1/1/1 │
115
+ │ times │ 297.19 i/s │ 2.62x slower │ 2/1/2 │
116
+ │ times │ 292.33 i/s │ 2.66x slower │ 1/1/2 │
117
+ └──────────────────┴─────────────┴──────────────┴────────┘
118
+ ```
119
+
120
+ ## Install
121
+
122
+ Install via `rubygems` with:
123
+
124
+ ```
125
+ gem install winr
126
+ ```
127
+
128
+ ## Options
129
+
130
+ ```
131
+ $ winr -h
132
+
133
+ usage: winr [options] <dir ...>
134
+ -c, --[no-]color Enable color output (default is true)
135
+ -d, --debug Enable debug mode
136
+ -h, --help Show help and command usage
137
+ -i, --iterations <count> Force the number of iterations for each task
138
+ -r, --reverse Show contexts vertically and tasks horizontally
139
+ -s, --stats Comma-separated list of stats (loops, time, ips, spi)
140
+ -v, --verbose Show command, version details, and markdown backticks
141
+
142
+ Available statistics:
143
+
144
+ ips iterations per second
145
+ loops number of iterations
146
+ spi seconds for iteration
147
+ time time to run all iterations
148
+ ```
149
+
150
+ ## License
151
+
152
+ This software is licensed under terms of the MIT License.
data/bin/winr ADDED
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # ============================================================================
4
+ # winr - A quick and lightweight benchmarking tool for Ruby
5
+ #
6
+ # Author: Steve Shreeve (steve.shreeve@gmail.com)
7
+ # Date: Feb 14, 2023
8
+ # ============================================================================
9
+ # GOALS:
10
+ # 1. Provide a simple way to benchmark code
11
+ # 2. Easy to configure and compare results
12
+ # 3. Accurately measure times, see http://bit.ly/3ltE7MP
13
+ #
14
+ # TODO:
15
+ # 1. Enable YAML config files
16
+ # ============================================================================
17
+
18
+ trap("INT" ) { abort "\n" }
19
+
20
+ require "erb"
21
+ require "optparse"
22
+ require "shellwords"
23
+ require "tempfile"
24
+
25
+ OptionParser.new.instance_eval do
26
+ @banner = "usage: #{program_name} [options] <dir ...>"
27
+
28
+ on "-c" , "--[no-]color", "Enable color output (default is true)", TrueClass
29
+ on "-d" , "--debug" , "Enable debug mode", TrueClass
30
+ on "-h" , "--help" , "Show help and command usage" do Kernel.abort to_s; end
31
+ on "-i <count>" , "--iterations", "Force the number of iterations for each task", Integer
32
+ on "-r" , "--reverse" , "Show contexts vertically and tasks horizontally", TrueClass
33
+ on "-s <time,ips,spi>", "--stats " , "Comma-separated list of stats (loops, time, ips, spi)"
34
+ on "-v" , "--verbose" , "Show command, version details, and markdown backticks", TrueClass
35
+
36
+ separator <<~"end"
37
+
38
+ Available statistics:
39
+
40
+ ips iterations per second
41
+ loops number of iterations
42
+ spi seconds for iteration
43
+ time time to run all iterations
44
+ end
45
+
46
+ self
47
+ end.parse!(into: opts={}) rescue abort($!.message)
48
+
49
+ # option munging
50
+ ansi = opts[:color]; ansi = true if ansi.nil?
51
+ hack = opts[:debug]
52
+ hush = !opts[:verbose]
53
+ runs = opts[:iterations]; abort "invalid number of runs" if runs && runs < 1
54
+ show = opts[:stats] || "time,ips,spi"
55
+ show = show.downcase.scan(/[a-z]+/i).uniq & %w[ ips loops spi time ]
56
+ swap = !opts[:reverse]
57
+
58
+ # option errors
59
+ show.empty? and abort "invalid list of statistics #{opts[:stats].inspect}"
60
+
61
+ # ==[ Define some constants, ansi codes, and make hashes more flexible ]==
62
+
63
+ Infinity = 1.0 / 0
64
+ Overflow = "\n\nERROR: numeric overflow"
65
+
66
+ module Ansi
67
+ refine String do
68
+ $ansi = <<~"".scan(/(\d+)(?:\/(\d+))?=(\w+)/).inject({}) do |ansi, (code, undo, name)|
69
+ 0=reset 1/22=bold 2/22=dim 3/23=italic 4/24=under 5/25=blink 7/27=inverse 9/29=strike
70
+ 30=black 31=red 32=green 33=yellow 34=blue 35=magenta 36=cyan 37=white 39=default
71
+ 40=black 41=red 42=green 43=yellow 44=blue 45=magenta 46=cyan 47=white 49=default
72
+
73
+ ansi[name + "_off" ] = undo if undo
74
+ ansi[name + (code[0,2] =~ /4\d/ ? "_bg" : "")] = code
75
+ ansi
76
+ end
77
+ def ansi (*list); list.map {|code| "\e[#{$ansi[code.to_s] || 0}m"}.join + self; end
78
+ def ansi!(*list); ansi(*list) + "\e[0m"; end
79
+ end
80
+ end
81
+
82
+ using Ansi
83
+
84
+ module FlexHash
85
+ refine Hash do
86
+ alias_method :default_lookup, :[]
87
+
88
+ def [](key, miss=nil) # method_missing calls this with key as a symbol
89
+ key?(key) and return default_lookup(key) || miss
90
+
91
+ ary = key.to_s.split(/(?:[.\/\[]|\][.\/]?)/)
92
+ val = ary.inject(self) do |obj, sub|
93
+ if obj == self then default_lookup(sub.to_sym)
94
+ elsif obj == nil then break
95
+ elsif sub =~ /\A-?\d*\z/ then obj[sub.to_i]
96
+ else obj[sub.to_sym]
97
+ end
98
+ end or miss
99
+ end
100
+ end
101
+ end
102
+
103
+ using FlexHash
104
+
105
+ class Hash
106
+ def method_missing(name, *args)
107
+ name =~ /=$/ ? send(:[]=, $`.to_sym, *args) : send(:[], name, *args)
108
+ end
109
+ end
110
+
111
+ # ==[ Templates ]==
112
+
113
+ def compile(task, path)
114
+ <<~"|".strip
115
+ #{ task.begin }
116
+
117
+ # number of loops requested
118
+ __winr_iters = #{ task.loops.to_i }
119
+
120
+ # calculate loops if not supplied
121
+ if __winr_iters == 0
122
+ __winr_until = __winr_timer + #{ $config.warmup(3) }
123
+ while __winr_timer < __winr_until
124
+ #{ task.script&.strip }
125
+ __winr_iters += 1
126
+ end
127
+ end
128
+
129
+ # calculate time wasted on loop overhead
130
+ __winr_waste = 0
131
+ __winr_loops = 0
132
+ __winr_begin = __winr_timer
133
+ while __winr_loops < __winr_iters
134
+ __winr_loops += 1
135
+ end
136
+ __winr_waste = __winr_timer - __winr_begin
137
+
138
+ # calculate time spent running our task
139
+ __winr_loops = 0
140
+ __winr_begin = __winr_timer
141
+ while __winr_loops < __winr_iters
142
+ #{ task.script&.strip }
143
+ __winr_loops += 1
144
+ end
145
+ __winr_delay = __winr_timer - __winr_begin
146
+
147
+ File.write(#{ path.inspect }, [__winr_loops, __winr_delay - __winr_waste].inspect)
148
+
149
+ #{ task.end }
150
+ |
151
+ end
152
+
153
+ # ==[ Helpers ]==
154
+
155
+ def boxlines(main, cols, runs=1)
156
+ [ "┌┬──┐",
157
+ "├┼┬─┤",
158
+ "└┴┴─┘" ]
159
+ .map do |str|
160
+ list = [main, *(cols * runs)]
161
+ list.map.with_index do |col, i|
162
+ chr = str[i < 2 ? i : (i - 1) % cols.size == 0 ? 1 : 2]
163
+ chr + str[3] * (col + 2)
164
+ end.join + str[-1]
165
+ end
166
+ end
167
+
168
+ def execute(command, path)
169
+ IO.popen(["ruby", path].join(" "), &:read)
170
+ $?.success? or raise
171
+ eval(File.read(path))
172
+ end
173
+
174
+ def scale(show, unit)
175
+ slot = 3
176
+ span = ["G", "M", "K", " ", "m", "µ", "p"]
177
+ [0, Infinity].include?(show) and abort Overflow
178
+ show *= 1000.0 and slot += 1 while show > 0 && show < 1.0
179
+ show /= 1000.0 and slot -= 1 while show >= 1000.0
180
+ slot.between?(0, 6) or abort Overflow
181
+ "%6.2f %s%s" % [show, span[slot], unit]
182
+ end
183
+
184
+ def stats(list, scope=nil)
185
+ list.map do |item|
186
+ pair = case item
187
+ when "loops" then ["runs" , "times"]
188
+ when "time" then ["time" , "s" ]
189
+ when "ips" then ["runs/time", "i/s" ]
190
+ when "spi" then ["time/runs", "s/i" ]
191
+ else abort "unknown statistic #{item.inspect}"
192
+ end
193
+ scope ? eval(pair[0], scope) : pair[1]
194
+ end
195
+ end
196
+
197
+ def write(file, data)
198
+ file.puts(data)
199
+ file.close
200
+ yield file.path
201
+ end
202
+
203
+ # ==[ Workflow ]==
204
+
205
+ # read the winr script
206
+ winr = ARGV.first or abort "missing winr script"
207
+ tmpl = ERB.new(DATA.read)
208
+
209
+ # grok the config
210
+ $config = eval(File.read(winr))
211
+ es = $config.environments || [{}]
212
+ cs = $config.contexts || [{}]
213
+ ts = $config.tasks || [{}]
214
+
215
+ # box drawing
216
+ cols = stats(show)
217
+ full = cols.map(&:size).sum + cols.size * 11 - 3
218
+ wide = [*es.map {|e| e.name("").size}, *ts.map {|t| t.name("").size}].max
219
+ rank = []
220
+
221
+ # row: top, middle, bottom
222
+ rt, rm, rb = boxlines(wide, cols.map {|e| e.size + 8 }, (swap ? cs : ts).size)
223
+
224
+ # begin output
225
+ puts "```", [$0, *ARGV].shelljoin, "" unless hush
226
+
227
+ # loop over environment(s)
228
+ es.each_with_index do |e, ei|
229
+ puts IO.popen(["ruby", "-v"].join(" "), &:read) unless hush
230
+ puts rt
231
+
232
+ command = ["/usr/bin/env ruby"]
233
+
234
+ # loop over context(s) and task(s)
235
+ ys, xs = swap ? [ts, cs] : [cs, ts]
236
+
237
+ # row: content, header
238
+ rc = "Task" # or "Context"
239
+ rh = "│ %-*.*s │" % [wide, wide, e.name(es.size > 1 ? "Env ##{ei + 1}" : rc)]
240
+ rh = xs.inject(rh) {|s, x| s << " %-*.*s │" % [full, full, x.name("Results").center(full)] }
241
+ puts rh, rm
242
+
243
+ ys.each_with_index do |y, yi|
244
+ print "│ %-*.*s │" % [wide, wide, y.name("Results")]
245
+ xs.each_with_index do |x, xi|
246
+ t, ti, c, ci = swap ? [y, yi, x, xi] : [x, xi, y, yi]
247
+ delay = Tempfile.open(['winr-', '.rb']) do |file|
248
+ t.loops = runs if runs
249
+ code = tmpl.result(binding).rstrip + "\n"
250
+ write(file, code) do |path|
251
+ runs, time = execute(command, path)
252
+ t.loops ||= runs
253
+ vals = stats(show, binding)
254
+ rank << [runs/time, ei, ci, ti]
255
+ print vals.zip(cols).map {|pair| " %s │" % scale(*pair) }.join
256
+ end
257
+ puts "", code, "=" * 78 if hack
258
+ end
259
+ end
260
+ print "\n"
261
+ end
262
+ puts rb, ""
263
+ end
264
+
265
+ # show the comparison
266
+ rank.sort! {|a, b| b[0] <=> a[0] }
267
+ fast = rank.first[0]
268
+ slow = rank.last[0]
269
+ pict = "%.2fx slower"
270
+ room = (pict % [fast / slow]).size
271
+ cols = [11, room, 6]
272
+ cols.pop if (es.size == 1) && (cs.size == 1)
273
+ full = cols.sum + (cols.size - 1) * 3
274
+ rt, rm, rb = boxlines(wide, cols)
275
+ rh = "│ %-*.*s │" % [wide, wide, "Rank"]
276
+ rh << " %-*.*s │" % [full, full, "Performance".center(full)]
277
+
278
+ puts rt, rh, rm
279
+ flip = cs.size > 1 && ts.size == 1
280
+ rank.each do |ips, ei, ci, ti|
281
+ name = (flip ? cs[ci] : ts[ti]).name
282
+ print "│ %-*.*s │ %s │ " % [wide, wide, name, scale(ips, "i/s")]
283
+ if ips.round(2) == fast.round(2)
284
+ text = "fastest".center(room)
285
+ print ansi ? text.ansi!(:green, :bold) : text
286
+ else
287
+ print "%*.*s" % [room, room, pict % [fast/ips]]
288
+ end
289
+ print " │ %-6s" % ([ei+1,ci+1,ti+1] * "/") if cols.size > 2
290
+ print " │\n"
291
+ end
292
+ puts rb
293
+ puts "```" unless hush
294
+
295
+ __END__
296
+
297
+ # ============================================================================
298
+ # Environment <%= ei + 1 %>: <%= e.name %>
299
+ # Context <%= ci + 1 %>: <%= c.name %>
300
+ # Task <%= ti + 1 %>: <%= t.name %>
301
+ # ============================================================================
302
+
303
+ trap("INT") { exit }
304
+
305
+ # def __winr_timer; Process.clock_gettime(Process::CLOCK_MONOTONIC); end
306
+ def __winr_timer; Time.now.to_f; end
307
+
308
+ <%= e.begin %>
309
+ <%= c.begin %>
310
+
311
+ <%= compile(t, file.path) %>
312
+
313
+ <%= c.end %>
314
+ <%= e.end %>
data/test/sum.rb ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ environments: [
3
+ {
4
+ name: "Shiny new Ruby",
5
+ command: "/opt/ruby-3.2.0/bin/ruby",
6
+ },
7
+ {
8
+ name: "Old trusted Ruby",
9
+ command: "/opt/ruby-2.7.6/bin/ruby",
10
+ },
11
+ ],
12
+ contexts: [
13
+ {
14
+ begin: <<~"|",
15
+ require "digest/md5"
16
+
17
+ max = 1e5.to_i
18
+ |
19
+ },
20
+ ],
21
+ tasks: [
22
+ {
23
+ name: "array splat",
24
+ script: <<~"|",
25
+ ary = [*1..max]
26
+ sum = ary.sum
27
+ |
28
+ },
29
+ {
30
+ name: "times",
31
+ script: <<~"|",
32
+ sum = 0
33
+ max.to_i.times {|n| sum += n }
34
+ |
35
+ },
36
+ ],
37
+ warmup: 3,
38
+ }
data/winr.gemspec ADDED
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "winr"
5
+ s.version = "1.0"
6
+ s.author = "Steve Shreeve"
7
+ s.email = "steve.shreeve@gmail.com"
8
+ s.summary =
9
+ s.description = "A quick and lightweight benchmarking tool for Ruby"
10
+ s.homepage = "https://github.com/shreeve/winr"
11
+ s.license = "MIT"
12
+ s.files = `git ls-files`.split("\n") - %w[.gitignore]
13
+ s.executables = `cd bin && git ls-files .`.split("\n")
14
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: winr
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Steve Shreeve
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A quick and lightweight benchmarking tool for Ruby
14
+ email: steve.shreeve@gmail.com
15
+ executables:
16
+ - winr
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - Gemfile
21
+ - LICENSE
22
+ - README.md
23
+ - bin/winr
24
+ - test/sum.rb
25
+ - winr.gemspec
26
+ homepage: https://github.com/shreeve/winr
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.4.6
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: A quick and lightweight benchmarking tool for Ruby
49
+ test_files: []