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,45 @@
1
+ module Duel
2
+ # The Ruby each version runs on (Config#ruby).
3
+ # @api private
4
+ module Rubies
5
+ # The files that name a Ruby, in the order they're looked at.
6
+ VERSION_FILES: Array[String]
7
+
8
+ # Where chruby finds Rubies.
9
+ CHRUBY: Array[String]
10
+
11
+ # Where each manager has a version of Ruby installed, or nil.
12
+ MANAGERS: Hash[String, ^(String version) -> String?]
13
+
14
+ # A Ruby's own variables, set by a manager for the Ruby it chose.
15
+ VARIABLES: Array[String]
16
+
17
+ # The Ruby that +choice+ gives the version +name+d at +root+.
18
+ def self.find: (Config::ruby choice, String root, String name) -> String
19
+
20
+ # Whether +ruby+ is the runner's own.
21
+ def self.runner?: (String ruby) -> bool
22
+
23
+ # What a version's processes on +ruby+ get in their environment.
24
+ def self.environment: (String ruby, ?path: String?) -> Hash[String, String?]
25
+
26
+ # The nearest version file that names a Ruby, and the version.
27
+ def self.requested: (String root) -> [String, String]?
28
+
29
+ # The version of Ruby a version file names, or nil.
30
+ def self.read: (String file) -> String?
31
+
32
+ # Where chruby has +version+.
33
+ def self.chruby: (String version) -> String?
34
+
35
+ # Whether +version+, as a version file names it, is the runner's own
36
+ # Ruby.
37
+ def self.runner_asked?: (String version, ?engine: String, ?engine_version: String) -> bool
38
+
39
+ # What a command prints, or nil if it fails.
40
+ def self.command: (*String command) -> String?
41
+
42
+ # The Ruby the nearest version file names.
43
+ private def self.from_version_file: (String root, String name) -> String
44
+ end
45
+ end
@@ -0,0 +1,530 @@
1
+ module Duel
2
+ # Where reports go ($stdout, or a StringIO in its place).
3
+ # @api private
4
+ interface _Output
5
+ def puts: (*_ToS objects) -> void
6
+
7
+ def print: (*_ToS objects) -> void
8
+ end
9
+
10
+ # Seconds each benchmark runs before it's measured, unless --warm says.
11
+ # @api private
12
+ JIT_WARM: Float
13
+
14
+ # Seconds each benchmark runs before it's measured where a worker has
15
+ # YJIT on, unless --warm says.
16
+ # @api private
17
+ YJIT_WARM: Float
18
+
19
+ # How a run is set up (see Duel::CLI for where each comes from).
20
+ # @api private
21
+ class Settings < Struct[String | Regexp | Integer | Float | bool | Config::ruby | nil]
22
+ attr_accessor base: String?
23
+ attr_accessor base_dir: String?
24
+ attr_accessor candidate_dir: String?
25
+ attr_accessor filter: Regexp
26
+ attr_accessor jobs: Integer
27
+ attr_accessor processes: Integer
28
+ attr_accessor limit: Float
29
+ attr_accessor confidence: Float
30
+ attr_accessor warm: Float?
31
+ attr_accessor force: bool
32
+ attr_accessor config_path: String?
33
+ attr_accessor timeout: Float
34
+ # Each version's Ruby (--ruby), over Config#ruby.
35
+ attr_accessor ruby: Config::ruby
36
+
37
+ def self.new: (?String? base, ?String? base_dir, ?String? candidate_dir, ?Regexp filter, ?Integer jobs,
38
+ ?Integer processes, ?Float limit, ?Float confidence, ?Float? warm, ?bool force, ?String? config_path,
39
+ ?Float timeout, ?Config::ruby ruby) -> instance
40
+ | (?base: String?, ?base_dir: String?, ?candidate_dir: String?, ?filter: Regexp, ?jobs: Integer,
41
+ ?processes: Integer, ?limit: Float, ?confidence: Float, ?warm: Float?, ?force: bool,
42
+ ?config_path: String?, ?timeout: Float, ?ruby: Config::ruby) -> instance
43
+
44
+ def self.defaults: () -> Settings
45
+ end
46
+
47
+ # One version as a run has it: its tree, the Ruby its processes run on,
48
+ # and what they get in their environment.
49
+ # @api private
50
+ class Side < Struct[String | Hash[String, String?]]
51
+ attr_accessor name: String
52
+ attr_accessor root: String
53
+ attr_accessor ruby: String
54
+ attr_accessor env: Hash[String, String?]
55
+
56
+ def self.new: (String name, String root, String ruby, Hash[String, String?] env) -> instance
57
+
58
+ # Each version with nothing prepared: its tree, on the runner's Ruby.
59
+ def self.unprepared: (Config config, Settings settings, String base_root) -> Hash[String, Side]
60
+ end
61
+
62
+ # A worker process (Duel::Worker) running one version of the code.
63
+ # @api private
64
+ class WorkerProcess
65
+ # A worker's reply to a request (see Worker#handle), parsed from JSON.
66
+ type reply = untyped
67
+
68
+ LIB: String
69
+ POLL: Float
70
+ STOP_WAIT: Integer
71
+ # The gems that are Duel's: Duel, and its plugins.
72
+ DUEL_GEM: Regexp
73
+
74
+ @label: String
75
+ @timeout: Float
76
+ @root: String
77
+ @dir: String
78
+ @env: Hash[String, String?]
79
+ @ruby: String
80
+ @runs: ProcessSample?
81
+ @description: String
82
+ @extensions: Hash[String, String]
83
+ @gem_dirs: Array[String]
84
+ @pid: Integer
85
+ @socket: UNIXSocket
86
+ @benchmarks: Hash[String, Definition::summary]
87
+ @metrics: Array[Metric::spec]
88
+ @memory: bool
89
+ @profiler: bool
90
+ @yjit: bool
91
+
92
+ attr_reader label: String
93
+
94
+ attr_reader benchmarks: Hash[String, Definition::summary]
95
+
96
+ attr_reader metrics: Array[Metric::spec]
97
+
98
+ attr_reader memory: bool
99
+
100
+ attr_reader profiler: bool
101
+
102
+ attr_reader yjit: bool
103
+
104
+ # The Ruby it runs on (RUBY_DESCRIPTION).
105
+ attr_reader description: String
106
+
107
+ # The native extensions it loaded, by feature.
108
+ attr_reader extensions: Hash[String, String]
109
+
110
+ # The directories gems are installed in, as real paths ending in "/".
111
+ attr_reader gem_dirs: Array[String]
112
+
113
+ # Where a worker on +ruby+ loads Duel from, ahead of anything else.
114
+ def self.load_path: (String ruby, ?specs: Hash[String, Gem::BasicSpecification]) -> Array[String]
115
+
116
+ def initialize: (String label, String root, String candidate_root, String dir, config_path: String?,
117
+ env: Hash[String, String?], timeout: Float, ruby: String) -> void
118
+
119
+ # Sends a request, and returns the reply, or raises Failed with its
120
+ # error.
121
+ def request: (String op, String name, **untyped args) -> reply
122
+
123
+ def sample: (String name, Integer n, ?runs: Integer, ?count: bool) -> reply
124
+
125
+ # A run of a process benchmark: its process, fresh, started as this
126
+ # worker was.
127
+ def run: (Definition::process process) -> ProcessSample::sampled
128
+
129
+ # Closing the connection ends the worker. One still running +wait+
130
+ # seconds later is killed.
131
+ def stop: (?wait: Numeric) -> void
132
+
133
+ private
134
+
135
+ def accept: (UNIXServer server, String path) -> UNIXSocket
136
+
137
+ def read: () -> reply
138
+
139
+ # Kills a worker that didn't +what+ within the timeout, and fails.
140
+ def hung: (String what) -> bot
141
+
142
+ def kill: () -> void
143
+
144
+ def monotonic: () -> Float
145
+ end
146
+
147
+ # What Duel raises when something a run needs fails: a resource that
148
+ # didn't start, a worker that died or hung.
149
+ class Failed < Error
150
+ end
151
+
152
+ # Workers of both versions, taking turns.
153
+ # @api private
154
+ class Lane
155
+ TARGET_NS: Integer
156
+ RUNS: Integer
157
+ WARMUP: Integer
158
+ HEAP_CAP: Integer
159
+ STEP: Integer
160
+ MAX_PAIRS: Integer
161
+ COUNTS: Integer
162
+ MEMORY_SAMPLES: Integer
163
+ PROFILE_SAMPLES: Integer
164
+ # A process benchmark's pairs before any are measured, and its runs a
165
+ # sample (unless it says), each sample the fastest of them, as many
166
+ # as take PROCESS_SAMPLE_NS of CPU time.
167
+ PROCESS_WARMUP: Integer
168
+ PROCESS_RUNS: Integer
169
+ PROCESS_SAMPLE_NS: Integer
170
+ # The longest TMPDIR a lane's directory goes in.
171
+ SHORT: Integer
172
+
173
+ @dir: String
174
+ @resources: Array[Resource]
175
+ @workers: Array[WorkerProcess]
176
+ @lock: Thread::Mutex
177
+ @config: Config
178
+ @settings: Settings
179
+ @sides: Hash[String, Side]
180
+ # A process benchmark's peak memory samples, by version.
181
+ @peaks: Hash[String, Array[Integer?]]
182
+ @bases: Array[WorkerProcess]
183
+ @candidates: Array[WorkerProcess]
184
+ @profiler: bool
185
+ # The warm-up's seconds (see #warm).
186
+ @warm: Float
187
+
188
+ attr_reader profiler: bool
189
+
190
+ def initialize: (Config config, Settings settings, Hash[String, Side] sides, ?processes: Integer) -> void
191
+
192
+ # A directory for a lane's sockets.
193
+ def self.tmpdir: () -> String
194
+
195
+ # Where lanes' directories go: TMPDIR if it's short, else /tmp.
196
+ def self.short_tmp: (?String tmpdir) -> String
197
+
198
+ # Opens a lane, yields it and always closes it.
199
+ def self.open: [T] (Config config, Settings settings, Hash[String, Side] sides, ?processes: Integer) { (Lane lane) -> T } -> T
200
+
201
+ def benchmarks: () -> Hash[String, Definition::summary]
202
+
203
+ # The Ruby each version runs on, by version.
204
+ def rubies: () -> Hash[String, String]
205
+
206
+ def stop: () -> void
207
+
208
+ # Measures one benchmark on both versions.
209
+ def compare: (String name) -> Result
210
+
211
+ # Profiles of the benchmark on each version, or nil without StackProf.
212
+ def profile: (String name, Integer n) -> Hash[String, Profile::profile]?
213
+
214
+ private
215
+
216
+ def spawn: (String side, Integer index) -> WorkerProcess
217
+
218
+ # Fails if a version's workers loaded a native extension that isn't a
219
+ # build of its own.
220
+ def check_builds: () -> void
221
+
222
+ # Whether +path+ (that +worker+ loaded) is in +side+'s tree, and not
223
+ # in an installed gem.
224
+ def inside?: (String path, String side, WorkerProcess worker) -> bool
225
+
226
+ def each_worker: () { (WorkerProcess worker) -> void } -> void
227
+
228
+ def set_up: (String name, Result result) -> void
229
+
230
+ def metrics: () -> Array[Metric::spec]
231
+
232
+ # What the block returns for each item, each on a thread of its own.
233
+ # Adds an item to a list, from a thread of its own.
234
+ def keep: [T] (Array[T] list, T item) -> void
235
+
236
+ # Whether workers start (and stop) at once.
237
+ PARALLEL: bool
238
+
239
+ # (Untyped: Steep loses a generic in a thread's block.)
240
+ def in_parallel: (Array[untyped] items) { (untyped item) -> untyped } -> Array[untyped]
241
+
242
+ def measure: (String name, Result result) -> void
243
+
244
+ # A process benchmark's samples.
245
+ def measure_process: (String name, Result result) -> void
246
+
247
+ # The runs a process benchmark's sample takes, by its warm-up's.
248
+ def process_runs: (Array[[WorkerProcess::reply, WorkerProcess::reply]] floors) -> Integer
249
+
250
+ def warm: (String name, Integer n) -> void
251
+
252
+ def judge: (String name, Integer n, Array[[WorkerProcess::reply, WorkerProcess::reply]] floors, Result result) -> void
253
+
254
+ # Whether both versions' CPU time is below its floor.
255
+ def below_cpu_floor?: (Array[[Worker::sampled, Worker::sampled]] floors) -> bool
256
+
257
+ # Whether one version's samples are above a spec's floors.
258
+ def above_floors?: (Array[Hash[String, Metric::value]] samples, Metric::spec spec) -> bool
259
+
260
+ def limit: (Metric::spec spec) -> Float
261
+
262
+ def verdict: (Array[Float] ratios, Metric::spec spec) -> Stats::verdict
263
+
264
+ # The verdicts that decide a measure at the end of a round, and after
265
+ # any other pair.
266
+ DECISIVE: Hash[bool, Array[Stats::verdict]]
267
+
268
+ # Whether +verdict+ decides a measure, at the end of a round or not.
269
+ def decided?: (Stats::verdict verdict, bool round) -> bool
270
+
271
+ def calibrate: (String name, Integer target, Integer n) -> Integer
272
+
273
+ # The most iterations whose allocations fit HEAP_CAP (infinite when it
274
+ # allocated nothing).
275
+ def most_iterations: (Integer n, Integer heap) -> (Integer | Float)
276
+
277
+ # The +index+th pair: [baseline's reply, candidate's].
278
+ def pair: (String name, Integer n, Integer index, ?runs: Integer, ?count: bool) -> [WorkerProcess::reply, WorkerProcess::reply]
279
+
280
+ def count: (String name, Integer n, Result result) -> void
281
+
282
+ def peak_memory: (String name, Integer n, Result result) -> void
283
+
284
+ def fresh: [T] (String side) { (WorkerProcess worker) -> T } -> T
285
+
286
+ def monotonic: () -> Float
287
+ end
288
+
289
+ # What one comparison of a benchmark found (Testing::Comparison#results
290
+ # has them).
291
+ class Result
292
+ # A ratio metric's verdict, and each version's (the median of its
293
+ # samples).
294
+ type timed = { "median" => Float, "limit" => Float, "across" => bool, "unit" => String?, "plural" => String?, "interval" => Array[Float]?, "pairs" => Integer,
295
+ "verdict" => String, "baseline" => Metric::value, "candidate" => Metric::value }
296
+
297
+ # A counted metric's fewest, on each version, in +n+ iterations.
298
+ type counted = { "baseline" => Metric::value, "candidate" => Metric::value, "n" => Integer,
299
+ "tolerance" => Metric::value, "allowance" => Metric::value, "unit" => String?, "plural" => String? }
300
+
301
+ # Where a regression's time went (see Profile#to_h), or why there's no
302
+ # telling.
303
+ # @api private
304
+ type profile = Profile::report | { "unavailable" => String }
305
+
306
+ # Peak memory grows by more than MEMORY_RATIO, and by MEMORY_FLOOR
307
+ # bytes at least, before it's worse.
308
+ # @api private
309
+ MEMORY_RATIO: Float
310
+
311
+ # @api private
312
+ MEMORY_FLOOR: Integer
313
+
314
+ # What a sample says when no metric said anything.
315
+ # @api private
316
+ NOTHING_SAID: Hash[String, String]
317
+
318
+ # The verdicts that say a timing isn't slower.
319
+ # @api private
320
+ CLEARED: Array[String]
321
+
322
+ @name: String
323
+ @timed: Hash[String, timed]
324
+ @counted: Hash[String, counted]
325
+ @details: Hash[String, Hash[String, String]]
326
+ @confirmations: Array[Result]
327
+ @undecided: Array[Result]
328
+ @n: Integer?
329
+ @seconds: Float?
330
+ @skipped: String?
331
+ @error: String?
332
+ @memory: Hash[String, Integer]?
333
+ @profile: profile?
334
+
335
+ # The benchmark's name.
336
+ attr_reader name: String
337
+
338
+ # Each ratio measure's verdict and medians, by name.
339
+ attr_reader timed: Hash[String, timed]
340
+
341
+ # Each counted measure's fewest on each version, by name.
342
+ attr_reader counted: Hash[String, counted]
343
+
344
+ # What metrics said of the latest samples (Metric#details): by
345
+ # measure, by version.
346
+ attr_reader details: Hash[String, Hash[String, String]]
347
+
348
+ # The measurements that confirmed it was worse.
349
+ attr_reader confirmations: Array[Result]
350
+
351
+ # The measurements again that were too close to call.
352
+ # @api private
353
+ attr_reader undecided: Array[Result]
354
+
355
+ # Its iterations a sample.
356
+ attr_reader n: Integer?
357
+
358
+ # @api private
359
+ attr_writer n: Integer?
360
+
361
+ # The seconds it took.
362
+ attr_reader seconds: Float?
363
+
364
+ # @api private
365
+ attr_writer seconds: Float?
366
+
367
+ # Why it was skipped (the baseline couldn't run it), or nil.
368
+ attr_reader skipped: String?
369
+
370
+ # @api private
371
+ attr_writer skipped: String?
372
+
373
+ # How it failed, or nil.
374
+ attr_reader error: String?
375
+
376
+ # @api private
377
+ attr_writer error: String?
378
+
379
+ # Each version's peak memory, in bytes.
380
+ attr_reader memory: Hash[String, Integer]?
381
+
382
+ # @api private
383
+ attr_writer memory: Hash[String, Integer]?
384
+
385
+ # Where a regression's time went.
386
+ # @api private
387
+ attr_accessor profile: profile?
388
+
389
+ # @api private
390
+ def initialize: (String name) -> void
391
+
392
+ # Keeps what metrics said of a pair of samples.
393
+ # @api private
394
+ def note: (Worker::sampled base, Worker::sampled candidate, ?String? measure) -> void
395
+
396
+ # The metrics on which the candidate did worse.
397
+ # @api private
398
+ def worse: () -> Array[String]
399
+
400
+ # The timed metrics on which the candidate was slower.
401
+ # @api private
402
+ def slower: () -> Array[String]
403
+
404
+ # Whether its timings say slower: one is, and none says it isn't.
405
+ # @api private
406
+ def slower_in_time?: () -> bool
407
+
408
+ # Whether it's to be measured again.
409
+ # @api private
410
+ def confirming?: () -> bool
411
+
412
+ # Keeps a measurement again as a confirmation, or sets it aside
413
+ # where it's too close to call. Whether it was set aside.
414
+ # @api private
415
+ def confirm: (Result again) -> bool
416
+
417
+ # Whether its peak memory grew by too much.
418
+ # @api private
419
+ def more_memory?: () -> bool
420
+
421
+ # Worse on a metric in the first measurement and in every confirmation,
422
+ # or slower in time in all of them (see #mixed?).
423
+ def regressed: () -> Array[String]
424
+
425
+ # Whether it regressed in time on one timed metric in some
426
+ # measurements and on another in the rest.
427
+ # @api private
428
+ def mixed?: () -> bool
429
+
430
+ # Whether it regressed on nothing, and didn't fail.
431
+ def passed?: () -> bool
432
+
433
+ # Whether a timed measure's verdict was too close to the limit to settle.
434
+ # @api private
435
+ def unsettled?: () -> bool
436
+
437
+ private
438
+
439
+ # The first measurement and its confirmations.
440
+ def measurements: () -> Array[Result]
441
+ end
442
+
443
+ # Measures every benchmark, confirms what looks worse, and reports.
444
+ # @api private
445
+ class Runner
446
+ CONFIRM: Integer
447
+
448
+ # +number+ of +noun+, in its plural for any number but one.
449
+ def self.count: (Integer number, String noun, ?String plural) -> String
450
+
451
+ @config: Config
452
+ @settings: Settings
453
+ @base_root: String
454
+ @out: _Output
455
+ @across: Hash[String, Across::verdict]
456
+ @prepared: Array[Resource]
457
+ @dir: String
458
+ @sides: Hash[String, Side]
459
+ # What the first lane learned of the benchmarks.
460
+ @benchmarks: Hash[String, Definition::summary]
461
+
462
+ @rubies: Hash[String, String]
463
+ @seconds: Float
464
+
465
+ # Across the board: {metric => verdict}. Set by #run.
466
+ attr_reader across: Hash[String, Across::verdict]
467
+
468
+ # The Ruby each version ran on, by version. Set by #run.
469
+ attr_reader rubies: Hash[String, String]
470
+
471
+ # The seconds the run took. Set by #run.
472
+ attr_reader seconds: Float
473
+
474
+ def initialize: (Config config, Settings settings, String base_root, ?out: _Output) -> void
475
+
476
+ # Returns the Results.
477
+ def run: () -> Array[Result]
478
+
479
+ # Whether nothing got slower, and none failed.
480
+ def passed?: (Array[Result] results) -> bool
481
+
482
+ def regressed_across: () -> Array[String]
483
+
484
+ # The plugins the configuration used, by name.
485
+ def plugins: () -> Array[Symbol]
486
+
487
+ # What a regression's report says of it, line by line.
488
+ def explained: (Result result) -> Array[String]
489
+
490
+ private
491
+
492
+ # What #run does.
493
+ def compare_all: (Float started) -> Array[Result]
494
+
495
+ # Each version, prepared once for the run.
496
+ def prepare: () -> Hash[String, Side]
497
+
498
+ def with_lanes: [T] () { (Array[Lane] lanes) -> T } -> T
499
+
500
+ def judge_across: (Array[Result] results, Array[String] names) -> Hash[String, Across::verdict]
501
+
502
+ def open_lane: () -> Lane
503
+
504
+ def measure_all: (Array[Lane] lanes, Array[String] names) -> Array[Result]
505
+
506
+ def attempt: (Lane lane, String name) -> Result
507
+
508
+ def confirm: (Array[Result] suspects) -> void
509
+
510
+ def profile: (Array[Result] regressed) -> void
511
+
512
+ def report: (Array[Result] results, Float seconds) -> void
513
+
514
+ def line: (Result result) -> String
515
+
516
+ def mb: (Integer bytes) -> String
517
+
518
+ # The unit of +measured+, for +number+ of it: singular for one.
519
+ def unit: (Metric::value number, Result::timed | Result::counted measured) -> String?
520
+
521
+ # What each worse measure measured, an iteration, and what its metric
522
+ # said of it.
523
+ def explanation: (Result result) -> Array[String]
524
+
525
+ # A sample's value an iteration.
526
+ def amount: (Metric::value value, Integer n, Result::timed | Result::counted measured) -> String
527
+
528
+ def profile_lines: (Result::profile? profile) -> Array[String]
529
+ end
530
+ end
@@ -0,0 +1,26 @@
1
+ module Duel
2
+ # The statistics behind a verdict: a distribution-free confidence
3
+ # interval for the median of paired ratios, and the decision it supports.
4
+ # @api private
5
+ module Stats
6
+ type verdict = :slower | :faster | :same | :undecided
7
+
8
+ # The 1-based ranks [lower, upper] bounding a +confidence+ interval for
9
+ # the median of +n+ samples, or nil when +n+ is too small for one.
10
+ def self.median_ranks: (Integer n, Float confidence) -> [Integer, Integer]?
11
+
12
+ # [low, high] bounds of the +confidence+ interval for the median of
13
+ # +samples+, or nil.
14
+ def self.median_interval: (Array[Float] samples, Float confidence) -> [Float, Float]?
15
+
16
+ def self.median: (Array[Float] samples) -> Float
17
+ | (Array[Metric::value] samples) -> Metric::value
18
+
19
+ # What paired ratios say against +limit+.
20
+ def self.verdict: (Array[Float] ratios, limit: Float, confidence: Float) -> verdict
21
+
22
+ # What a confidence interval for the median ratio (nil: too few) says
23
+ # against +limit+.
24
+ def self.decide: ([Float, Float]? interval, Float limit) -> verdict
25
+ end
26
+ end
@@ -0,0 +1,87 @@
1
+ module Duel
2
+ # A run's report in Markdown, for a pull request.
3
+ # @api private
4
+ class Summary
5
+ # The most characters it has.
6
+ LIMIT: Integer
7
+
8
+ # The most lines of an error it shows.
9
+ ERROR_LINES: Integer
10
+
11
+ # A benchmark's verdict, and where it goes in the table.
12
+ VERDICTS: Array[String]
13
+
14
+ # The verdicts that fail a run.
15
+ FAILED: Array[String]
16
+
17
+ @runner: Runner
18
+ @results: Array[Result]
19
+ @settings: Settings
20
+ @versions: Hash[String, String]
21
+ @regressed: Array[Result]
22
+
23
+ # +text+ as code, in Markdown.
24
+ def self.code: (String text) -> String
25
+
26
+ # A run that didn't compare, with what it said.
27
+ def self.note: (String title, String message) -> String
28
+
29
+ # +text+ in a fenced block of its own.
30
+ def self.block: (String text) -> String
31
+
32
+ # +result+'s verdict.
33
+ def self.verdict: (Result result) -> String
34
+
35
+ # The counted measures that differ, an iteration.
36
+ def self.changed_counts: (Result result) -> Hash[String, [Float, Float]]
37
+
38
+ def initialize: (Runner runner, Array[Result] results, settings: Settings, baseline: String, candidate: String) -> void
39
+
40
+ # The Markdown.
41
+ def markdown: () -> String
42
+
43
+ private
44
+
45
+ # The Markdown, with the first +explained+ regressions explained.
46
+ def render: (Integer explained) -> String
47
+
48
+ def heading: () -> String
49
+
50
+ # Each version, on its Ruby where theirs differ.
51
+ def versions: () -> String
52
+
53
+ def overview: () -> String
54
+
55
+ # The results, by verdict.
56
+ def sorted: () -> Array[Result]
57
+
58
+ def table: (Array[Result] changed) -> String?
59
+
60
+ # Those that didn't change, collapsed.
61
+ def unchanged: (Array[Result] same) -> String?
62
+
63
+ # A table's row of +cells+.
64
+ def row: (*String cells) -> String
65
+
66
+ # A benchmark's name, for a table's cell.
67
+ def name: (Result result) -> String
68
+
69
+ # Each timed measure's ratio and its interval.
70
+ def timings: (Result result) -> String
71
+
72
+ # Each counted measure that changed, an iteration.
73
+ def counts: (Result result) -> String
74
+
75
+ def mb: (Integer bytes) -> String
76
+
77
+ # The first +explained+ regressions, explained.
78
+ def regressions: (Integer explained) -> String?
79
+
80
+ # The verdicts across the board.
81
+ def across: () -> String?
82
+
83
+ def unsettled: () -> String?
84
+
85
+ def errors: () -> String?
86
+ end
87
+ end