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.
- checksums.yaml +7 -0
- data/AGENT_PROMPT.md +211 -0
- data/CHANGELOG.md +281 -0
- data/LICENSE +21 -0
- data/README.md +354 -0
- data/docs/README.md +23 -0
- data/docs/benchmarks.md +289 -0
- data/docs/ci.md +141 -0
- data/docs/configuration.md +429 -0
- data/docs/how-it-works.md +207 -0
- data/docs/json.md +306 -0
- data/docs/plugins.md +408 -0
- data/docs/troubleshooting.md +105 -0
- data/exe/duel +6 -0
- data/lib/duel/across.rb +48 -0
- data/lib/duel/baseline.rb +166 -0
- data/lib/duel/cli.rb +264 -0
- data/lib/duel/config.rb +343 -0
- data/lib/duel/context.rb +630 -0
- data/lib/duel/init.rb +211 -0
- data/lib/duel/json_report.rb +107 -0
- data/lib/duel/lock.rb +38 -0
- data/lib/duel/metric.rb +496 -0
- data/lib/duel/native.rb +162 -0
- data/lib/duel/process_sample.rb +144 -0
- data/lib/duel/profile.rb +116 -0
- data/lib/duel/rake_task.rb +25 -0
- data/lib/duel/resource.rb +209 -0
- data/lib/duel/rubies.rb +149 -0
- data/lib/duel/runner.rb +1114 -0
- data/lib/duel/stats.rb +73 -0
- data/lib/duel/summary.rb +237 -0
- data/lib/duel/templates/benchmark_duel.rb.erb +34 -0
- data/lib/duel/templates/duel.rb.erb +35 -0
- data/lib/duel/templates/perf.yml.erb +97 -0
- data/lib/duel/testing.rb +241 -0
- data/lib/duel/version.rb +5 -0
- data/lib/duel/worker.rb +211 -0
- data/lib/duel.rb +413 -0
- data/sig/duel/across.rbs +21 -0
- data/sig/duel/baseline.rbs +61 -0
- data/sig/duel/cli.rbs +95 -0
- data/sig/duel/config.rbs +181 -0
- data/sig/duel/context.rbs +387 -0
- data/sig/duel/init.rbs +97 -0
- data/sig/duel/json_report.rbs +47 -0
- data/sig/duel/lock.rbs +12 -0
- data/sig/duel/metric.rbs +351 -0
- data/sig/duel/native.rbs +98 -0
- data/sig/duel/process_sample.rbs +54 -0
- data/sig/duel/profile.rbs +64 -0
- data/sig/duel/rake_task.rbs +9 -0
- data/sig/duel/resource.rbs +106 -0
- data/sig/duel/rubies.rbs +45 -0
- data/sig/duel/runner.rbs +530 -0
- data/sig/duel/stats.rbs +26 -0
- data/sig/duel/summary.rbs +87 -0
- data/sig/duel/testing.rbs +91 -0
- data/sig/duel/version.rbs +3 -0
- data/sig/duel/worker.rbs +74 -0
- data/sig/duel.rbs +276 -0
- data/sig/manifest.yaml +4 -0
- metadata +124 -0
data/sig/duel/rubies.rbs
ADDED
|
@@ -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
|
data/sig/duel/runner.rbs
ADDED
|
@@ -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
|
data/sig/duel/stats.rbs
ADDED
|
@@ -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
|