changed
.formatter.exs
|
@@ -3,6 +3,7 @@
|
3
3
|
"lib/**/*.{ex,exs}",
|
4
4
|
"test/**/*.{ex,exs}",
|
5
5
|
"mix/**/*.{ex,exs}",
|
6
|
- "./mix.exs"
|
6
|
+ "./mix.exs",
|
7
|
+ "samples/**/*.{ex,exs}"
|
7
8
|
]
|
8
9
|
]
|
changed
CHANGELOG.md
|
@@ -1,5 +1,41 @@
|
1
|
+ ## 1.0.0
|
2
|
+
|
3
|
+ It's 0.99.0 without the deprecation warnings. Specifically:
|
4
|
+
|
5
|
+ * Old way of passing formatters (`:formatter_options`) vs. new `:formatters` with modules, tuples or functions with one arg
|
6
|
+ * The configuration needs to be passed as the second argument to `Benchee.run/2`
|
7
|
+ * `Benchee.collect/1` replaces `Benchee.measure/1`
|
8
|
+ * `unit_scaling` is a top level configuration option, not for the console formatter
|
9
|
+ * the warning for memory measurements not working on OTP <= 18 will also be dropped (we already officially dropped OTP 18 support in 0.14.0)
|
10
|
+
|
11
|
+ We're aiming to follow Semantic Versioning as we go forward. That means formatters should be safe to use `~> 1.0` (or even `>= 0.99.0 and < 2.0.0`).
|
12
|
+
|
13
|
+ ## 0.99.0 (2019-03-28)
|
14
|
+
|
15
|
+ The "we're almost 1.0!" release - all the last small features, a bag of polish and deprecation warnings. If you run this release succesfully without deprecation warnings you should be safe to upgrade to 1.0.0, if not - it's a bug :)
|
16
|
+
|
17
|
+ ### Breaking Changes (User Facing)
|
18
|
+ * changed official Elixir compatibility to `~> 1.6`, 1.4+ should still work but aren't guaranteed or tested against.
|
19
|
+
|
20
|
+ ### Features (User Facing)
|
21
|
+ * the console comparison now also displays the absolute difference in the average (like +12 ms) so that you have an idea to how much time that translates to in your applications not just that it's 100x faster
|
22
|
+ * Overhaul of README, documentation, update samples etc. - a whole lot of things have also been marked `@doc false` as they're considered internal
|
23
|
+
|
24
|
+ ### Bugfixes (User Facing)
|
25
|
+ * Remove double empty line after configuration display
|
26
|
+ * Fix some wrong type specs
|
27
|
+
|
28
|
+ ### Breaking Changes (Plugins)
|
29
|
+ * `Scenario` made it to the big leagues, it's no longer `Benchee.Benchmark.Scenario` but `Benchee.Scenario` - as it is arguably one of our most important data structures.
|
30
|
+ * The `Scenario` struct had some keys changed (last time before 2.0 I promise!) - instead of `:run_times`/`:run_time_statistics` you now have one `run_time_data` key that contains `Benchee.CollectionData` which has the keys `:samples` and `:statistics`. Same for `memory_usage`. This was done to be able to handle different kinds of measurements more uniformly as we will add more of them.
|
31
|
+
|
32
|
+ ### Features (Plugins)
|
33
|
+ * `Benchee.Statistics` comes with 3 new values: `:relative_more`, `:relative_less`, `:absolute_difference` so that you don't have to calculate these relative values yourself :)
|
34
|
+
|
1
35
|
## 0.14.0 (2019-02-10)
|
2
36
|
|
37
|
+ Highlights of this release are a new way to specify formatter options closer to the formatters themselves as well as maximum precision measurements.
|
38
|
+
|
3
39
|
### Breaking Changes (User Facing)
|
4
40
|
* dropped support for Erlang 18.x
|
5
41
|
* Formatters no longer have an `output/1` method, instead use `Formatter.output/3` please
|
changed
README.md
|
@@ -1,31 +1,35 @@
|
1
|
- # Benchee [![Hex Version](https://img.shields.io/hexpm/v/benchee.svg)](https://hex.pm/packages/benchee) [![docs](https://img.shields.io/badge/docs-hexpm-blue.svg)](https://hexdocs.pm/benchee/) [![Build Status Travis/Linux](https://travis-ci.org/PragTob/benchee.svg?branch=master)](https://travis-ci.org/PragTob/benchee) [![Build status AppVeyor/Windows](https://ci.appveyor.com/api/projects/status/0b5nw0ar9s232oan/branch/master?svg=true)](https://ci.appveyor.com/project/PragTob/benchee/branch/master) [![Coverage Status](https://coveralls.io/repos/github/PragTob/benchee/badge.svg?branch=master)](https://coveralls.io/github/PragTob/benchee?branch=master) [![Inline docs](https://inch-ci.org/github/PragTob/benchee.svg)](https://inch-ci.org/github/PragTob/benchee)
|
1
|
+ # Benchee [![Hex Version](https://img.shields.io/hexpm/v/benchee.svg)](https://hex.pm/packages/benchee) [![docs](https://img.shields.io/badge/docs-hexpm-blue.svg)](https://hexdocs.pm/benchee/) [![Build Status Travis/Linux](https://travis-ci.org/bencheeorg/benchee.svg?branch=master)](https://travis-ci.org/bencheeorg/benchee) [![Appveyor/Windows Build status](https://ci.appveyor.com/api/projects/status/egujslxrg405bnr7?svg=true)](https://ci.appveyor.com/project/PragTob/benchee-8e4gw) [![Coverage Status](https://coveralls.io/repos/github/bencheeorg/benchee/badge.svg?branch=master)](https://coveralls.io/github/bencheeorg/benchee?branch=master)
|
2
2
|
|
3
|
- Library for easy and nice (micro) benchmarking in Elixir. It allows you to compare the performance of different pieces of code at a glance. Benchee is also versatile and extensible, relying only on functions! There are also a bunch of [plugins](#plugins) to draw pretty graphs and more!
|
3
|
+ Library for easy and nice (micro) benchmarking in Elixir. Benchee allows you to compare the performance of different pieces of code at a glance. It is also versatile and extensible, relying only on functions. There are also a bunch of [plugins](#plugins) to draw pretty graphs and more!
|
4
4
|
|
5
|
- Benchee runs each of your functions for a given amount of time after an initial warmup, it then measures their run time and optionally memory consumption. It then shows different statistical values like average, iterations per second and the standard deviation.
|
5
|
+ benchee runs each of your functions for a given amount of time after an initial warmup, it then measures their run time and optionally memory consumption. It then shows different statistical values like average, standard deviation etc. See [features](#features)
|
6
6
|
|
7
|
- Benchee has a nice and concise main interface, its behavior can be altered through lots of [configuration options](#configuration):
|
7
|
+ benchee has a nice and concise main interface, its behavior can be altered through lots of [configuration options](#configuration):
|
8
8
|
|
9
9
|
```elixir
|
10
10
|
list = Enum.to_list(1..10_000)
|
11
|
- map_fun = fn(i) -> [i, i * i] end
|
11
|
+ map_fun = fn i -> [i, i * i] end
|
12
12
|
|
13
|
- Benchee.run(%{
|
14
|
- "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
|
15
|
- "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten end
|
16
|
- }, time: 10, memory_time: 2)
|
13
|
+ Benchee.run(
|
14
|
+ %{
|
15
|
+ "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
|
16
|
+ "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
|
17
|
+ },
|
18
|
+ time: 10,
|
19
|
+ memory_time: 2
|
20
|
+ )
|
17
21
|
```
|
18
22
|
|
19
23
|
Produces the following output on the console:
|
20
24
|
|
21
25
|
```
|
22
|
- tobi@speedy:~/github/benchee(master)$ mix run samples/run.exs
|
23
|
- Operating System: Linux"
|
26
|
+ tobi@speedy:$ mix run samples/run.exs
|
27
|
+ Operating System: Linux
|
24
28
|
CPU Information: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
|
25
29
|
Number of Available Cores: 8
|
26
30
|
Available memory: 15.61 GB
|
27
|
- Elixir 1.6.4
|
28
|
- Erlang 20.3
|
31
|
+ Elixir 1.8.1
|
32
|
+ Erlang 21.2.7
|
29
33
|
|
30
34
|
Benchmark suite executing with the following configuration:
|
31
35
|
warmup: 2 s
|
|
@@ -35,58 +39,56 @@ parallel: 1
|
35
39
|
inputs: none specified
|
36
40
|
Estimated total run time: 28 s
|
37
41
|
|
38
|
-
|
39
42
|
Benchmarking flat_map...
|
40
43
|
Benchmarking map.flatten...
|
41
44
|
|
42
45
|
Name ips average deviation median 99th %
|
43
|
- flat_map 2.31 K 433.25 μs ±8.64% 428 μs 729 μs
|
44
|
- map.flatten 1.22 K 822.22 μs ±16.43% 787 μs 1203 μs
|
46
|
+ flat_map 2.34 K 426.84 μs ±9.88% 418.72 μs 720.20 μs
|
47
|
+ map.flatten 1.18 K 844.08 μs ±19.73% 778.10 μs 1314.87 μs
|
45
48
|
|
46
49
|
Comparison:
|
47
|
- flat_map 2.31 K
|
48
|
- map.flatten 1.22 K - 1.90x slower
|
50
|
+ flat_map 2.34 K
|
51
|
+ map.flatten 1.18 K - 1.98x slower +417.24 μs
|
49
52
|
|
50
53
|
Memory usage statistics:
|
51
54
|
|
52
55
|
Name Memory usage
|
53
|
- flat_map 625.54 KB
|
54
|
- map.flatten 781.85 KB - 1.25x memory usage
|
56
|
+ flat_map 624.97 KB
|
57
|
+ map.flatten 781.25 KB - 1.25x memory usage +156.28 KB
|
55
58
|
|
56
59
|
**All measurements for memory usage were the same**
|
57
|
-
|
58
60
|
```
|
59
61
|
|
60
|
- The aforementioned [plugins](#plugins) like [benchee_html](https://github.com/PragTob/benchee_html) make it possible to generate nice looking [html reports](https://www.pragtob.info/benchee/flat_map.html), where individual graphs can also be exported as PNG images:
|
62
|
+ The aforementioned [plugins](#plugins) like [benchee_html](https://github.com/bencheeorg/benchee_html) make it possible to generate nice looking [html reports](https://www.pragtob.info/benchee/flat_map.html), where individual graphs can also be exported as PNG images:
|
61
63
|
|
62
64
|
![report](https://www.pragtob.info/benchee/images/report.png)
|
63
65
|
|
64
66
|
## Features
|
65
67
|
|
66
|
- * first runs the functions for a given warmup time without recording the results, to simulate a _"warm"_ running system
|
67
|
- * [measures memory](#measuring-memory-consumption)
|
68
|
+ * first runs the functions for a given warmup time without recording the results, to simulate a _"warm"/running_ system
|
69
|
+ * [measures memory usage](#measuring-memory-consumption)
|
68
70
|
* provides you with lots of statistics - check the next list
|
69
|
- * plugin/extensible friendly architecture so you can use different formatters to generate [CSV, HTML and more](#plugins)
|
70
|
- * nicely formatted console output with units scaled to appropriate units
|
71
|
- * [hooks](#hooks-setup-teardown-etc) to execute something before/after a benchmark
|
71
|
+ * plugin/extensible friendly architecture so you can use different formatters to display benchmarking results as [HTML, markdown, JSON and more](#plugins)
|
72
|
+ * nicely formatted console output with units scaled to appropriately (nanoseconds to minutes)
|
73
|
+ * measures the overhead of function calls so that the measured/reported times really are the execution time of _your_code_ without that overhead.
|
74
|
+ * [hooks](#hooks-setup-teardown-etc) to execute something before/after a benchmarking invocation
|
72
75
|
* execute benchmark jobs in parallel to gather more results in the same time, or simulate a system under load
|
73
|
- * well tested
|
74
|
- * well documented
|
76
|
+ * well documented & well tested
|
75
77
|
|
76
78
|
Provides you with the following **statistical data**:
|
77
79
|
|
78
|
- * **average** - average execution time (the lower the better)
|
79
|
- * **ips** - iterations per second, aka how often can the given function be executed within one second (the higher the better)
|
80
|
+ * **average** - average execution time/memory usage (the lower the better)
|
81
|
+ * **ips** - iterations per second, aka how often can the given function be executed within one second (the higher the better - good for graphing), only for run times
|
80
82
|
* **deviation** - standard deviation (how much do the results vary), given as a percentage of the average (raw absolute values also available)
|
81
|
- * **median** - when all measured times are sorted, this is the middle value (or average of the two middle values when the number of samples is even). More stable than the average and somewhat more likely to be a typical value you see. (the lower the better)
|
82
|
- * **99th %** - 99th percentile, 99% of all run times are less than this
|
83
|
+ * **median** - when all measured values are sorted, this is the middle value. More stable than the average and somewhat more likely to be a typical value you see, for the msot typical value see mode. (the lower the better)
|
84
|
+ * **99th %** - 99th percentile, 99% of all measured values are less than this - worst case performance-ish
|
83
85
|
|
84
86
|
In addition, you can optionally output an extended set of statistics:
|
85
87
|
|
86
|
- * **minimum** - the smallest (fastest) run time measured for the job
|
87
|
- * **maximum** - the biggest (slowest) run time measured for the job
|
88
|
- * **sample size** - the number of run time measurements taken
|
89
|
- * **mode** - the run time(s) that occur the most. Often one value, but can be multiple values if they occur the same amount of times. If no value occurs at least twice, this value will be nil.
|
88
|
+ * **minimum** - the smallest value measured for the job (fastest/least consumption)
|
89
|
+ * **maximum** - the biggest run time measured for the job (slowest/most consumption)
|
90
|
+ * **sample size** - the number of measurements taken
|
91
|
+ * **mode** - the measured values that occur the most. Often one value, but can be multiple values if they occur the same amount of times. If no value occurs at least twice, this value will be nil.
|
90
92
|
|
91
93
|
## Installation
|
92
94
|
|
|
@@ -94,13 +96,13 @@ Add benchee to your list of dependencies in `mix.exs`:
|
94
96
|
|
95
97
|
```elixir
|
96
98
|
defp deps do
|
97
|
- [{:benchee, "~> 0.13", only: :dev}]
|
99
|
+ [{:benchee, "~> 0.14", only: :dev}]
|
98
100
|
end
|
99
101
|
```
|
100
102
|
|
101
103
|
Install via `mix deps.get` and then happy benchmarking as described in [Usage](#usage) :)
|
102
104
|
|
103
|
- Elixir versions supported are 1.4+.
|
105
|
+ Elixir versions supported/tested against are 1.6+. It _should_ theoretically still work with 1.4 & 1.5 but we don't actively test/support it.
|
104
106
|
|
105
107
|
## Usage
|
106
108
|
|
|
@@ -108,12 +110,14 @@ After installing just write a little Elixir benchmarking script:
|
108
110
|
|
109
111
|
```elixir
|
110
112
|
list = Enum.to_list(1..10_000)
|
111
|
- map_fun = fn(i) -> [i, i * i] end
|
113
|
+ map_fun = fn i -> [i, i * i] end
|
112
114
|
|
113
|
- Benchee.run(%{
|
114
|
- "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
|
115
|
- "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten end
|
116
|
- })
|
115
|
+ Benchee.run(
|
116
|
+ %{
|
117
|
+ "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
|
118
|
+ "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
|
119
|
+ }
|
120
|
+ )
|
117
121
|
```
|
118
122
|
|
119
123
|
(names can also be specified as `:atoms` if you want to)
|
|
@@ -121,37 +125,43 @@ Benchee.run(%{
|
121
125
|
This produces the following output:
|
122
126
|
|
123
127
|
```
|
124
|
- tobi@speedy ~/github/benchee $ mix run samples/run.exs
|
125
|
- Elixir 1.4.0
|
126
|
- Erlang 19.1
|
128
|
+ tobi@speedy:$ mix run samples/run_defaults.exs
|
129
|
+ Operating System: Linux
|
130
|
+ CPU Information: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
|
131
|
+ Number of Available Cores: 8
|
132
|
+ Available memory: 15.61 GB
|
133
|
+ Elixir 1.8.1
|
134
|
+ Erlang 21.2.7
|
135
|
+
|
127
136
|
Benchmark suite executing with the following configuration:
|
128
|
- warmup: 2.0s
|
129
|
- time: 5.0s
|
137
|
+ warmup: 2 s
|
138
|
+ time: 5 s
|
139
|
+ memory time: 0 ns
|
130
140
|
parallel: 1
|
131
141
|
inputs: none specified
|
132
|
- Estimated total run time: 14.0s
|
142
|
+ Estimated total run time: 14 s
|
133
143
|
|
134
144
|
Benchmarking flat_map...
|
135
145
|
Benchmarking map.flatten...
|
136
146
|
|
137
|
- Name ips average deviation median
|
138
|
- flat_map 2.28 K 438.07 μs ±16.66% 419 μs
|
139
|
- map.flatten 1.25 K 802.99 μs ±13.40% 782 μs
|
147
|
+ Name ips average deviation median 99th %
|
148
|
+ flat_map 2.34 K 427.78 μs ±16.02% 406.29 μs 743.01 μs
|
149
|
+ map.flatten 1.22 K 820.87 μs ±19.29% 772.61 μs 1286.35 μs
|
140
150
|
|
141
151
|
Comparison:
|
142
|
- flat_map 2.28 K
|
143
|
- map.flatten 1.25 K - 1.83x slower
|
152
|
+ flat_map 2.34 K
|
153
|
+ map.flatten 1.22 K - 1.92x slower +393.09 μs
|
144
154
|
```
|
145
155
|
|
146
156
|
See [Features](#features) for a description of the different statistical values and what they mean.
|
147
157
|
|
148
|
- If you're looking to see how to make something specific work, please refer to the [samples](https://github.com/PragTob/benchee/tree/master/samples) directory. Also, especially when wanting to extend benchee check out the [hexdocs](https://hexdocs.pm/benchee/api-reference.html).
|
158
|
+ If you're looking to see how to make something specific work, please refer to the [samples](https://github.com/PragTob/benchee/tree/master/samples) directory. Also, especially when wanting to extend benchee, check out the [hexdocs](https://hexdocs.pm/benchee/api-reference.html).
|
149
159
|
|
150
160
|
### Configuration
|
151
161
|
|
152
162
|
Benchee takes a wealth of configuration options, however those are entirely optional. Benchee ships with sensible defaults for all of these.
|
153
163
|
|
154
|
- In the most common `Benchee.run/2` interface configuration options are passed as the second argument in the form of an optional keyword list:
|
164
|
+ In the most common `Benchee.run/2` interface configuration options are passed as the second argument in the form of an keyword list:
|
155
165
|
|
156
166
|
```elixir
|
157
167
|
Benchee.run(%{"some function" => fn -> magic end}, print: [benchmarking: false])
|
|
@@ -159,56 +169,54 @@ Benchee.run(%{"some function" => fn -> magic end}, print: [benchmarking: false])
|
159
169
|
|
160
170
|
The available options are the following (also documented in [hexdocs](https://hexdocs.pm/benchee/Benchee.Configuration.html#init/1)).
|
161
171
|
|
162
|
- * `warmup` - the time in seconds for which a benchmarking job should be run without measuring times before "real" measurements start. This simulates a _"warm"_ running system. Defaults to 2.
|
163
|
- * `time` - the time in seconds for how long each individual benchmarking job should be run for measuring the execution times (run time performance). Defaults to 5.
|
172
|
+ * `warmup` - the time in seconds for which a benchmarking job should be run without measuring anything before "real" measurements start. This simulates a _"warm"/running_ system. Defaults to 2.
|
173
|
+ * `time` - the time in seconds for how long each individual scenario (benchmarking job x input) should be run for measuring the execution times (run time performance). Defaults to 5.
|
164
174
|
* `memory_time` - the time in seconds for how long [memory measurements](measuring-memory-consumption) should be conducted. Defaults to 0 (turned off).
|
165
175
|
* `inputs` - a map or list of two element tuples. If a map, they keys are descriptive input names and values are the actual input values. If a list of tuples, the first element in each tuple is the input name, and the second element in each tuple is the actual input value. Your benchmarking jobs will then be run with each of these inputs. For this to work your benchmarking function gets the current input passed in as an argument into the function. Defaults to `nil`, aka no input specified and functions are called without an argument. See [Inputs](#inputs).
|
166
|
- * `formatters` - list of formatters either as a module implementing the formatter behaviour, a tuple of said module and options it should take or formatter functions. They are run when using `Benchee.run/2` or you can invoktem them through `Benchee.Formatter.output/1`. Functions need to accept one argument (which is the benchmarking suite with all data) and then use that to produce output. Used for plugins. Defaults to the builtin console formatter `Benchee.Formatters.Console`. See [Formatters](#formatters).
|
176
|
+ * `formatters` - list of formatters either as a module implementing the formatter behaviour, a tuple of said module and options it should take or formatter functions. They are run when using `Benchee.run/2` or you can invoke them through `Benchee.Formatter.output/1`. Functions need to accept one argument (which is the benchmarking suite with all data) and then use that to produce output. Used for plugins & configuration. Also allows the configuration of the console formatter to print extended statistics. Defaults to the builtin console formatter `Benchee.Formatters.Console`. See [Formatters](#formatters).
|
177
|
+ * `:measure_function_call_overhead` - Measure how long an empty function call takes and deduct this from each measured run time. Defaults to true.
|
167
178
|
* `pre_check` - whether or not to run each job with each input - including all given before or after scenario or each hooks - before the benchmarks are measured to ensure that your code executes without error. This can save time while developing your suites. Defaults to `false`.
|
168
|
- * `parallel` - the function of each benchmarking job will be executed in `parallel` number processes. If `parallel: 4` then 4 processes will be spawned that all execute the _same_ function for the given time. When these finish/the time is up 4 new processes will be spawned for the next job/function. This gives you more data in the same time, but also puts a load on the system interfering with benchmark results. For more on the pros and cons of parallel benchmarking [check the wiki](https://github.com/PragTob/benchee/wiki/Parallel-Benchmarking). Defaults to 1 (no parallel execution).
|
179
|
+ * `parallel` - the function of each benchmarking job will be executed in `parallel` number processes. If `parallel: 4` then 4 processes will be spawned that all execute the _same_ function for the given time. When these finish/the time is up 4 new processes will be spawned for the next scenario. This gives you more data in the same time, but also puts a load on the system interfering with benchmark results. For more on the pros and cons of parallel benchmarking [check the wiki](https://github.com/bencheeorg/benchee/wiki/Parallel-Benchmarking). Defaults to 1 (no parallel execution).
|
169
180
|
* `save` - specify a `path` where to store the results of the current benchmarking suite, tagged with the specified `tag`. See [Saving & Loading](#saving-loading-and-comparing-previous-runs).
|
170
181
|
* `load` - load saved suit or suits to compare your current benchmarks against. Can be a string or a list of strings or patterns. See [Saving & Loading](#saving-loading-and-comparing-previous-runs).
|
171
|
- * `print` - a map from atoms to `true` or `false` to configure if the output identified by the atom will be printed during the standard Benchee benchmarking process. All options are enabled by default (true). Options are:
|
172
|
- * `:benchmarking` - print when Benchee starts benchmarking a new job (Benchmarking name ..)
|
182
|
+ * `print` - a map or keyword list from atoms to `true` or `false` to configure if the output identified by the atom will be printed during the standard benchee benchmarking process. All options are enabled by default (true). Options are:
|
183
|
+ * `:benchmarking` - print when Benchee starts benchmarking a new job (`Benchmarking name ...`)
|
173
184
|
* `:configuration` - a summary of configured benchmarking options including estimated total run time is printed before benchmarking starts
|
174
185
|
* `:fast_warning` - warnings are displayed if functions are executed too fast leading to inaccurate measures
|
175
|
- * `console` - options for the built-in console formatter:
|
176
|
- * `:comparison` - if the comparison of the different benchmarking jobs (x times slower than) is shown. Enabled by default.
|
177
|
- * `extended_statistics` - display more statistics, aka `minimum`, `maximum`, `sample_size` and `mode`. Disabled by default.
|
178
|
- * `:unit_scaling` - the strategy for choosing a unit for durations and
|
179
|
- counts. May or may not be implemented by a given formatter (The console
|
180
|
- formatter implements it). When scaling a value, Benchee finds the "best fit"
|
186
|
+ * `:unit_scaling` - the strategy for choosing a unit for durations,
|
187
|
+ counts & memory measurements. May or may not be implemented by a given formatter (The console formatter implements it).
|
188
|
+ When scaling a value, benchee finds the "best fit"
|
181
189
|
unit (the largest unit for which the result is at least 1). For example,
|
182
190
|
1_200_000 scales to `1.2 M`, while `800_000` scales to `800 K`. The
|
183
|
- `unit_scaling` strategy determines how Benchee chooses the best fit unit for
|
191
|
+ `unit_scaling` strategy determines how benchee chooses the best fit unit for
|
184
192
|
an entire list of values, when the individual values in the list may have
|
185
193
|
different best fit units. There are four strategies, defaulting to `:best`:
|
186
194
|
* `:best` - the most frequent best fit unit will be used, a tie
|
187
195
|
will result in the larger unit being selected.
|
188
|
- * `:largest` - the largest best fit unit will be used (i.e. thousand
|
189
|
- and seconds if values are large enough).
|
190
|
- * `:smallest` - the smallest best fit unit will be used (i.e.
|
191
|
- millisecond and one)
|
192
|
- * `:none` - no unit scaling will occur. Durations will be displayed
|
193
|
- in microseconds, and counts will be displayed in ones (this is
|
194
|
- equivalent to the behaviour Benchee had pre 0.5.0)
|
196
|
+ * `:largest` - the largest best fit unit will be used
|
197
|
+ * `:smallest` - the smallest best fit unit will be used
|
198
|
+ * `:none` - no unit scaling will occur.
|
195
199
|
* `:before_scenario`/`after_scenario`/`before_each`/`after_each` - read up on them in the [hooks section](#hooks-setup-teardown-etc)
|
196
|
- * `:measure_function_call_overhead` - Measure how long an empty function call takes and deduct this from each measure run time. Defaults to true.
|
197
200
|
|
198
201
|
### Measuring memory consumption
|
199
202
|
|
200
|
- Starting with version 0.13, users can now get measurements of how much memory their benchmarks use. This measurement is **not** the actual effect on the size of the BEAM VM size, but the total amount of memory that was allocated during the execution of a given scenario. This includes all memory that was garbage collected during the execution of that scenario. It **does not** include any memory used in any process other than the original one in which the scenario is run.
|
203
|
+ Starting with version 0.13, users can now get measurements of how much memory their benchmarked scenarios use. The measurement is **limited to the process that benche executes your provided code in** - i.e. other processes (like worker pools)/the whole BEAM isn't taken into account.
|
204
|
+
|
205
|
+ This measurement is **not** the actual effect on the size of the BEAM VM size, but the total amount of memory that was allocated during the execution of a given scenario. This includes all memory that was garbage collected during the execution of that scenario.
|
201
206
|
|
202
207
|
This measurement of memory does not affect the measurement of run times.
|
203
208
|
|
204
|
- In cases where all measurements of memory consumption are identical, which happens very frequently, the full statistics will be omitted from the standard console formatter. If your function is deterministic, this will always be the case. Only in functions with some amount of randomness will there be variation in memory usage.
|
209
|
+ In cases where all measurements of memory consumption are identical, which happens very frequently, the full statistics will be omitted from the standard console formatter. If your function is deterministic, this should always be the case. Only in functions with some amount of randomness will there be variation in memory usage.
|
205
210
|
|
206
211
|
Memory measurement is disabled by default, and you can choose to enable it by passing `memory_time: your_seconds` option to `Benchee.run/2`:
|
207
212
|
|
208
213
|
```elixir
|
209
|
- Benchee.run(%{
|
210
|
- "something_great" => fn -> cool_stuff end
|
211
|
- }, memory_time: 2)
|
214
|
+ Benchee.run(
|
215
|
+ %{
|
216
|
+ "something_great" => fn -> cool_stuff end
|
217
|
+ },
|
218
|
+ memory_time: 2
|
219
|
+ )
|
212
220
|
```
|
213
221
|
|
214
222
|
Memory time can be specified separately as it will often be constant - so it might not need as much measuring time.
|
|
@@ -218,41 +226,64 @@ A full example, including an example of the console output, can be found
|
218
226
|
|
219
227
|
### Inputs
|
220
228
|
|
221
|
- `:inputs` is a very useful configuration that allows you to run the same benchmarking jobs with different inputs. You specify the inputs as either a map from name (String or atom) to the actual input value or a list of tuples where the first element in each tuple is the name and the second element in the tuple is the value. Functions can have different performance characteristics on differently shaped inputs - be that structure or input size.
|
229
|
+ `:inputs` is a very useful configuration that allows you to run the same benchmarking jobs with different inputs. We call this combination a _"scenario"_. You specify the inputs as either a map from name (String or atom) to the actual input value or a list of tuples where the first element in each tuple is the name and the second element in the tuple is the value.
|
222
230
|
|
223
|
- One of such cases is comparing tail-recursive and body-recursive implementations of `map`. More information in the [repository with the benchmark](https://github.com/PragTob/elixir_playground/blob/master/bench/tco_blog_post_focussed_inputs.exs) and the [blog post](https://pragtob.wordpress.com/2016/06/16/tail-call-optimization-in-elixir-erlang-not-as-efficient-and-important-as-you-probably-think/).
|
231
|
+ Why do this? Functions can have different performance characteristics on differently shaped inputs - be that structure or input size. One of such cases is comparing tail-recursive and body-recursive implementations of `map`. More information in the [repository with the benchmark](https://github.com/PragTob/elixir_playground/blob/master/bench/tco_blog_post_focussed_inputs.exs) and the [blog post](https://pragtob.wordpress.com/2016/06/16/tail-call-optimization-in-elixir-erlang-not-as-efficient-and-important-as-you-probably-think/).
|
232
|
+
|
233
|
+ As a little sample:
|
224
234
|
|
225
235
|
```elixir
|
226
|
- map_fun = fn(i) -> i + 1 end
|
227
|
- inputs = %{
|
228
|
- "Small (1 Thousand)" => Enum.to_list(1..1_000),
|
229
|
- "Middle (100 Thousand)" => Enum.to_list(1..100_000),
|
230
|
- "Big (10 Million)" => Enum.to_list(1..10_000_000)
|
231
|
- }
|
236
|
+ map_fun = fn i -> [i, i * i] end
|
232
237
|
|
233
|
- # Or inputs could also look like this:
|
234
|
- #
|
235
|
- # inputs = [
|
236
|
- # {"Small (1 Thousand)", Enum.to_list(1..1_000)},
|
237
|
- # {"Middle (100 Thousand)", Enum.to_list(1..100_000)},
|
238
|
- # {"Big (10 Million)", Enum.to_list(1..10_000_000)}
|
239
|
- # ]
|
240
|
-
|
241
|
- Benchee.run %{
|
242
|
- "map tail-recursive" =>
|
243
|
- fn(list) -> MyMap.map_tco(list, map_fun) end,
|
244
|
- "stdlib map" =>
|
245
|
- fn(list) -> Enum.map(list, map_fun) end,
|
246
|
- "map simple body-recursive" =>
|
247
|
- fn(list) -> MyMap.map_body(list, map_fun) end,
|
248
|
- "map tail-recursive different argument order" =>
|
249
|
- fn(list) -> MyMap.map_tco_arg_order(list, map_fun) end
|
250
|
- }, time: 15, warmup: 5, inputs: inputs
|
238
|
+ Benchee.run(
|
239
|
+ %{
|
240
|
+ "flat_map" => fn input -> Enum.flat_map(input, map_fun) end,
|
241
|
+ "map.flatten" => fn input -> input |> Enum.map(map_fun) |> List.flatten() end
|
242
|
+ },
|
243
|
+ inputs: %{
|
244
|
+ "Small" => Enum.to_list(1..1_000),
|
245
|
+ "Medium" => Enum.to_list(1..10_000),
|
246
|
+ "Bigger" => Enum.to_list(1..100_000)
|
247
|
+ }
|
248
|
+ )
|
251
249
|
```
|
252
250
|
|
253
|
- This means each function will be benchmarked with each input that is specified in the inputs. Then you'll get the output divided by input so you can see which function is fastest for which input.
|
251
|
+ This means each function will be benchmarked with each input that is specified in the inputs. Then you'll get the output divided by input so you can see which function is fastest for which input, like so:
|
254
252
|
|
255
|
- Therefore, I **highly recommend** using this feature and checking different realistically structured and sized inputs for the functions you benchmark!
|
253
|
+ ```
|
254
|
+ tobi@speedy:~/github/benchee(readme-overhaul)$ mix run samples/multiple_inputs.exs
|
255
|
+
|
256
|
+ (... general information ...)
|
257
|
+
|
258
|
+ ##### With input Bigger #####
|
259
|
+ Name ips average deviation median 99th %
|
260
|
+ flat_map 150.81 6.63 ms ±12.65% 6.57 ms 8.74 ms
|
261
|
+ map.flatten 114.05 8.77 ms ±16.22% 8.42 ms 12.76 ms
|
262
|
+
|
263
|
+ Comparison:
|
264
|
+ flat_map 150.81
|
265
|
+ map.flatten 114.05 - 1.32x slower +2.14 ms
|
266
|
+
|
267
|
+ ##### With input Medium #####
|
268
|
+ Name ips average deviation median 99th %
|
269
|
+ flat_map 2.28 K 437.80 μs ±10.72% 425.63 μs 725.09 μs
|
270
|
+ map.flatten 1.78 K 561.18 μs ±5.55% 553.98 μs 675.98 μs
|
271
|
+
|
272
|
+ Comparison:
|
273
|
+ flat_map 2.28 K
|
274
|
+ map.flatten 1.78 K - 1.28x slower +123.37 μs
|
275
|
+
|
276
|
+ ##### With input Small #####
|
277
|
+ Name ips average deviation median 99th %
|
278
|
+ flat_map 26.31 K 38.01 μs ±15.47% 36.69 μs 67.08 μs
|
279
|
+ map.flatten 18.65 K 53.61 μs ±11.32% 52.79 μs 70.17 μs
|
280
|
+
|
281
|
+ Comparison:
|
282
|
+ flat_map 26.31 K
|
283
|
+ map.flatten 18.65 K - 1.41x slower +15.61 μs
|
284
|
+ ```
|
285
|
+
|
286
|
+ Therefore, we **highly recommend** using this feature and checking different realistically structured and sized inputs for the functions you benchmark!
|
256
287
|
|
257
288
|
### Formatters
|
258
289
|
|
|
@@ -263,16 +294,17 @@ The `:formatters` option is specified a list of:
|
263
294
|
* a tuple of a module specified above and options for it `{module, options}`
|
264
295
|
* functions that take one argument (the benchmarking suite with all its results) and then do whatever you want them to
|
265
296
|
|
266
|
- So if you are using the [HTML plugin](https://github.com/PragTob/benchee_html) and you want to run both the console formatter and the HTML formatter this looks like this (after you installed it of course):
|
297
|
+ So if you are using the [HTML plugin](https://github.com/bencheeorg/benchee_html) and you want to run both the console formatter and the HTML formatter this looks like this (after you installed it of course):
|
267
298
|
|
268
299
|
```elixir
|
269
300
|
list = Enum.to_list(1..10_000)
|
270
|
- map_fun = fn(i) -> [i, i * i] end
|
301
|
+ map_fun = fn i -> [i, i * i] end
|
271
302
|
|
272
|
- Benchee.run(%{
|
273
|
- "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
|
274
|
- "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten end
|
275
|
- },
|
303
|
+ Benchee.run(
|
304
|
+ %{
|
305
|
+ "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
|
306
|
+ "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten end
|
307
|
+ },
|
276
308
|
formatters: [
|
277
309
|
{Benchee.Formatters.HTML, file: "samples_output/my.html"},
|
278
310
|
Benchee.Formatters.Console
|
|
@@ -280,29 +312,39 @@ Benchee.run(%{
|
280
312
|
)
|
281
313
|
```
|
282
314
|
|
283
|
- ### Extended Console Formatter Statistics
|
315
|
+ #### Console Formatter options
|
284
316
|
|
285
|
- Showing more statistics such as `minimum`, `maximum`, `sample_size` and `mode` is as simple as passing `extended_statistics: true` to the console formatter.
|
317
|
+ The console formatter supports 2 configuration options:
|
318
|
+
|
319
|
+ * `:comparison` - if the comparison of the different benchmarking jobs (x times slower than) is shown. Enabled by default.
|
320
|
+ * `extended_statistics` - display more statistics, aka `minimum`, `maximum`, `sample_size` and `mode`. Disabled by default.
|
321
|
+
|
322
|
+ So if you want to see more statistics you simple pass `extended_statistics: true` to the console formatter:
|
286
323
|
|
287
324
|
```elixir
|
288
325
|
list = Enum.to_list(1..10_000)
|
289
|
- map_fun = fn(i) -> [i, i * i] end
|
326
|
+ map_fun = fn i -> [i, i * i] end
|
290
327
|
|
291
|
- Benchee.run(%{
|
292
|
- "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
|
293
|
- "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten end
|
294
|
- }, time: 10, formatters: [{Benchee.Formatters.Console, extended_statistics: true}])
|
328
|
+ Benchee.run(
|
329
|
+ %{
|
330
|
+ "flat_map" => fn -> Enum.flat_map(list, map_fun) end,
|
331
|
+ "map.flatten" => fn -> list |> Enum.map(map_fun) |> List.flatten() end
|
332
|
+ },
|
333
|
+ time: 10,
|
334
|
+ formatters: [{Benchee.Formatters.Console, extended_statistics: true}]
|
335
|
+ )
|
295
336
|
```
|
296
337
|
|
297
338
|
Which produces:
|
298
339
|
|
299
340
|
```
|
300
|
- # your normal output...
|
341
|
+ (... normal output ...)
|
301
342
|
|
302
343
|
Extended statistics:
|
344
|
+
|
303
345
|
Name minimum maximum sample size mode
|
304
|
- flat_map 365 μs 1371 μs 22.88 K 430 μs
|
305
|
- map.flatten 514 μs 1926 μs 13.01 K 517 μs
|
346
|
+ flat_map 345.43 μs 1195.89 μs 23.73 K 405.64 μs
|
347
|
+ map.flatten 522.99 μs 2105.56 μs 12.03 K 767.83 μs, 768.44 μs
|
306
348
|
```
|
307
349
|
|
308
350
|
(Btw. notice how the modes of both are much closer and for `map.flatten` much less than the average of `766.99`, see `samples/run_extended_statistics`)
|
|
@@ -314,12 +356,13 @@ Benchee can store the results of previous runs in a file and then load them agai
|
314
356
|
**Saving** is done through the `save` configuration option. You can specify a `path` where results are saved, or you can use the default option of`"benchmark.benchee"` if you don't pass a `path`. You can also pass a `tag` option which annotates these results (for instance with a branch name). The default option for the `tag` is a timestamp of when the benchmark was run.
|
315
357
|
|
316
358
|
**Loading** is done through the `load` option specifying a path to the file to
|
317
|
- load (for instance `benchmark.benchee`). You can also specify multiple files to load through a list of paths (`["my.benchee", "master_save.benchee"]`) - each one of those can also be a glob expression to match even more files glob (`"save_number*.benchee"`).
|
359
|
+ load (for instance `"benchmark.benchee"`). You can also specify multiple files to load through a list of paths (`["my.benchee", "master_save.benchee"]`) - each one of those can also be a glob expression to match even more files glob (`"save_number*.benchee"`).
|
318
360
|
|
319
361
|
```elixir
|
320
|
- Benchee.run(%{
|
321
|
- "something_great" => fn -> cool_stuff end
|
322
|
- },
|
362
|
+ Benchee.run(
|
363
|
+ %{
|
364
|
+ "something_great" => fn -> cool_stuff end
|
365
|
+ },
|
323
366
|
save: [path: "save.benchee", tag: "first-try"]
|
324
367
|
)
|
325
368
|
|
|
@@ -330,7 +373,7 @@ In the more verbose API this is triggered via `Benchee.load/1`.
|
330
373
|
|
331
374
|
### Hooks (Setup, Teardown etc.)
|
332
375
|
|
333
|
- Most of the time, it's best to keep your benchmarks as simple as possible: plain old immutable functions work best. But sometimes you need other things to happen. When you want to add before or after hooks to your benchmarks, we've got you covered! Before you dig into this section, **you usually don't need hooks**.
|
376
|
+ Most of the time, it's best to keep your benchmarks as simple as possible: plain old immutable functions work best. But sometimes you need other things to happen. When you want to add before or after hooks to your benchmarks, we've got you covered! Before you dig into this section though remember one thing: **you usually don't need hooks!**
|
334
377
|
|
335
378
|
Benchee has three types of hooks:
|
336
379
|
|
|
@@ -339,7 +382,7 @@ Benchee has three types of hooks:
|
339
382
|
* [Benchmarking function hooks](#benchmarking-function-hooks)
|
340
383
|
|
341
384
|
|
342
|
- Of course, **hooks are not included in the measured run times**. So they are there especially if you want to do something and want it to **not be included in the measured times**. Sadly there is the notable exception of _too_fast_functions_ (the ones that execute in less than 10 microseconds). As we need to measure their repeated invocations to get halfway good measurements `before_each` and `after_each` hooks are included there.
|
385
|
+ Of course, **hooks are not included in the measurements**. So they are there especially if you want to do something and want it to **not be included in the measurements**. Sadly there is the notable exception of _too_fast_functions_ (the ones that execute faster than we can measure in _native_ resolution). As we need to measure their repeated invocations to get halfway good measurements `before_each` and `after_each` hooks are included there. However, to the best of our knowledge this should only ever happen on Windows (because of the bad run time measurement accuracy).
|
343
386
|
|
344
387
|
#### Suite hooks
|
345
388
|
|
|
@@ -348,7 +391,7 @@ It is very easy in benchee to do setup and teardown for the whole benchmarking s
|
348
391
|
```elixir
|
349
392
|
your_setup()
|
350
393
|
|
351
|
- Benchee.run %{"Awesome stuff" => fn -> magic end }
|
394
|
+ Benchee.run(%{"Awesome stuff" => fn -> magic end})
|
352
395
|
|
353
396
|
your_teardown()
|
354
397
|
```
|
|
@@ -367,13 +410,16 @@ For the following discussions, it's important to know what benchee considers a "
|
367
410
|
A scenario is the combination of one benchmarking function and one input. So, given this benchmark:
|
368
411
|
|
369
412
|
```elixir
|
370
|
- Benchee.run %{
|
371
|
- "foo" => fn(input) -> ... end,
|
372
|
- "bar" => fn(input) -> ... end
|
373
|
- }, inputs: %{
|
374
|
- "input 1" => 1,
|
375
|
- "input 2" => 2
|
376
|
- }
|
413
|
+ Benchee.run(
|
414
|
+ %{
|
415
|
+ "foo" => fn input -> ... end,
|
416
|
+ "bar" => fn input -> ... end
|
417
|
+ },
|
418
|
+ inputs: %{
|
419
|
+ "input 1" => 1,
|
420
|
+ "input 2" => 2
|
421
|
+ }
|
422
|
+ )
|
377
423
|
```
|
378
424
|
|
379
425
|
there are 4 scenarios:
|
|
@@ -383,11 +429,11 @@ there are 4 scenarios:
|
383
429
|
3. bar with input 1
|
384
430
|
4. bar with input 2
|
385
431
|
|
386
|
- A scenario includes warmup and actual run time.
|
432
|
+ A scenario includes warmup and actual run time (+ other measurements like memory).
|
387
433
|
|
388
434
|
##### before_scenario
|
389
435
|
|
390
|
- Is executed before every [scenario](#what-is-a-scenario) that it applies to (see [hook configuration](#hook-configuration-global-versus-local)). Before scenario hooks take the input of the scenario as an argument.
|
436
|
+ Is executed before every [scenario](#what-is-a-scenario) that it applies to (see [hook configuration](#hook-configuration-global-versus-local)). `before_scenario` hooks take the input of the scenario as an argument.
|
391
437
|
|
392
438
|
Since the return value of a `before_scenario` becomes the input for next steps (see [hook arguments and return values](#hook-arguments-and-return-values)), there usually are 3 kinds of before scenarios:
|
393
439
|
|
|
@@ -400,22 +446,25 @@ For before scenario hooks, the _global_ hook is invoked first, then the _local_
|
400
446
|
Usage:
|
401
447
|
|
402
448
|
```elixir
|
403
|
- Benchee.run %{
|
404
|
- "foo" =>
|
405
|
- {
|
406
|
- fn({input, resource}) -> foo(input, resource) end,
|
407
|
- before_scenario: fn({input, resource}) ->
|
408
|
- resource = alter_resource(resource)
|
409
|
- {input, resource}
|
410
|
- end
|
411
|
- },
|
412
|
- "bar" =>
|
413
|
- fn({input, resource}) -> bar(input, resource) end
|
414
|
- }, inputs: %{"input 1" => 1},
|
415
|
- before_scenario: fn(input) ->
|
416
|
- resource = start_expensive_resource()
|
417
|
- {input, resource}
|
418
|
- end
|
449
|
+ Benchee.run(
|
450
|
+ %{
|
451
|
+ "foo" =>
|
452
|
+ {
|
453
|
+ fn {input, resource} -> foo(input, resource) end,
|
454
|
+ before_scenario: fn {input, resource} ->
|
455
|
+ resource = alter_resource(resource)
|
456
|
+ {input, resource}
|
457
|
+ end
|
458
|
+ },
|
459
|
+ "bar" =>
|
460
|
+ fn {input, resource} -> bar(input, resource) end
|
461
|
+ },
|
462
|
+ inputs: %{"input 1" => 1},
|
463
|
+ before_scenario: fn input ->
|
464
|
+ resource = start_expensive_resource()
|
465
|
+ {input, resource}
|
466
|
+ end
|
467
|
+ )
|
419
468
|
```
|
420
469
|
|
421
470
|
_When might this be useful?_
|
|
@@ -433,9 +482,12 @@ For after scenario hooks, the _local_ hook is invoked first, then the _global_ (
|
433
482
|
Usage:
|
434
483
|
|
435
484
|
```elixir
|
436
|
- Benchee.run %{
|
437
|
- "bar" => fn -> bar() end
|
438
|
- }, after_scenario: fn(_input) -> bust_my_cache() end
|
485
|
+ Benchee.run(
|
486
|
+ %{
|
487
|
+ "bar" => fn -> bar() end
|
488
|
+ },
|
489
|
+ after_scenario: fn _input -> bust_my_cache() end
|
490
|
+ )
|
439
491
|
```
|
440
492
|
|
441
493
|
_When might this be useful?_
|
|
@@ -457,17 +509,21 @@ For before each hooks, the _global_ hook is invoked first, then the _local_ (see
|
457
509
|
Usage:
|
458
510
|
|
459
511
|
```elixir
|
460
|
- Benchee.run %{
|
461
|
- "bar" => fn(record) -> bar(record) end
|
462
|
- }, inputs: %{ "record id" => 1},
|
463
|
- before_each: fn(input) -> get_from_db(input) end
|
512
|
+ Benchee.run(
|
513
|
+ %{
|
514
|
+ "bar" => fn record -> bar(record) end
|
515
|
+ },
|
516
|
+ inputs: %{ "record id" => 1},
|
517
|
+ before_each: fn input -> get_from_db(input) end
|
518
|
+ )
|
464
519
|
```
|
465
520
|
|
466
521
|
_When might this be useful?_
|
467
522
|
|
468
|
- * Retrieving a record from the database and passing it on to the benchmarking function to do something(tm) without the retrieval from the database adding to the benchmark measurement
|
523
|
+ * Retrieving a record from the database and passing it on to the benchmarking function to do _something(tm)_ without the retrieval from the database adding to the benchmark measurement
|
469
524
|
* Busting caches so that all measurements are taken in an uncached state
|
470
525
|
* Picking a random value from a collection and passing it to the benchmarking function for measuring performance with a wider spread of values
|
526
|
+ * you could also use this to benchmark with random data like `StreamData`, [devon shows how it's done here](https://devonestes.herokuapp.com/benchmarking-with-stream-data)
|
471
527
|
|
472
528
|
##### after_each
|
473
529
|
|
|
@@ -478,10 +534,13 @@ For after each hooks, the _local_ hook is invoked first, then the _global_ (see
|
478
534
|
Usage:
|
479
535
|
|
480
536
|
```elixir
|
481
|
- Benchee.run %{
|
482
|
- "bar" => fn(input) -> bar(input) end
|
483
|
- }, inputs: %{ "input 1" => 1}.
|
484
|
- after_each: fn(result) -> assert result == 42 end
|
537
|
+ Benchee.run(
|
538
|
+ %{
|
539
|
+ "bar" => fn input -> bar(input) end
|
540
|
+ },
|
541
|
+ inputs: %{ "input 1" => 1},
|
542
|
+ after_each: fn result -> assert result == 42 end
|
543
|
+ )
|
485
544
|
```
|
486
545
|
|
487
546
|
_When might this be useful?_
|
|
@@ -507,14 +566,17 @@ Hooks can be defined either _globally_ as part of the configuration or _locally_
|
507
566
|
Global hooks are specified as part of the general benchee configuration:
|
508
567
|
|
509
568
|
```elixir
|
510
|
- Benchee.run %{
|
511
|
- "foo" => fn(input) -> ... end,
|
512
|
- "bar" => fn(input) -> ... end
|
513
|
- }, inputs: %{
|
514
|
- "input 1" => 1,
|
515
|
- "input 2" => 2,
|
569
|
+ Benchee.run(
|
570
|
+ %{
|
571
|
+ "foo" => fn input -> ... end,
|
572
|
+ "bar" => fn input -> ... end
|
516
573
|
},
|
517
|
- before_scenario: fn(input) -> ... end
|
574
|
+ inputs: %{
|
575
|
+ "input 1" => 1,
|
576
|
+ "input 2" => 2,
|
577
|
+ },
|
578
|
+ before_scenario: fn input -> ... end
|
579
|
+ )
|
518
580
|
```
|
519
581
|
|
520
582
|
Here the `before_scenario` function will be executed for all 4 scenarios present in this benchmarking suite.
|
|
@@ -524,13 +586,16 @@ Here the `before_scenario` function will be executed for all 4 scenarios present
|
524
586
|
Local hooks are defined alongside the benchmarking function. To define a local hook, pass a tuple in the initial map, instead of just a single function. The benchmarking function comes first, followed by a keyword list specifying the hooks to run:
|
525
587
|
|
526
588
|
```elixir
|
527
|
- Benchee.run %{
|
528
|
- "foo" => {fn(input) -> ... end, before_scenario: fn(input) -> ... end},
|
529
|
- "bar" => fn(input) -> ... end
|
530
|
- }, inputs: %{
|
531
|
- "input 1" => 1,
|
532
|
- "input 2" => 2
|
533
|
- }
|
589
|
+ Benchee.run(
|
590
|
+ %{
|
591
|
+ "foo" => {fn input -> ... end, before_scenario: fn input -> ... end},
|
592
|
+ "bar" => fn input -> ... end
|
593
|
+ },
|
594
|
+ inputs: %{
|
595
|
+ "input 1" => 1,
|
596
|
+ "input 2" => 2
|
597
|
+ }
|
598
|
+ )
|
534
599
|
```
|
535
600
|
|
536
601
|
Here `before_scenario` is only run for the 2 scenarios associated with `"foo"`, i.e. foo with input 1 and foo with input 2. It is _not_ run for any `"bar"` benchmarks.
|
|
@@ -546,42 +611,45 @@ Given the following code:
|
546
611
|
|
547
612
|
suite_set_up()
|
548
613
|
|
549
|
- Benchee.run %{
|
550
|
- "foo" =>
|
551
|
- {
|
552
|
- fn(input) -> foo(input) end,
|
553
|
- before_scenario: fn(input) ->
|
554
|
- local_before_scenario(input)
|
555
|
- input + 1
|
556
|
- end,
|
557
|
- before_each: fn(input) ->
|
558
|
- local_before_each(input)
|
559
|
- input + 1
|
560
|
- end,
|
561
|
- after_each: fn(value) ->
|
562
|
- local_after_each(value)
|
563
|
- end,
|
564
|
- after_scenario: fn(input) ->
|
565
|
- local_after_scenario(input)
|
566
|
- end
|
567
|
- },
|
568
|
- "bar" =>
|
569
|
- fn(input) -> bar(input) end
|
570
|
- }, inputs: %{"input 1" => 1},
|
571
|
- before_scenario: fn(input) ->
|
572
|
- global_before_scenario(input)
|
573
|
- input + 1
|
574
|
- end,
|
575
|
- before_each: fn(input) ->
|
576
|
- global_before_each(input)
|
577
|
- input + 1
|
578
|
- end,
|
579
|
- after_each: fn(value) ->
|
580
|
- global_after_each(value)
|
581
|
- end,
|
582
|
- after_scenario: fn(input) ->
|
583
|
- global_after_scenario(input)
|
584
|
- end
|
614
|
+ Benchee.run(
|
615
|
+ %{
|
616
|
+ "foo" =>
|
617
|
+ {
|
618
|
+ fn input -> foo(input) end,
|
619
|
+ before_scenario: fn input ->
|
620
|
+ local_before_scenario(input)
|
621
|
+ input + 1
|
622
|
+ end,
|
623
|
+ before_each: fn input ->
|
624
|
+ local_before_each(input)
|
625
|
+ input + 1
|
626
|
+ end,
|
627
|
+ after_each: fn value ->
|
628
|
+ local_after_each(value)
|
629
|
+ end,
|
630
|
+ after_scenario: fn input ->
|
631
|
+ local_after_scenario(input)
|
632
|
+ end
|
633
|
+ },
|
634
|
+ "bar" =>
|
635
|
+ fn input -> bar(input) end
|
636
|
+ },
|
637
|
+ inputs: %{"input 1" => 1},
|
638
|
+ before_scenario: fn input ->
|
639
|
+ global_before_scenario(input)
|
640
|
+ input + 1
|
641
|
+ end,
|
642
|
+ before_each: fn input ->
|
643
|
+ global_before_each(input)
|
644
|
+ input + 1
|
645
|
+ end,
|
646
|
+ after_each: fn value ->
|
647
|
+ global_after_each(value)
|
648
|
+ end,
|
649
|
+ after_scenario: fn input ->
|
650
|
+ global_after_scenario(input)
|
651
|
+ end
|
652
|
+ )
|
585
653
|
|
586
654
|
suite_tear_down()
|
587
655
|
```
|
|
@@ -638,17 +706,20 @@ It is important to note that the benchmarking code shown in the beginning is the
|
638
706
|
|
639
707
|
```elixir
|
640
708
|
list = Enum.to_list(1..10_000)
|
641
|
- map_fun = fn(i) -> [i, i * i] end
|
709
|
+ map_fun = fn i -> [i, i * i] end
|
642
710
|
|
643
|
- Benchee.init(time: 3)
|
644
|
- |> Benchee.system
|
711
|
+ [time: 3]
|
712
|
+ |> Benchee.init()
|
713
|
+ |> Benchee.system()
|
645
714
|
|> Benchee.benchmark("flat_map", fn -> Enum.flat_map(list, map_fun) end)
|
646
|
- |> Benchee.benchmark("map.flatten",
|
647
|
- fn -> list |> Enum.map(map_fun) |> List.flatten end)
|
648
|
- |> Benchee.measure
|
649
|
- |> Benchee.statistics
|
650
|
- |> Benchee.load # can be omitted when you don't want to/need to load scenarios
|
651
|
- |> Benchee.Formatter.output(Benchee.Formatters.Console, %{})
|
715
|
+ |> Benchee.benchmark(
|
716
|
+ "map.flatten",
|
717
|
+ fn -> list |> Enum.map(map_fun) |> List.flatten() end
|
718
|
+ )
|
719
|
+ |> Benchee.collect()
|
720
|
+ |> Benchee.statistics()
|
721
|
+ |> Benchee.relative_statistics()
|
722
|
+ |> Benchee.Formatter.output(Benchee.Formatters.Console)
|
652
723
|
# Instead of the last call you could also just use Benchee.Formatter.output()
|
653
724
|
# to just output all configured formatters
|
654
725
|
```
|
|
@@ -659,12 +730,13 @@ This is a take on the _functional transformation_ of data applied to benchmarks:
|
659
730
|
2. Gather System data
|
660
731
|
3. Define the functions to be benchmarked
|
661
732
|
4. Run benchmarks with the given configuration gathering raw run times per function
|
662
|
- 5. Generate statistics based on the raw run times
|
663
|
- 6. Format the statistics in a suitable way and print them out
|
733
|
+ 5. Calculate statistics based on the raw run times
|
734
|
+ 6. Calculate statistics between the scenarios (faster/slower...)
|
735
|
+ 7. Format the statistics in a suitable way and print them out
|
664
736
|
|
665
737
|
This is also part of the **official API** and allows for more **fine grained control**. (It's also what benchee does internally when you use `Benchee.run/2`).
|
666
738
|
|
667
|
- Do you just want to have all the raw run times? Just work with the result of `Benchee.measure/1`! Just want to have the calculated statistics and use your own formatting? Grab the result of `Benchee.statistics/1`! Or, maybe you want to write to a file or send an HTTP post to some online service? Just grab the complete suite after statistics were generated.
|
739
|
+ Do you just want to have all the raw run times? Just work with the result of `Benchee.collect/1`! Just want to have the calculated statistics and use your own formatting? Grab the result of `Benchee.statistics/1`! Or, maybe you want to write to a file or send an HTTP post to some online service? Just grab the complete suite after statistics were generated.
|
668
740
|
|
669
741
|
It also allows you to alter behaviour, normally `Benchee.load/1` is called right before the formatters so that neither the benchmarks are run again or statistics are computed again. However, you might want to run the benchmarks again or recompute the statistics. Then you can call `Benchee.load/1` right at the start.
|
670
742
|
|
|
@@ -737,8 +809,9 @@ This doesn't seem to be too reliable right now, so suggestions and input are ver
|
737
809
|
Benchee only has small runtime dependencies that were initially extracted from it. Further functionality is provided through plugins that then pull in dependencies, such as HTML generation and CSV export. They help provide excellent visualization or interoperability.
|
738
810
|
|
739
811
|
* [benchee_html](//github.com/PragTob/benchee_html) - generate HTML including a data table and many different graphs with the possibility to export individual graphs as PNG :)
|
740
|
- * [benchee_csv](//github.com/PragTob/benchee_csv) - generate CSV from your Benchee benchmark results so you can import them into your favorite spreadsheet tool and make fancy graphs
|
812
|
+ * [benchee_csv](//github.com/PragTob/benchee_csv) - generate CSV from your benchee benchmark results so you can import them into your favorite spreadsheet tool and make fancy graphs
|
741
813
|
* [benchee_json](//github.com/PragTob/benchee_json) - export suite results as JSON to feed anywhere or feed it to your JavaScript and make magic happen :)
|
814
|
+ * [benchee_markdown](//github.com/hrzndhrn/benchee_markdown) - write markdown files containing your benchmarking results
|
742
815
|
|
743
816
|
With the HTML plugin for instance you can get fancy graphs like this boxplot:
|
744
817
|
|
|
@@ -748,23 +821,17 @@ Of course there also are normal bar charts including standard deviation:
|
748
821
|
|
749
822
|
![flat_map_ips](https://www.pragtob.info/benchee/images/flat_map_ips.png)
|
750
823
|
|
751
|
- ## Presentation + general benchmarking advice
|
752
|
-
|
753
|
- If you're into watching videos of conference talks and also want to learn more about benchmarking in general I can recommend watching my talk from [ElixirLive 2016](https://www.elixirlive.com/). [Slides can be found here](https://pragtob.wordpress.com/2016/12/03/slides-how-fast-is-it-really-benchmarking-in-elixir/), video - click the washed out image below ;)
|
754
|
-
|
755
|
- [![Benchee Video](https://www.pragtob.info/images/elixir_live_slide.png)](https://www.youtube.com/watch?v=7-mE5CKXjkw)
|
756
|
-
|
757
824
|
## Contributing [![Open Source Helpers](https://www.codetriage.com/pragtob/benchee/badges/users.svg)](https://www.codetriage.com/pragtob/benchee)
|
758
825
|
|
759
826
|
Contributions to benchee are **very welcome**! Bug reports, documentation, spelling corrections, whole features, feature ideas, bugfixes, new plugins, fancy graphics... all of those (and probably more) are much appreciated contributions!
|
760
827
|
|
761
828
|
Keep in mind that the [plugins](#plugins) live in their own repositories with their own issue tracker and they also like to get contributions :)
|
762
829
|
|
763
|
- Please respect the [Code of Conduct](//github.com/PragTob/benchee/blob/master/CODE_OF_CONDUCT.md).
|
830
|
+ Please respect the [Code of Conduct](//github.com/bencheeorg/benchee/blob/master/CODE_OF_CONDUCT.md).
|
764
831
|
|
765
832
|
In addition to contributing code, you can help to triage issues. This can include reproducing bug reports, or asking for vital information such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to pragtob/benchee on CodeTriage](https://www.codetriage.com/pragtob/benchee).
|
766
833
|
|
767
|
- You can also look directly at the [open issues](https://github.com/PragTob/benchee/issues). There are `help wanted` and `good first issue` labels - those are meant as guidance, of course other issues can be tackled :)
|
834
|
+ You can also look directly at the [open issues](https://github.com/bencheeorg/benchee/issues). There are `help wanted` and `good first issue` labels - those are meant as guidance, of course other issues can be tackled :)
|
768
835
|
|
769
836
|
A couple of (hopefully) helpful points:
|
changed
hex_metadata.config
|
@@ -2,10 +2,11 @@
|
2
2
|
{<<"build_tools">>,[<<"mix">>]}.
|
3
3
|
{<<"description">>,
|
4
4
|
<<"Versatile (micro) benchmarking that is extensible. Get statistics such as:\naverage, iterations per second, standard deviation and the median.">>}.
|
5
|
- {<<"elixir">>,<<"~> 1.4">>}.
|
5
|
+ {<<"elixir">>,<<"~> 1.6">>}.
|
6
6
|
{<<"files">>,
|
7
7
|
[<<"lib">>,<<"lib/benchee.ex">>,<<"lib/benchee">>,
|
8
|
- <<"lib/benchee/formatters">>,<<"lib/benchee/formatters/console">>,
|
8
|
+ <<"lib/benchee/scenario.ex">>,<<"lib/benchee/formatters">>,
|
9
|
+ <<"lib/benchee/formatters/console">>,
|
9
10
|
<<"lib/benchee/formatters/console/memory.ex">>,
|
10
11
|
<<"lib/benchee/formatters/console/run_time.ex">>,
|
11
12
|
<<"lib/benchee/formatters/console/helpers.ex">>,
|
|
@@ -13,19 +14,17 @@
|
13
14
|
<<"lib/benchee/formatters/tagged_save.ex">>,<<"lib/benchee/output">>,
|
14
15
|
<<"lib/benchee/output/benchmark_printer.ex">>,<<"lib/benchee/system.ex">>,
|
15
16
|
<<"lib/benchee/statistics.ex">>,<<"lib/benchee/benchmark">>,
|
16
|
- <<"lib/benchee/benchmark/scenario.ex">>,
|
17
|
- <<"lib/benchee/benchmark/measure.ex">>,
|
18
17
|
<<"lib/benchee/benchmark/runner.ex">>,
|
19
18
|
<<"lib/benchee/benchmark/repeated_measurement.ex">>,
|
20
|
- <<"lib/benchee/benchmark/hooks.ex">>,
|
19
|
+ <<"lib/benchee/benchmark/collect.ex">>,<<"lib/benchee/benchmark/hooks.ex">>,
|
20
|
+ <<"lib/benchee/benchmark/collect">>,
|
21
|
+ <<"lib/benchee/benchmark/collect/time.ex">>,
|
22
|
+ <<"lib/benchee/benchmark/collect/native_time.ex">>,
|
23
|
+ <<"lib/benchee/benchmark/collect/memory.ex">>,
|
21
24
|
<<"lib/benchee/benchmark/scenario_context.ex">>,
|
22
|
- <<"lib/benchee/benchmark/measure">>,
|
23
|
- <<"lib/benchee/benchmark/measure/time.ex">>,
|
24
|
- <<"lib/benchee/benchmark/measure/native_time.ex">>,
|
25
|
- <<"lib/benchee/benchmark/measure/memory.ex">>,
|
26
|
- <<"lib/benchee/conversion.ex">>,<<"lib/benchee/configuration.ex">>,
|
27
|
- <<"lib/benchee/conversion">>,<<"lib/benchee/conversion/count.ex">>,
|
28
|
- <<"lib/benchee/conversion/scale.ex">>,
|
25
|
+ <<"lib/benchee/collection_data.ex">>,<<"lib/benchee/conversion.ex">>,
|
26
|
+ <<"lib/benchee/configuration.ex">>,<<"lib/benchee/conversion">>,
|
27
|
+ <<"lib/benchee/conversion/count.ex">>,<<"lib/benchee/conversion/scale.ex">>,
|
29
28
|
<<"lib/benchee/conversion/format.ex">>,
|
30
29
|
<<"lib/benchee/conversion/memory.ex">>,
|
31
30
|
<<"lib/benchee/conversion/deviation_percent.ex">>,
|
|
@@ -37,7 +36,8 @@
|
37
36
|
<<"lib/benchee/utility/file_creation.ex">>,
|
38
37
|
<<"lib/benchee/utility/parallel.ex">>,<<"lib/benchee/statistics">>,
|
39
38
|
<<"lib/benchee/statistics/mode.ex">>,
|
40
|
- <<"lib/benchee/statistics/percentile.ex">>,<<"lib/benchee/suite.ex">>,
|
39
|
+ <<"lib/benchee/statistics/percentile.ex">>,
|
40
|
+ <<"lib/benchee/relative_statistics.ex">>,<<"lib/benchee/suite.ex">>,
|
41
41
|
<<"lib/benchee/formatter.ex">>,<<"lib/benchee/benchmark.ex">>,
|
42
42
|
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>,
|
43
43
|
<<"CHANGELOG.md">>]}.
|
|
@@ -51,5 +51,5 @@
|
51
51
|
{<<"name">>,<<"deep_merge">>},
|
52
52
|
{<<"optional">>,false},
|
53
53
|
{<<"repository">>,<<"hexpm">>},
|
54
|
- {<<"requirement">>,<<"~> 0.1">>}]]}.
|
55
|
- {<<"version">>,<<"0.14.0">>}.
|
54
|
+ {<<"requirement">>,<<"~> 1.0">>}]]}.
|
55
|
+ {<<"version">>,<<"0.99.0">>}.
|
changed
lib/benchee.ex
|
@@ -3,7 +3,7 @@
|
3
3
|
# https://github.com/PragTob/benchee/commit/b3ddbc132e641cdf1eec0928b322ced1dab8553f#commitcomment-23381474
|
4
4
|
|
5
5
|
elixir_doc = """
|
6
|
- Top level module providing convenience access to needed functions as well
|
6
|
+ Top level module providing convenience access to needed functions as well
|
7
7
|
as the very high level `Benchee.run` API.
|
8
8
|
|
9
9
|
Intended Elixir interface.
|
|
@@ -44,6 +44,11 @@ for {module, moduledoc} <- [{Benchee, elixir_doc}, {:benchee, erlang_doc}] do
|
44
44
|
end
|
45
45
|
|
46
46
|
def run(config, jobs) when is_map(jobs) do
|
47
|
+ IO.puts("""
|
48
|
+ Passing configuration as the first argument to Benchee.run/2 is deprecated.
|
49
|
+ Please see the documentation for Benchee.run/2 for updated usage instructions.
|
50
|
+ """)
|
51
|
+
|
47
52
|
# pre 0.6.0 way of passing in the config first and as a map
|
48
53
|
do_run(jobs, config)
|
49
54
|
end
|
|
@@ -53,9 +58,10 @@ for {module, moduledoc} <- [{Benchee, elixir_doc}, {:benchee, erlang_doc}] do
|
53
58
|
|> Benchee.init()
|
54
59
|
|> Benchee.system()
|
55
60
|
|> add_benchmarking_jobs(jobs)
|
56
|
- |> Benchee.measure()
|
61
|
+ |> Benchee.collect()
|
57
62
|
|> Benchee.statistics()
|
58
63
|
|> Benchee.load()
|
64
|
+ |> Benchee.relative_statistics()
|
59
65
|
|> Formatter.output()
|
60
66
|
end
|
61
67
|
|
|
@@ -65,14 +71,58 @@ for {module, moduledoc} <- [{Benchee, elixir_doc}, {:benchee, erlang_doc}] do
|
65
71
|
end)
|
66
72
|
end
|
67
73
|
|
74
|
+ @doc false
|
75
|
+ def measure(suite) do
|
76
|
+ IO.puts("""
|
77
|
+ Benchee.measure/1 is deprecated, please use Benchee.collect/1.
|
78
|
+ """)
|
79
|
+
|
80
|
+ Benchee.Benchmark.collect(suite)
|
81
|
+ end
|
82
|
+
|
83
|
+ @doc """
|
84
|
+ See `Benchee.Configuration.init/1`
|
85
|
+ """
|
68
86
|
defdelegate init(), to: Benchee.Configuration
|
87
|
+
|
88
|
+ @doc """
|
89
|
+ See `Benchee.Configuration.init/1`
|
90
|
+ """
|
69
91
|
defdelegate init(config), to: Benchee.Configuration
|
92
|
+
|
93
|
+ @doc """
|
94
|
+ See `Benchee.System.system/1`
|
95
|
+ """
|
70
96
|
defdelegate system(suite), to: Benchee.System
|
97
|
+
|
98
|
+ @doc """
|
99
|
+ See `Benchee.Benchmark.benchmark/3`
|
100
|
+ """
|
71
101
|
defdelegate benchmark(suite, name, function), to: Benchee.Benchmark
|
102
|
+ @doc false
|
72
103
|
defdelegate benchmark(suite, name, function, printer), to: Benchee.Benchmark
|
73
|
- defdelegate measure(suite), to: Benchee.Benchmark
|
74
|
- defdelegate measure(suite, printer), to: Benchee.Benchmark
|
104
|
+
|
105
|
+ @doc """
|
106
|
+ See `Benchee.Benchmark.collect/1`
|
107
|
+ """
|
108
|
+ defdelegate collect(suite), to: Benchee.Benchmark
|
109
|
+
|
110
|
+ @doc false
|
111
|
+ defdelegate collect(suite, printer), to: Benchee.Benchmark
|
112
|
+
|
113
|
+ @doc """
|
114
|
+ See `Benchee.Statistics.statistics/1`
|
115
|
+ """
|
75
116
|
defdelegate statistics(suite), to: Benchee.Statistics
|
117
|
+
|
118
|
+ @doc """
|
119
|
+ See `Benchee.RelativeStatistics.relative_statistics/1`
|
120
|
+ """
|
121
|
+ defdelegate relative_statistics(suite), to: Benchee.RelativeStatistics
|
122
|
+
|
123
|
+ @doc """
|
124
|
+ See `Benchee.ScenarioLoader.load/1`
|
125
|
+ """
|
76
126
|
defdelegate load(suite), to: Benchee.ScenarioLoader
|
77
127
|
end
|
78
128
|
end
|
changed
lib/benchee/benchmark.ex
|
@@ -1,29 +1,29 @@
|
1
1
|
defmodule Benchee.Benchmark do
|
2
2
|
@moduledoc """
|
3
3
|
Functions related to building and running benchmarking scenarios.
|
4
|
- Exposes `benchmark/4` and `measure/3` functions.
|
4
|
+ Exposes `benchmark/4` and `collect/3` functions.
|
5
5
|
"""
|
6
6
|
|
7
|
- alias Benchee.Benchmark.{Runner, Scenario, ScenarioContext}
|
7
|
+ alias Benchee.Benchmark.{Runner, ScenarioContext}
|
8
8
|
alias Benchee.Output.BenchmarkPrinter, as: Printer
|
9
|
+ alias Benchee.Scenario
|
9
10
|
alias Benchee.Suite
|
10
11
|
alias Benchee.Utility.DeepConvert
|
11
12
|
|
12
|
- @type job_name :: String.t() | atom
|
13
13
|
@no_input :__no_input
|
14
14
|
|
15
15
|
@doc """
|
16
|
- Public access for the key representing no input for a scenario.
|
16
|
+ Public access for the special key representing no input for a scenario.
|
17
17
|
"""
|
18
18
|
def no_input, do: @no_input
|
19
19
|
|
20
20
|
@doc """
|
21
21
|
Takes the current suite and adds a new benchmarking scenario (represented by a
|
22
|
- %Scenario{} struct) to the suite's scenarios. If there are inputs in the
|
23
|
- suite's config, a scenario will be added for the given function for each
|
24
|
- input.
|
22
|
+ %Scenario{} struct) with the given name and function to the suite's scenarios.
|
23
|
+ If there are inputs in the suite's config, a scenario will be added for the given
|
24
|
+ function for each input.
|
25
25
|
"""
|
26
|
- @spec benchmark(Suite.t(), job_name, fun, module) :: Suite.t()
|
26
|
+ @spec benchmark(Suite.t(), Suite.key(), fun, module) :: Suite.t()
|
27
27
|
def benchmark(suite = %Suite{scenarios: scenarios}, job_name, function, printer \\ Printer) do
|
28
28
|
normalized_name = to_string(job_name)
|
29
29
|
|
|
@@ -99,13 +99,14 @@ defmodule Benchee.Benchmark do
|
99
99
|
end
|
100
100
|
|
101
101
|
@doc """
|
102
|
- Kicks off the benchmarking of all scenarios in the suite by passing the list
|
103
|
- of scenarios and a scenario context to our benchmark runner. For more
|
104
|
- information on how bencharmks are actually run, see
|
105
|
- `Benchee.Benchmark.Runner.run_scenarios/2`.
|
102
|
+ Kicks off the benchmarking of all scenarios defined in the given suite.
|
103
|
+
|
104
|
+ Hence, this might take a while ;) Passes a list of scenarios and a scenario context to our
|
105
|
+ benchmark runner. For more information on how benchmarks are actually run, see the
|
106
|
+ `Benchee.Benchmark.Runner` code (API considered private).
|
106
107
|
"""
|
107
|
- @spec measure(Suite.t(), module, module) :: Suite.t()
|
108
|
- def measure(
|
108
|
+ @spec collect(Suite.t(), module, module) :: Suite.t()
|
109
|
+ def collect(
|
109
110
|
suite = %Suite{scenarios: scenarios, configuration: config},
|
110
111
|
printer \\ Printer,
|
111
112
|
runner \\ Runner
|
added
lib/benchee/benchmark/collect.ex
|
@@ -0,0 +1,15 @@
|
1
|
+ defmodule Benchee.Benchmark.Collect do
|
2
|
+ @moduledoc false
|
3
|
+
|
4
|
+ # A thing that collects a data point about a function execution - like time
|
5
|
+ # or memory needed.
|
6
|
+
|
7
|
+ @doc """
|
8
|
+ Takes an anonymous 0 arity function to measure and returns the measurement
|
9
|
+ and the return value of the function in a tuple.
|
10
|
+
|
11
|
+ The returned measurement may be `nil` if the measurement failed for some
|
12
|
+ reason - it will then be ignored and not counted.
|
13
|
+ """
|
14
|
+ @callback collect((() -> any)) :: {non_neg_integer | nil, any}
|
15
|
+ end
|
added
lib/benchee/benchmark/collect/memory.ex
|
@@ -0,0 +1,149 @@
|
1
|
+ defmodule Benchee.Benchmark.Collect.Memory do
|
2
|
+ @moduledoc false
|
3
|
+
|
4
|
+ # Measure memory consumption of a function.
|
5
|
+ #
|
6
|
+ # This is somewhat tricky and hence some resources can be recommended reading alongside
|
7
|
+ # this code:
|
8
|
+ # * description of the approach: https://devonestes.herokuapp.com/using-erlang-trace-3
|
9
|
+ # * devon describing the journey that this feature put us through (includes remarks
|
10
|
+ # on why certain parts are very important: https://www.youtube.com/watch?v=aqLujfzvUgM)
|
11
|
+ # * erlang docs on the info data structure we use:
|
12
|
+ # https://erlang.org/doc/man/erlang.html#gc_minor_start
|
13
|
+ #
|
14
|
+ # Returns `{nil, return_value}` in case the memory measurement went bad.
|
15
|
+
|
16
|
+ @behaviour Benchee.Benchmark.Collect
|
17
|
+
|
18
|
+ def collect(fun) do
|
19
|
+ ref = make_ref()
|
20
|
+ Process.flag(:trap_exit, true)
|
21
|
+ start_runner(fun, ref)
|
22
|
+ await_results(nil, ref)
|
23
|
+ end
|
24
|
+
|
25
|
+ defp await_results(return_value, ref) do
|
26
|
+ receive do
|
27
|
+ {^ref, memory_usage} ->
|
28
|
+ return_memory({memory_usage, return_value})
|
29
|
+
|
30
|
+ {^ref, :shutdown} ->
|
31
|
+ nil
|
32
|
+
|
33
|
+ # we need a really basic pattern here because sending anything other than
|
34
|
+ # just what's returned from the function that we're benchmarking will
|
35
|
+ # involve allocating a new term, which will skew the measurements.
|
36
|
+ # We need to be very careful to always send the `ref` in every other
|
37
|
+ # message to this process.
|
38
|
+ new_result ->
|
39
|
+ await_results(new_result, ref)
|
40
|
+ end
|
41
|
+ end
|
42
|
+
|
43
|
+ defp start_runner(fun, ref) do
|
44
|
+ parent = self()
|
45
|
+
|
46
|
+ spawn_link(fn ->
|
47
|
+ tracer = start_tracer(self())
|
48
|
+
|
49
|
+ try do
|
50
|
+ _ = measure_memory(fun, tracer, parent)
|
51
|
+ word_size = :erlang.system_info(:wordsize)
|
52
|
+ memory_used = get_collected_memory(tracer)
|
53
|
+ send(parent, {ref, memory_used * word_size})
|
54
|
+ catch
|
55
|
+ kind, reason ->
|
56
|
+ # would love to have this in a separate function, but elixir 1.7 complains
|
57
|
+ send(tracer, :done)
|
58
|
+ send(parent, {ref, :shutdown})
|
59
|
+ stacktrace = System.stacktrace()
|
60
|
+ IO.puts(Exception.format(kind, reason, stacktrace))
|
61
|
+ exit(:normal)
|
62
|
+ after
|
63
|
+ send(tracer, :done)
|
64
|
+ end
|
65
|
+ end)
|
66
|
+ end
|
67
|
+
|
68
|
+ defp return_memory({memory_usage, return_value}) when memory_usage < 0, do: {nil, return_value}
|
69
|
+ defp return_memory(memory_usage_info), do: memory_usage_info
|
70
|
+
|
71
|
+ defp measure_memory(fun, tracer, parent) do
|
72
|
+ :erlang.garbage_collect()
|
73
|
+ send(tracer, :begin_collection)
|
74
|
+
|
75
|
+ receive do
|
76
|
+ :ready_to_begin -> nil
|
77
|
+ end
|
78
|
+
|
79
|
+ return_value = fun.()
|
80
|
+ send(parent, return_value)
|
81
|
+
|
82
|
+ :erlang.garbage_collect()
|
83
|
+ send(tracer, :end_collection)
|
84
|
+
|
85
|
+ receive do
|
86
|
+ :ready_to_end -> nil
|
87
|
+ end
|
88
|
+
|
89
|
+ # We need to reference these variables after we end our collection so
|
90
|
+ # these don't get GC'd and counted towards the memory usage of the function
|
91
|
+ # we're benchmarking.
|
92
|
+ {parent, fun}
|
93
|
+ end
|
94
|
+
|
95
|
+ defp get_collected_memory(tracer) do
|
96
|
+ ref = Process.monitor(tracer)
|
97
|
+ send(tracer, {:get_collected_memory, self(), ref})
|
98
|
+
|
99
|
+ receive do
|
100
|
+ {:DOWN, ^ref, _, _, _} -> nil
|
101
|
+ {^ref, collected} -> collected
|
102
|
+ end
|
103
|
+ end
|
104
|
+
|
105
|
+ defp start_tracer(pid) do
|
106
|
+ spawn(fn -> tracer_loop(pid, 0) end)
|
107
|
+ end
|
108
|
+
|
109
|
+ defp tracer_loop(pid, acc) do
|
110
|
+ receive do
|
111
|
+ :begin_collection ->
|
112
|
+ :erlang.trace(pid, true, [:garbage_collection, tracer: self()])
|
113
|
+ send(pid, :ready_to_begin)
|
114
|
+ tracer_loop(pid, acc)
|
115
|
+
|
116
|
+ :end_collection ->
|
117
|
+ :erlang.trace(pid, false, [:garbage_collection])
|
118
|
+ send(pid, :ready_to_end)
|
119
|
+ tracer_loop(pid, acc)
|
120
|
+
|
121
|
+ {:get_collected_memory, reply_to, ref} ->
|
122
|
+ send(reply_to, {ref, acc})
|
123
|
+
|
124
|
+ {:trace, ^pid, :gc_minor_start, info} ->
|
125
|
+ listen_gc_end(pid, :gc_minor_end, acc, total_memory(info))
|
126
|
+
|
127
|
+ {:trace, ^pid, :gc_major_start, info} ->
|
128
|
+ listen_gc_end(pid, :gc_major_end, acc, total_memory(info))
|
129
|
+
|
130
|
+ :done ->
|
131
|
+ exit(:normal)
|
132
|
+ end
|
133
|
+ end
|
134
|
+
|
135
|
+ defp listen_gc_end(pid, tag, acc, mem_before) do
|
136
|
+ receive do
|
137
|
+ {:trace, ^pid, ^tag, info} ->
|
138
|
+ mem_after = total_memory(info)
|
139
|
+ tracer_loop(pid, acc + mem_before - mem_after)
|
140
|
+ end
|
141
|
+ end
|
142
|
+
|
143
|
+ defp total_memory(info) do
|
144
|
+ # `:heap_size` seems to only contain the memory size of the youngest
|
145
|
+ # generation `:old_heap_size` has the old generation. There is also
|
146
|
+ # `:recent_size` but that seems to already be accounted for.
|
147
|
+ Keyword.fetch!(info, :heap_size) + Keyword.fetch!(info, :old_heap_size)
|
148
|
+ end
|
149
|
+ end
|
added
lib/benchee/benchmark/collect/native_time.ex
|
@@ -0,0 +1,18 @@
|
1
|
+ defmodule Benchee.Benchmark.Collect.NativeTime do
|
2
|
+ @moduledoc false
|
3
|
+
|
4
|
+ # Measure the time elapsed while executing a given function.
|
5
|
+ #
|
6
|
+ # Uses only the time unit native to the platform. Used for determining how many times a function
|
7
|
+ # should be repeated in `Benchee.Benchmark.Runner.determine_n_times/3` (private method though).
|
8
|
+
|
9
|
+ @behaviour Benchee.Benchmark.Collect
|
10
|
+
|
11
|
+ def collect(function) do
|
12
|
+ start = :erlang.monotonic_time()
|
13
|
+ result = function.()
|
14
|
+ finish = :erlang.monotonic_time()
|
15
|
+
|
16
|
+ {finish - start, result}
|
17
|
+ end
|
18
|
+ end
|
added
lib/benchee/benchmark/collect/time.ex
|
@@ -0,0 +1,23 @@
|
1
|
+ defmodule Benchee.Benchmark.Collect.Time do
|
2
|
+ @moduledoc false
|
3
|
+
|
4
|
+ # Measure the time elapsed while executing a given function.
|
5
|
+ #
|
6
|
+ # In contrast to `:timer.tc/1` it always returns the result in nano seconds instead of micro
|
7
|
+ # seconds. This helps us avoid losing precision as both Linux and MacOSX seem to be able to
|
8
|
+ # measure in nano seconds. `:timer.tc/n`
|
9
|
+ # [forfeits this precision](
|
10
|
+ # https://github.com/erlang/otp/blob/master/lib/stdlib/src/timer.erl#L164-L169).
|
11
|
+
|
12
|
+ @behaviour Benchee.Benchmark.Collect
|
13
|
+
|
14
|
+ def collect(function) do
|
15
|
+ start = :erlang.monotonic_time()
|
16
|
+ result = function.()
|
17
|
+ finish = :erlang.monotonic_time()
|
18
|
+
|
19
|
+ duration_nano_seconds = :erlang.convert_time_unit(finish - start, :native, :nanosecond)
|
20
|
+
|
21
|
+ {duration_nano_seconds, result}
|
22
|
+ end
|
23
|
+ end
|
changed
lib/benchee/benchmark/hooks.ex
|
@@ -2,7 +2,8 @@ defmodule Benchee.Benchmark.Hooks do
|
2
2
|
@moduledoc false
|
3
3
|
# Non benchee code should not rely on this module.
|
4
4
|
|
5
|
- alias Benchee.Benchmark.{Scenario, ScenarioContext}
|
5
|
+ alias Benchee.Benchmark.ScenarioContext
|
6
|
+ alias Benchee.Scenario
|
6
7
|
|
7
8
|
def run_before_scenario(
|
8
9
|
%Scenario{
|
removed
lib/benchee/benchmark/measure.ex
|
@@ -1,15 +0,0 @@
|
1
|
- defmodule Benchee.Benchmark.Measure do
|
2
|
- @moduledoc false
|
3
|
-
|
4
|
- # A thing that measures something about a function execution - like time or
|
5
|
- # memory needed.
|
6
|
-
|
7
|
- @doc """
|
8
|
- Takes an anonymous 0 arity function to measure and returns the measurement
|
9
|
- and the return value of the function in a tuple.
|
10
|
-
|
11
|
- The returned measurement may be `nil` if the measurement failed for some
|
12
|
- reason - it will then be ignored and not counted.
|
13
|
- """
|
14
|
- @callback measure((() -> any)) :: {non_neg_integer | nil, any}
|
15
|
- end
|
removed
lib/benchee/benchmark/measure/memory.ex
|
@@ -1,149 +0,0 @@
|
1
|
- defmodule Benchee.Benchmark.Measure.Memory do
|
2
|
- @moduledoc false
|
3
|
-
|
4
|
- # Measure memory consumption of a function.
|
5
|
- #
|
6
|
- # This is somewhat tricky and hence some resources can be recommended reading alongside
|
7
|
- # this code:
|
8
|
- # * description of the approach: https://devonestes.herokuapp.com/using-erlang-trace-3
|
9
|
- # * devon describing the journey that this feature put us through (includes remarks
|
10
|
- # on why certain parts are very important: https://www.youtube.com/watch?v=aqLujfzvUgM)
|
11
|
- # * erlang docs on the info data structure we use:
|
12
|
- # https://erlang.org/doc/man/erlang.html#gc_minor_start
|
13
|
- #
|
14
|
- # Returns `{nil, return_value}` in case the memory measurement went bad.
|
15
|
-
|
16
|
- @behaviour Benchee.Benchmark.Measure
|
17
|
-
|
18
|
- def measure(fun) do
|
19
|
- ref = make_ref()
|
20
|
- Process.flag(:trap_exit, true)
|
21
|
- start_runner(fun, ref)
|
22
|
- await_results(nil, ref)
|
23
|
- end
|
24
|
-
|
25
|
- defp await_results(return_value, ref) do
|
26
|
- receive do
|
27
|
- {^ref, memory_usage} ->
|
28
|
- return_memory({memory_usage, return_value})
|
29
|
-
|
30
|
- {^ref, :shutdown} ->
|
31
|
- nil
|
32
|
-
|
33
|
- # we need a really basic pattern here because sending anything other than
|
34
|
- # just what's returned from the function that we're benchmarking will
|
35
|
- # involve allocating a new term, which will skew the measurements.
|
36
|
- # We need to be very careful to always send the `ref` in every other
|
37
|
- # message to this process.
|
38
|
- new_result ->
|
39
|
- await_results(new_result, ref)
|
40
|
- end
|
41
|
- end
|
42
|
-
|
43
|
- defp start_runner(fun, ref) do
|
44
|
- parent = self()
|
45
|
-
|
46
|
- spawn_link(fn ->
|
47
|
- tracer = start_tracer(self())
|
48
|
-
|
49
|
- try do
|
50
|
- _ = measure_memory(fun, tracer, parent)
|
51
|
- word_size = :erlang.system_info(:wordsize)
|
52
|
- memory_used = get_collected_memory(tracer)
|
53
|
- send(parent, {ref, memory_used * word_size})
|
54
|
- catch
|
55
|
- kind, reason ->
|
56
|
- # would love to have this in a separate function, but elixir 1.7 complains
|
57
|
- send(tracer, :done)
|
58
|
- send(parent, {ref, :shutdown})
|
59
|
- stacktrace = System.stacktrace()
|
60
|
- IO.puts(Exception.format(kind, reason, stacktrace))
|
61
|
- exit(:normal)
|
62
|
- after
|
63
|
- send(tracer, :done)
|
64
|
- end
|
65
|
- end)
|
66
|
- end
|
67
|
-
|
68
|
- defp return_memory({memory_usage, return_value}) when memory_usage < 0, do: {nil, return_value}
|
69
|
- defp return_memory(memory_usage_info), do: memory_usage_info
|
70
|
-
|
71
|
- defp measure_memory(fun, tracer, parent) do
|
72
|
- :erlang.garbage_collect()
|
73
|
- send(tracer, :begin_collection)
|
74
|
-
|
75
|
- receive do
|
76
|
- :ready_to_begin -> nil
|
77
|
- end
|
78
|
-
|
79
|
- return_value = fun.()
|
80
|
- send(parent, return_value)
|
81
|
-
|
82
|
- :erlang.garbage_collect()
|
83
|
- send(tracer, :end_collection)
|
84
|
-
|
85
|
- receive do
|
86
|
- :ready_to_end -> nil
|
87
|
- end
|
88
|
-
|
89
|
- # We need to reference these variables after we end our collection so
|
90
|
- # these don't get GC'd and counted towards the memory usage of the function
|
91
|
- # we're benchmarking.
|
92
|
- {parent, fun}
|
93
|
- end
|
94
|
-
|
95
|
- defp get_collected_memory(tracer) do
|
96
|
- ref = Process.monitor(tracer)
|
97
|
- send(tracer, {:get_collected_memory, self(), ref})
|
98
|
-
|
99
|
- receive do
|
100
|
- {:DOWN, ^ref, _, _, _} -> nil
|
101
|
- {^ref, collected} -> collected
|
102
|
- end
|
103
|
- end
|
104
|
-
|
105
|
- defp start_tracer(pid) do
|
106
|
- spawn(fn -> tracer_loop(pid, 0) end)
|
107
|
- end
|
108
|
-
|
109
|
- defp tracer_loop(pid, acc) do
|
110
|
- receive do
|
111
|
- :begin_collection ->
|
112
|
- :erlang.trace(pid, true, [:garbage_collection, tracer: self()])
|
113
|
- send(pid, :ready_to_begin)
|
114
|
- tracer_loop(pid, acc)
|
115
|
-
|
116
|
- :end_collection ->
|
117
|
- :erlang.trace(pid, false, [:garbage_collection])
|
118
|
- send(pid, :ready_to_end)
|
119
|
- tracer_loop(pid, acc)
|
120
|
-
|
121
|
- {:get_collected_memory, reply_to, ref} ->
|
122
|
- send(reply_to, {ref, acc})
|
123
|
-
|
124
|
- {:trace, ^pid, :gc_minor_start, info} ->
|
125
|
- listen_gc_end(pid, :gc_minor_end, acc, total_memory(info))
|
126
|
-
|
127
|
- {:trace, ^pid, :gc_major_start, info} ->
|
128
|
- listen_gc_end(pid, :gc_major_end, acc, total_memory(info))
|
129
|
-
|
130
|
- :done ->
|
131
|
- exit(:normal)
|
132
|
- end
|
133
|
- end
|
134
|
-
|
135
|
- defp listen_gc_end(pid, tag, acc, mem_before) do
|
136
|
- receive do
|
137
|
- {:trace, ^pid, ^tag, info} ->
|
138
|
- mem_after = total_memory(info)
|
139
|
- tracer_loop(pid, acc + mem_before - mem_after)
|
140
|
- end
|
141
|
- end
|
142
|
-
|
143
|
- defp total_memory(info) do
|
144
|
- # `:heap_size` seems to only contain the memory size of the youngest
|
145
|
- # generation `:old_heap_size` has the old generation. There is also
|
146
|
- # `:recent_size` but that seems to already be accounted for.
|
147
|
- Keyword.fetch!(info, :heap_size) + Keyword.fetch!(info, :old_heap_size)
|
148
|
- end
|
149
|
- end
|
removed
lib/benchee/benchmark/measure/native_time.ex
|
@@ -1,18 +0,0 @@
|
1
|
- defmodule Benchee.Benchmark.Measure.NativeTime do
|
2
|
- @moduledoc false
|
3
|
-
|
4
|
- # Measure the time elapsed while executing a given function.
|
5
|
- #
|
6
|
- # Uses only the time unit native to the platform. Used for determining how many times a function
|
7
|
- # should be repeated in `Benchee.Benchmark.Runner.determine_n_times/3` (private method though).
|
8
|
-
|
9
|
- @behaviour Benchee.Benchmark.Measure
|
10
|
-
|
11
|
- def measure(function) do
|
12
|
- start = :erlang.monotonic_time()
|
13
|
- result = function.()
|
14
|
- finish = :erlang.monotonic_time()
|
15
|
-
|
16
|
- {finish - start, result}
|
17
|
- end
|
18
|
- end
|
removed
lib/benchee/benchmark/measure/time.ex
|
@@ -1,23 +0,0 @@
|
1
|
- defmodule Benchee.Benchmark.Measure.Time do
|
2
|
- @moduledoc false
|
3
|
-
|
4
|
- # Measure the time elapsed while executing a given function.
|
5
|
- #
|
6
|
- # In contrast to `:timer.tc/1` it always returns the result in nano seconds instead of micro
|
7
|
- # seconds. This helps us avoid losing precision as both Linux and MacOSX seem to be able to
|
8
|
- # measure in nano seconds. `:timer.tc/n`
|
9
|
- # [forfeits this precision](
|
10
|
- # https://github.com/erlang/otp/blob/master/lib/stdlib/src/timer.erl#L164-L169).
|
11
|
-
|
12
|
- @behaviour Benchee.Benchmark.Measure
|
13
|
-
|
14
|
- def measure(function) do
|
15
|
- start = :erlang.monotonic_time()
|
16
|
- result = function.()
|
17
|
- finish = :erlang.monotonic_time()
|
18
|
-
|
19
|
- duration_nano_seconds = :erlang.convert_time_unit(finish - start, :native, :nanosecond)
|
20
|
-
|
21
|
- {duration_nano_seconds, result}
|
22
|
- end
|
23
|
- end
|
changed
lib/benchee/benchmark/repeated_measurement.ex
|
@@ -21,7 +21,8 @@ defmodule Benchee.Benchmark.RepeatedMeasurement do
|
21
21
|
# with too high variance. Therefore determine an n how often it should be
|
22
22
|
# executed in the measurement cycle.
|
23
23
|
|
24
|
- alias Benchee.Benchmark.{Hooks, Measure, Runner, Scenario, ScenarioContext}
|
24
|
+ alias Benchee.Benchmark.{Collect, Hooks, Runner, ScenarioContext}
|
25
|
+ alias Benchee.Scenario
|
25
26
|
alias Benchee.Utility.RepeatN
|
26
27
|
|
27
28
|
@minimum_execution_time 10
|
|
@@ -33,12 +34,12 @@ defmodule Benchee.Benchmark.RepeatedMeasurement do
|
33
34
|
printer: printer
|
34
35
|
},
|
35
36
|
fast_warning,
|
36
|
- measurer \\ Measure.NativeTime
|
37
|
+ collector \\ Collect.NativeTime
|
37
38
|
) do
|
38
|
- run_time = measure_iteration(scenario, scenario_context, measurer)
|
39
|
+ run_time = measure_iteration(scenario, scenario_context, collector)
|
39
40
|
|
40
41
|
if run_time >= @minimum_execution_time do
|
41
|
- {num_iterations, adjust_for_iterations(run_time, num_iterations)}
|
42
|
+ {num_iterations, report_time(run_time, num_iterations)}
|
42
43
|
else
|
43
44
|
if fast_warning, do: printer.fast_warning()
|
44
45
|
|
|
@@ -47,21 +48,29 @@ defmodule Benchee.Benchmark.RepeatedMeasurement do
|
47
48
|
| num_iterations: num_iterations * @times_multiplier
|
48
49
|
}
|
49
50
|
|
50
|
- determine_n_times(scenario, new_context, false, measurer)
|
51
|
+ determine_n_times(scenario, new_context, false, collector)
|
51
52
|
end
|
52
53
|
end
|
53
54
|
|
55
|
+ # we need to convert the time here since we measure native time to see when we have enough
|
56
|
+ # repetitions but the first time is used in the actual samples
|
57
|
+ defp report_time(measurement, num_iterations) do
|
58
|
+ measurement
|
59
|
+ |> :erlang.convert_time_unit(:native, :nanosecond)
|
60
|
+ |> adjust_for_iterations(num_iterations)
|
61
|
+ end
|
62
|
+
|
54
63
|
defp adjust_for_iterations(measurement, 1), do: measurement
|
55
64
|
defp adjust_for_iterations(measurement, num_iterations), do: measurement / num_iterations
|
56
65
|
|
57
|
- def measure(
|
66
|
+ def collect(
|
58
67
|
scenario,
|
59
68
|
scenario_context = %ScenarioContext{
|
60
69
|
num_iterations: num_iterations
|
61
70
|
},
|
62
|
- measurer
|
71
|
+ collector
|
63
72
|
) do
|
64
|
- measurement = measure_iteration(scenario, scenario_context, measurer)
|
73
|
+ measurement = measure_iteration(scenario, scenario_context, collector)
|
65
74
|
|
66
75
|
adjust_for_iterations(measurement, num_iterations)
|
67
76
|
end
|
|
@@ -71,9 +80,9 @@ defmodule Benchee.Benchmark.RepeatedMeasurement do
|
71
80
|
scenario_context = %ScenarioContext{
|
72
81
|
num_iterations: 1
|
73
82
|
},
|
74
|
- measurer
|
83
|
+ collector
|
75
84
|
) do
|
76
|
- Runner.measure(scenario, scenario_context, measurer)
|
85
|
+ Runner.collect(scenario, scenario_context, collector)
|
77
86
|
end
|
78
87
|
|
79
88
|
defp measure_iteration(
|
|
@@ -81,7 +90,7 @@ defmodule Benchee.Benchmark.RepeatedMeasurement do
|
81
90
|
scenario_context = %ScenarioContext{
|
82
91
|
num_iterations: iterations
|
83
92
|
},
|
84
|
- measurer
|
93
|
+ collector
|
85
94
|
)
|
86
95
|
when iterations > 1 do
|
87
96
|
# When we have more than one iteration, then the repetition and calling
|
|
@@ -89,7 +98,7 @@ defmodule Benchee.Benchmark.RepeatedMeasurement do
|
89
98
|
# `build_benchmarking_function/2`
|
90
99
|
function = build_benchmarking_function(scenario, scenario_context)
|
91
100
|
|
92
|
- {measurement, _return_value} = measurer.measure(function)
|
101
|
+ {measurement, _return_value} = collector.collect(function)
|
93
102
|
|
94
103
|
measurement
|
95
104
|
end
|
changed
lib/benchee/benchmark/runner.ex
|
@@ -4,8 +4,8 @@ defmodule Benchee.Benchmark.Runner do
|
4
4
|
# This module actually runs our benchmark scenarios, adding information about
|
5
5
|
# run time and memory usage to each scenario.
|
6
6
|
|
7
|
- alias Benchee.{Benchmark, Configuration, Conversion, Statistics, Utility.Parallel}
|
8
|
- alias Benchmark.{Hooks, Measure, RepeatedMeasurement, Scenario, ScenarioContext}
|
7
|
+ alias Benchee.{Benchmark, Configuration, Conversion, Scenario, Statistics, Utility.Parallel}
|
8
|
+ alias Benchmark.{Collect, Hooks, RepeatedMeasurement, ScenarioContext}
|
9
9
|
|
10
10
|
@doc """
|
11
11
|
Executes the benchmarks defined before by first running the defined functions
|
|
@@ -47,7 +47,7 @@ defmodule Benchee.Benchmark.Runner do
|
47
47
|
defp pre_check(scenario, scenario_context) do
|
48
48
|
scenario_input = Hooks.run_before_scenario(scenario, scenario_context)
|
49
49
|
scenario_context = %ScenarioContext{scenario_context | scenario_input: scenario_input}
|
50
|
- _ = measure(scenario, scenario_context, Measure.Time)
|
50
|
+ _ = collect(scenario, scenario_context, Collect.Time)
|
51
51
|
_ = Hooks.run_after_scenario(scenario, scenario_context)
|
52
52
|
nil
|
53
53
|
end
|
|
@@ -94,7 +94,11 @@ defmodule Benchee.Benchmark.Runner do
|
94
94
|
run_times = Enum.flat_map(measurements, fn {run_times, _} -> run_times end)
|
95
95
|
memory_usages = Enum.flat_map(measurements, fn {_, memory_usages} -> memory_usages end)
|
96
96
|
|
97
|
- %Scenario{scenario | run_times: run_times, memory_usages: memory_usages}
|
97
|
+ %{
|
98
|
+ scenario
|
99
|
+ | run_time_data: %{scenario.run_time_data | samples: run_times},
|
100
|
+ memory_usage_data: %{scenario.memory_usage_data | samples: memory_usages}
|
101
|
+ }
|
98
102
|
end
|
99
103
|
|
100
104
|
defp measure_scenario(scenario, scenario_context) do
|
|
@@ -164,7 +168,7 @@ defmodule Benchee.Benchmark.Runner do
|
164
168
|
end_time: end_time
|
165
169
|
}
|
166
170
|
|
167
|
- do_benchmark(scenario, new_context, Measure.Memory, [])
|
171
|
+ do_benchmark(scenario, new_context, Collect.Memory, [])
|
168
172
|
end
|
169
173
|
|
170
174
|
defp measure_runtimes(scenario, context, run_time, fast_warning)
|
|
@@ -184,7 +188,7 @@ defmodule Benchee.Benchmark.Runner do
|
184
188
|
num_iterations: num_iterations
|
185
189
|
}
|
186
190
|
|
187
|
- do_benchmark(scenario, new_context, Measure.Time, [initial_run_time])
|
191
|
+ do_benchmark(scenario, new_context, Collect.Time, [initial_run_time])
|
188
192
|
end
|
189
193
|
|
190
194
|
defp current_time, do: :erlang.system_time(:nano_seconds)
|
|
@@ -200,7 +204,7 @@ defmodule Benchee.Benchmark.Runner do
|
200
204
|
current_time: current_time,
|
201
205
|
end_time: end_time
|
202
206
|
},
|
203
|
- _measurer,
|
207
|
+ _collector,
|
204
208
|
measurements
|
205
209
|
)
|
206
210
|
when current_time > end_time do
|
|
@@ -208,14 +212,14 @@ defmodule Benchee.Benchmark.Runner do
|
208
212
|
Enum.reverse(measurements)
|
209
213
|
end
|
210
214
|
|
211
|
- defp do_benchmark(scenario, scenario_context, measurer, measurements) do
|
212
|
- measurement = measure(scenario, scenario_context, measurer)
|
215
|
+ defp do_benchmark(scenario, scenario_context, collector, measurements) do
|
216
|
+ measurement = collect(scenario, scenario_context, collector)
|
213
217
|
updated_context = %ScenarioContext{scenario_context | current_time: current_time()}
|
214
218
|
|
215
219
|
do_benchmark(
|
216
220
|
scenario,
|
217
221
|
updated_context,
|
218
|
- measurer,
|
222
|
+ collector,
|
219
223
|
updated_measurements(measurement, measurements)
|
220
224
|
)
|
221
225
|
end
|
|
@@ -225,35 +229,35 @@ defmodule Benchee.Benchmark.Runner do
|
225
229
|
defp updated_measurements(measurement, measurements), do: [measurement | measurements]
|
226
230
|
|
227
231
|
@doc """
|
228
|
- Takes one measure with the given measurer.
|
232
|
+ Takes one measure with the given collector.
|
229
233
|
|
230
234
|
Correctly dispatches based on the number of iterations to perform.
|
231
235
|
"""
|
232
|
- def measure(
|
236
|
+ def collect(
|
233
237
|
scenario = %Scenario{function: function},
|
234
238
|
scenario_context = %ScenarioContext{
|
235
239
|
num_iterations: 1
|
236
240
|
},
|
237
|
- measurer
|
241
|
+ collector
|
238
242
|
) do
|
239
243
|
new_input = Hooks.run_before_each(scenario, scenario_context)
|
240
244
|
function = main_function(function, new_input)
|
241
245
|
|
242
|
- {measurement, return_value} = measurer.measure(function)
|
246
|
+ {measurement, return_value} = collector.collect(function)
|
243
247
|
|
244
248
|
Hooks.run_after_each(return_value, scenario, scenario_context)
|
245
249
|
measurement
|
246
250
|
end
|
247
251
|
|
248
|
- def measure(
|
252
|
+ def collect(
|
249
253
|
scenario,
|
250
254
|
scenario_context = %ScenarioContext{
|
251
255
|
num_iterations: iterations
|
252
256
|
},
|
253
|
- measurer
|
257
|
+ collector
|
254
258
|
)
|
255
259
|
when iterations > 1 do
|
256
|
- RepeatedMeasurement.measure(scenario, scenario_context, measurer)
|
260
|
+ RepeatedMeasurement.collect(scenario, scenario_context, collector)
|
257
261
|
end
|
258
262
|
|
259
263
|
def main_function(function, @no_input), do: function
|
removed
lib/benchee/benchmark/scenario.ex
|
@@ -1,102 +0,0 @@
|
1
|
- defmodule Benchee.Benchmark.Scenario do
|
2
|
- @moduledoc """
|
3
|
- A Scenario in Benchee is a particular case of a whole benchmarking suite. That
|
4
|
- is the combination of a particular function to benchmark (`job_name` and
|
5
|
- `function`) in combination with a specific input (`input_name` and `input`).
|
6
|
-
|
7
|
- It then gathers all data measured for this particular combination during
|
8
|
- `Benchee.Benchmark.measure/3` (`run_times` and `memory_usages`),
|
9
|
- which are then used later in the process by `Benchee.Statistics` to compute
|
10
|
- the relevant statistics (`run_time_statistics` and `memory_usage_statistics`).
|
11
|
-
|
12
|
- `name` is the name that should be used by formatters to display scenarios as
|
13
|
- it potentially includes the `tag` present when loading scenarios that were
|
14
|
- saved before. See `display_name/1`.
|
15
|
- """
|
16
|
- defstruct [
|
17
|
- :name,
|
18
|
- :job_name,
|
19
|
- :function,
|
20
|
- :input_name,
|
21
|
- :input,
|
22
|
- :run_time_statistics,
|
23
|
- :memory_usage_statistics,
|
24
|
- run_times: [],
|
25
|
- memory_usages: [],
|
26
|
- before_each: nil,
|
27
|
- after_each: nil,
|
28
|
- before_scenario: nil,
|
29
|
- after_scenario: nil,
|
30
|
- tag: nil
|
31
|
- ]
|
32
|
-
|
33
|
- @type t :: %__MODULE__{
|
34
|
- name: String.t(),
|
35
|
- job_name: String.t(),
|
36
|
- function: fun,
|
37
|
- input_name: String.t() | nil,
|
38
|
- input: any | nil,
|
39
|
- run_times: [float],
|
40
|
- run_time_statistics: Benchee.Statistics.t() | nil,
|
41
|
- memory_usages: [non_neg_integer],
|
42
|
- memory_usage_statistics: Benchee.Statistics.t() | nil,
|
43
|
- before_each: fun | nil,
|
44
|
- after_each: fun | nil,
|
45
|
- before_scenario: fun | nil,
|
46
|
- after_scenario: fun | nil,
|
47
|
- tag: String.t() | nil
|
48
|
- }
|
49
|
-
|
50
|
- @doc """
|
51
|
- Returns the correct name to display of the given scenario data.
|
52
|
-
|
53
|
- In the normal case this is `job_name`, however when scenarios are loaded they
|
54
|
- are tagged and these tags should be shown for disambiguation.
|
55
|
-
|
56
|
- ## Examples
|
57
|
-
|
58
|
- iex> alias Benchee.Benchmark.Scenario
|
59
|
- iex> Scenario.display_name(%Scenario{job_name: "flat_map"})
|
60
|
- "flat_map"
|
61
|
- iex> Scenario.display_name(%Scenario{job_name: "flat_map", tag: "master"})
|
62
|
- "flat_map (master)"
|
63
|
- iex> Scenario.display_name(%{job_name: "flat_map"})
|
64
|
- "flat_map"
|
65
|
- """
|
66
|
- @spec display_name(t) :: String.t()
|
67
|
- def display_name(%{job_name: job_name, tag: nil}), do: job_name
|
68
|
- def display_name(%{job_name: job_name, tag: tag}), do: "#{job_name} (#{tag})"
|
69
|
- def display_name(%{job_name: job_name}), do: job_name
|
70
|
-
|
71
|
- @doc """
|
72
|
- Returns `true` if data of the provided type has been fully procsessed, `false` otherwise.
|
73
|
-
|
74
|
- Current available types are `run_time` and `memory`. Reasons they might not have been processed
|
75
|
- yet are:
|
76
|
- * Suite wasn't configured to collect them at all
|
77
|
- * `Benchee.statistics/1` hasn't been called yet so that data was collected but statistics
|
78
|
- aren't present yet
|
79
|
-
|
80
|
- ## Examples
|
81
|
-
|
82
|
- iex> alias Benchee.Benchmark.Scenario
|
83
|
- iex> alias Benchee.Statistics
|
84
|
- iex> scenario = %Scenario{run_time_statistics: %Statistics{sample_size: 100}}
|
85
|
- iex> Scenario.data_processed?(scenario, :run_time)
|
86
|
- true
|
87
|
- iex> scenario = %Scenario{memory_usage_statistics: %Statistics{sample_size: 1}}
|
88
|
- iex> Scenario.data_processed?(scenario, :memory)
|
89
|
- true
|
90
|
- iex> scenario = %Scenario{memory_usage_statistics: %Statistics{sample_size: 0}}
|
91
|
- iex> Scenario.data_processed?(scenario, :memory)
|
92
|
- false
|
93
|
- """
|
94
|
- @spec data_processed?(t, :run_time | :memory) :: boolean
|
95
|
- def data_processed?(scenario, :run_time) do
|
96
|
- scenario.run_time_statistics.sample_size > 0
|
97
|
- end
|
98
|
-
|
99
|
- def data_processed?(scenario, :memory) do
|
100
|
- scenario.memory_usage_statistics.sample_size > 0
|
101
|
- end
|
102
|
- end
|
added
lib/benchee/collection_data.ex
|
@@ -0,0 +1,19 @@
|
1
|
+ defmodule Benchee.CollectionData do
|
2
|
+ @moduledoc """
|
3
|
+ The unified data structure for a given collection of data.
|
4
|
+
|
5
|
+ Consists of the recorded `samples` and the statistics computed from them.
|
6
|
+ """
|
7
|
+
|
8
|
+ defstruct statistics: %Benchee.Statistics{}, samples: []
|
9
|
+
|
10
|
+ @typedoc """
|
11
|
+ Samples and statistics.
|
12
|
+
|
13
|
+ Statistics might only come later when they are computed.
|
14
|
+ """
|
15
|
+ @type t :: %__MODULE__{
|
16
|
+ samples: [float | non_neg_integer],
|
17
|
+ statistics: Benchee.Statistics.t() | nil
|
18
|
+ }
|
19
|
+ end
|
changed
lib/benchee/configuration.ex
|
@@ -31,12 +31,7 @@ defmodule Benchee.Configuration do
|
31
31
|
load: false,
|
32
32
|
# formatters should end up here but known once are still picked up at
|
33
33
|
# the top level for now
|
34
|
- formatter_options: %{
|
35
|
- console: %{
|
36
|
- comparison: true,
|
37
|
- extended_statistics: false
|
38
|
- }
|
39
|
- },
|
34
|
+ formatter_options: %{},
|
40
35
|
unit_scaling: :best,
|
41
36
|
# If you/your plugin/whatever needs it your data can go here
|
42
37
|
assigns: %{},
|
|
@@ -47,37 +42,10 @@ defmodule Benchee.Configuration do
|
47
42
|
measure_function_call_overhead: true,
|
48
43
|
title: nil
|
49
44
|
|
50
|
- @type t :: %__MODULE__{
|
51
|
- parallel: integer,
|
52
|
- time: number,
|
53
|
- warmup: number,
|
54
|
- memory_time: number,
|
55
|
- pre_check: boolean,
|
56
|
- formatters: [(Suite.t() -> Suite.t())],
|
57
|
- print: map,
|
58
|
- inputs: %{Suite.key() => any} | [{Suite.key(), any}] | nil,
|
59
|
- save: map | false,
|
60
|
- load: String.t() | [String.t()] | false,
|
61
|
- formatter_options: map,
|
62
|
- unit_scaling: Scale.scaling_strategy(),
|
63
|
- assigns: map,
|
64
|
- before_each: fun | nil,
|
65
|
- after_each: fun | nil,
|
66
|
- before_scenario: fun | nil,
|
67
|
- after_scenario: fun | nil,
|
68
|
- measure_function_call_overhead: boolean,
|
69
|
- title: String.t() | nil
|
70
|
- }
|
45
|
+ @typedoc """
|
46
|
+ The configuration supplied by the user as either a map or a keyword list
|
71
47
|
|
72
|
- @type user_configuration :: map | keyword
|
73
|
- @time_keys [:time, :warmup, :memory_time]
|
74
|
-
|
75
|
- @doc """
|
76
|
- Returns the initial benchmark configuration for Benchee, composed of defaults
|
77
|
- and an optional custom configuration.
|
78
|
-
|
79
|
- Configuration times are given in seconds, but are converted to microseconds
|
80
|
- internally.
|
48
|
+ Possible options are:
|
81
49
|
|
82
50
|
Possible options:
|
83
51
|
|
|
@@ -157,6 +125,47 @@ defmodule Benchee.Configuration do
|
157
125
|
equivalent to the behaviour Benchee had pre 0.5.0)
|
158
126
|
* `:before_scenario`/`after_scenario`/`before_each`/`after_each` - read up on them in the hooks section in the README
|
159
127
|
* `:measure_function_call_overhead` - Measure how long an empty function call takes and deduct this from each measure run time. Defaults to true.
|
128
|
+ """
|
129
|
+ @type user_configuration :: map | keyword
|
130
|
+
|
131
|
+ @typedoc """
|
132
|
+ Generated configuration struct from the user supplied configuration options.
|
133
|
+
|
134
|
+ Filled in with a lot of defaults. Also notably every option is already converted to
|
135
|
+ a map or struct at this point for easier handling in benchee.
|
136
|
+ """
|
137
|
+ @type t :: %__MODULE__{
|
138
|
+ parallel: integer,
|
139
|
+ time: number,
|
140
|
+ warmup: number,
|
141
|
+ memory_time: number,
|
142
|
+ pre_check: boolean,
|
143
|
+ formatters: [(Suite.t() -> Suite.t()) | module | {module, map}],
|
144
|
+ print: map,
|
145
|
+ inputs: %{Suite.key() => any} | [{Suite.key(), any}] | nil,
|
146
|
+ save: map | false,
|
147
|
+ load: String.t() | [String.t()] | false,
|
148
|
+ formatter_options: map,
|
149
|
+ unit_scaling: Scale.scaling_strategy(),
|
150
|
+ assigns: map,
|
151
|
+ before_each: fun | nil,
|
152
|
+ after_each: fun | nil,
|
153
|
+ before_scenario: fun | nil,
|
154
|
+ after_scenario: fun | nil,
|
155
|
+ measure_function_call_overhead: boolean,
|
156
|
+ title: String.t() | nil
|
157
|
+ }
|
158
|
+
|
159
|
+ @time_keys [:time, :warmup, :memory_time]
|
160
|
+
|
161
|
+ @doc """
|
162
|
+ Returns the initial benchmark configuration for Benchee, composed of defaults
|
163
|
+ and an optional custom configuration.
|
164
|
+
|
165
|
+ Configuration times are given in seconds, but are converted to microseconds
|
166
|
+ internally.
|
167
|
+
|
168
|
+ For a list of all possible options see `t:user_configuration/0`
|
160
169
|
|
161
170
|
## Examples
|
162
171
|
|
|
@@ -170,12 +179,7 @@ defmodule Benchee.Configuration do
|
170
179
|
inputs: nil,
|
171
180
|
save: false,
|
172
181
|
load: false,
|
173
|
- formatters: [
|
174
|
- {
|
175
|
- Benchee.Formatters.Console,
|
176
|
- %{comparison: true, extended_statistics: false}
|
177
|
- }
|
178
|
- ],
|
182
|
+ formatters: [Benchee.Formatters.Console],
|
179
183
|
print: %{
|
180
184
|
benchmarking: true,
|
181
185
|
fast_warning: true,
|
|
@@ -203,12 +207,7 @@ defmodule Benchee.Configuration do
|
203
207
|
inputs: nil,
|
204
208
|
save: false,
|
205
209
|
load: false,
|
206
|
- formatters: [
|
207
|
- {
|
208
|
- Benchee.Formatters.Console,
|
209
|
- %{comparison: true, extended_statistics: false}
|
210
|
- }
|
211
|
- ],
|
210
|
+ formatters: [Benchee.Formatters.Console],
|
212
211
|
print: %{
|
213
212
|
benchmarking: true,
|
214
213
|
fast_warning: true,
|
|
@@ -236,12 +235,7 @@ defmodule Benchee.Configuration do
|
236
235
|
inputs: nil,
|
237
236
|
save: false,
|
238
237
|
load: false,
|
239
|
- formatters: [
|
240
|
- {
|
241
|
- Benchee.Formatters.Console,
|
242
|
- %{comparison: true, extended_statistics: false}
|
243
|
- }
|
244
|
- ],
|
238
|
+ formatters: [Benchee.Formatters.Console],
|
245
239
|
print: %{
|
246
240
|
benchmarking: true,
|
247
241
|
fast_warning: true,
|
|
@@ -265,9 +259,7 @@ defmodule Benchee.Configuration do
|
265
259
|
...> warmup: 0.2,
|
266
260
|
...> formatters: [&IO.puts/1],
|
267
261
|
...> print: [fast_warning: false],
|
268
|
- ...> console: [comparison: false],
|
269
262
|
...> inputs: %{"Small" => 5, "Big" => 9999},
|
270
|
- ...> formatter_options: [some: "option"],
|
271
263
|
...> unit_scaling: :smallest)
|
272
264
|
%Benchee.Suite{
|
273
265
|
configuration:
|
|
@@ -284,13 +276,7 @@ defmodule Benchee.Configuration do
|
284
276
|
fast_warning: false,
|
285
277
|
configuration: true
|
286
278
|
},
|
287
|
- formatter_options: %{
|
288
|
- console: %{
|
289
|
- comparison: false,
|
290
|
- extended_statistics: false
|
291
|
- },
|
292
|
- some: "option"
|
293
|
- },
|
279
|
+ formatter_options: %{},
|
294
280
|
percentiles: [50, 99],
|
295
281
|
unit_scaling: :smallest,
|
296
282
|
assigns: %{},
|
|
@@ -347,6 +333,14 @@ defmodule Benchee.Configuration do
|
347
333
|
|
348
334
|
defp formatter_configuration_from_options(config, module, legacy_option_key) do
|
349
335
|
if Map.has_key?(config.formatter_options, legacy_option_key) do
|
336
|
+ IO.puts("""
|
337
|
+
|
338
|
+ Using :formatter_options to configure formatters is now deprecated.
|
339
|
+ Please configure them in `:formatters` - see the documentation for
|
340
|
+ Benchee.Configuration.init/1 for details.
|
341
|
+
|
342
|
+ """)
|
343
|
+
|
350
344
|
{module, config.formatter_options[legacy_option_key]}
|
351
345
|
else
|
352
346
|
module
|
|
@@ -405,7 +399,9 @@ defmodule Benchee.Configuration do
|
405
399
|
|
406
400
|
Measuring memory consumption is only available on OTP 19 or greater.
|
407
401
|
If you would like to benchmark memory consumption, please upgrade to a
|
408
|
- newer verion of OTP.
|
402
|
+ newer version of OTP.
|
403
|
+ Benchee now also only supports elixir ~> 1.6 which doesn't support OTP 18.
|
404
|
+ Please upgrade :)
|
409
405
|
|
410
406
|
""")
|
411
407
|
end
|
changed
lib/benchee/conversion.ex
|
@@ -5,7 +5,7 @@ defmodule Benchee.Conversion do
|
5
5
|
Can be used by plugins to use benchee unit scaling logic.
|
6
6
|
"""
|
7
7
|
|
8
|
- alias Benchee.Benchmark.Scenario
|
8
|
+ alias Benchee.Scenario
|
9
9
|
alias Benchee.Conversion.{Count, Duration, Memory}
|
10
10
|
|
11
11
|
@doc """
|
|
@@ -19,9 +19,9 @@ defmodule Benchee.Conversion do
|
19
19
|
## Examples
|
20
20
|
|
21
21
|
iex> statistics = %Benchee.Statistics{average: 1_000_000.0, ips: 1000.0}
|
22
|
- iex> scenario = %Benchee.Benchmark.Scenario{
|
23
|
- ...> run_time_statistics: statistics,
|
24
|
- ...> memory_usage_statistics: statistics
|
22
|
+ iex> scenario = %Benchee.Scenario{
|
23
|
+ ...> run_time_data: %Benchee.CollectionData{statistics: statistics},
|
24
|
+ ...> memory_usage_data: %Benchee.CollectionData{statistics: statistics}
|
25
25
|
...> }
|
26
26
|
iex> Benchee.Conversion.units([scenario], :best)
|
27
27
|
%{
|
|
@@ -48,16 +48,16 @@ defmodule Benchee.Conversion do
|
48
48
|
def units(scenarios, scaling_strategy) do
|
49
49
|
run_time_measurements =
|
50
50
|
scenarios
|
51
|
- |> Enum.flat_map(fn scenario -> Map.to_list(scenario.run_time_statistics) end)
|
51
|
+ |> Enum.flat_map(fn scenario -> Map.to_list(scenario.run_time_data.statistics) end)
|
52
52
|
|> Enum.group_by(fn {stat_name, _} -> stat_name end, fn {_, value} -> value end)
|
53
53
|
|
54
54
|
memory_measurements =
|
55
55
|
scenarios
|
56
56
|
|> Enum.flat_map(fn
|
57
|
- %Scenario{memory_usage_statistics: nil} ->
|
57
|
+ %Scenario{memory_usage_data: %{statistics: nil}} ->
|
58
58
|
[]
|
59
59
|
|
60
|
- %Scenario{memory_usage_statistics: memory_usage_statistics} ->
|
60
|
+ %Scenario{memory_usage_data: %{statistics: memory_usage_statistics}} ->
|
61
61
|
Map.to_list(memory_usage_statistics)
|
62
62
|
end)
|
63
63
|
|> Enum.group_by(fn {stat_name, _} -> stat_name end, fn {_, value} -> value end)
|
changed
lib/benchee/conversion/count.ex
|
@@ -1,6 +1,8 @@
|
1
1
|
defmodule Benchee.Conversion.Count do
|
2
2
|
@moduledoc """
|
3
3
|
Unit scaling for counts, such that 1000000 can be converted to 1 Million.
|
4
|
+
|
5
|
+ Only benchee plugins should use this code.
|
4
6
|
"""
|
5
7
|
|
6
8
|
alias Benchee.Conversion.{Format, Scale, Unit}
|
changed
lib/benchee/conversion/deviation_percent.ex
|
@@ -2,6 +2,8 @@ defmodule Benchee.Conversion.DeviationPercent do
|
2
2
|
@moduledoc """
|
3
3
|
Helps with formattiong for the standard deviation ratio converting it into the
|
4
4
|
more common percent form.
|
5
|
+
|
6
|
+ Only benchee plugins should use this code.
|
5
7
|
"""
|
6
8
|
|
7
9
|
alias Benchee.Conversion.Format
|
changed
lib/benchee/conversion/duration.ex
|
@@ -1,6 +1,8 @@
|
1
1
|
defmodule Benchee.Conversion.Duration do
|
2
2
|
@moduledoc """
|
3
3
|
Unit scaling for duration converting from microseconds to minutes and others.
|
4
|
+
|
5
|
+ Only benchee plugins should use this code.
|
4
6
|
"""
|
5
7
|
|
6
8
|
alias Benchee.Conversion.{Format, Scale, Unit}
|
changed
lib/benchee/conversion/format.ex
|
@@ -3,7 +3,7 @@ defmodule Benchee.Conversion.Format do
|
3
3
|
Functions for formatting values and their unit labels. Different domains
|
4
4
|
handle this task differently, for example durations and counts.
|
5
5
|
|
6
|
- See `Benchee.Conversion.Count` and `Benchee.Conversion.Duration` for examples
|
6
|
+ See `Benchee.Conversion.Count` and `Benchee.Conversion.Duration` for examples.
|
7
7
|
"""
|
8
8
|
|
9
9
|
alias Benchee.Conversion.Unit
|
changed
lib/benchee/conversion/memory.ex
|
@@ -1,6 +1,8 @@
|
1
1
|
defmodule Benchee.Conversion.Memory do
|
2
2
|
@moduledoc """
|
3
3
|
Unit scaling for memory converting from bytes to kilobytes and others.
|
4
|
+
|
5
|
+ Only benchee plugins should use this code.
|
4
6
|
"""
|
5
7
|
|
6
8
|
alias Benchee.Conversion.{Format, Scale, Unit}
|
changed
lib/benchee/conversion/unit.ex
|
@@ -1,7 +1,10 @@
|
1
1
|
defmodule Benchee.Conversion.Unit do
|
2
2
|
@moduledoc """
|
3
|
- A representation of the different units used in `Benchee.Conversion.Duration`
|
4
|
- and `Benchee.Conversion.Count`. A unit is characterized by:
|
3
|
+ A representation of the different units used in `Benchee.Conversion.Format`
|
4
|
+ and `Benchee.Conversion.Scale` as well as the modules implementing these
|
5
|
+ behaviours.
|
6
|
+
|
7
|
+ A unit is characterized by:
|
5
8
|
|
6
9
|
* name - an atom representation of the unit for easy access (`:microseconds`,
|
7
10
|
`thousand`)
|
changed
lib/benchee/formatter.ex
|
@@ -1,7 +1,6 @@
|
1
1
|
defmodule Benchee.Formatter do
|
2
2
|
@moduledoc """
|
3
|
- Defines a behaviour for formatters in Benchee, and also defines functions to
|
4
|
- handle invoking that defined behavior.
|
3
|
+ Defines a behaviour for formatters in benchee, and functions to work with these.
|
5
4
|
|
6
5
|
When implementing a benchee formatter as a behaviour please adopt this
|
7
6
|
behaviour, as it helps with uniformity and also allows at least the `.format`
|
|
@@ -15,6 +14,9 @@ defmodule Benchee.Formatter do
|
15
14
|
alias Benchee.{Suite, Utility.Parallel}
|
16
15
|
alias Benchee.Utility.DeepConvert
|
17
16
|
|
17
|
+ @typedoc """
|
18
|
+ Options given to formatters, entirely defined by formatter authors.
|
19
|
+ """
|
18
20
|
@type options :: any
|
19
21
|
|
20
22
|
@doc """
|
|
@@ -37,7 +39,7 @@ defmodule Benchee.Formatter do
|
37
39
|
@doc """
|
38
40
|
Format and output all configured formatters and formatting functions.
|
39
41
|
|
40
|
- Expects a suite that already has been run through all previous functions so has the aggregated
|
42
|
+ Expects a suite that already has been run through all previous steps so has the aggregated
|
41
43
|
statistics etc. that the formatters rely on.
|
42
44
|
|
43
45
|
Works by invoking the `format/2` and `write/2` functions defined in this module. The `format/2`
|
|
@@ -46,8 +48,7 @@ defmodule Benchee.Formatter do
|
46
48
|
|
47
49
|
Also handles pure functions that will then be called with the suite.
|
48
50
|
|
49
|
- You can't rely on the formatters being called in pre determined order. Right now first those
|
50
|
- that are parallelizable (benchee formatter modules) are called, then normal functions.
|
51
|
+ You can't rely on the formatters being called in pre determined order.
|
51
52
|
"""
|
52
53
|
@spec output(Suite.t()) :: Suite.t()
|
53
54
|
def output(suite = %{configuration: %{formatters: formatters}}) do
|
|
@@ -73,7 +74,20 @@ defmodule Benchee.Formatter do
|
73
74
|
{formatter, @default_opts}
|
74
75
|
end
|
75
76
|
|
76
|
- defp normalize_module_configuration(formatter), do: formatter
|
77
|
+ defp normalize_module_configuration(formatter) when is_function(formatter, 1), do: formatter
|
78
|
+
|
79
|
+ defp normalize_module_configuration(formatter) do
|
80
|
+ IO.puts("""
|
81
|
+
|
82
|
+ All formaters that are not modules implementing the Benchee.Formatter
|
83
|
+ behaviour or functions with an arity of 1 are deprecated.
|
84
|
+ Please see the documentation for Benchee.Configuration.init/1 for
|
85
|
+ current usage instructions.
|
86
|
+
|
87
|
+ """)
|
88
|
+
|
89
|
+ formatter
|
90
|
+ end
|
77
91
|
|
78
92
|
defp is_formatter_module?({formatter, _options}) when is_atom(formatter) do
|
79
93
|
module_attributes = formatter.module_info(:attributes)
|
|
@@ -93,7 +107,7 @@ defmodule Benchee.Formatter do
|
93
107
|
to the documentation of the formatters you use.
|
94
108
|
"""
|
95
109
|
@spec output(Suite.t(), module, options) :: Suite.t()
|
96
|
- def output(suite, formatter, options) do
|
110
|
+ def output(suite, formatter, options \\ %{}) do
|
97
111
|
:ok =
|
98
112
|
suite
|
99
113
|
|> formatter.format(options)
|
changed
lib/benchee/formatters/console.ex
|
@@ -1,7 +1,24 @@
|
1
1
|
defmodule Benchee.Formatters.Console do
|
2
2
|
@moduledoc """
|
3
|
- Formatter to transform the statistics output into a structure suitable for
|
4
|
- output through `IO.write` on the console.
|
3
|
+ Formatter to print out the results of benchmarking suite to the console.
|
4
|
+
|
5
|
+ Example:
|
6
|
+
|
7
|
+ Name ips average deviation median 99th %
|
8
|
+ flat_map 2.40 K 417.00 μs ±9.40% 411.45 μs 715.21 μs
|
9
|
+ map.flatten 1.24 K 806.89 μs ±16.62% 768.02 μs 1170.67 μs
|
10
|
+
|
11
|
+ Comparison:
|
12
|
+ flat_map 2.40 K
|
13
|
+ map.flatten 1.24 K - 1.93x slower
|
14
|
+
|
15
|
+ Memory usage statistics:
|
16
|
+
|
17
|
+ Name Memory usage
|
18
|
+ flat_map 624.97 KB
|
19
|
+ map.flatten 781.25 KB - 1.25x memory usage
|
20
|
+
|
21
|
+ **All measurements for memory usage were the same**
|
5
22
|
"""
|
6
23
|
|
7
24
|
@behaviour Benchee.Formatter
|
|
@@ -9,8 +26,6 @@ defmodule Benchee.Formatters.Console do
|
9
26
|
alias Benchee.Suite
|
10
27
|
alias Benchee.Formatters.Console.{Memory, RunTime}
|
11
28
|
|
12
|
- def format(suite), do: format(suite, %{})
|
13
|
-
|
14
29
|
@doc """
|
15
30
|
Formats the benchmark statistics to a report suitable for output on the CLI.
|
16
31
|
|
|
@@ -22,27 +37,29 @@ defmodule Benchee.Formatters.Console do
|
22
37
|
|
23
38
|
```
|
24
39
|
iex> scenarios = [
|
25
|
- ...> %Benchee.Benchmark.Scenario{
|
26
|
- ...> name: "My Job", input_name: "My input", run_time_statistics: %Benchee.Statistics{
|
27
|
- ...> average: 200.0,
|
28
|
- ...> ips: 5000.0,
|
29
|
- ...> std_dev_ratio: 0.1,
|
30
|
- ...> median: 190.0,
|
31
|
- ...> percentiles: %{99 => 300.1},
|
32
|
- ...> sample_size: 200
|
33
|
- ...> },
|
34
|
- ...> memory_usage_statistics: %Benchee.Statistics{}
|
40
|
+ ...> %Benchee.Scenario{
|
41
|
+ ...> name: "My Job", input_name: "My input", run_time_data: %Benchee.CollectionData{
|
42
|
+ ...> statistics: %Benchee.Statistics{
|
43
|
+ ...> average: 200.0,
|
44
|
+ ...> ips: 5000.0,
|
45
|
+ ...> std_dev_ratio: 0.1,
|
46
|
+ ...> median: 190.0,
|
47
|
+ ...> percentiles: %{99 => 300.1},
|
48
|
+ ...> sample_size: 200
|
49
|
+ ...> }
|
50
|
+ ...> }
|
35
51
|
...> },
|
36
|
- ...> %Benchee.Benchmark.Scenario{
|
37
|
- ...> name: "Job 2", input_name: "My input", run_time_statistics: %Benchee.Statistics{
|
38
|
- ...> average: 400.0,
|
39
|
- ...> ips: 2500.0,
|
40
|
- ...> std_dev_ratio: 0.2,
|
41
|
- ...> median: 390.0,
|
42
|
- ...> percentiles: %{99 => 500.1},
|
43
|
- ...> sample_size: 200
|
44
|
- ...> },
|
45
|
- ...> memory_usage_statistics: %Benchee.Statistics{}
|
52
|
+ ...> %Benchee.Scenario{
|
53
|
+ ...> name: "Job 2", input_name: "My input", run_time_data: %Benchee.CollectionData{
|
54
|
+ ...> statistics: %Benchee.Statistics{
|
55
|
+ ...> average: 400.0,
|
56
|
+ ...> ips: 2500.0,
|
57
|
+ ...> std_dev_ratio: 0.2,
|
58
|
+ ...> median: 390.0,
|
59
|
+ ...> percentiles: %{99 => 500.1},
|
60
|
+ ...> sample_size: 200
|
61
|
+ ...> }
|
62
|
+ ...> }
|
46
63
|
...> }
|
47
64
|
...> ]
|
48
65
|
iex> suite = %Benchee.Suite{
|
|
@@ -61,7 +78,7 @@ defmodule Benchee.Formatters.Console do
|
61
78
|
"""
|
62
79
|
@impl true
|
63
80
|
@spec format(Suite.t(), map) :: [any]
|
64
|
- def format(%Suite{scenarios: scenarios, configuration: config}, options) do
|
81
|
+ def format(%Suite{scenarios: scenarios, configuration: config}, options \\ %{}) do
|
65
82
|
if Map.has_key?(options, :unit_scaling), do: warn_unit_scaling()
|
66
83
|
|
67
84
|
config =
|
|
@@ -90,14 +107,12 @@ defmodule Benchee.Formatters.Console do
|
90
107
|
end
|
91
108
|
end
|
92
109
|
|
93
|
- def write(suite), do: write(suite, %{})
|
94
|
-
|
95
110
|
@doc """
|
96
111
|
Takes the output of `format/1` and writes that to the console.
|
97
112
|
"""
|
98
113
|
@impl true
|
99
114
|
@spec write(any, map) :: :ok | {:error, String.t()}
|
100
|
- def write(output, _options) do
|
115
|
+ def write(output, _options \\ %{}) do
|
101
116
|
IO.write(output)
|
102
117
|
rescue
|
103
118
|
_ -> {:error, "Unknown Error"}
|
changed
lib/benchee/formatters/console/helpers.ex
|
@@ -1,8 +1,8 @@
|
1
1
|
defmodule Benchee.Formatters.Console.Helpers do
|
2
|
- @moduledoc """
|
3
|
- These are common functions shared between the formatting of the run time and
|
4
|
- memory usage statistics.
|
5
|
- """
|
2
|
+ @moduledoc false
|
3
|
+
|
4
|
+ # These are common functions shared between the formatting of the run time and
|
5
|
+ # memory usage statistics.
|
6
6
|
|
7
7
|
alias Benchee.Conversion.{Count, DeviationPercent, Format, Scale, Unit}
|
8
8
|
alias Benchee.Statistics
|
|
@@ -49,4 +49,51 @@ defmodule Benchee.Formatters.Console.Helpers do
|
49
49
|
|
50
50
|
@spec descriptor(String.t()) :: String.t()
|
51
51
|
def descriptor(header_str), do: "\n#{header_str}: \n"
|
52
|
+
|
53
|
+ def format_comparison(
|
54
|
+ name,
|
55
|
+ statistics,
|
56
|
+ display_value,
|
57
|
+ comparison_name,
|
58
|
+ display_unit,
|
59
|
+ label_width,
|
60
|
+ column_width
|
61
|
+ ) do
|
62
|
+ "~*s~*s ~ts"
|
63
|
+ |> :io_lib.format([
|
64
|
+ -label_width,
|
65
|
+ name,
|
66
|
+ column_width,
|
67
|
+ display_value,
|
68
|
+ comparison_display(statistics, comparison_name, display_unit)
|
69
|
+ ])
|
70
|
+ |> to_string
|
71
|
+ end
|
72
|
+
|
73
|
+ defp comparison_display(%Statistics{relative_more: nil, absolute_difference: nil}, _, _), do: ""
|
74
|
+
|
75
|
+ defp comparison_display(statistics, comparison_name, unit) do
|
76
|
+ "- #{comparison_text(statistics, comparison_name)} #{
|
77
|
+ absolute_difference_text(statistics, unit)
|
78
|
+ }\n"
|
79
|
+ end
|
80
|
+
|
81
|
+ defp comparison_text(%Statistics{relative_more: :infinity}, name), do: "∞ x #{name}"
|
82
|
+ defp comparison_text(%Statistics{relative_more: nil}, _), do: "N/A"
|
83
|
+
|
84
|
+ defp comparison_text(statistics, comparison_name) do
|
85
|
+ "~.2fx ~s"
|
86
|
+ |> :io_lib.format([statistics.relative_more, comparison_name])
|
87
|
+ |> to_string
|
88
|
+ end
|
89
|
+
|
90
|
+ defp absolute_difference_text(statistics, unit) do
|
91
|
+ formatted_value = Format.format({Scale.scale(statistics.absolute_difference, unit), unit})
|
92
|
+
|
93
|
+ if statistics.absolute_difference >= 0 do
|
94
|
+ "+#{formatted_value}"
|
95
|
+ else
|
96
|
+ formatted_value
|
97
|
+ end
|
98
|
+ end
|
52
99
|
end
|
changed
lib/benchee/formatters/console/memory.ex
|
@@ -1,18 +1,18 @@
|
1
1
|
defmodule Benchee.Formatters.Console.Memory do
|
2
|
- @moduledoc """
|
3
|
- This deals with just the formatting of the run time results. They are similar
|
4
|
- to the way the memory results are formatted, but different enough to where the
|
5
|
- abstractions start to break down pretty significantly, so I wanted to extract
|
6
|
- these two things into separate modules to avoid confusion.
|
7
|
- """
|
2
|
+ @moduledoc false
|
3
|
+
|
4
|
+ # This deals with just the formatting of the run time results. They are similar
|
5
|
+ # to the way the memory results are formatted, but different enough to where the
|
6
|
+ # abstractions start to break down pretty significantly, so I wanted to extract
|
7
|
+ # these two things into separate modules to avoid confusion.
|
8
8
|
|
9
9
|
alias Benchee.{
|
10
|
- Benchmark.Scenario,
|
11
10
|
Conversion,
|
12
11
|
Conversion.Count,
|
13
12
|
Conversion.Memory,
|
14
13
|
Conversion.Unit,
|
15
14
|
Formatters.Console.Helpers,
|
15
|
+ Scenario,
|
16
16
|
Statistics
|
17
17
|
}
|
18
18
|
|
|
@@ -45,7 +45,7 @@ defmodule Benchee.Formatters.Console.Memory do
|
45
45
|
|
46
46
|
defp memory_measurements_present?(scenarios) do
|
47
47
|
Enum.any?(scenarios, fn scenario ->
|
48
|
- scenario.memory_usage_statistics.sample_size > 0
|
48
|
+ scenario.memory_usage_data.statistics.sample_size > 0
|
49
49
|
end)
|
50
50
|
end
|
51
51
|
|
|
@@ -66,7 +66,7 @@ defmodule Benchee.Formatters.Console.Memory do
|
66
66
|
|
67
67
|
defp all_have_deviation_of_0?(scenarios) do
|
68
68
|
Enum.all?(scenarios, fn scenario ->
|
69
|
- scenario.memory_usage_statistics.std_dev == 0.0
|
69
|
+ scenario.memory_usage_data.statistics.std_dev == 0.0
|
70
70
|
end)
|
71
71
|
end
|
72
72
|
|
|
@@ -106,7 +106,7 @@ defmodule Benchee.Formatters.Console.Memory do
|
106
106
|
defp scenario_reports([scenario | other_scenarios], units, label_width, true) do
|
107
107
|
[
|
108
108
|
reference_report(scenario, units, label_width),
|
109
|
- comparisons(scenario, units, label_width, other_scenarios),
|
109
|
+ comparisons(other_scenarios, units, label_width),
|
110
110
|
"\n**All measurements for memory usage were the same**\n"
|
111
111
|
]
|
112
112
|
end
|
|
@@ -123,7 +123,7 @@ defmodule Benchee.Formatters.Console.Memory do
|
123
123
|
defp format_scenario(scenario, units, label_width, hide_statistics)
|
124
124
|
|
125
125
|
defp format_scenario(
|
126
|
- scenario = %Scenario{memory_usage_statistics: %{sample_size: 0}},
|
126
|
+ scenario = %Scenario{memory_usage_data: %{statistics: %{sample_size: 0}}},
|
127
127
|
_,
|
128
128
|
label_width,
|
129
129
|
_
|
|
@@ -149,11 +149,13 @@ defmodule Benchee.Formatters.Console.Memory do
|
149
149
|
defp format_scenario(scenario, %{memory: memory_unit}, label_width, false) do
|
150
150
|
%Scenario{
|
151
151
|
name: name,
|
152
|
- memory_usage_statistics: %Statistics{
|
153
|
- average: average,
|
154
|
- std_dev_ratio: std_dev_ratio,
|
155
|
- median: median,
|
156
|
- percentiles: %{99 => percentile_99}
|
152
|
+ memory_usage_data: %{
|
153
|
+ statistics: %Statistics{
|
154
|
+ average: average,
|
155
|
+ std_dev_ratio: std_dev_ratio,
|
156
|
+ median: median,
|
157
|
+ percentiles: %{99 => percentile_99}
|
158
|
+ }
|
157
159
|
}
|
158
160
|
} = scenario
|
159
161
|
|
|
@@ -176,8 +178,10 @@ defmodule Benchee.Formatters.Console.Memory do
|
176
178
|
defp format_scenario(scenario, %{memory: memory_unit}, label_width, true) do
|
177
179
|
%Scenario{
|
178
180
|
name: name,
|
179
|
- memory_usage_statistics: %Statistics{
|
180
|
- average: average
|
181
|
+ memory_usage_data: %{
|
182
|
+ statistics: %Statistics{
|
183
|
+ average: average
|
184
|
+ }
|
181
185
|
}
|
182
186
|
} = scenario
|
183
187
|
|
|
@@ -205,15 +209,13 @@ defmodule Benchee.Formatters.Console.Memory do
|
205
209
|
[
|
206
210
|
Helpers.descriptor("Comparison"),
|
207
211
|
reference_report(scenario, units, label_width)
|
208
|
- | comparisons(scenario, units, label_width, other_scenarios)
|
212
|
+ | comparisons(other_scenarios, units, label_width)
|
209
213
|
]
|
210
214
|
end
|
211
215
|
|
212
216
|
defp reference_report(scenario, %{memory: memory_unit}, label_width) do
|
213
|
- %Scenario{
|
214
|
- name: name,
|
215
|
- memory_usage_statistics: %Statistics{median: median}
|
216
|
- } = scenario
|
217
|
+ %Scenario{name: name, memory_usage_data: %{statistics: %Statistics{median: median}}} =
|
218
|
+ scenario
|
217
219
|
|
218
220
|
"~*s~*s\n"
|
219
221
|
|> :io_lib.format([
|
|
@@ -225,43 +227,25 @@ defmodule Benchee.Formatters.Console.Memory do
|
225
227
|
|> to_string
|
226
228
|
end
|
227
229
|
|
228
|
- @spec comparisons(Scenario.t(), unit_per_statistic, integer, [Scenario.t()]) :: [String.t()]
|
229
|
- defp comparisons(scenario, units, label_width, scenarios_to_compare) do
|
230
|
- %Scenario{memory_usage_statistics: reference_stats} = scenario
|
230
|
+ @spec comparisons([Scenario.t()], unit_per_statistic, integer) :: [String.t()]
|
231
|
+ defp comparisons(scenarios_to_compare, units, label_width) do
|
232
|
+ Enum.map(
|
233
|
+ scenarios_to_compare,
|
234
|
+ fn scenario ->
|
235
|
+ statistics = scenario.memory_usage_data.statistics
|
236
|
+ memory_format = memory_output(statistics.average, units.memory)
|
231
237
|
|
232
|
- Enum.map(scenarios_to_compare, fn scenario = %Scenario{memory_usage_statistics: job_stats} ->
|
233
|
- slower = calculate_slower_value(job_stats.median, reference_stats.median)
|
234
|
-
|
235
|
- format_comparison(scenario, units, label_width, slower)
|
236
|
- end)
|
237
|
- end
|
238
|
-
|
239
|
- defp calculate_slower_value(job_median, reference_median)
|
240
|
- when job_median == 0 or is_nil(job_median) or reference_median == 0 or
|
241
|
- is_nil(reference_median) do
|
242
|
- @na
|
243
|
- end
|
244
|
-
|
245
|
- defp calculate_slower_value(job_median, reference_median) do
|
246
|
- job_median / reference_median
|
247
|
- end
|
248
|
-
|
249
|
- defp format_comparison(scenario, %{memory: memory_unit}, label_width, @na) do
|
250
|
- %Scenario{name: name, memory_usage_statistics: %Statistics{median: median}} = scenario
|
251
|
- median_format = memory_output(median, memory_unit)
|
252
|
-
|
253
|
- "~*s~*s\n"
|
254
|
- |> :io_lib.format([-label_width, name, @median_width, median_format])
|
255
|
- |> to_string
|
256
|
- end
|
257
|
-
|
258
|
- defp format_comparison(scenario, %{memory: memory_unit}, label_width, slower) do
|
259
|
- %Scenario{name: name, memory_usage_statistics: %Statistics{median: median}} = scenario
|
260
|
- median_format = memory_output(median, memory_unit)
|
261
|
-
|
262
|
- "~*s~*s - ~.2fx memory usage\n"
|
263
|
- |> :io_lib.format([-label_width, name, @median_width, median_format, slower])
|
264
|
- |> to_string
|
238
|
+ Helpers.format_comparison(
|
239
|
+ scenario.name,
|
240
|
+ statistics,
|
241
|
+ memory_format,
|
242
|
+ "memory usage",
|
243
|
+ units.memory,
|
244
|
+ label_width,
|
245
|
+ @median_width
|
246
|
+ )
|
247
|
+ end
|
248
|
+ )
|
265
249
|
end
|
266
250
|
|
267
251
|
defp memory_output(nil, _unit), do: "N/A"
|
|
@@ -307,11 +291,13 @@ defmodule Benchee.Formatters.Console.Memory do
|
307
291
|
defp format_scenario_extended(scenario, %{memory: memory_unit}, label_width) do
|
308
292
|
%Scenario{
|
309
293
|
name: name,
|
310
|
- memory_usage_statistics: %Statistics{
|
311
|
- minimum: minimum,
|
312
|
- maximum: maximum,
|
313
|
- sample_size: sample_size,
|
314
|
- mode: mode
|
294
|
+ memory_usage_data: %{
|
295
|
+ statistics: %Statistics{
|
296
|
+ minimum: minimum,
|
297
|
+ maximum: maximum,
|
298
|
+ sample_size: sample_size,
|
299
|
+ mode: mode
|
300
|
+ }
|
315
301
|
}
|
316
302
|
} = scenario
|
changed
lib/benchee/formatters/console/run_time.ex
|
@@ -1,18 +1,18 @@
|
1
1
|
defmodule Benchee.Formatters.Console.RunTime do
|
2
|
- @moduledoc """
|
3
|
- This deals with just the formatting of the run time results. They are similar
|
4
|
- to the way the memory results are formatted, but different enough to where the
|
5
|
- abstractions start to break down pretty significantly, so I wanted to extract
|
6
|
- these two things into separate modules to avoid confusion.
|
7
|
- """
|
2
|
+ @moduledoc false
|
3
|
+
|
4
|
+ # This deals with just the formatting of the run time results. They are similar
|
5
|
+ # to the way the memory results are formatted, but different enough to where the
|
6
|
+ # abstractions start to break down pretty significantly, so I wanted to extract
|
7
|
+ # these two things into separate modules to avoid confusion.
|
8
8
|
|
9
9
|
alias Benchee.{
|
10
|
- Benchmark.Scenario,
|
11
10
|
Conversion,
|
12
11
|
Conversion.Count,
|
13
12
|
Conversion.Duration,
|
14
13
|
Conversion.Unit,
|
15
14
|
Formatters.Console.Helpers,
|
15
|
+ Scenario,
|
16
16
|
Statistics
|
17
17
|
}
|
18
18
|
|
|
@@ -36,21 +36,25 @@ defmodule Benchee.Formatters.Console.RunTime do
|
36
36
|
```
|
37
37
|
iex> memory_statistics = %Benchee.Statistics{average: 100.0}
|
38
38
|
iex> scenarios = [
|
39
|
- ...> %Benchee.Benchmark.Scenario{
|
39
|
+ ...> %Benchee.Scenario{
|
40
40
|
...> name: "My Job",
|
41
|
- ...> run_time_statistics: %Benchee.Statistics{
|
42
|
- ...> average: 200.0, ips: 5000.0,std_dev_ratio: 0.1, median: 190.0, percentiles: %{99 => 300.1},
|
43
|
- ...> minimum: 100.1, maximum: 200.2, sample_size: 10_101, mode: 333.2
|
41
|
+ ...> run_time_data: %Benchee.CollectionData{
|
42
|
+ ...> statistics: %Benchee.Statistics{
|
43
|
+ ...> average: 200.0, ips: 5000.0,std_dev_ratio: 0.1, median: 190.0, percentiles: %{99 => 300.1},
|
44
|
+ ...> minimum: 100.1, maximum: 200.2, sample_size: 10_101, mode: 333.2
|
45
|
+ ...> },
|
44
46
|
...> },
|
45
|
- ...> memory_usage_statistics: memory_statistics
|
47
|
+ ...> memory_usage_data: %Benchee.CollectionData{statistics: memory_statistics}
|
46
48
|
...> },
|
47
|
- ...> %Benchee.Benchmark.Scenario{
|
49
|
+ ...> %Benchee.Scenario{
|
48
50
|
...> name: "Job 2",
|
49
|
- ...> run_time_statistics: %Benchee.Statistics{
|
50
|
- ...> average: 400.0, ips: 2500.0, std_dev_ratio: 0.2, median: 390.0, percentiles: %{99 => 500.1},
|
51
|
- ...> minimum: 200.2, maximum: 400.4, sample_size: 20_202, mode: [612.3, 554.1]
|
51
|
+ ...> run_time_data: %Benchee.CollectionData{
|
52
|
+ ...> statistics: %Benchee.Statistics{
|
53
|
+ ...> average: 400.0, ips: 2500.0, std_dev_ratio: 0.2, median: 390.0, percentiles: %{99 => 500.1},
|
54
|
+ ...> minimum: 200.2, maximum: 400.4, sample_size: 20_202, mode: [612.3, 554.1]
|
55
|
+ ...> }
|
52
56
|
...> },
|
53
|
- ...> memory_usage_statistics: memory_statistics
|
57
|
+ ...> memory_usage_data: %Benchee.CollectionData{statistics: memory_statistics}
|
54
58
|
...> }
|
55
59
|
...> ]
|
56
60
|
iex> configuration = %{comparison: false, unit_scaling: :best, extended_statistics: true}
|
|
@@ -77,7 +81,7 @@ defmodule Benchee.Formatters.Console.RunTime do
|
77
81
|
|
78
82
|
defp run_time_measurements_present?(scenarios) do
|
79
83
|
Enum.any?(scenarios, fn scenario ->
|
80
|
- scenario.run_time_statistics.sample_size > 0
|
84
|
+ scenario.run_time_data.statistics.sample_size > 0
|
81
85
|
end)
|
82
86
|
end
|
83
87
|
|
|
@@ -120,11 +124,13 @@ defmodule Benchee.Formatters.Console.RunTime do
|
120
124
|
defp format_scenario_extended(scenario, %{run_time: run_time_unit}, label_width) do
|
121
125
|
%Scenario{
|
122
126
|
name: name,
|
123
|
- run_time_statistics: %Statistics{
|
124
|
- minimum: minimum,
|
125
|
- maximum: maximum,
|
126
|
- sample_size: sample_size,
|
127
|
- mode: mode
|
127
|
+ run_time_data: %{
|
128
|
+ statistics: %Statistics{
|
129
|
+ minimum: minimum,
|
130
|
+ maximum: maximum,
|
131
|
+ sample_size: sample_size,
|
132
|
+ mode: mode
|
133
|
+ }
|
128
134
|
}
|
129
135
|
} = scenario
|
130
136
|
|
|
@@ -193,12 +199,14 @@ defmodule Benchee.Formatters.Console.RunTime do
|
193
199
|
defp format_scenario(scenario, %{run_time: run_time_unit, ips: ips_unit}, label_width) do
|
194
200
|
%Scenario{
|
195
201
|
name: name,
|
196
|
- run_time_statistics: %Statistics{
|
197
|
- average: average,
|
198
|
- ips: ips,
|
199
|
- std_dev_ratio: std_dev_ratio,
|
200
|
- median: median,
|
201
|
- percentiles: %{99 => percentile_99}
|
202
|
+ run_time_data: %{
|
203
|
+ statistics: %Statistics{
|
204
|
+ average: average,
|
205
|
+ ips: ips,
|
206
|
+ std_dev_ratio: std_dev_ratio,
|
207
|
+ median: median,
|
208
|
+ percentiles: %{99 => percentile_99}
|
209
|
+ }
|
202
210
|
}
|
203
211
|
} = scenario
|
204
212
|
|
|
@@ -231,38 +239,37 @@ defmodule Benchee.Formatters.Console.RunTime do
|
231
239
|
[
|
232
240
|
Helpers.descriptor("Comparison"),
|
233
241
|
reference_report(scenario, units, label_width)
|
234
|
- | comparisons(scenario, units, label_width, other_scenarios)
|
242
|
+ | comparisons(other_scenarios, units, label_width)
|
235
243
|
]
|
236
244
|
end
|
237
245
|
|
238
246
|
defp reference_report(scenario, %{ips: ips_unit}, label_width) do
|
239
|
- %Scenario{
|
240
|
- name: name,
|
241
|
- run_time_statistics: %Statistics{ips: ips}
|
242
|
- } = scenario
|
247
|
+ %Scenario{name: name, run_time_data: %{statistics: %Statistics{ips: ips}}} = scenario
|
243
248
|
|
244
249
|
"~*s~*s\n"
|
245
250
|
|> :io_lib.format([-label_width, name, @ips_width, Helpers.count_output(ips, ips_unit)])
|
246
251
|
|> to_string
|
247
252
|
end
|
248
253
|
|
249
|
- @spec comparisons(Scenario.t(), unit_per_statistic, integer, [Scenario.t()]) :: [String.t()]
|
250
|
- defp comparisons(scenario, units, label_width, scenarios_to_compare) do
|
251
|
- %Scenario{run_time_statistics: reference_stats} = scenario
|
254
|
+ @spec comparisons([Scenario.t()], unit_per_statistic, integer) :: [String.t()]
|
255
|
+ defp comparisons(scenarios_to_compare, units, label_width) do
|
256
|
+ Enum.map(
|
257
|
+ scenarios_to_compare,
|
258
|
+ fn scenario ->
|
259
|
+ statistics = scenario.run_time_data.statistics
|
260
|
+ ips_format = Helpers.count_output(statistics.ips, units.ips)
|
252
261
|
|
253
|
- Enum.map(scenarios_to_compare, fn scenario = %Scenario{run_time_statistics: job_stats} ->
|
254
|
- slower = reference_stats.ips / job_stats.ips
|
255
|
- format_comparison(scenario, units, label_width, slower)
|
256
|
- end)
|
257
|
- end
|
258
|
-
|
259
|
- defp format_comparison(scenario, %{ips: ips_unit}, label_width, slower) do
|
260
|
- %Scenario{name: name, run_time_statistics: %Statistics{ips: ips}} = scenario
|
261
|
- ips_format = Helpers.count_output(ips, ips_unit)
|
262
|
-
|
263
|
- "~*s~*s - ~.2fx slower\n"
|
264
|
- |> :io_lib.format([-label_width, name, @ips_width, ips_format, slower])
|
265
|
- |> to_string
|
262
|
+ Helpers.format_comparison(
|
263
|
+ scenario.name,
|
264
|
+ statistics,
|
265
|
+ ips_format,
|
266
|
+ "slower",
|
267
|
+ units.run_time,
|
268
|
+ label_width,
|
269
|
+ @ips_width
|
270
|
+ )
|
271
|
+ end
|
272
|
+ )
|
266
273
|
end
|
267
274
|
|
268
275
|
defp duration_output(duration, unit) do
|
changed
lib/benchee/formatters/tagged_save.ex
|
@@ -2,18 +2,21 @@ defmodule Benchee.Formatters.TaggedSave do
|
2
2
|
@moduledoc """
|
3
3
|
Store the whole suite in the Erlang `ExternalTermFormat` while tagging the
|
4
4
|
scenarios of the current run with a specified tag - can be used for storing
|
5
|
- and loading the results of previous runs.
|
5
|
+ and later loading the results of previous runs with `Benchee.ScenarioLoader`.
|
6
6
|
|
7
|
- Automatically invoked and appended when specifiying the `save` option in the
|
8
|
- configuration.
|
7
|
+ Automatically configured as the last formatter to run when specifiying the
|
8
|
+ `save` option in the configuration - see `Benchee.Configuration`.
|
9
9
|
"""
|
10
10
|
|
11
11
|
@behaviour Benchee.Formatter
|
12
12
|
|
13
|
- alias Benchee.Benchmark.Scenario
|
13
|
+ alias Benchee.Scenario
|
14
14
|
alias Benchee.Suite
|
15
15
|
alias Benchee.Utility.FileCreation
|
16
16
|
|
17
|
+ @doc """
|
18
|
+ Tags all scenario with the desired tag and returns it in term_to_binary along with save path.
|
19
|
+ """
|
17
20
|
@impl true
|
18
21
|
@spec format(Suite.t(), map) :: {binary, String.t()}
|
19
22
|
def format(suite = %Suite{scenarios: scenarios}, formatter_config) do
|
|
@@ -71,6 +74,9 @@ defmodule Benchee.Formatters.TaggedSave do
|
71
74
|
%Scenario{scenario | name: Scenario.display_name(scenario)}
|
72
75
|
end
|
73
76
|
|
77
|
+ @doc """
|
78
|
+ Writes the binary returned by `format/2` to the indicated location, telling you where that is.
|
79
|
+ """
|
74
80
|
@spec write({binary, String.t()}, map) :: :ok
|
75
81
|
@impl true
|
76
82
|
def write({term_binary, filename}, _) do
|
changed
lib/benchee/output/benchmark_printer.ex
|
@@ -64,7 +64,6 @@ defmodule Benchee.Output.BenchmarkPrinter do
|
64
64
|
parallel: #{parallel}
|
65
65
|
inputs: #{inputs_out(inputs)}
|
66
66
|
Estimated total run time: #{Duration.format(total_time)}
|
67
|
-
|
68
67
|
""")
|
69
68
|
end
|
added
lib/benchee/relative_statistics.ex
|
@@ -0,0 +1,98 @@
|
1
|
+ defmodule Benchee.RelativeStatistics do
|
2
|
+ @moduledoc """
|
3
|
+ Statistics that are relative from one scenario to another.
|
4
|
+
|
5
|
+ Such as how much slower/faster something is or what the absolute difference is in the measured
|
6
|
+ values.
|
7
|
+ Is its own step because it has to be executed after scenarios have been loaded via
|
8
|
+ `Benchee.ScenarioLoader` to include them in the calculation, while `Benchee.Statistics`
|
9
|
+ has to happen before they are loaded to avoid recalculating their statistics.
|
10
|
+ """
|
11
|
+
|
12
|
+ alias Benchee.{Scenario, Statistics, Suite}
|
13
|
+
|
14
|
+ @doc """
|
15
|
+ Calculate the statistics of scenarios relative to each other and sorts scenarios.
|
16
|
+
|
17
|
+ Such as `relative_more`, `relative_less` and `absolute_difference`,
|
18
|
+ see `t:Benchee.Statistics.t/0` for more.
|
19
|
+
|
20
|
+ The sorting of scenarios is important so that they always have the same order in
|
21
|
+ all formatters. Scenarios are sorted first by run time average, then by memory average.
|
22
|
+ """
|
23
|
+ @spec relative_statistics(Suite.t()) :: Suite.t()
|
24
|
+ def relative_statistics(suite) do
|
25
|
+ scenarios =
|
26
|
+ suite.scenarios
|
27
|
+ |> sort()
|
28
|
+ |> calculate_relative_statistics(suite.configuration.inputs)
|
29
|
+
|
30
|
+ %Suite{suite | scenarios: scenarios}
|
31
|
+ end
|
32
|
+
|
33
|
+ defp calculate_relative_statistics([], _inputs), do: []
|
34
|
+
|
35
|
+ defp calculate_relative_statistics(scenarios, inputs) do
|
36
|
+ scenarios
|
37
|
+ |> scenarios_by_input(inputs)
|
38
|
+ |> Enum.flat_map(fn scenarios_with_same_input ->
|
39
|
+ {reference, others} = split_reference_scenario(scenarios_with_same_input)
|
40
|
+ others_with_relative = statistics_relative_to(others, reference)
|
41
|
+ [reference | others_with_relative]
|
42
|
+ end)
|
43
|
+ end
|
44
|
+
|
45
|
+ @spec sort([Scenario.t()]) :: [Scenario.t()]
|
46
|
+ defp sort(scenarios) do
|
47
|
+ Enum.sort_by(scenarios, fn scenario ->
|
48
|
+ {scenario.run_time_data.statistics.average, scenario.memory_usage_data.statistics.average}
|
49
|
+ end)
|
50
|
+ end
|
51
|
+
|
52
|
+ defp scenarios_by_input(scenarios, nil), do: [scenarios]
|
53
|
+
|
54
|
+ # we can't just group_by `input_name` because that'd lose the order of inputs which might
|
55
|
+ # be important
|
56
|
+ defp scenarios_by_input(scenarios, inputs) do
|
57
|
+ Enum.map(inputs, fn {input_name, _} ->
|
58
|
+ Enum.filter(scenarios, fn scenario -> scenario.input_name == input_name end)
|
59
|
+ end)
|
60
|
+ end
|
61
|
+
|
62
|
+ # right now we take the first scenario as we sorted them and it is the fastest,
|
63
|
+ # whenever we implement #179 though this becomesd more involved
|
64
|
+ defp split_reference_scenario(scenarios) do
|
65
|
+ [reference | others] = scenarios
|
66
|
+ {reference, others}
|
67
|
+ end
|
68
|
+
|
69
|
+ defp statistics_relative_to(scenarios, reference) do
|
70
|
+ Enum.map(scenarios, fn scenario ->
|
71
|
+ scenario
|
72
|
+ |> update_in([Access.key!(:run_time_data), Access.key!(:statistics)], fn statistics ->
|
73
|
+ add_relative_statistics(statistics, reference.run_time_data.statistics)
|
74
|
+ end)
|
75
|
+ |> update_in([Access.key!(:memory_usage_data), Access.key!(:statistics)], fn statistics ->
|
76
|
+ add_relative_statistics(statistics, reference.memory_usage_data.statistics)
|
77
|
+ end)
|
78
|
+ end)
|
79
|
+ end
|
80
|
+
|
81
|
+ # we might not run time/memory --> we shouldn't crash then ;)
|
82
|
+ defp add_relative_statistics(statistics = %{average: nil}, _reference), do: statistics
|
83
|
+ defp add_relative_statistics(statistics, %{average: nil}), do: statistics
|
84
|
+
|
85
|
+ defp add_relative_statistics(statistics, reference_statistics) do
|
86
|
+ %Statistics{
|
87
|
+ statistics
|
88
|
+ | relative_more: zero_safe_division(statistics.average, reference_statistics.average),
|
89
|
+ relative_less: zero_safe_division(reference_statistics.average, statistics.average),
|
90
|
+ absolute_difference: statistics.average - reference_statistics.average
|
91
|
+ }
|
92
|
+ end
|
93
|
+
|
94
|
+ defp zero_safe_division(0.0, 0.0), do: 1.0
|
95
|
+ defp zero_safe_division(_, 0), do: :infinity
|
96
|
+ defp zero_safe_division(_, 0.0), do: :infinity
|
97
|
+ defp zero_safe_division(a, b), do: a / b
|
98
|
+ end
|
added
lib/benchee/scenario.ex
|
@@ -0,0 +1,110 @@
|
1
|
+ defmodule Benchee.Scenario do
|
2
|
+ @moduledoc """
|
3
|
+ Core data structure representing one particular case (combination of function and input).
|
4
|
+
|
5
|
+ Represents the combination of a particular function to benchmark (also called "job" defined
|
6
|
+ by `job_name` and `function`) in combination with a specific input (`input_name` and `input`).
|
7
|
+ When no input is given, the combined value is representative of "no input".
|
8
|
+
|
9
|
+ A scenario then further gathers all data collected for this particular combination during
|
10
|
+ `Benchee.Benchmark.collect/3`, which are then used later in the process by `Benchee.Statistics`
|
11
|
+ to compute the relevant statistics which are then also added to the scenario.
|
12
|
+ It is the home of the aggregated knowledge regarding this particular case/scenario.
|
13
|
+
|
14
|
+ `name` is the name that should be used by formatters to display scenarios as
|
15
|
+ it potentially includes the `tag` present when loading scenarios that were
|
16
|
+ saved before. See `display_name/1`.
|
17
|
+ """
|
18
|
+
|
19
|
+ alias Benchee.CollectionData
|
20
|
+
|
21
|
+ defstruct [
|
22
|
+ :name,
|
23
|
+ :job_name,
|
24
|
+ :function,
|
25
|
+ :input_name,
|
26
|
+ :input,
|
27
|
+ run_time_data: %CollectionData{},
|
28
|
+ memory_usage_data: %CollectionData{},
|
29
|
+ before_each: nil,
|
30
|
+ after_each: nil,
|
31
|
+ before_scenario: nil,
|
32
|
+ after_scenario: nil,
|
33
|
+ tag: nil
|
34
|
+ ]
|
35
|
+
|
36
|
+ @typedoc """
|
37
|
+ All the data collected for a scenario (combination of function and input)
|
38
|
+
|
39
|
+ Among all the data required to execute the scenario (function, input, all the hooks aka
|
40
|
+ after_*/before_*), data needed to display (name, job_name, input_name, tag) and of course
|
41
|
+ run_time_data and memory_data with all the samples and computed statistics.
|
42
|
+ """
|
43
|
+ @type t :: %__MODULE__{
|
44
|
+ name: String.t(),
|
45
|
+ job_name: String.t(),
|
46
|
+ function: fun,
|
47
|
+ input_name: String.t() | nil,
|
48
|
+ input: any | nil,
|
49
|
+ run_time_data: CollectionData.t(),
|
50
|
+ memory_usage_data: CollectionData.t(),
|
51
|
+ before_each: fun | nil,
|
52
|
+ after_each: fun | nil,
|
53
|
+ before_scenario: fun | nil,
|
54
|
+ after_scenario: fun | nil,
|
55
|
+ tag: String.t() | nil
|
56
|
+ }
|
57
|
+
|
58
|
+ @doc """
|
59
|
+ Returns the correct name to display of the given scenario data.
|
60
|
+
|
61
|
+ In the normal case this is `job_name`, however when scenarios are loaded they
|
62
|
+ are tagged and these tags should be shown for disambiguation.
|
63
|
+
|
64
|
+ ## Examples
|
65
|
+
|
66
|
+ iex> alias Benchee.Scenario
|
67
|
+ iex> Scenario.display_name(%Scenario{job_name: "flat_map"})
|
68
|
+ "flat_map"
|
69
|
+ iex> Scenario.display_name(%Scenario{job_name: "flat_map", tag: "master"})
|
70
|
+ "flat_map (master)"
|
71
|
+ iex> Scenario.display_name(%{job_name: "flat_map"})
|
72
|
+ "flat_map"
|
73
|
+ """
|
74
|
+ @spec display_name(t) :: String.t()
|
75
|
+ def display_name(%{job_name: job_name, tag: nil}), do: job_name
|
76
|
+ def display_name(%{job_name: job_name, tag: tag}), do: "#{job_name} (#{tag})"
|
77
|
+ def display_name(%{job_name: job_name}), do: job_name
|
78
|
+
|
79
|
+ @doc """
|
80
|
+ Returns `true` if data of the provided type has been fully procsessed, `false` otherwise.
|
81
|
+
|
82
|
+ Current available types are `run_time` and `memory`. Reasons they might not have been processed
|
83
|
+ yet are:
|
84
|
+ * Suite wasn't configured to collect them at all
|
85
|
+ * `Benchee.statistics/1` hasn't been called yet so that data was collected but statistics
|
86
|
+ aren't present yet
|
87
|
+
|
88
|
+ ## Examples
|
89
|
+
|
90
|
+ iex> alias Benchee.Scenario
|
91
|
+ iex> alias Benchee.Statistics
|
92
|
+ iex> scenario = %Scenario{run_time_data: %Benchee.CollectionData{statistics: %Statistics{sample_size: 100}}}
|
93
|
+ iex> Scenario.data_processed?(scenario, :run_time)
|
94
|
+ true
|
95
|
+ iex> scenario = %Scenario{memory_usage_data: %Benchee.CollectionData{statistics: %Statistics{sample_size: 1}}}
|
96
|
+ iex> Scenario.data_processed?(scenario, :memory)
|
97
|
+ true
|
98
|
+ iex> scenario = %Scenario{memory_usage_data: %Benchee.CollectionData{statistics: %Statistics{sample_size: 0}}}
|
99
|
+ iex> Scenario.data_processed?(scenario, :memory)
|
100
|
+ false
|
101
|
+ """
|
102
|
+ @spec data_processed?(t, :run_time | :memory) :: boolean
|
103
|
+ def data_processed?(scenario, :run_time) do
|
104
|
+ scenario.run_time_data.statistics.sample_size > 0
|
105
|
+ end
|
106
|
+
|
107
|
+ def data_processed?(scenario, :memory) do
|
108
|
+ scenario.memory_usage_data.statistics.sample_size > 0
|
109
|
+ end
|
110
|
+ end
|
changed
lib/benchee/scenario_loader.ex
|
@@ -5,15 +5,15 @@ defmodule Benchee.ScenarioLoader do
|
5
5
|
Usually this is done right before the formatters run (that's when it happens
|
6
6
|
in `Benchee.run/2`) as all measurements and statistics should be there.
|
7
7
|
However, if you want to recompute statistics or others you can load them at
|
8
|
- any time. Just be aware that if you load them before `Benchee.measure/1` then
|
8
|
+ any time. Just be aware that if you load them before `Benchee.collect/1` then
|
9
9
|
they'll be rerun and measurements overridden.
|
10
10
|
"""
|
11
11
|
|
12
12
|
alias Benchee.Suite
|
13
13
|
|
14
14
|
@doc """
|
15
|
- Load the file(s) specified as `load_path` and add the scenarios to the list of
|
16
|
- the current scenarios.
|
15
|
+ Load the file(s) specified as `load_path` and add the scenarios to the list of the
|
16
|
+ current scenarios in the suite.
|
17
17
|
"""
|
18
18
|
def load(suite = %{configuration: %{load: load_path}, scenarios: scenarios}) do
|
19
19
|
loaded = load_scenarios(load_path)
|
changed
lib/benchee/statistics.ex
|
@@ -1,10 +1,12 @@
|
1
1
|
defmodule Benchee.Statistics do
|
2
2
|
@moduledoc """
|
3
|
- Statistics related functionality that is meant to take the raw benchmark run
|
4
|
- times and then compute statistics like the average and the standard deviation.
|
3
|
+ Statistics related functionality that is meant to take the raw benchmark data
|
4
|
+ and then compute statistics like the average and the standard deviation etc.
|
5
|
+
|
6
|
+ See `statistics/1` for a breakdown of the included statistics.
|
5
7
|
"""
|
6
8
|
|
7
|
- alias Benchee.{Benchmark.Scenario, Conversion.Duration, Suite, Utility.Parallel}
|
9
|
+ alias Benchee.{Conversion.Duration, Suite, Utility.Parallel}
|
8
10
|
|
9
11
|
alias Benchee.Statistics.Mode
|
10
12
|
alias Benchee.Statistics.Percentile
|
|
@@ -21,30 +23,21 @@ defmodule Benchee.Statistics do
|
21
23
|
:mode,
|
22
24
|
:minimum,
|
23
25
|
:maximum,
|
26
|
+ :relative_more,
|
27
|
+ :relative_less,
|
28
|
+ :absolute_difference,
|
24
29
|
sample_size: 0
|
25
30
|
]
|
26
31
|
|
32
|
+ @typedoc """
|
33
|
+ Careful with the mode, might be multiple values, one value or nothing.😱
|
34
|
+ """
|
27
35
|
@type mode :: [number] | number | nil
|
28
36
|
|
29
|
- @type t :: %__MODULE__{
|
30
|
- average: float,
|
31
|
- ips: float | nil,
|
32
|
- std_dev: float,
|
33
|
- std_dev_ratio: float,
|
34
|
- std_dev_ips: float | nil,
|
35
|
- median: number,
|
36
|
- percentiles: %{number => float},
|
37
|
- mode: mode,
|
38
|
- minimum: number,
|
39
|
- maximum: number,
|
40
|
- sample_size: integer
|
41
|
- }
|
37
|
+ @typedoc """
|
38
|
+ All the statistics `statistics/1` computes from the samples.
|
42
39
|
|
43
|
- @type samples :: [number]
|
44
|
-
|
45
|
- @doc """
|
46
|
- Takes a job suite with job run times, returns a map representing the
|
47
|
- statistics of the job suite as follows:
|
40
|
+ Overview of all the statistics benchee currently provides:
|
48
41
|
|
49
42
|
* average - average run time of the job in μs (the lower the better)
|
50
43
|
* ips - iterations per second, how often can the given function be
|
|
@@ -66,22 +59,59 @@ defmodule Benchee.Statistics do
|
66
59
|
* mode - the run time(s) that occur the most. Often one value, but
|
67
60
|
can be multiple values if they occur the same amount of times. If no value
|
68
61
|
occurs at least twice, this value will be nil.
|
69
|
- * minimum - the smallest (fastest) run time measured for the job
|
70
|
- * maximum - the biggest (slowest) run time measured for the job
|
62
|
+ * minimum - the smallest sample measured for the scenario
|
63
|
+ * maximum - the biggest sample measured for the scenario
|
64
|
+ * relative_more - relative to the reference (usually the fastest scenario) how much more
|
65
|
+ was the average of this scenario. E.g. for reference at 100, this scenario 200 then it
|
66
|
+ is 2.0.
|
67
|
+ * relative_less - relative to the reference (usually the fastest scenario) how much less
|
68
|
+ was the average of this scenario. E.g. for reference at 100, this scenario 200 then it
|
69
|
+ is 0.5.
|
70
|
+ * absolute_difference - relative to the reference (usually the fastest scenario) what is
|
71
|
+ the difference of the averages of the scenarios. e.g. for reference at 100, this
|
72
|
+ scenario 200 then it is 100.
|
71
73
|
* sample_size - the number of run time measurements taken
|
74
|
+ """
|
75
|
+ @type t :: %__MODULE__{
|
76
|
+ average: float,
|
77
|
+ ips: float | nil,
|
78
|
+ std_dev: float,
|
79
|
+ std_dev_ratio: float,
|
80
|
+ std_dev_ips: float | nil,
|
81
|
+ median: number,
|
82
|
+ percentiles: %{number => float},
|
83
|
+ mode: mode,
|
84
|
+ minimum: number,
|
85
|
+ maximum: number,
|
86
|
+ relative_more: float | nil | :infinity,
|
87
|
+ relative_less: float | nil | :infinity,
|
88
|
+ absolute_difference: float | nil,
|
89
|
+ sample_size: integer
|
90
|
+ }
|
72
91
|
|
73
|
- ## Parameters
|
92
|
+ @typedoc """
|
93
|
+ The samples a `Benchee.Collect` collected to compute statistics from.
|
94
|
+ """
|
95
|
+ @type samples :: [number]
|
74
96
|
|
75
|
- * `suite` - the job suite represented as a map after running the measurements,
|
76
|
- required to have the run_times available under the `run_times` key
|
97
|
+ @doc """
|
98
|
+ Takes a suite with scenarios and their data samples, adds the statistics to the
|
99
|
+ scenarios. For an overview of what the statistics mean see `t:t/0`.
|
100
|
+
|
101
|
+ Note that this will also sort the scenarios fastest to slowest to ensure a consistent order
|
102
|
+ of scenarios in all used formatters.
|
77
103
|
|
78
104
|
## Examples
|
79
105
|
|
80
106
|
iex> scenarios = [
|
81
|
- ...> %Benchee.Benchmark.Scenario{
|
107
|
+ ...> %Benchee.Scenario{
|
82
108
|
...> job_name: "My Job",
|
83
|
- ...> run_times: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
84
|
- ...> memory_usages: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
109
|
+ ...> run_time_data: %Benchee.CollectionData{
|
110
|
+ ...> samples: [200, 400, 400, 400, 500, 500, 500, 700, 900]
|
111
|
+ ...> },
|
112
|
+ ...> memory_usage_data: %Benchee.CollectionData{
|
113
|
+ ...> samples: [200, 400, 400, 400, 500, 500, 500, 700, 900]
|
114
|
+ ...> },
|
85
115
|
...> input_name: "Input",
|
86
116
|
...> input: "Input"
|
87
117
|
...> }
|
|
@@ -90,37 +120,41 @@ defmodule Benchee.Statistics do
|
90
120
|
iex> Benchee.Statistics.statistics(suite)
|
91
121
|
%Benchee.Suite{
|
92
122
|
scenarios: [
|
93
|
- %Benchee.Benchmark.Scenario{
|
123
|
+ %Benchee.Scenario{
|
94
124
|
job_name: "My Job",
|
95
|
- run_times: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
96
|
- memory_usages: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
97
125
|
input_name: "Input",
|
98
126
|
input: "Input",
|
99
|
- run_time_statistics: %Benchee.Statistics{
|
100
|
- average: 500.0,
|
101
|
- ips: 2000_000.0,
|
102
|
- std_dev: 200.0,
|
103
|
- std_dev_ratio: 0.4,
|
104
|
- std_dev_ips: 800_000.0,
|
105
|
- median: 500.0,
|
106
|
- percentiles: %{50 => 500.0, 99 => 900.0},
|
107
|
- mode: [500, 400],
|
108
|
- minimum: 200,
|
109
|
- maximum: 900,
|
110
|
- sample_size: 9
|
127
|
+ run_time_data: %Benchee.CollectionData{
|
128
|
+ samples: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
129
|
+ statistics: %Benchee.Statistics{
|
130
|
+ average: 500.0,
|
131
|
+ ips: 2000_000.0,
|
132
|
+ std_dev: 200.0,
|
133
|
+ std_dev_ratio: 0.4,
|
134
|
+ std_dev_ips: 800_000.0,
|
135
|
+ median: 500.0,
|
136
|
+ percentiles: %{50 => 500.0, 99 => 900.0},
|
137
|
+ mode: [500, 400],
|
138
|
+ minimum: 200,
|
139
|
+ maximum: 900,
|
140
|
+ sample_size: 9
|
141
|
+ }
|
111
142
|
},
|
112
|
- memory_usage_statistics: %Benchee.Statistics{
|
113
|
- average: 500.0,
|
114
|
- ips: nil,
|
115
|
- std_dev: 200.0,
|
116
|
- std_dev_ratio: 0.4,
|
117
|
- std_dev_ips: nil,
|
118
|
- median: 500.0,
|
119
|
- percentiles: %{50 => 500.0, 99 => 900.0},
|
120
|
- mode: [500, 400],
|
121
|
- minimum: 200,
|
122
|
- maximum: 900,
|
123
|
- sample_size: 9
|
143
|
+ memory_usage_data: %Benchee.CollectionData{
|
144
|
+ samples: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
145
|
+ statistics: %Benchee.Statistics{
|
146
|
+ average: 500.0,
|
147
|
+ ips: nil,
|
148
|
+ std_dev: 200.0,
|
149
|
+ std_dev_ratio: 0.4,
|
150
|
+ std_dev_ips: nil,
|
151
|
+ median: 500.0,
|
152
|
+ percentiles: %{50 => 500.0, 99 => 900.0},
|
153
|
+ mode: [500, 400],
|
154
|
+ minimum: 200,
|
155
|
+ maximum: 900,
|
156
|
+ sample_size: 9
|
157
|
+ }
|
124
158
|
}
|
125
159
|
}
|
126
160
|
],
|
|
@@ -132,92 +166,42 @@ defmodule Benchee.Statistics do
|
132
166
|
def statistics(suite = %Suite{scenarios: scenarios}) do
|
133
167
|
config = suite.configuration
|
134
168
|
|
135
|
- percentiles = Enum.uniq([50 | config.percentiles])
|
169
|
+ scenarios_with_statistics = calculate_per_scenario_statistics(scenarios, config)
|
136
170
|
|
137
|
- scenarios_with_statistics =
|
138
|
- Parallel.map(scenarios, fn scenario ->
|
139
|
- run_time_stats = scenario.run_times |> job_statistics(percentiles) |> add_ips
|
140
|
- memory_stats = job_statistics(scenario.memory_usages, percentiles)
|
141
|
-
|
142
|
- %Scenario{
|
143
|
- scenario
|
144
|
- | run_time_statistics: run_time_stats,
|
145
|
- memory_usage_statistics: memory_stats
|
146
|
- }
|
147
|
- end)
|
148
|
-
|
149
|
- %Suite{suite | scenarios: sort(scenarios_with_statistics)}
|
171
|
+ %Suite{suite | scenarios: scenarios_with_statistics}
|
150
172
|
end
|
151
173
|
|
152
|
- @doc """
|
153
|
- Calculates statistical data based on a series of run times for a job
|
154
|
- in microseconds.
|
174
|
+ defp calculate_per_scenario_statistics(scenarios, config) do
|
175
|
+ percentiles = Enum.uniq([50 | config.percentiles])
|
155
176
|
|
156
|
- ## Examples
|
177
|
+ Parallel.map(scenarios, fn scenario ->
|
178
|
+ run_time_stats = scenario.run_time_data.samples |> job_statistics(percentiles) |> add_ips
|
179
|
+ memory_stats = job_statistics(scenario.memory_usage_data.samples, percentiles)
|
157
180
|
|
158
|
- iex> run_times = [200, 400, 400, 400, 500, 500, 500, 700, 900]
|
159
|
- iex> Benchee.Statistics.job_statistics(run_times, [50, 99])
|
160
|
- %Benchee.Statistics{
|
161
|
- average: 500.0,
|
162
|
- ips: nil,
|
163
|
- std_dev: 200.0,
|
164
|
- std_dev_ratio: 0.4,
|
165
|
- std_dev_ips: nil,
|
166
|
- median: 500.0,
|
167
|
- percentiles: %{50 => 500.0, 99 => 900.0},
|
168
|
- mode: [500, 400],
|
169
|
- minimum: 200,
|
170
|
- maximum: 900,
|
171
|
- sample_size: 9
|
181
|
+ %{
|
182
|
+ scenario
|
183
|
+ | run_time_data: %{scenario.run_time_data | statistics: run_time_stats},
|
184
|
+ memory_usage_data: %{scenario.memory_usage_data | statistics: memory_stats}
|
172
185
|
}
|
186
|
+ end)
|
187
|
+ end
|
173
188
|
|
174
|
- iex> Benchee.Statistics.job_statistics([100], [50, 99])
|
175
|
- %Benchee.Statistics{
|
176
|
- average: 100.0,
|
177
|
- ips: nil,
|
178
|
- std_dev: 0,
|
179
|
- std_dev_ratio: 0.0,
|
180
|
- std_dev_ips: nil,
|
181
|
- median: 100.0,
|
182
|
- percentiles: %{50 => 100.0, 99 => 100.0},
|
183
|
- mode: nil,
|
184
|
- minimum: 100,
|
185
|
- maximum: 100,
|
186
|
- sample_size: 1
|
187
|
- }
|
188
|
-
|
189
|
- iex> Benchee.Statistics.job_statistics([], [])
|
190
|
- %Benchee.Statistics{
|
191
|
- average: nil,
|
192
|
- ips: nil,
|
193
|
- std_dev: nil,
|
194
|
- std_dev_ratio: nil,
|
195
|
- std_dev_ips: nil,
|
196
|
- median: nil,
|
197
|
- percentiles: nil,
|
198
|
- mode: nil,
|
199
|
- minimum: nil,
|
200
|
- maximum: nil,
|
201
|
- sample_size: 0
|
202
|
- }
|
203
|
-
|
204
|
- """
|
205
189
|
@spec job_statistics(samples, list) :: __MODULE__.t()
|
206
|
- def job_statistics([], _) do
|
190
|
+ defp job_statistics([], _) do
|
207
191
|
%__MODULE__{sample_size: 0}
|
208
192
|
end
|
209
193
|
|
210
|
- def job_statistics(measurements, percentiles) do
|
211
|
- total = Enum.sum(measurements)
|
212
|
- num_iterations = length(measurements)
|
194
|
+ defp job_statistics(samples, percentiles) do
|
195
|
+ total = Enum.sum(samples)
|
196
|
+ num_iterations = length(samples)
|
213
197
|
average = total / num_iterations
|
214
|
- deviation = standard_deviation(measurements, average, num_iterations)
|
198
|
+ deviation = standard_deviation(samples, average, num_iterations)
|
215
199
|
standard_dev_ratio = if average == 0, do: 0, else: deviation / average
|
216
|
- percentiles = Percentile.percentiles(measurements, percentiles)
|
200
|
+ percentiles = Percentile.percentiles(samples, percentiles)
|
217
201
|
median = Map.fetch!(percentiles, 50)
|
218
|
- mode = Mode.mode(measurements)
|
219
|
- minimum = Enum.min(measurements)
|
220
|
- maximum = Enum.max(measurements)
|
202
|
+ mode = Mode.mode(samples)
|
203
|
+ minimum = Enum.min(samples)
|
204
|
+ maximum = Enum.max(samples)
|
221
205
|
|
222
206
|
%__MODULE__{
|
223
207
|
average: average,
|
|
@@ -257,80 +241,4 @@ defmodule Benchee.Statistics do
|
257
241
|
std_dev_ips: standard_dev_ips
|
258
242
|
}
|
259
243
|
end
|
260
|
-
|
261
|
- @doc """
|
262
|
- Calculate additional percentiles and add them to the `run_time_statistics`.
|
263
|
- Should only be used after `statistics/1`, to calculate extra values that
|
264
|
- may be needed for reporting.
|
265
|
-
|
266
|
- ## Examples
|
267
|
-
|
268
|
- iex> scenarios = [
|
269
|
- ...> %Benchee.Benchmark.Scenario{
|
270
|
- ...> job_name: "My Job",
|
271
|
- ...> run_times: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
272
|
- ...> memory_usages: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
273
|
- ...> input_name: "Input",
|
274
|
- ...> input: "Input"
|
275
|
- ...> }
|
276
|
- ...> ]
|
277
|
- iex> %Benchee.Suite{scenarios: scenarios}
|
278
|
- ...> |> Benchee.Statistics.statistics
|
279
|
- ...> |> Benchee.Statistics.add_percentiles([25, 75])
|
280
|
- %Benchee.Suite{
|
281
|
- scenarios: [
|
282
|
- %Benchee.Benchmark.Scenario{
|
283
|
- job_name: "My Job",
|
284
|
- run_times: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
285
|
- memory_usages: [200, 400, 400, 400, 500, 500, 500, 700, 900],
|
286
|
- input_name: "Input",
|
287
|
- input: "Input",
|
288
|
- run_time_statistics: %Benchee.Statistics{
|
289
|
- average: 500.0,
|
290
|
- ips: 2_000_000.0,
|
291
|
- std_dev: 200.0,
|
292
|
- std_dev_ratio: 0.4,
|
293
|
- std_dev_ips: 800_000.0,
|
294
|
- median: 500.0,
|
295
|
- percentiles: %{25 => 400.0, 50 => 500.0, 75 => 600.0, 99 => 900.0},
|
296
|
- mode: [500, 400],
|
297
|
- minimum: 200,
|
298
|
- maximum: 900,
|
299
|
- sample_size: 9
|
300
|
- },
|
301
|
- memory_usage_statistics: %Benchee.Statistics{
|
302
|
- average: 500.0,
|
303
|
- ips: nil,
|
304
|
- std_dev: 200.0,
|
305
|
- std_dev_ratio: 0.4,
|
306
|
- std_dev_ips: nil,
|
307
|
- median: 500.0,
|
308
|
- percentiles: %{50 => 500.0, 99 => 900.0},
|
309
|
- mode: [500, 400],
|
310
|
- minimum: 200,
|
311
|
- maximum: 900,
|
312
|
- sample_size: 9
|
313
|
- }
|
314
|
- }
|
315
|
- ]
|
316
|
- }
|
317
|
- """
|
318
|
- def add_percentiles(suite = %Suite{scenarios: scenarios}, percentile_ranks) do
|
319
|
- new_scenarios =
|
320
|
- Parallel.map(scenarios, fn scenario ->
|
321
|
- update_in(scenario.run_time_statistics.percentiles, fn existing ->
|
322
|
- new = Percentile.percentiles(scenario.run_times, percentile_ranks)
|
323
|
- Map.merge(existing, new)
|
324
|
- end)
|
325
|
- end)
|
326
|
-
|
327
|
- %Suite{suite | scenarios: new_scenarios}
|
328
|
- end
|
329
|
-
|
330
|
- @spec sort([Scenario.t()]) :: [Scenario.t()]
|
331
|
- defp sort(scenarios) do
|
332
|
- Enum.sort_by(scenarios, fn scenario ->
|
333
|
- {scenario.run_time_statistics.average, scenario.memory_usage_statistics.average}
|
334
|
- end)
|
335
|
- end
|
336
244
|
end
|
changed
lib/benchee/suite.ex
|
@@ -4,8 +4,8 @@ defmodule Benchee.Suite do
|
4
4
|
|
5
5
|
Different layers of the benchmarking rely on different data being present
|
6
6
|
here. For instance for `Benchee.Statistics.statistics/1` to work the
|
7
|
- `run_times` key needs to be filled with the results from
|
8
|
- `Benchee.Benchmark.measure/1`.
|
7
|
+ `run_time_data` key of each scenario needs to be filled with the samples
|
8
|
+ collected by `Benchee.Benchmark.collect/1`.
|
9
9
|
|
10
10
|
Formatters can then use the data to display all of the results and the
|
11
11
|
configuration.
|
|
@@ -16,12 +16,19 @@ defmodule Benchee.Suite do
|
16
16
|
scenarios: []
|
17
17
|
]
|
18
18
|
|
19
|
- @type key :: atom | String.t()
|
20
|
- @type optional_map :: map | nil
|
19
|
+ @typedoc """
|
20
|
+ Valid key for either input or benchmarking job names.
|
21
|
+ """
|
22
|
+ @type key :: String.t() | atom
|
23
|
+
|
24
|
+ @typedoc """
|
25
|
+ The main suite consisting of the configuration data, information about the system and most
|
26
|
+ importantly a list of `t:Benchee.Scenario.t/0`.
|
27
|
+ """
|
21
28
|
@type t :: %__MODULE__{
|
22
29
|
configuration: Benchee.Configuration.t() | nil,
|
23
|
- system: optional_map,
|
24
|
- scenarios: [] | [Benchee.Benchmark.Scenario.t()]
|
30
|
+ system: map | nil,
|
31
|
+ scenarios: [] | [Benchee.Scenario.t()]
|
25
32
|
}
|
26
33
|
end
|
changed
lib/benchee/system.ex
|
@@ -1,6 +1,10 @@
|
1
1
|
defmodule Benchee.System do
|
2
2
|
@moduledoc """
|
3
3
|
Provides information about the system the benchmarks are run on.
|
4
|
+
|
5
|
+ Includes information such as elixir/erlang version, OS, CPU and memory.
|
6
|
+
|
7
|
+ So far supports/should work for Linux, MacOS, FreeBSD and Windows.
|
4
8
|
"""
|
5
9
|
|
6
10
|
alias Benchee.Conversion.Memory
|
|
@@ -25,15 +29,9 @@ defmodule Benchee.System do
|
25
29
|
%Suite{suite | system: system_info}
|
26
30
|
end
|
27
31
|
|
28
|
- @doc """
|
29
|
- Returns current Elixir version in use.
|
30
|
- """
|
31
|
- def elixir, do: System.version()
|
32
|
+ defp elixir, do: System.version()
|
32
33
|
|
33
|
- @doc """
|
34
|
- Returns the current erlang/otp version in use.
|
35
|
- """
|
36
|
- def erlang do
|
34
|
+ defp erlang do
|
37
35
|
otp_release = :erlang.system_info(:otp_release)
|
38
36
|
file = Path.join([:code.root_dir(), "releases", otp_release, "OTP_VERSION"])
|
39
37
|
|
|
@@ -46,17 +44,11 @@ defmodule Benchee.System do
|
46
44
|
end
|
47
45
|
end
|
48
46
|
|
49
|
- @doc """
|
50
|
- Returns the number of cores available for the currently running VM.
|
51
|
- """
|
52
|
- def num_cores do
|
47
|
+ defp num_cores do
|
53
48
|
System.schedulers_online()
|
54
49
|
end
|
55
50
|
|
56
|
- @doc """
|
57
|
- Returns an atom representing the platform the VM is running on.
|
58
|
- """
|
59
|
- def os do
|
51
|
+ defp os do
|
60
52
|
{_, name} = :os.type()
|
61
53
|
os(name)
|
62
54
|
end
|
|
@@ -66,11 +58,7 @@ defmodule Benchee.System do
|
66
58
|
defp os(:freebsd), do: :FreeBSD
|
67
59
|
defp os(_), do: :Linux
|
68
60
|
|
69
|
- @doc """
|
70
|
- Returns a string with detailed information about the CPU the benchmarks are
|
71
|
- being performed on.
|
72
|
- """
|
73
|
- def cpu_speed, do: cpu_speed(os())
|
61
|
+ defp cpu_speed, do: cpu_speed(os())
|
74
62
|
|
75
63
|
defp cpu_speed(:Windows) do
|
76
64
|
parse_cpu_for(:Windows, system_cmd("WMIC", ["CPU", "GET", "NAME"]))
|
|
@@ -90,6 +78,7 @@ defmodule Benchee.System do
|
90
78
|
|
91
79
|
@linux_cpuinfo_regex ~r/model name.*:([\w \(\)\-\@\.]*)/i
|
92
80
|
|
81
|
+ @doc false
|
93
82
|
def parse_cpu_for(_, "N/A"), do: "N/A"
|
94
83
|
|
95
84
|
def parse_cpu_for(:Windows, raw_output) do
|
|
@@ -110,11 +99,7 @@ defmodule Benchee.System do
|
110
99
|
end
|
111
100
|
end
|
112
101
|
|
113
|
- @doc """
|
114
|
- Returns an integer with the total number of available memory on the machine
|
115
|
- running the benchmarks.
|
116
|
- """
|
117
|
- def available_memory, do: available_memory(os())
|
102
|
+ defp available_memory, do: available_memory(os())
|
118
103
|
|
119
104
|
defp available_memory(:Windows) do
|
120
105
|
parse_memory_for(
|
|
@@ -171,6 +156,7 @@ defmodule Benchee.System do
|
171
156
|
Memory.format(memory_in_bytes)
|
172
157
|
end
|
173
158
|
|
159
|
+ @doc false
|
174
160
|
def system_cmd(cmd, args, system_func \\ &System.cmd/2) do
|
175
161
|
{output, exit_code} = system_func.(cmd, args)
|
changed
lib/benchee/utility/file_creation.ex
|
@@ -1,15 +1,15 @@
|
1
1
|
defmodule Benchee.Utility.FileCreation do
|
2
2
|
@moduledoc """
|
3
|
- Methods to create files used in plugins.
|
3
|
+ Methods to easily handle file creation used in plugins.
|
4
4
|
"""
|
5
5
|
|
6
6
|
alias Benchee.Benchmark
|
7
7
|
|
8
8
|
@doc """
|
9
|
- Open a file for write for all key/value pairs, interleaves the file name and
|
10
|
- calls function with file, content and filename.
|
9
|
+ Open a file for write for all key/value pairs, interleaves the file name with
|
10
|
+ the key and calls the given function with file, content and filename.
|
11
11
|
|
12
|
- Uses `Benchee.Utility.FileCreation.interlave/2` to get the base filename and
|
12
|
+ Uses `interleave/2` to get the base filename and
|
13
13
|
the given keys together to one nice file name, then creates these files and
|
14
14
|
calls the function with the file and the content from the given map so that
|
15
15
|
data can be written to the file.
|
|
@@ -22,7 +22,8 @@ defmodule Benchee.Utility.FileCreation do
|
22
22
|
the corresponding file
|
23
23
|
* filename - the base file name as desired by the user
|
24
24
|
* function - a function that is then called for every file with the associated
|
25
|
- file content from the map
|
25
|
+ file content from the map, defaults to just writing the file content via
|
26
|
+ `IO.write/2` and printing where it put the file.
|
26
27
|
|
27
28
|
## Examples
|
changed
lib/benchee/utility/parallel.ex
|
@@ -3,6 +3,9 @@ defmodule Benchee.Utility.Parallel do
|
3
3
|
|
4
4
|
@doc """
|
5
5
|
A utility function for mapping over an enumerable collection in parallel.
|
6
|
+
|
7
|
+ Take note that this spawns a process for every element in the collection
|
8
|
+ which is only advisable if the function does some heavy lifting.
|
6
9
|
"""
|
7
10
|
@spec map(Enum.t(), fun) :: list
|
8
11
|
def map(collection, func) do
|
changed
lib/benchee/utility/repeat_n.ex
|
@@ -2,7 +2,7 @@ defmodule Benchee.Utility.RepeatN do
|
2
2
|
@moduledoc false
|
3
3
|
|
4
4
|
@doc """
|
5
|
- Calls the given function n times.
|
5
|
+ Calls the given function n times.
|
6
6
|
"""
|
7
7
|
def repeat_n(_function, 0) do
|
8
8
|
# noop
|
changed
mix.exs
|
@@ -1,13 +1,13 @@
|
1
1
|
defmodule Benchee.Mixfile do
|
2
2
|
use Mix.Project
|
3
3
|
|
4
|
- @version "0.14.0"
|
4
|
+ @version "0.99.0"
|
5
5
|
|
6
6
|
def project do
|
7
7
|
[
|
8
8
|
app: :benchee,
|
9
9
|
version: @version,
|
10
|
- elixir: "~> 1.4",
|
10
|
+ elixir: "~> 1.6",
|
11
11
|
elixirc_paths: elixirc_paths(Mix.env()),
|
12
12
|
consolidate_protocols: true,
|
13
13
|
build_embedded: Mix.env() == :prod,
|
|
@@ -44,18 +44,18 @@ defmodule Benchee.Mixfile do
|
44
44
|
defp elixirc_paths(_), do: ["lib"]
|
45
45
|
|
46
46
|
def application do
|
47
|
- [applications: [:logger, :deep_merge]]
|
47
|
+ []
|
48
48
|
end
|
49
49
|
|
50
50
|
defp deps do
|
51
51
|
[
|
52
|
- {:deep_merge, "~> 0.1"},
|
52
|
+ {:deep_merge, "~> 1.0"},
|
53
53
|
{:ex_guard, "~> 1.3", only: :dev},
|
54
54
|
{:credo, "~> 1.0.0", only: :dev},
|
55
55
|
{:ex_doc, "~> 0.19.0", only: :dev},
|
56
56
|
{:earmark, "~> 1.0", only: :dev},
|
57
57
|
{:excoveralls, "~> 0.7", only: :test},
|
58
|
- {:inch_ex, "~> 0.5", only: :docs},
|
58
|
+ {:inch_ex, "~> 2.0", only: :docs},
|
59
59
|
{:dialyxir, "~> 1.0.0-rc.4", only: :dev, runtime: false}
|
60
60
|
]
|
61
61
|
end
|