duel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/AGENT_PROMPT.md +211 -0
  3. data/CHANGELOG.md +281 -0
  4. data/LICENSE +21 -0
  5. data/README.md +354 -0
  6. data/docs/README.md +23 -0
  7. data/docs/benchmarks.md +289 -0
  8. data/docs/ci.md +141 -0
  9. data/docs/configuration.md +429 -0
  10. data/docs/how-it-works.md +207 -0
  11. data/docs/json.md +306 -0
  12. data/docs/plugins.md +408 -0
  13. data/docs/troubleshooting.md +105 -0
  14. data/exe/duel +6 -0
  15. data/lib/duel/across.rb +48 -0
  16. data/lib/duel/baseline.rb +166 -0
  17. data/lib/duel/cli.rb +264 -0
  18. data/lib/duel/config.rb +343 -0
  19. data/lib/duel/context.rb +630 -0
  20. data/lib/duel/init.rb +211 -0
  21. data/lib/duel/json_report.rb +107 -0
  22. data/lib/duel/lock.rb +38 -0
  23. data/lib/duel/metric.rb +496 -0
  24. data/lib/duel/native.rb +162 -0
  25. data/lib/duel/process_sample.rb +144 -0
  26. data/lib/duel/profile.rb +116 -0
  27. data/lib/duel/rake_task.rb +25 -0
  28. data/lib/duel/resource.rb +209 -0
  29. data/lib/duel/rubies.rb +149 -0
  30. data/lib/duel/runner.rb +1114 -0
  31. data/lib/duel/stats.rb +73 -0
  32. data/lib/duel/summary.rb +237 -0
  33. data/lib/duel/templates/benchmark_duel.rb.erb +34 -0
  34. data/lib/duel/templates/duel.rb.erb +35 -0
  35. data/lib/duel/templates/perf.yml.erb +97 -0
  36. data/lib/duel/testing.rb +241 -0
  37. data/lib/duel/version.rb +5 -0
  38. data/lib/duel/worker.rb +211 -0
  39. data/lib/duel.rb +413 -0
  40. data/sig/duel/across.rbs +21 -0
  41. data/sig/duel/baseline.rbs +61 -0
  42. data/sig/duel/cli.rbs +95 -0
  43. data/sig/duel/config.rbs +181 -0
  44. data/sig/duel/context.rbs +387 -0
  45. data/sig/duel/init.rbs +97 -0
  46. data/sig/duel/json_report.rbs +47 -0
  47. data/sig/duel/lock.rbs +12 -0
  48. data/sig/duel/metric.rbs +351 -0
  49. data/sig/duel/native.rbs +98 -0
  50. data/sig/duel/process_sample.rbs +54 -0
  51. data/sig/duel/profile.rbs +64 -0
  52. data/sig/duel/rake_task.rbs +9 -0
  53. data/sig/duel/resource.rbs +106 -0
  54. data/sig/duel/rubies.rbs +45 -0
  55. data/sig/duel/runner.rbs +530 -0
  56. data/sig/duel/stats.rbs +26 -0
  57. data/sig/duel/summary.rbs +87 -0
  58. data/sig/duel/testing.rbs +91 -0
  59. data/sig/duel/version.rbs +3 -0
  60. data/sig/duel/worker.rbs +74 -0
  61. data/sig/duel.rbs +276 -0
  62. data/sig/manifest.yaml +4 -0
  63. metadata +124 -0
