winr 1.0

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