benchmark-trend 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +287 -0
- data/Rakefile +8 -0
- data/benchmark-trend.gemspec +28 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/bench-trend +3 -0
- data/lib/benchmark-trend.rb +1 -0
- data/lib/benchmark/trend.rb +303 -0
- data/lib/benchmark/trend/version.rb +7 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/unit/fit_at_spec.rb +39 -0
- data/spec/unit/fit_exp_spec.rb +26 -0
- data/spec/unit/fit_linear_spec.rb +56 -0
- data/spec/unit/fit_log_spec.rb +26 -0
- data/spec/unit/fit_power_spec.rb +26 -0
- data/spec/unit/format_fit_spec.rb +29 -0
- data/spec/unit/infer_trend_spec.rb +80 -0
- data/spec/unit/measure_execution_time_spec.rb +19 -0
- data/spec/unit/range_spec.rb +31 -0
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -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
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/exe/bench-trend
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/tasks/console.rake
ADDED
data/tasks/coverage.rake
ADDED
data/tasks/spec.rake
ADDED
@@ -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: []
|