benchmark-trend 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3396e746a6b1b03c60abcfce59c39bf7ad59eb182092b6900bd5cef6d9a21b8d
4
+ data.tar.gz: b6dc71b8aa5aa02dd45d6cfe4e3c423006c0aa153573a84f1f476d13dbee5b57
5
+ SHA512:
6
+ metadata.gz: 03466650b2858047f192477c9c69e9d4f41b27ca6138acfed84f6ef2024346672b3b7d087aabd85d8259c24d858410e6b8efaa05d4669a607a70d824136295c7
7
+ data.tar.gz: 8036b5662f6392c6b13a9d8d69a03b700294e909f1496de46c8582a95fbdcbcf6d62b61e901d6af1b659e2056d5e724d185ab2c061dc3750d32c7857de9c9843
@@ -0,0 +1,7 @@
1
+ # Change log
2
+
3
+ ## [v0.1.0] - 2018-09-08
4
+
5
+ * Inital implementation and release
6
+
7
+ [v0.1.0]: https://github.com/piotrmurach/benchmark-trend/compare/v0.1.0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Piotr Murach
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,287 @@
1
+ # Benchmark::Trend
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/benchmark-trend.svg)][gem]
4
+ [![Build Status](https://secure.travis-ci.org/piotrmurach/benchmark-trend.svg?branch=master)][travis]
5
+ [![Build status](https://ci.appveyor.com/api/projects/status/798apneaa8ixg5dk?svg=true)][appveyor]
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/782faa4a8a4662c86792/maintainability)][codeclimate]
7
+ [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/benchmark-trend/badge.svg?branch=master)][coverage]
8
+ [![Inline docs](http://inch-ci.org/github/piotrmurach/benchmark-trend.svg?branch=master)][inchpages]
9
+
10
+ [gem]: http://badge.fury.io/rb/benchmark-trend
11
+ [travis]: http://travis-ci.org/piotrmurach/benchmark-trend
12
+ [appveyor]: https://ci.appveyor.com/project/piotrmurach/benchmark-trend
13
+ [codeclimate]: https://codeclimate.com/github/piotrmurach/benchmark-trend/maintainability
14
+ [coverage]: https://coveralls.io/github/piotrmurach/benchmark-trend?branch=master
15
+ [inchpages]: http://inch-ci.org/github/piotrmurach/benchmark-trend
16
+
17
+ > Measure pefromance trends of Ruby code based on the input size distribution.
18
+
19
+ **Benchmark::Trend** will help you estimate the computational complexity of Ruby code by running it on inputs increasing in size, measuring their execution times, and then fitting these observations into a model that best predicts how a given Ruby code will scale as a function of growing workload.
20
+
21
+ ## Why?
22
+
23
+ Tests provide safety net that ensures your code works correctly. What you don't know is how fast your code is! How does it scale with different input sizes? Your code may have computational complexity that doens't scale with large workloads. It would be good to know before your application goes into production, wouldn't it?
24
+
25
+ **Benchmark::Trend** will allow you to uncover performance bugs or confirm that a Ruby code performance scales as expected.
26
+
27
+ ## Installation
28
+
29
+ Add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ gem 'benchmark-trend'
33
+ ```
34
+
35
+ And then execute:
36
+
37
+ $ bundle
38
+
39
+ Or install it yourself as:
40
+
41
+ $ gem install benchmark-trend
42
+
43
+ ## Contents
44
+
45
+ * [1. Usage](#1-usage)
46
+ * [2. API](#2--api)
47
+ * [2.1 range](#21-range)
48
+ * [2.2 infer_trend](#22-infer_trend)
49
+ * [2.3 fit](#23-fit)
50
+ * [2.4 fit_at](#24-fit_at)
51
+
52
+ ## 1. Usage
53
+
54
+ Let's assume we would like to find out behaviour of a Fibonnacci algorithm:
55
+
56
+ ```ruby
57
+ def fibonacci(n)
58
+ n == 1 || n == 0 ? n : fibonacci(n - 1) + fibonacci(n - 2)
59
+ end
60
+ ```
61
+
62
+ To measure the actual complexity of above function, we will use `infer_tren` method and pass it as a first argument an array of integer sizes and a block to execute the method:
63
+
64
+ ```ruby
65
+ numbers = Benchmark::Trend.range(1, 28, ratio: 2)
66
+
67
+ trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
68
+ fibonacci(n)
69
+ end
70
+ ```
71
+
72
+ The return type will provide a best trend name:
73
+
74
+ ```ruby
75
+ print trend
76
+ # => exponential
77
+ ```
78
+
79
+ and a Hash of all the trend data:
80
+
81
+ ```ruby
82
+ print trends
83
+ # =>
84
+ # {:exponential=>
85
+ # {:trend=>"1.38 * 0.00^x",
86
+ # :slope=>1.382889711685203,
87
+ # :intercept=>3.822775903539121e-06,
88
+ # :residual=>0.9052392775178072},
89
+ # :power=>
90
+ # {:trend=>"0.00 * x^2.11",
91
+ # :slope=>2.4911044372815657e-06,
92
+ # :intercept=>2.1138475434240918,
93
+ # :residual=>0.5623418036957115},
94
+ # :linear=>
95
+ # {:trend=>"0.00 + -0.01*x",
96
+ # :slope=>0.0028434594496586007,
97
+ # :intercept=>-0.01370769842204958,
98
+ # :residual=>0.7290365425188893},
99
+ # :logarithmic=>
100
+ # {:trend=>"0.02 + -0.02*ln(x)",
101
+ # :slope=>0.01738674709454521,
102
+ # :intercept=>-0.015489004560847924,
103
+ # :residual=>0.3982368125757882}}
104
+ ```
105
+
106
+ You can see information for the best trend by passing name into trends hash:
107
+
108
+ ```ruby
109
+ print trends[trend]
110
+ # =>
111
+ # {:trend=>"1.38 * 0.00^x",
112
+ # :slope=>1.382889711685203,
113
+ # :intercept=>3.822775903539121e-06,
114
+ # :residual=>0.9052392775178072},
115
+ ```
116
+
117
+ ## 2. API
118
+
119
+ ### 2.1 range
120
+
121
+ To generate a range of values for testing code fitness use the `range` method. It will generate a geometric sequence of numbers, where intermediate values are powers of range multiplier, by default 8:
122
+
123
+ ```ruby
124
+ Benchmark::Trend.range(8, 8 << 10)
125
+ # => [8, 64, 512, 4096, 8192]
126
+ ```
127
+
128
+ You can change the default sequence power by using `:ratio` keyword:
129
+
130
+ ```ruby
131
+ Benchmark::Trend.range(8, 8 << 10, ratio: 2)
132
+ # => [8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]
133
+ ```
134
+
135
+ ### 2.2 infer_trend
136
+
137
+ To calculate an asymptotic behaviour of Rub code by inferring its computational complexity use `infer_trend`. This method takes as an argument an array of inputs which can be generated using [range](#21-range). The code to measure needs to be provided inside a block.
138
+
139
+ For example, let's assume you would like to find out asymptotic behaviour of a Fibonacci algorithm:
140
+
141
+ ```ruby
142
+ def fibonacci(n)
143
+ n == 1 || n == 0 ? n : fibonacci(n - 1) + fibonacci(n - 2)
144
+ end
145
+ ```
146
+
147
+ You could start by generating a range of inputs in powers of 2:
148
+
149
+ ```ruby
150
+ numbers = Benchmark::Trend.range(1, 32, ratio: 2)
151
+ # => [1, 2, 4, 8, 16, 32]
152
+ ```
153
+
154
+ Then measure the performance of the Fibonacci algorithm for each of the data points and fit the observations into a model to predict behaviour as a function of input size:
155
+
156
+ ```ruby
157
+ trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
158
+ fibonacci(n)
159
+ end
160
+ ```
161
+
162
+ The return includes the best fit name:
163
+
164
+ ```ruby
165
+ print trend
166
+ # => exponential
167
+ ```
168
+
169
+ And a Hash of all measurements:
170
+
171
+ ```ruby
172
+ print trends
173
+ # =>
174
+ # {:exponential=>
175
+ # {:trend=>"1.38 * 0.00^x",
176
+ # :slope=>1.382889711685203,
177
+ # :intercept=>3.822775903539121e-06,
178
+ # :residual=>0.9052392775178072},
179
+ # :power=>
180
+ # {:trend=>"0.00 * x^2.11",
181
+ # :slope=>2.4911044372815657e-06,
182
+ # :intercept=>2.1138475434240918,
183
+ # :residual=>0.5623418036957115},
184
+ # :linear=>
185
+ # {:trend=>"0.00 + -0.01*x",
186
+ # :slope=>0.0028434594496586007,
187
+ # :intercept=>-0.01370769842204958,
188
+ # :residual=>0.7290365425188893},
189
+ # :logarithmic=>
190
+ # {:trend=>"0.02 + -0.02*ln(x)",
191
+ # :slope=>0.01738674709454521,
192
+ # :intercept=>-0.015489004560847924,
193
+ # :residual=>0.3982368125757882}}
194
+ ```
195
+
196
+ In order to retrieve trend data for the best fit do:
197
+
198
+ ```ruby
199
+ print trends[trend]
200
+ # =>
201
+ # {:trend=>"1.38 * 0.00^x",
202
+ # :slope=>1.382889711685203,
203
+ # :intercept=>3.822775903539121e-06,
204
+ # :residual=>0.9052392775178072}
205
+ ```
206
+
207
+ ### 2.3 fit
208
+
209
+ Use `fit` method if you wish to fit arbitrary data into a model with a slope and intercept parameters that minimize the error.
210
+
211
+ For example, given a set of data points that exhibit linear behaviour:
212
+
213
+ ```ruby
214
+ xs = [1, 2, 3, 4, 5]
215
+ ys = xs.map { |x| 3.0 * x + 1.0 }
216
+ ```
217
+
218
+ Fit the data into a model:
219
+
220
+ ```ruby
221
+ slope, intercept, error = Benchmark::Trend.fit(xs, ys)
222
+ ```
223
+
224
+ And printing the values we get confirmation of the linear behaviour of the data points:
225
+
226
+ ```ruby
227
+ print slope
228
+ # => 3.0
229
+ print intercept
230
+ # => 1.0
231
+ print error
232
+ # => 1.0
233
+ ```
234
+
235
+ ### 2.4 fit_at
236
+
237
+ If you are interestd how a model scales for a given input use `fit_at`. This method expects that there is a fit model generated using [infer_trend](#22-infer_trend).
238
+
239
+ For example, measuring Fibonacci recursive algorithm we have the following results:
240
+
241
+ ```ruby
242
+ # =>
243
+ {:trend=>"1.38 * 0.00^x",
244
+ :slope=>1.382889711685203,
245
+ :intercept=>3.822775903539121e-06,
246
+ :residual=>0.9052392775178072}
247
+ ```
248
+
249
+ And checking model at input of `50`:
250
+
251
+ ```ruby
252
+ Benchamrk::Trend.fit_at(:exponential, slope: 1.382889711685203, intercept: 3.822775903539121e-06, n: 50)
253
+ # => 41.8558455915123
254
+ ```
255
+
256
+ We can see that Fibonacci with just a number 50 will take around 42 seconds to get the result!
257
+
258
+ How about Fibonacci with 100 as an input?
259
+
260
+ ```ruby
261
+ Benchamrk::Trend.fit_at(:exponential, slope: 1.382889711685203, intercept: 3.822775903539121e-06, n: 100)
262
+ # => 458282633.9777338
263
+ ```
264
+
265
+ This means Fibonacci recursive algorithm will take about 1.45 year to complete!
266
+
267
+ ## Development
268
+
269
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
270
+
271
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
272
+
273
+ ## Contributing
274
+
275
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/benchmark-trend. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
276
+
277
+ ## License
278
+
279
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
280
+
281
+ ## Code of Conduct
282
+
283
+ Everyone interacting in the Benchmark::Trend project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/benchmark-trend/blob/master/CODE_OF_CONDUCT.md).
284
+
285
+ ## Copyright
286
+
287
+ Copyright (c) 2018 Piotr Murach. See LICENSE for further details.
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ FileList['tasks/**/*.rake'].each(&method(:import))
4
+
5
+ desc 'Run all specs'
6
+ task ci: %w[ spec ]
7
+
8
+ task default: :spec
@@ -0,0 +1,28 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'benchmark/trend/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "benchmark-trend"
7
+ spec.version = Benchmark::Trend::VERSION
8
+ spec.authors = ["Piotr Murach"]
9
+ spec.email = [""]
10
+
11
+ spec.summary = %q{Measure pefromance trends of Ruby code based on the input size distribution.}
12
+ spec.description = %q{Benchmark::Trend will help you estimate the computational complexity of Ruby code by running it on inputs increasing in size, measuring their execution times, and then fitting these observations into a model that best predicts how a given Ruby code will scale as a function of growing workload.}
13
+ spec.homepage = "https://github.com/piotrmurach/benchmark-trend"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = Dir['{lib,spec,examples}/**/*.rb']
17
+ spec.files += Dir['{bin,exe,tasks}/*', 'benchmark-trend.gemspec']
18
+ spec.files += Dir['README.md', 'CHANGELOG.md', 'LICENSE.txt', 'Rakefile']
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.required_ruby_version = '>= 2.0.0'
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.16"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "benchmark/trend"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "benchmark/trend"
@@ -0,0 +1 @@
1
+ require_relative 'benchmark/trend'
@@ -0,0 +1,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+
5
+ require_relative 'trend/version'
6
+
7
+ module Benchmark
8
+ module Trend
9
+ # Change module function visiblity to private
10
+ #
11
+ # @api private
12
+ def self.private_module_function(method)
13
+ module_function(method)
14
+ private_class_method(method)
15
+ end
16
+
17
+ # Generate a range of inputs spaced by powers.
18
+ #
19
+ # The default range is generated in the multiples of 8.
20
+ #
21
+ # @example
22
+ # Benchmark::Trend.range(8, 8 << 10)
23
+ # # => [8, 64, 512, 4096, 8192]
24
+ #
25
+ # @param [Integer] start
26
+ # @param [Integer] limit
27
+ # @param [Integer] ratio
28
+ #
29
+ # @api public
30
+ def range(start, limit, ratio: 8)
31
+ check_greater(start, 0)
32
+ check_greater(limit, start)
33
+ check_greater(ratio, 2)
34
+
35
+ items = []
36
+ count = start
37
+ items << count
38
+ (limit / ratio).times do
39
+ count *= ratio
40
+ break if count >= limit
41
+ items << count
42
+ end
43
+ items << limit if start != limit
44
+ items
45
+ end
46
+ module_function :range
47
+
48
+ # Check if expected value is greater than minimum
49
+ #
50
+ # @param [Numeric] expected
51
+ # @param [Numeric] min
52
+ #
53
+ # @raise [ArgumentError]
54
+ #
55
+ # @api private
56
+ def check_greater(expected, min)
57
+ unless expected >= min
58
+ raise ArgumentError,
59
+ "Range value: #{expected} needs to be greater than #{min}"
60
+ end
61
+ end
62
+ private_module_function :check_greater
63
+
64
+ # Gather times for each input against an algorithm
65
+ #
66
+ # @param [Array[Numeric]] data
67
+ # the data to run measurements for
68
+ #
69
+ # @return [Array[Array, Array]]
70
+ #
71
+ # @api public
72
+ def measure_execution_time(data = nil, &work)
73
+ inputs = data || range(1, 10_000)
74
+ times = []
75
+
76
+ inputs.each do |input|
77
+ GC.start
78
+ times << ::Benchmark.realtime do
79
+ work.(input)
80
+ end
81
+ end
82
+ [inputs, times]
83
+ end
84
+ module_function :measure_execution_time
85
+
86
+ # Finds a line of best fit that approximates linear function
87
+ #
88
+ # Function form: y = ax + b
89
+ #
90
+ # @param [Array[Numeric]] xs
91
+ # the data points along X axis
92
+ #
93
+ # @param [Array[Numeric]] ys
94
+ # the data points along Y axis
95
+ #
96
+ # @return [Numeric, Numeric, Numeric]
97
+ # return a slope, b intercept and rr correlation coefficient
98
+ #
99
+ # @api public
100
+ def fit_linear(xs, ys)
101
+ fit(xs, ys)
102
+ end
103
+ module_function :fit_linear
104
+
105
+ # Find a line of best fit that approximates logarithmic function
106
+ #
107
+ # Model form: y = a*lnx + b
108
+ #
109
+ # @param [Array[Numeric]] xs
110
+ # the data points along X axis
111
+ #
112
+ # @param [Array[Numeric]] ys
113
+ # the data points along Y axis
114
+ #
115
+ # @return [Numeric, Numeric, Numeric]
116
+ # returns a, b, and rr values
117
+ #
118
+ # @api public
119
+ def fit_logarithmic(xs, ys)
120
+ fit(xs, ys, tran_x: ->(x) { Math.log(x) })
121
+ end
122
+ module_function :fit_logarithmic
123
+
124
+ alias fit_log fit_logarithmic
125
+ module_function :fit_log
126
+
127
+ # Finds a line of best fit that approxmimates power function
128
+ #
129
+ # Function form: y = ax^b
130
+ #
131
+ # @return [Numeric, Numeric, Numeric]
132
+ # returns a, b, and rr values
133
+ #
134
+ # @api public
135
+ def fit_power(xs, ys)
136
+ a, b, rr = fit(xs, ys, tran_x: ->(x) { Math.log(x) },
137
+ tran_y: ->(y) { Math.log(y) })
138
+
139
+ [Math.exp(b), a, rr]
140
+ end
141
+ module_function :fit_power
142
+
143
+ # Find a line of best fit that approximates exponential function
144
+ #
145
+ # Model form: y = ab^x
146
+ #
147
+ # @return [Numeric, Numeric, Numeric]
148
+ # returns a, b, and rr values
149
+ #
150
+ # @api public
151
+ def fit_exponential(xs, ys)
152
+ a, b, rr = fit(xs, ys, tran_y: ->(y) { Math.log(y) })
153
+
154
+ [Math.exp(a), Math.exp(b), rr]
155
+ end
156
+ module_function :fit_exponential
157
+
158
+ alias fit_exp fit_exponential
159
+ module_function :fit_exp
160
+
161
+ # Fit the performance measurements to construct a model with
162
+ # slope and intercept parameters that minimize the error.
163
+ #
164
+ # @param [Array[Numeric]] xs
165
+ # the data points along X axis
166
+ #
167
+ # @param [Array[Numeric]] ys
168
+ # the data points along Y axis
169
+ #
170
+ # @return [Array[Numeric, Numeric, Numeric]
171
+ # returns slope, intercept and model's goodness-of-fit
172
+ #
173
+ # @api public
174
+ def fit(xs, ys, tran_x: ->(x) { x }, tran_y: ->(y) { y })
175
+ eps = 0.000001
176
+ n = 0
177
+ sum_x = 0.0
178
+ sum_x2 = 0.0
179
+ sum_y = 0.0
180
+ sum_y2 = 0.0
181
+ sum_xy = 0.0
182
+
183
+ xs.zip(ys).each do |x, y|
184
+ n += 1
185
+ sum_x += tran_x.(x)
186
+ sum_y += tran_y.(y)
187
+ sum_x2 += tran_x.(x) ** 2
188
+ sum_y2 += tran_y.(y) ** 2
189
+ sum_xy += tran_x.(x) * tran_y.(y)
190
+ end
191
+
192
+ txy = n * sum_xy - sum_x * sum_y
193
+ tx = n * sum_x2 - sum_x ** 2
194
+ ty = n * sum_y2 - sum_y ** 2
195
+
196
+ if tx.abs < eps # no variation in xs
197
+ raise ArgumentError, "No variation in data #{xs}"
198
+ elsif ty.abs < eps # no variation in ys - constant fit
199
+ slope = 0
200
+ intercept = sum_y / n
201
+ residual_sq = 1 # doesn't exist
202
+ else
203
+ slope = txy / tx
204
+ intercept = (sum_y - slope * sum_x) / n
205
+ residual_sq = (txy ** 2) / (tx * ty)
206
+ end
207
+
208
+ [slope, intercept, residual_sq]
209
+ end
210
+ module_function :fit
211
+
212
+ # Take a fit and estimate behaviour at input size n
213
+ #
214
+ # @example
215
+ # fit_at(:power, slope: 1.5, intercept: 2, n: 10)
216
+ #
217
+ # @return
218
+ # fit model value for input n
219
+ #
220
+ # @api public
221
+ def fit_at(type, slope: nil, intercept: nil, n: nil)
222
+ raise ArgumentError, "Incorrect input size: #{n}" unless n > 0
223
+
224
+ case type
225
+ when :logarithmic, :log
226
+ intercept + slope * Math.log(n)
227
+ when :linear
228
+ intercept + slope * n
229
+ when :power
230
+ intercept * (n ** slope)
231
+ when :exponential, :exp
232
+ intercept * (slope ** n)
233
+ else
234
+ raise ArgumentError, "Unknown fit type: #{type}"
235
+ end
236
+ end
237
+ module_function :fit_at
238
+
239
+ # A mathematical notation template for a trend type
240
+ #
241
+ # @param [String] type
242
+ # the fit model type
243
+ #
244
+ # @return [String]
245
+ # the formatted mathematical function template
246
+ #
247
+ # @api private
248
+ def format_fit(type)
249
+ case type
250
+ when :logarithmic, :log
251
+ "%.2f + %.2f*ln(x)"
252
+ when :linear
253
+ "%.2f + %.2f*x"
254
+ when :power
255
+ "%.2f * x^%.2f"
256
+ when :exponential, :exp
257
+ "%.2f * %.2f^x"
258
+ else
259
+ raise ArgumentError, "Unknown type: '#{type}'"
260
+ end
261
+ end
262
+ module_function :format_fit
263
+
264
+ # the trends to consider
265
+ FIT_TYPES = [:exponential, :power, :linear, :logarithmic]
266
+
267
+ # Infer trend from the execution times
268
+ #
269
+ # Fits the executiom times for each range to several fit models.
270
+ #
271
+ # @yieldparam work
272
+ #
273
+ # @return [Array[Symbol, Hash]]
274
+ # the best fitting and all the trends
275
+ #
276
+ # @api public
277
+ def infer_trend(data, &work)
278
+ ns, times = *measure_execution_time(data, &work)
279
+ best_fit = :none
280
+ best_residual = 0
281
+ fitted = {}
282
+ n = ns.size.to_f
283
+ aic = -1.0/0
284
+ best_aic = -1.0/0
285
+
286
+ FIT_TYPES.each do |fit|
287
+ a, b, rr = *send(:"fit_#{fit}", ns, times)
288
+ # goodness of model
289
+ aic = n * (Math.log(Math::PI) + 1) + n * Math.log(rr / n)
290
+ fitted[fit] = { trend: format_fit(fit) % [a, b],
291
+ slope: a, intercept: b, residual: rr }
292
+ if rr > best_residual && aic > best_aic
293
+ best_residual = rr
294
+ best_fit = fit
295
+ best_aic = aic
296
+ end
297
+ end
298
+
299
+ [best_fit, fitted]
300
+ end
301
+ module_function :infer_trend
302
+ end # Trend
303
+ end # Benchmark
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Benchmark
4
+ module Trend
5
+ VERSION = "0.1.0"
6
+ end # Trend
7
+ end # Benchmark
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ if ENV['COVERAGE'] || ENV['TRAVIS']
4
+ require 'simplecov'
5
+ require 'coveralls'
6
+
7
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
8
+ SimpleCov::Formatter::HTMLFormatter,
9
+ Coveralls::SimpleCov::Formatter
10
+ ]
11
+
12
+ SimpleCov.start do
13
+ command_name 'spec'
14
+ add_filter 'spec'
15
+ end
16
+ end
17
+
18
+ require "bundler/setup"
19
+ require "benchmark/trend"
20
+
21
+ RSpec.configure do |config|
22
+ # Enable flags like --only-failures and --next-failure
23
+ config.example_status_persistence_file_path = ".rspec_status"
24
+
25
+ # Disable RSpec exposing methods globally on `Module` and `main`
26
+ config.disable_monkey_patching!
27
+
28
+ config.expect_with :rspec do |c|
29
+ c.syntax = :expect
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Benchmark::Trend, '#fit_at' do
4
+ it "evalutes logarithmic fit model at a given value" do
5
+ val = Benchmark::Trend.fit_at(:logarithmic, slope: 1.5, intercept: 2, n: 10)
6
+
7
+ expect(val).to be_within(0.1).of(5.45)
8
+ end
9
+
10
+ it "evalutes linear fit model at a given value" do
11
+ val = Benchmark::Trend.fit_at(:linear, slope: 1.5, intercept: 2, n: 10)
12
+
13
+ expect(val).to eq(17)
14
+ end
15
+
16
+ it "evalutes power fit model at a given value" do
17
+ val = Benchmark::Trend.fit_at(:power, slope: 1.5, intercept: 2, n: 10)
18
+
19
+ expect(val).to be_within(0.1).of(63.24)
20
+ end
21
+
22
+ it "evalutes power fit model at a given value" do
23
+ val = Benchmark::Trend.fit_at(:exponential, slope: 1.5, intercept: 2, n: 10)
24
+
25
+ expect(val).to be_within(0.1).of(115.33)
26
+ end
27
+
28
+ it "doesn't recognise fit model" do
29
+ expect {
30
+ Benchmark::Trend.fit_at(:unknown, slope: 1.5, intercept: 2, n: 10)
31
+ }.to raise_error(ArgumentError, "Unknown fit type: unknown")
32
+ end
33
+
34
+ it "doesn't allow incorrect input size" do
35
+ expect {
36
+ Benchmark::Trend.fit_at(:linear, slope: 1.5, intercept: 2, n: -1)
37
+ }.to raise_error(ArgumentError, "Incorrect input size: -1")
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Benchmark::Trend, '#fit_exp' do
4
+ it "calculates a perfect exponential fit" do
5
+ xs = [1, 2, 3, 4, 5]
6
+ ys = xs.map { |x| 1.5 * (2 ** x) }
7
+
8
+ a, b, rr = Benchmark::Trend.fit_exp(xs, ys)
9
+
10
+ expect(a).to be_within(0.001).of(2.0)
11
+ expect(b).to be_within(0.001).of(1.5)
12
+ expect(rr).to be_within(0.001).of(0.999)
13
+ end
14
+
15
+ it "calculates best exponential fit of y = 1.30*(1.46)^x" do
16
+ # the number y (in millions) of mobiles subscriberes from 1988 to 1997 USA
17
+ xs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
18
+ ys = [1.6, 2.7, 4.4, 6.4, 8.9, 13.1, 19.3, 28.2, 38.2, 48.7]
19
+
20
+ a, b, rr = Benchmark::Trend.fit_exp(xs, ys)
21
+
22
+ expect(a).to be_within(0.001).of(1.458)
23
+ expect(b).to be_within(0.001).of(1.300)
24
+ expect(rr).to be_within(0.001).of(0.993)
25
+ end
26
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Benchmark::Trend, '#fit_linear' do
4
+ it "calculates perfect linear fit" do
5
+ xs = [1, 2, 3, 4, 5]
6
+ ys = xs.map { |x| 3.0 * x + 1.0 }
7
+
8
+ a, b, rr = Benchmark::Trend.fit_linear(xs, ys)
9
+
10
+ expect(a).to eq(3.0)
11
+ expect(b).to eq(1.0)
12
+ expect(rr).to be_within(0.1).of(1)
13
+ end
14
+
15
+ it "calculates linear fit with noise" do
16
+ xs = [3.4, 3.8, 4.1, 2.2, 2.6, 2.9, 2.0, 2.7, 1.9, 3.4]
17
+ ys = [5.5, 5.9, 6.5, 3.3, 3.6, 4.6, 2.9, 3.6, 3.1, 4.9]
18
+
19
+ a, b, rr = Benchmark::Trend.fit_linear(xs, ys)
20
+
21
+ expect(a).to be_within(0.1).of(1.64)
22
+ expect(b).to be_within(0.1).of(-0.36)
23
+ expect(rr).to be_within(0.001).of(0.953)
24
+ end
25
+
26
+ it "calculates perfect constant fit" do
27
+ xs = [1, 2, 3, 4, 5]
28
+ ys = [6.0, 6.0, 6.0, 6.0, 6.0]
29
+
30
+ a, b, rr = Benchmark::Trend.fit_linear(xs, ys)
31
+
32
+ expect(a).to eq(0)
33
+ expect(b).to eq(6)
34
+ expect(rr).to eq(1)
35
+ end
36
+
37
+ it "calculates constant fit with noise" do
38
+ xs = [1, 2, 3, 4, 5]
39
+ ys = [1.0, 0.9, 1.0, 1.1, 1.0]
40
+
41
+ a, b, rr = Benchmark::Trend.fit_linear(xs, ys)
42
+
43
+ expect(a).to eq(0.02)
44
+ expect(b).to be_within(0.01).of(0.94)
45
+ expect(rr).to be_within(0.01).of(0.19)
46
+ end
47
+
48
+ it "raises when no variation in data" do
49
+ xs = [1, 1, 1, 1, 1]
50
+ ys = [1.0, 0.9, 1.0, 1.1, 1.0]
51
+
52
+ expect {
53
+ Benchmark::Trend.fit_linear(xs, ys)
54
+ }.to raise_error(ArgumentError, "No variation in data [1, 1, 1, 1, 1]")
55
+ end
56
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Benchmark::Trend, '#fit_log' do
4
+ it "calculates perfect logarithmic fit" do
5
+ xs = [1, 2, 3, 4, 5]
6
+ ys = xs.map { |x| 1.5 * Math.log(x) + 1.0 }
7
+
8
+ a, b, rr = Benchmark::Trend.fit_log(xs, ys)
9
+
10
+ expect(a).to be_within(0.001).of(1.5)
11
+ expect(b).to be_within(0.001).of(1.0)
12
+ expect(rr).to be_within(0.001).of(1)
13
+ end
14
+
15
+ it "calculates logarithmic fit with noise" do
16
+ # life expectancy in USA data from 1900 in 10 years periods
17
+ xs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
18
+ ys = [47.3, 50.0, 54.1, 59.7, 62.9, 68.2, 69.7, 70.8, 73.7, 75.4, 76.8, 78.7]
19
+
20
+ a, b, rr = Benchmark::Trend.fit_log(xs, ys)
21
+
22
+ expect(a).to be_within(0.001).of(13.857)
23
+ expect(b).to be_within(0.001).of(42.527)
24
+ expect(rr).to be_within(0.001).of(0.956)
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Benchmark::Trend, '#fit_power' do
4
+ it 'calculates perfect power fit' do
5
+ xs = [1, 2, 3, 4, 5]
6
+ ys = xs.map { |x| 1.5*(x ** 2) }
7
+
8
+ a, b, rr = Benchmark::Trend.fit_power(xs, ys)
9
+
10
+ expect(a).to be_within(0.001).of(1.5)
11
+ expect(b).to be_within(0.001).of(2.0)
12
+ expect(rr).to be_within(0.001).of(1.0)
13
+ end
14
+
15
+ it "calcualtes best power fit of y = x^1.5" do
16
+ # Mercury Venus Earth Mars Jupiter Saturn
17
+ xs = [0.387, 0.723, 1.00, 1.524, 5.203, 9.539] # distance from the sun
18
+ ys = [0.241, 0.615, 1.00, 1.881, 11.862, 29.458] # period in Earth's years
19
+
20
+ a, b, rr = Benchmark::Trend.fit_power(xs, ys)
21
+
22
+ expect(a).to be_within(0.001).of(1.0)
23
+ expect(b).to be_within(0.001).of(1.5)
24
+ expect(rr).to be_within(0.001).of(0.999)
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Benchmark::Trend, '#format_fit' do
4
+ it "returns a logarithmic template" do
5
+ format = Benchmark::Trend.format_fit(:logarithmic)
6
+ expect(format).to eq("%.2f + %.2f*ln(x)")
7
+ end
8
+
9
+ it "returns a linear template" do
10
+ format = Benchmark::Trend.format_fit(:linear)
11
+ expect(format).to eq("%.2f + %.2f*x")
12
+ end
13
+
14
+ it "returns a power template" do
15
+ format = Benchmark::Trend.format_fit(:power)
16
+ expect(format).to eq("%.2f * x^%.2f")
17
+ end
18
+
19
+ it "returns a exponential template" do
20
+ format = Benchmark::Trend.format_fit(:exponential)
21
+ expect(format).to eq("%.2f * %.2f^x")
22
+ end
23
+
24
+ it "fails to recognise fit type" do
25
+ expect {
26
+ Benchmark::Trend.format_fit(:unknown)
27
+ }.to raise_error(ArgumentError, "Unknown type: 'unknown'")
28
+ end
29
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Benchmark::Trend, '#infer_trend' do
4
+ # exponential
5
+ def fibonacci(n)
6
+ n == 1 || n == 0 ? n : fibonacci(n - 1) + fibonacci(n - 2)
7
+ end
8
+
9
+ # linear
10
+ def fib_mem(n, acc = {"0" => 0, "1" => 1})
11
+ return n if n < 2
12
+
13
+ if !acc.key?(n.to_s)
14
+ acc[n.to_s] = fib_mem(n - 1, acc) + fib_mem(n - 2, acc)
15
+ end
16
+ acc[n.to_s]
17
+ end
18
+
19
+ # linear
20
+ def fib_iter(n)
21
+ a, b = 0, 1
22
+ n.times { a, b = b, a + b}
23
+ a
24
+ end
25
+
26
+ # constant
27
+ def fib_const(n)
28
+ phi = (1 + Math.sqrt(5))/2
29
+ (phi ** n / Math.sqrt(5)).round
30
+ end
31
+
32
+ it "infers fibonacci classic algorithm trend to be exponential" do
33
+ numbers = Benchmark::Trend.range(1, 28, ratio: 2)
34
+ trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
35
+ fibonacci(n)
36
+ end
37
+
38
+ expect(trend).to eq(:exponential)
39
+ expect(trends).to match(
40
+ hash_including(:exponential, :power, :linear, :logarithmic))
41
+ expect(trends[:exponential]).to match(
42
+ hash_including(:trend, :slope, :intercept, :residual)
43
+ )
44
+ end
45
+
46
+ it "infers fibonacci iterative algorithm trend to be linear" do
47
+ numbers = Benchmark::Trend.range(1, 20_000)
48
+ trend, _ = Benchmark::Trend.infer_trend(numbers) do |n|
49
+ fib_iter(n)
50
+ end
51
+
52
+ expect(trend).to eq(:linear)
53
+ end
54
+
55
+ it "infers fibonacci constant algorithm trend to be linear" do
56
+ numbers = Benchmark::Trend.range(1, 500)
57
+ trend, trends = Benchmark::Trend.infer_trend(numbers) do |n|
58
+ fib_const(n)
59
+ end
60
+
61
+ expect(trend).to eq(:linear)
62
+ expect(trends[trend][:slope]).to eq(0)
63
+ end
64
+
65
+ it "infers finding maximum value trend to be linear" do
66
+ array_sizes = Benchmark::Trend.range(1, 100_000)
67
+ number_arrays = array_sizes.map { |n| Array.new(n) { rand(n) } }.each
68
+
69
+ trend, trends = Benchmark::Trend.infer_trend(array_sizes) do
70
+ number_arrays.next.max
71
+ end
72
+
73
+ expect(trend).to eq(:linear)
74
+ expect(trends).to match(
75
+ hash_including(:exponential, :power, :linear, :logarithmic))
76
+ expect(trends[:exponential]).to match(
77
+ hash_including(:trend, :slope, :intercept, :residual)
78
+ )
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ RSpec.describe Benchmark::Trend, "#measure_execution_time" do
4
+ it "measures performance times" do
5
+ func = -> (x) { x ** 2 }
6
+
7
+ data = Benchmark::Trend.measure_execution_time(&func)
8
+
9
+ expect(data[0]).to eq([1, 8, 64, 512, 4096, 10000])
10
+ expect(data[1]).to match([
11
+ be_within(0.001).of(0.00001),
12
+ be_within(0.001).of(0.00001),
13
+ be_within(0.001).of(0.00001),
14
+ be_within(0.001).of(0.00001),
15
+ be_within(0.001).of(0.00001),
16
+ be_within(0.001).of(0.00001)
17
+ ])
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Benchmark::Trend, '#range' do
4
+ it "creates default range" do
5
+ range = Benchmark::Trend.range(8, 8 << 10)
6
+ expect(range).to eq([8, 64, 512, 4096, 8192])
7
+ end
8
+
9
+ it "creates range with 2 multiplier" do
10
+ range = Benchmark::Trend.range(8, 8 << 10, ratio: 2)
11
+ expect(range).to eq([8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192])
12
+ end
13
+
14
+ it "checks range start to be valid" do
15
+ expect {
16
+ Benchmark::Trend.range(-2, 10_000)
17
+ }.to raise_error(ArgumentError, "Range value: -2 needs to be greater than 0")
18
+ end
19
+
20
+ it "checks range end to be greater than start" do
21
+ expect {
22
+ Benchmark::Trend.range(8, 2)
23
+ }.to raise_error(ArgumentError, "Range value: 2 needs to be greater than 8")
24
+ end
25
+
26
+ it "checks multiplier to be valid" do
27
+ expect {
28
+ Benchmark::Trend.range(8, 32, ratio: 1)
29
+ }.to raise_error(ArgumentError, "Range value: 1 needs to be greater than 2")
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ desc 'Load gem inside irb console'
4
+ task :console do
5
+ require 'irb'
6
+ require 'irb/completion'
7
+ require File.join(__FILE__, '../../lib/benchmark-trend')
8
+ ARGV.clear
9
+ IRB.start
10
+ end
11
+ task c: %w[ console ]
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ desc 'Measure code coverage'
4
+ task :coverage do
5
+ begin
6
+ original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true'
7
+ Rake::Task['spec'].invoke
8
+ ensure
9
+ ENV['COVERAGE'] = original
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc 'Run all specs'
7
+ RSpec::Core::RakeTask.new(:spec) do |task|
8
+ task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb'
9
+ end
10
+
11
+ namespace :spec do
12
+ desc 'Run unit specs'
13
+ RSpec::Core::RakeTask.new(:unit) do |task|
14
+ task.pattern = 'spec/unit{,/*/**}/*_spec.rb'
15
+ end
16
+
17
+ desc 'Run integration specs'
18
+ RSpec::Core::RakeTask.new(:integration) do |task|
19
+ task.pattern = 'spec/integration{,/*/**}/*_spec.rb'
20
+ end
21
+ end
22
+
23
+ rescue LoadError
24
+ %w[spec spec:unit spec:integration].each do |name|
25
+ task name do
26
+ $stderr.puts "In order to run #{name}, do `gem install rspec`"
27
+ end
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: benchmark-trend
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Murach
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-09-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Benchmark::Trend will help you estimate the computational complexity
56
+ of Ruby code by running it on inputs increasing in size, measuring their execution
57
+ times, and then fitting these observations into a model that best predicts how a
58
+ given Ruby code will scale as a function of growing workload.
59
+ email:
60
+ - ''
61
+ executables:
62
+ - bench-trend
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - CHANGELOG.md
67
+ - LICENSE.txt
68
+ - README.md
69
+ - Rakefile
70
+ - benchmark-trend.gemspec
71
+ - bin/console
72
+ - bin/setup
73
+ - exe/bench-trend
74
+ - lib/benchmark-trend.rb
75
+ - lib/benchmark/trend.rb
76
+ - lib/benchmark/trend/version.rb
77
+ - spec/spec_helper.rb
78
+ - spec/unit/fit_at_spec.rb
79
+ - spec/unit/fit_exp_spec.rb
80
+ - spec/unit/fit_linear_spec.rb
81
+ - spec/unit/fit_log_spec.rb
82
+ - spec/unit/fit_power_spec.rb
83
+ - spec/unit/format_fit_spec.rb
84
+ - spec/unit/infer_trend_spec.rb
85
+ - spec/unit/measure_execution_time_spec.rb
86
+ - spec/unit/range_spec.rb
87
+ - tasks/console.rake
88
+ - tasks/coverage.rake
89
+ - tasks/spec.rake
90
+ homepage: https://github.com/piotrmurach/benchmark-trend
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 2.0.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.7.3
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Measure pefromance trends of Ruby code based on the input size distribution.
114
+ test_files: []