@@ -0,0 +1,207 @@
1
+ # How Duel works
2
+
3
+ This page explains how Duel decides that a change made your code slower,
4
+ what it measures, how it explains a regression, where the baseline comes
5
+ from, and how it handles JIT compilers.
6
+
7
+ ## How Duel tells a regression from noise
8
+
9
+ A policy of zero tolerance only works if the guard never cries wolf.
10
+ After one false alarm, people learn to rerun until the build is green.
11
+ So Duel is built to fail only when your code really got slower, and
12
+ never on noise, so that it can gate every pull request:
13
+
14
+ - **CPU time, not elapsed time.** Duel times each benchmark by its
15
+ thread's CPU time, which other processes on the machine don't
16
+ inflate. A sample is the fastest of several runs, because interference
17
+ only ever adds time. Garbage collection is kept out of samples, and
18
+ its cost shows in the allocation count instead, which is exact.
19
+ - **Side by side, many times over.** The two versions run in worker
20
+ processes of their own, three of each by default, at the same time on
21
+ the same machine. They take turns: one sample from each, in
22
+ alternating order, and each pair from a different pair of processes.
23
+ That way, a busy moment weighs on both versions alike, and no one
24
+ process's luck decides anything. (Where things land in a process's
25
+ memory can make the very same code a few percent faster or slower for
26
+ the process's whole life.) There are no stored numbers and no
27
+ thresholds to maintain, and any machine will do.
28
+ - **A confidence interval, not an average.** A timing is slower only
29
+ when a 99% distribution-free confidence interval for the median of
30
+ the paired ratios (candidate / baseline) lies entirely above the
31
+ limit, which is 1.02 by default. No one outlier sample can decide it.
32
+ Duel takes samples in rounds of 10 pairs, up to 60, until the interval
33
+ settles the question. A process benchmark's sample is the fastest of
34
+ a few fresh processes, and it can also stop after any pair once the
35
+ interval shows that it isn't slower, which only ever ends a
36
+ measurement before it could find a slowdown.
37
+ Duel reports a timing that's still too close to the limit to call, and
38
+ doesn't fail on it.
39
+ - **Measured again before failing.** Duel measures whatever looks
40
+ slower again from scratch, up to three times, in fresh processes. It
41
+ fails only if every measurement finds the same metric worse. A
42
+ measurement whose timings are all still too close to call neither
43
+ confirms a slowdown nor clears it, so Duel measures once more in its
44
+ place, up to three more times. Every measurement that counts still has
45
+ to find it worse, so identical code, which only rarely looks slower in
46
+ any one measurement, still has to look slower in four or more.
47
+ - **Slower in time, whichever timing shows it.** A slowdown near the
48
+ limits can be slower in CPU time in one measurement and in
49
+ instructions in the next, with the other timing too close to call
50
+ each time. Duel fails on that too, if in every measurement a timing
51
+ is slower and none is the same or faster. Any timing can be the
52
+ slower one, not only one in particular, so Duel measures once more
53
+ before it fails, which keeps identical code from failing more often
54
+ than it otherwise would.
55
+ - **Exact counts.** Allocations (on CRuby), and any counted metric of
56
+ your own, fail on any increase: one more allocation per two
57
+ iterations.
58
+ - **Across the board, too.** A slowdown that's too small for any one
59
+ benchmark to settle, such as 1% everywhere, still shows in all of the
60
+ benchmarks together. For each metric that's judged by ratio, every
61
+ benchmark's median ratio is a sample. The candidate fails when a
62
+ confidence interval for their median lies above a quarter of the
63
+ metric's margin: 1.005, for CPU time's 1.02. This takes 8 benchmarks,
64
+ so with fewer, Duel doesn't report it. Duel measures again before it
65
+ fails, as it does for the rest.
66
+
67
+ ## What Duel measures
68
+
69
+ Duel judges every benchmark on the following metrics:
70
+
71
+ | Metric | What it is | A regression |
72
+ |---|---|---|
73
+ | `cpu` | The benchmark thread's CPU time (or the process's) | More than 2% slower, with 99% confidence |
74
+ | `instructions` | Instructions retired, from the CPU's own counters | More than 1% more, with 99% confidence |
75
+ | `allocations` | Ruby objects allocated (on CRuby) | One more per two iterations |
76
+ | `peak_memory` | The most resident memory, above the start, that measured work reached (not `once` or `untimed`), with garbage collection running as usual (when asked for) | More than 10% and 1 MB more |
77
+ | Yours | See [Add your own metrics](configuration.md#add-your-own-metrics) | |
78
+
79
+ Instruction counts barely vary from run to run, so they catch slowdowns
80
+ that are too small for CPU time to settle. CPU time still catches what
81
+ instructions miss, such as cache misses.
82
+
83
+ Instruction counts come from the CPU's counters, where the operating
84
+ system lets Duel read them:
85
+
86
+ - On Linux, they're the benchmark thread's, through `perf_event_open`,
87
+ which many virtual machines, including CI runners, don't offer. So
88
+ Linux doesn't count instructions for benchmarks with threads of their
89
+ own.
90
+ - On macOS, they're the whole process's.
91
+ - Elsewhere, and on JRuby and TruffleRuby, there's no `instructions`
92
+ metric, and CPU time judges alone.
93
+
94
+ ## Where the extra time went
95
+
96
+ Duel shows what regressed per iteration, on each version, with what a
97
+ metric said of it if it had something to say. For more information, see
98
+ [Add your own metrics](configuration.md#add-your-own-metrics). When something is slower
99
+ in time, Duel profiles both versions over the same iterations with
100
+ [StackProf](https://github.com/tmm1/stackprof), if it's in your bundle
101
+ (`duel init` and duel-rails's generator add it to your `Gemfile`), and
102
+ shows where the extra CPU time went:
103
+
104
+ ```
105
+ REGRESSION parse: a large document: worse instructions, cpu in every measurement
106
+ instructions: 1.521e+06 → 1.629e+06 an iteration (1.07x)
107
+ cpu: 112.4µs → 121.5µs an iteration (1.08x)
108
+ where the extra CPU went (as a share of the baseline's: in each frame itself, and with what it called):
109
+ +6.8% +7.1% MyParser::Lexer#next_token
110
+ +0.4% +0.4% String#byteslice
111
+ ```
112
+
113
+ A profile covers only the work that Duel measures. Work inside
114
+ `untimed`, and what an `around_iteration` hook does before and after
115
+ each iteration, is left out. What an `after_iterations` hook does is in
116
+ it, because Duel measures that with the iterations. The garbage collector is held off
117
+ during each sample, as it is when Duel measures. Collecting what each
118
+ sample left behind isn't the benchmark's code, so it isn't ranked with
119
+ the frames. If it changed, Duel shows it on a line of its own, `in
120
+ garbage collection, apart from the code`. On macOS, the signal that a CPU-time interval
121
+ has passed arrives only when the process calls into the kernel, so a
122
+ CPU-time profile would blame whatever code makes system calls. There,
123
+ Duel profiles elapsed time instead, which a benchmark that's busy on the
124
+ CPU spends the same way.
125
+
126
+ When no one timing is slower in every measurement, but every
127
+ measurement has one that's slower (see
128
+ [How Duel tells a regression from noise](#how-duel-tells-a-regression-from-noise)),
129
+ the report names the timings that were:
130
+
131
+ ```
132
+ REGRESSION parse: a large document: slower in every measurement, by one of cpu, instructions
133
+ ```
134
+
135
+ On each benchmark's line, counts such as `allocations` are per
136
+ iteration, as they are in the explanation of a regression.
137
+
138
+ ## Where the baseline comes from
139
+
140
+ The main branch is the one that `origin/HEAD` names. Without
141
+ `origin/HEAD`, Duel uses `origin/main`, and then `origin/master`. In a
142
+ repository without a remote, it uses `main`, and then `master`. To name
143
+ another branch, set `c.main_branch`.
144
+
145
+ Duel checks out a git baseline as a git worktree next to your project,
146
+ under a hidden name that's as long as your project's directory name, for
147
+ example `.3f9c2a1b` next to `my_parser`. Duel keeps it there for the
148
+ next run. If your project has an `ext/` directory, Duel builds the
149
+ baseline with `bundle exec rake compile`. To remove the baseline, git's
150
+ record of it, and the rest of `tmp/duel`, run `duel --clean`.
151
+
152
+ If your project is in a subdirectory of its repository, as in a
153
+ monorepo, run `duel` in that subdirectory. Duel checks out the whole
154
+ repository next to the repository's directory, and the baseline is the
155
+ same subdirectory in that checkout. The paths in `c.watch` are from your
156
+ project's directory.
157
+
158
+ If `HEAD` and the main branch have no commit in common, Duel says so.
159
+ This happens in a shallow clone, which doesn't have the history that the
160
+ baseline comes from. To fix it, fetch the whole history. For example, on
161
+ GitHub Actions, set `fetch-depth: 0` for `actions/checkout`.
162
+
163
+ The baseline's path is as long and as deep as your project's, in a
164
+ subdirectory too, because what code does with its own file paths, such
165
+ as looking for a file that a request might be for, takes longer on a
166
+ longer or deeper path. A
167
+ baseline at a longer path would measure slower, and hide a regression
168
+ that small. If Duel can't write next to your project, or something else
169
+ already has that name, it uses `tmp/duel/base` instead. Whenever the two
170
+ versions' paths differ in length, Duel says so.
171
+
172
+ If nothing under `lib/` or `ext/` changed since the baseline, there's
173
+ nothing to compare, and Duel says so. To compare anyway, use `--force`.
174
+
175
+ Duel also builds the candidate before every run. Builds are incremental,
176
+ so a build that's already done costs little. A stale or missing build,
177
+ or a C extension that your code quietly does without, never decides a
178
+ comparison.
179
+
180
+ Duel also checks that each version's workers load only native extensions
181
+ that were built in that version's directory. A version without a build
182
+ of its own would quietly load another build, such as the other
183
+ version's, or an installed gem's, and the comparison wouldn't be of its
184
+ code. If that happens, Duel stops and tells you to set `c.build`. An
185
+ installed gem's extension is never taken for a version's build, even
186
+ when the gems are installed in the project's directory, as
187
+ `vendor/bundle` (where setup-ruby's `bundler-cache` puts them).
188
+
189
+ ## JIT compilers
190
+
191
+ If YJIT is on in a worker, Duel runs each benchmark for two seconds
192
+ before it measures it. A small library's methods are compiled within
193
+ Duel's first samples, but in a big app, YJIT compiles a method only after
194
+ 120 calls. YJIT can be on because you turned it on (`RUBY_YJIT_ENABLE=1`,
195
+ for example), or because your code did as it loaded, as Rails 7.2 and
196
+ later do. To warm up for a different time, use `--warm`.
197
+
198
+ ### JRuby and TruffleRuby
199
+
200
+ JRuby's and TruffleRuby's JIT compilers keep compiling for seconds, so
201
+ Duel runs each benchmark for five seconds before it measures it
202
+ (`--warm`). Their timings stay noisier than CRuby's, so Duel often
203
+ leaves small timing regressions on them unsettled.
204
+
205
+ Neither has an allocation count, an instruction count, or profiles, and
206
+ each has peak memory only on Linux. StackProf runs on CRuby only: Duel
207
+ doesn't use it on JRuby or TruffleRuby, even where it's installed.
data/docs/json.md ADDED
@@ -0,0 +1,306 @@
1
+ # Read Duel's report as JSON
2
+
3
+ To read Duel's results from a program, such as a dashboard, a bot, or a
4
+ script that tracks benchmarks over time, write its report as JSON with
5
+ `--json`:
6
+
7
+ ```sh
8
+ duel --json tmp/duel.json
9
+ ```
10
+
11
+ The JSON report has the same results as the
12
+ [Markdown report](../README.md#report-on-a-pull-request), and more of
13
+ each: every timing's ratio, interval and verdict, each version's medians
14
+ an iteration, and every count. A relative path is relative to where
15
+ `duel` runs, and Duel makes the file's directory if it needs to. Each
16
+ run replaces the file.
17
+
18
+ Duel writes the report whatever the outcome: a comparison that passed or
19
+ failed, nothing to compare, or a comparison that couldn't run. It writes
20
+ nothing when the command line isn't valid, such as an unknown option,
21
+ and nothing for `--clean`, `--version`, or `duel init`, which aren't
22
+ comparisons. So a missing report means that `duel` didn't get as far as
23
+ comparing, or crashed.
24
+
25
+ ## Versions of the schema
26
+
27
+ The report's `schema` key is the version of its schema, now `1`. A new
28
+ key can appear in any release without a new version, so ignore the keys
29
+ that you don't know. Any other change, such as a key removed or renamed,
30
+ or a value that means something else, comes with a new version. Check
31
+ `schema` before you read the rest, and stop if it isn't one that you
32
+ know.
33
+
34
+ The report uses only plain JSON types: objects, arrays, strings, numbers,
35
+ `true`, `false`, and `null`. Numbers are numbers, never strings. A key
36
+ that doesn't apply, such as a skipped benchmark's `iterations`, is
37
+ `null`, not missing, so every object of a kind has the same keys.
38
+
39
+ ## An example
40
+
41
+ A run that found one regression, abridged to two benchmarks:
42
+
43
+ ```json
44
+ {
45
+ "schema": 1,
46
+ "duel_version": "0.1.0",
47
+ "status": "failed",
48
+ "exit": 1,
49
+ "message": null,
50
+ "settings": {
51
+ "confidence": 0.99,
52
+ "limit": 1.02,
53
+ "filter": "",
54
+ "jobs": 4,
55
+ "processes": 3,
56
+ "timeout": 60.0,
57
+ "warm": null,
58
+ "force": false
59
+ },
60
+ "seconds": 14.2,
61
+ "baseline": {
62
+ "ref": "origin/main",
63
+ "commit": "3f9c2a1d5e8b7c6a4f3e2d1c0b9a8f7e6d5c4b3a",
64
+ "root": "/home/runner/work/my_parser/.3f9c2a1b",
65
+ "ruby": "ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [x86_64-linux]"
66
+ },
67
+ "candidate": {
68
+ "commit": "8e4d0b7f1a2c3e4d5b6a7f8e9d0c1b2a3f4e5d6c",
69
+ "uncommitted_changes": false,
70
+ "root": "/home/runner/work/my_parser/my_parser",
71
+ "ruby": "ruby 3.4.1 (2024-12-25 revision 48d4efcb85) +PRISM [x86_64-linux]"
72
+ },
73
+ "plugins": [],
74
+ "benchmarks": [
75
+ {
76
+ "name": "parse: a large document",
77
+ "verdict": "regressed",
78
+ "passed": false,
79
+ "regressed": ["instructions", "cpu", "allocations"],
80
+ "unsettled": false,
81
+ "iterations": 9,
82
+ "seconds": 3.1,
83
+ "confirmations": 3,
84
+ "timed": {
85
+ "instructions": {
86
+ "ratio": 1.071,
87
+ "interval": [1.069, 1.073],
88
+ "limit": 1.01,
89
+ "pairs": 10,
90
+ "verdict": "slower",
91
+ "baseline": 1521000.0,
92
+ "candidate": 1629000.0,
93
+ "unit": null,
94
+ "plural": null,
95
+ "across": true
96
+ },
97
+ "cpu": {
98
+ "ratio": 1.081,
99
+ "interval": [1.062, 1.097],
100
+ "limit": 1.02,
101
+ "pairs": 10,
102
+ "verdict": "slower",
103
+ "baseline": 112400.0,
104
+ "candidate": 121500.0,
105
+ "unit": "ns",
106
+ "plural": "ns",
107
+ "across": true
108
+ }
109
+ },
110
+ "counted": {
111
+ "allocations": {
112
+ "baseline": 5790.0,
113
+ "candidate": 5791.0,
114
+ "unit": null,
115
+ "plural": null,
116
+ "worse": true
117
+ }
118
+ },
119
+ "peak_memory": null,
120
+ "details": {},
121
+ "explanation": [
122
+ "instructions: 1.521e+06 → 1.629e+06 an iteration (1.07x)",
123
+ "cpu: 112.4µs → 121.5µs an iteration (1.08x)",
124
+ "allocations: 5790 → 5791 an iteration",
125
+ "where the extra CPU went (as a share of the baseline's: in each frame itself, and with what it called):",
126
+ " +6.8% +7.1% MyParser::Lexer#next_token"
127
+ ],
128
+ "error": null,
129
+ "skipped": null
130
+ },
131
+ {
132
+ "name": "parse: an empty document",
133
+ "verdict": "same",
134
+ "passed": true,
135
+ "regressed": [],
136
+ "unsettled": false,
137
+ "iterations": 2400,
138
+ "seconds": 0.8,
139
+ "confirmations": 0,
140
+ "timed": {
141
+ "cpu": {
142
+ "ratio": 1.002,
143
+ "interval": [0.994, 1.009],
144
+ "limit": 1.02,
145
+ "pairs": 10,
146
+ "verdict": "same",
147
+ "baseline": 410.5,
148
+ "candidate": 411.2,
149
+ "unit": "ns",
150
+ "plural": "ns",
151
+ "across": true
152
+ }
153
+ },
154
+ "counted": {
155
+ "allocations": {
156
+ "baseline": 3.0,
157
+ "candidate": 3.0,
158
+ "unit": null,
159
+ "plural": null,
160
+ "worse": false
161
+ }
162
+ },
163
+ "peak_memory": null,
164
+ "details": {},
165
+ "explanation": [],
166
+ "error": null,
167
+ "skipped": null
168
+ }
169
+ ],
170
+ "across": {
171
+ "cpu": {
172
+ "ratio": 1.004,
173
+ "interval": [0.998, 1.012],
174
+ "limit": 1.005,
175
+ "benchmarks": 12,
176
+ "verdict": "undecided",
177
+ "confirmed": false
178
+ }
179
+ },
180
+ "errors": []
181
+ }
182
+ ```
183
+
184
+ A run with nothing to compare has the same keys, with nothing compared:
185
+
186
+ ```json
187
+ {
188
+ "schema": 1,
189
+ "duel_version": "0.1.0",
190
+ "status": "nothing_to_compare",
191
+ "exit": 0,
192
+ "message": "duel: lib, ext unchanged since the baseline, so there's nothing to compare (--force to anyway)",
193
+ "settings": {"confidence": 0.99, "limit": 1.02, "filter": "", "jobs": 4, "processes": 3, "timeout": 60.0, "warm": null, "force": false},
194
+ "seconds": null,
195
+ "baseline": null,
196
+ "candidate": null,
197
+ "plugins": [],
198
+ "benchmarks": [],
199
+ "across": {},
200
+ "errors": []
201
+ }
202
+ ```
203
+
204
+ For example, to list the benchmarks that regressed, with
205
+ [jq](https://jqlang.org):
206
+
207
+ ```sh
208
+ jq -r '.benchmarks[] | select(.verdict == "regressed") | .name' tmp/duel.json
209
+ ```
210
+
211
+ ## The report
212
+
213
+ | Key | What it is |
214
+ |---|---|
215
+ | `schema` | The schema's version: `1`. |
216
+ | `duel_version` | The version of Duel that wrote the report. |
217
+ | `status` | `"passed"`, `"failed"`, `"nothing_to_compare"`, or `"error"` (Duel couldn't compare). |
218
+ | `exit` | The exit status of `duel`: `0` for passed or nothing to compare, `1` for failed, `2` for an error. |
219
+ | `message` | What Duel said, for nothing to compare or an error, such as `"duel: no release tag (v*) before HEAD to compare against"`. Otherwise `null`. |
220
+ | `settings` | The settings that the run used (see below). |
221
+ | `seconds` | How long the comparison took. `null` without a comparison. |
222
+ | `baseline`, `candidate` | The two versions (see below). `null` without a comparison. |
223
+ | `plugins` | The names of the plugins in use, such as `["rails", "activerecord"]`. |
224
+ | `benchmarks` | Every benchmark compared, in the order Duel found them (see below). |
225
+ | `across` | The verdicts across the board, by metric (see below). |
226
+ | `errors` | The benchmarks that failed, each as `{"benchmark": name, "message": error}`. |
227
+
228
+ A run fails when a benchmark regressed, a benchmark failed, or a metric
229
+ was slower across the board and confirmed (`"confirmed": true`).
230
+
231
+ ### Settings
232
+
233
+ | Key | What it is |
234
+ |---|---|
235
+ | `confidence` | The confidence of each interval, such as `0.99`. |
236
+ | `limit` | The ratio above which a timing is slower (`--limit`). A metric can have its own limit, which its timings show. |
237
+ | `filter` | The regular expression that picked the benchmarks (`--filter`), as source. `""` matches every one. |
238
+ | `jobs` | Benchmarks measured at once (`--jobs`). |
239
+ | `processes` | Worker processes for each version (`--processes`). |
240
+ | `timeout` | The seconds that a worker may take to load or to answer (`--timeout`). |
241
+ | `warm` | The seconds of warm-up for each benchmark (`--warm`), or `null` for the default, which depends on the Ruby. |
242
+ | `force` | Whether `--force` was given. |
243
+
244
+ ### The versions
245
+
246
+ | Key | What it is |
247
+ |---|---|
248
+ | `ref` | The baseline's git ref: the one `--base` named, the release tag for `--base last-release`, or the main branch, such as `"origin/main"`, by default, where the baseline is the commit where `HEAD` left that branch. `null` for a baseline directory. |
249
+ | `commit` | The full hash of the commit compared. `null` for a baseline directory, or for a candidate outside a git repository. |
250
+ | `uncommitted_changes` | The candidate only: whether what `c.watch` names has changes that aren't committed. `null` outside a git repository. |
251
+ | `root` | The tree that was compared, as an absolute path. |
252
+ | `ruby` | The version's Ruby, as its `RUBY_DESCRIPTION`. |
253
+
254
+ ### Benchmarks
255
+
256
+ | Key | What it is |
257
+ |---|---|
258
+ | `name` | The benchmark's name. |
259
+ | `verdict` | `"regressed"`, `"error"`, `"unsettled"` (a timing too close to its limit to settle), `"faster"`, `"changed"` (a count changed), `"skipped"`, or `"same"`, as in the Markdown report's table. |
260
+ | `passed` | Whether it regressed on nothing and didn't fail. |
261
+ | `regressed` | The metrics that it regressed on, in every measurement, such as `["cpu", "allocations"]`. Peak memory is `"peak_memory"`. |
262
+ | `unsettled` | Whether a timing was too close to its limit to settle. |
263
+ | `iterations` | The iterations in each sample. `null` for a benchmark that failed or was skipped. |
264
+ | `seconds` | How long its first measurement took. `null` for a benchmark that failed or was skipped. |
265
+ | `confirmations` | How many times it was measured again, from scratch, because it looked worse. |
266
+ | `timed` | Its timings, by metric (see below). |
267
+ | `counted` | Its counts, by metric (see below). |
268
+ | `peak_memory` | Each version's peak memory, as `{"baseline": bytes, "candidate": bytes, "unit": "B", "worse": true or false}`, for a benchmark that measures it. Otherwise `null`. |
269
+ | `details` | What metrics said of the last samples, by metric and then by version, such as `{"allocations": {"baseline": "2 String", "candidate": "3 String"}}`. |
270
+ | `explanation` | For a regression, the lines that explain it, as the terminal report and the Markdown report show them. Otherwise `[]`. |
271
+ | `error` | Why it failed, with any backtrace. Otherwise `null`. |
272
+ | `skipped` | Why it was skipped, such as a benchmark that isn't on the baseline. Otherwise `null`. |
273
+
274
+ Each timing in `timed`, such as `cpu`:
275
+
276
+ | Key | What it is |
277
+ |---|---|
278
+ | `ratio` | The median of the paired ratios, the candidate's to the baseline's. Above 1 is the candidate slower. |
279
+ | `interval` | The confidence interval for that median, as `[low, high]`. |
280
+ | `limit` | The ratio above which it's slower. |
281
+ | `pairs` | The pairs of samples measured. |
282
+ | `verdict` | `"slower"`, `"faster"`, `"same"`, or `"undecided"` (too close to the limit to settle). |
283
+ | `baseline`, `candidate` | Each version's median for one iteration, in `unit`. |
284
+ | `unit`, `plural` | The metric's unit, singular and plural, such as `"ns"`, or `null` for a plain number. |
285
+ | `across` | Whether it counts across the board. |
286
+
287
+ Each count in `counted`, such as `allocations`:
288
+
289
+ | Key | What it is |
290
+ |---|---|
291
+ | `baseline`, `candidate` | Each version's fewest for one iteration, in `unit`. |
292
+ | `unit`, `plural` | The metric's unit, singular and plural, such as `"query"` and `"queries"`, or `null` for a plain number. |
293
+ | `worse` | Whether the candidate's count is worse, by more than the metric tolerates. |
294
+
295
+ ### Across the board
296
+
297
+ Each metric in `across`, such as `cpu`:
298
+
299
+ | Key | What it is |
300
+ |---|---|
301
+ | `ratio` | The median of every benchmark's ratio. |
302
+ | `interval` | The confidence interval for that median, as `[low, high]`, or `null` with too few benchmarks to tell. |
303
+ | `limit` | The ratio above which it's slower across the board. |
304
+ | `benchmarks` | The benchmarks that it's from. |
305
+ | `verdict` | `"slower"`, `"faster"`, `"same"`, or `"undecided"`. |
306
+ | `confirmed` | Whether it was slower again when every benchmark was measured a second time, which fails the run. |