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
data/sig/duel/lock.rbs ADDED
@@ -0,0 +1,12 @@
1
+ module Duel
2
+ # One run at a time in a project: an flock on tmp/duel/lock.
3
+ # @api private
4
+ module Lock
5
+ # An exclusive lock, or none if another holds it.
6
+ EXCLUSIVE: Integer
7
+
8
+ # Runs the block holding +dir+'s lock. Fails, naming the run that
9
+ # holds it, if another does.
10
+ def self.hold: [T] (String dir) { () -> T } -> T
11
+ end
12
+ end
@@ -0,0 +1,351 @@
1
+ module Duel
2
+ # Something measured around every sample of every benchmark (see
3
+ # Config#metric). Subclasses name what they measure, and each one's
4
+ # settings, in +measures+, and say whether they can measure here with
5
+ # +available?+.
6
+ class Metric
7
+ # :ratio (noisy: judged on paired ratios with a confidence interval)
8
+ # or :counted (exact).
9
+ type kind = :ratio | :counted
10
+
11
+ # What a measure measures.
12
+ type value = Integer | Float
13
+
14
+ # What it's in: a symbol ("B", "ns"), or a word as [singular, plural].
15
+ type unit = String | [String, String]
16
+
17
+ # A measure's settings, as declared: any left out as by default. (+calibrate+:
18
+ # running totals' only, see Snapshot, and +skip+: Config#measure's only.)
19
+ type settings = { ?kind: kind, ?unit: unit?, ?floor: value, ?cpu_floor: value, ?limit: Float?, ?unit_ns: value,
20
+ ?sizes_samples: bool, ?across: bool, ?tolerance: value, ?allowance: value, ?calibrate: bool, ?skip: bool }
21
+
22
+ # What #stop_sample returns: the measurement, a Hash of them (by name), or
23
+ # nil (none this time).
24
+ type measurement = value | Hash[String, value?] | nil
25
+
26
+ # A measure as the runner learns it.
27
+ # @api private
28
+ type spec = { "name" => String, "kind" => String, "unit" => String?, "plural" => String?, "floor" => value, "cpu_floor" => value, "limit" => Float?,
29
+ "unit_ns" => value, "sizes_samples" => bool, "across" => bool, "tolerance" => value, "allowance" => value }
30
+
31
+ # A measure's settings, every one, resolved (see Metric.resolve).
32
+ type resolved = { kind: kind, unit: String?, plural: String?, floor: value, cpu_floor: value, limit: Float?, unit_ns: value,
33
+ sizes_samples: bool, across: bool, tolerance: value, allowance: value }
34
+
35
+ # No measure's settings changed.
36
+ # @api private
37
+ UNCHANGED: Hash[String, Hash[Symbol, untyped]]
38
+
39
+ # No change to a measure's settings.
40
+ # @api private
41
+ NO_CHANGE: Hash[Symbol, untyped]
42
+
43
+ # Every setting there is.
44
+ # @api private
45
+ SETTINGS: Array[Symbol]
46
+
47
+ # A unit that's a word (or words), which needs its plural given.
48
+ # @api private
49
+ WORD: Regexp
50
+
51
+ # The units that are time, and the nanoseconds in each.
52
+ # @api private
53
+ TIME_UNITS: Hash[String, Integer]
54
+
55
+ @settings: Hash[String, resolved]?
56
+ @kinds: Hash[String, kind]?
57
+ @first_measure: String?
58
+ @only_measure: String?
59
+
60
+ # Whether it can measure here at all (false leaves it out).
61
+ def self.available?: () -> bool
62
+
63
+ # Whether reading it is costly (a round trip to a server, say), so it
64
+ # isn't paused around each iteration of a benchmark with an around
65
+ # block.
66
+ def self.costly?: () -> bool
67
+
68
+ # The metric's name in reports: by default, its class's, in snake case.
69
+ def self.metric_name: () -> String
70
+
71
+ # What it measures, by name.
72
+ def self.measures: () -> Hash[String, settings]
73
+
74
+ # Whether +more+ (what a count grew by, over +n+ iterations) is still
75
+ # within +tolerance+ an iteration and +allowance+ a sample.
76
+ def self.within?: (value more, Integer n, tolerance: value, allowance: value) -> bool
77
+
78
+ # Each measure's kind, by name.
79
+ # @api private
80
+ def self.kinds: () -> Hash[String, kind]
81
+
82
+ # Fails unless +settings+ (a measure's) are settings there are, with a
83
+ # kind there is.
84
+ # @api private
85
+ def self.check: (String name, Hash[Symbol, untyped] settings) -> void
86
+
87
+ # Each measure's settings, by name, every one filled in. (+overrides+:
88
+ # @api private.)
89
+ def self.settings: (?Hash[String, Hash[Symbol, untyped]] overrides) -> Hash[String, resolved]
90
+
91
+ # A measure's settings: those given, and the rest as by default.
92
+ # @api private
93
+ def self.resolve: (Hash[Symbol, untyped] given) -> resolved
94
+
95
+ # Its measures as the runner learns them, with +overrides+ by name.
96
+ # @api private
97
+ def self.specs: (?Hash[String, Hash[Symbol, untyped]] overrides) -> Array[spec]
98
+
99
+ # Each measure's settings, by name, with Config#measure's changes.
100
+ def settings: () -> Hash[String, resolved]
101
+
102
+ # @api private
103
+ def settings=: (Hash[String, resolved] settings) -> Hash[String, resolved]
104
+
105
+ # Each measure's kind, by name, as #settings has it.
106
+ # @api private
107
+ def kinds: () -> Hash[String, kind]
108
+
109
+ # The measure a measurement of one is: the first.
110
+ # @api private
111
+ def first_measure: () -> String
112
+
113
+ # A setting of +measure+: by default, of the one it measures.
114
+ def setting: (:kind key, ?String measure) -> kind
115
+ | (:unit | :plural key, ?String measure) -> String?
116
+ | (:floor | :cpu_floor | :unit_ns | :tolerance | :allowance key, ?String measure) -> value
117
+ | (:limit key, ?String measure) -> Float?
118
+ | (:sizes_samples | :across key, ?String measure) -> bool
119
+
120
+ # Whether +more+ (what +measure+'s count grew by, over +n+ iterations)
121
+ # is still within its tolerance and allowance.
122
+ def within?: (value more, Integer n, ?String measure) -> bool
123
+
124
+ # Sets up what it reads, once in each worker, before it measures
125
+ # anything.
126
+ def open: () -> void
127
+
128
+ # Begins measuring a sample (its benchmark, n).
129
+ def start_sample: (Sample sample) -> void
130
+
131
+ # Ends it, and returns the measurement, a Hash of them, or nil.
132
+ def stop_sample: () -> measurement
133
+
134
+ # Pauses it, for an untimed block, and returns what it measured so far.
135
+ def pause_sample: () -> measurement
136
+
137
+ # Resumes it after an untimed block.
138
+ def resume_sample: (Sample sample) -> void
139
+
140
+ # What it would say of the sample it last stopped: text, text by
141
+ # measure name, or nil.
142
+ def details: () -> (String | Hash[String, String] | nil)
143
+
144
+ # Lets go of what #open set up, when its worker ends.
145
+ def close: () -> void
146
+
147
+ private
148
+
149
+ # The one measure it measures. One that measures several (or none) fails.
150
+ def only_measure: () -> String
151
+ end
152
+
153
+ # A metric of running totals that its block reads (see Config#metric):
154
+ # a sample measures what they grew by, less what reading them adds to a
155
+ # counted one. Config#metric makes one, and it isn't made or subclassed
156
+ # otherwise.
157
+ class Snapshot < Metric
158
+ # Pairs of readings each worker takes to learn what reading costs.
159
+ # @api private
160
+ CALIBRATION: Integer
161
+
162
+ # Totals by name, each a T (Integer, say: see Config#metric). One it
163
+ # can't read this time is nil.
164
+ type totals[T] = Hash[String, T?]
165
+
166
+ # What a read block reads: the totals (a T, for one), or nil.
167
+ type read_totals[T] = T | totals[T] | nil
168
+
169
+ # What a read block returns: the totals, or them and what else was
170
+ # read with them (+C+: whatever its details block needs).
171
+ type read[T, C] = read_totals[T] | [read_totals[T], C]
172
+
173
+ # One reading: the totals, and what else was read with them (nil if
174
+ # nothing was).
175
+ class Reading[T, C] < Struct[untyped]
176
+ # The totals, by name.
177
+ attr_reader totals: totals[T]
178
+
179
+ # @api private
180
+ attr_writer totals: totals[T]
181
+
182
+ # What the block read with them, or nil.
183
+ attr_reader context: C
184
+
185
+ # @api private
186
+ attr_writer context: C
187
+
188
+ # @api private
189
+ def self.new: [T, C] (totals[T] totals, C context) -> Reading[T, C]
190
+
191
+ # The total named +name+ (it fails for one it couldn't read).
192
+ def total: (String name) -> T
193
+ end
194
+
195
+ # A sample's stretches: [started, stopped] readings.
196
+ type stretches[T, C] = Array[[Reading[T, C], Reading[T, C]]]
197
+
198
+ # What a sample's explained by: text, text by measure name, or nil.
199
+ type explained = String | Hash[String, String] | nil
200
+
201
+ # What explains a sample, given its stretches and the sample.
202
+ # @api private
203
+ type explainer[T, C] = ^(stretches[T, C] stretches, Sample sample) -> explained
204
+
205
+ # Running totals as Config#metric returns them: to explain them, typed
206
+ # by what their block read: the totals' T, and C with them.
207
+ interface _Explained[T, C]
208
+ # Explains each sample by the block, and returns the metric.
209
+ def details: () { (stretches[T, C] stretches, Sample sample) -> explained } -> _Explained[T, C]
210
+
211
+ # Runs the block in each worker as the metric is made, and returns
212
+ # the metric.
213
+ def open: () { () -> void } -> _Explained[T, C]
214
+
215
+ # Runs the block when each worker ends, and returns the metric.
216
+ def close: () { () -> void } -> _Explained[T, C]
217
+
218
+ # Says that reading it is costly, and returns the metric.
219
+ def costly: () -> _Explained[T, C]
220
+ end
221
+
222
+ # (Inside, the context is whatever the block gave: Config#metric types it.)
223
+ self.@measures: Hash[String, Metric::settings]
224
+ self.@read: ^() -> read[Metric::value, untyped]
225
+ self.@explainer: explainer[Metric::value, untyped]?
226
+ self.@opener: (^() -> void)?
227
+ self.@closer: (^() -> void)?
228
+ self.@costly: bool
229
+
230
+ @overhead: Hash[String, Metric::value]
231
+ @sample: Sample
232
+ @stretches: stretches[Metric::value, untyped]
233
+ @started: Reading[Metric::value, untyped]
234
+
235
+ attr_reader self.measures: Hash[String, Metric::settings]
236
+
237
+ # A Snapshot for +measures+, read by +read+.
238
+ # @api private
239
+ # (Its measures as Config#metric takes them: from keywords too.)
240
+ def self.define: (Hash[untyped, untyped] measures, (^() -> read[Metric::value, untyped])? read) -> singleton(Snapshot)
241
+
242
+ # Sets what it measures and how it reads them, and returns it.
243
+ # @api private
244
+ def self.reading: (Hash[untyped, untyped] measures, ^() -> read[Metric::value, untyped] read) -> singleton(Snapshot)
245
+
246
+ # Explains its samples by the block, and returns it.
247
+ def self.details: () { (stretches[Metric::value, untyped] stretches, Sample sample) -> explained } -> singleton(Snapshot)
248
+
249
+ # Runs the block in each worker as the metric is made, and returns it.
250
+ def self.open: () { () -> void } -> singleton(Snapshot)
251
+
252
+ # Runs the block when each worker ends, and returns it.
253
+ def self.close: () { () -> void } -> singleton(Snapshot)
254
+
255
+ # Whether reading it is costly.
256
+ def self.costly?: () -> bool
257
+
258
+ # Says that reading it is costly, and returns it.
259
+ def self.costly: () -> singleton(Snapshot)
260
+
261
+ # What explains a sample, or nil.
262
+ # @api private
263
+ def self.explainer: () -> explainer[Metric::value, untyped]?
264
+
265
+ # What opens it in a worker, or nil.
266
+ # @api private
267
+ def self.opener: () -> (^() -> void)?
268
+
269
+ # What closes it, or nil.
270
+ # @api private
271
+ def self.closer: () -> (^() -> void)?
272
+
273
+ # A Reading: the totals (those it measures, or it fails), and their
274
+ # context.
275
+ # @api private
276
+ def self.read: () -> Reading[Metric::value, untyped]
277
+
278
+ # Runs its open block, if it has one, then learns what reading its
279
+ # counted totals costs.
280
+ def open: () -> void
281
+
282
+ # A sample begins: its first stretch.
283
+ def start_sample: (Sample sample) -> void
284
+
285
+ # Another stretch of the same sample, after an untimed block.
286
+ def resume_sample: (Sample sample) -> void
287
+
288
+ def stop_sample: () -> totals[Metric::value]
289
+
290
+ # What its details block says of the sample it last stopped.
291
+ def details: () -> (String | Hash[String, String] | nil)
292
+
293
+ # Runs its close block, if it has one.
294
+ def close: () -> void
295
+
296
+ private
297
+
298
+ # What +name+'s total grew by in a reading after +before+.
299
+ def grew: (totals[Metric::value] before, String name) -> Metric::value
300
+ end
301
+
302
+ # CPU time in nanoseconds: the benchmark thread's, or the process's.
303
+ # @api private
304
+ module Clock
305
+ def self.thread: () -> Integer
306
+
307
+ def self.process: () -> Integer
308
+ end
309
+
310
+ # The built-in metrics: their measures are named ("cpu",
311
+ # "instructions", "allocations"), as Config#measure takes them.
312
+ # @api private
313
+ module Metrics
314
+ # CPU time, in nanoseconds.
315
+ class CPU < Metric
316
+ @clock: :thread | :process
317
+ @started: Integer
318
+
319
+ def start_sample: (Sample sample) -> void
320
+
321
+ def stop_sample: () -> Integer
322
+ end
323
+
324
+ # Instructions retired in user space, from the CPU's own counters.
325
+ class Instructions < Metric
326
+ self.@counter: Native::Counter?
327
+
328
+ @counter: Native::Counter
329
+ @counting: bool
330
+ @started: Integer
331
+
332
+ # This process's counter (see Native::Counter.open), or nil.
333
+ def self.counter: () -> Native::Counter?
334
+
335
+ def initialize: () -> void
336
+
337
+ def start_sample: (Sample sample) -> void
338
+
339
+ def stop_sample: () -> Integer?
340
+ end
341
+
342
+ # Ruby objects allocated (CRuby).
343
+ class Allocations < Metric
344
+ @started: Integer
345
+
346
+ def start_sample: (Sample _sample) -> void
347
+
348
+ def stop_sample: () -> Integer
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,98 @@
1
+ module Duel
2
+ # What the operating system can tell about this process, through Fiddle:
3
+ # instructions retired (hardware counters) and resident memory.
4
+ # @api private
5
+ module Native
6
+ # A C function (a Fiddle::Function) returning an integer.
7
+ interface _Function
8
+ def call: (*untyped args) -> Integer
9
+ end
10
+
11
+ FIDDLE: bool
12
+ OS: String
13
+ CPU: String
14
+ UINT64: Integer
15
+
16
+ # A C function of the process's own libraries, by name, taking and
17
+ # returning Fiddle types (:INT, :LONG, :VOIDP...).
18
+ def self.function: (String name, Array[Symbol] args, Symbol returns) -> _Function
19
+
20
+ # Instructions retired in user space, from the CPU's counters.
21
+ class Counter
22
+ @scope: "thread" | "process"
23
+
24
+ # The best counter here, or nil.
25
+ def self.open: (?fiddle: bool) -> Counter?
26
+
27
+ # Whose instructions it counts.
28
+ attr_reader scope: "thread" | "process"
29
+
30
+ def initialize: ("thread" | "process" scope) -> void
31
+
32
+ def read: () -> Integer
33
+ end
34
+
35
+ # Linux: perf_event_open(2), counting this thread's instructions in
36
+ # user space.
37
+ class Linux < Counter
38
+ SYSCALL: Hash[String, Integer]
39
+ PERF_TYPE_HARDWARE: Integer
40
+ PERF_COUNT_HW_INSTRUCTIONS: Integer
41
+ FLAGS: Integer
42
+ PERF_FLAG_FD_CLOEXEC: Integer
43
+ ATTR_SIZE: Integer
44
+
45
+ @io: IO
46
+
47
+ def self.open: (?platform: String, ?cpu: String, ?syscall: _Function?) -> Linux?
48
+
49
+ def initialize: (IO io) -> void
50
+
51
+ def read: () -> Integer
52
+ end
53
+
54
+ # macOS: proc_pid_rusage(RUSAGE_INFO_V4), the process's instructions
55
+ # and resident memory.
56
+ class MacOS < Counter
57
+ RUSAGE_INFO_V4: Integer
58
+ SIZE: Integer
59
+ RESIDENT: Integer
60
+ INSTRUCTIONS: Integer
61
+
62
+ @rusage: _Function
63
+ # The struct proc_pid_rusage fills in, in place.
64
+ @buffer: String
65
+
66
+ def self.open: (?platform: String, ?rusage: _Function?) -> MacOS?
67
+
68
+ def initialize: (_Function rusage) -> void
69
+
70
+ def read: () -> Integer
71
+
72
+ def resident: () -> Integer
73
+
74
+ private
75
+
76
+ def field: (Integer offset) -> Integer
77
+ end
78
+
79
+ # A child process of this one, waited for.
80
+ module Child
81
+ RUSAGE_SIZE: Integer
82
+ MAXRSS: Integer
83
+ EINTR: Integer
84
+
85
+ # Waits for the child +pid+: its exit status (or nil), the signal
86
+ # that killed it (or nil), and its peak memory in bytes (or nil).
87
+ def self.wait: (Integer pid, ?fiddle: bool, ?platform: String, ?wait4: _Function?) -> [Integer?, Integer?, Integer?]
88
+ end
89
+
90
+ # This process's resident memory in bytes, or nil where it can't tell.
91
+ module Memory
92
+ STATM: String
93
+ MACOS: MacOS?
94
+
95
+ def self.resident: (?statm: String, ?macos: MacOS?) -> Integer?
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,54 @@
1
+ module Duel
2
+ # The runs of a process benchmark for one worker: each a fresh process,
3
+ # run by a Ruby process of its own, kept for the worker's life.
4
+ # @api private
5
+ class ProcessSample
6
+ # A process's sample, as a worker's has it.
7
+ type sampled = { "timed" => Hash[String, Integer], "counted" => Hash[String, Integer], "heap" => Integer, "peak" => Integer? }
8
+
9
+ # What the process of its own reports of a run.
10
+ type ended = { "cpu" => Integer, "peak" => Integer?, "failed" => String? }
11
+
12
+ # The lines of its output a failure shows.
13
+ LOG_LINES: Integer
14
+
15
+ @label: String
16
+ @env: Hash[String, String?]
17
+ @chdir: String
18
+ @log: String
19
+ @timeout: Float
20
+ @pid: Integer?
21
+ @requests: IO
22
+ @replies: IO
23
+
24
+ def initialize: (String label, env: Hash[String, String?], chdir: String, log: String, timeout: Float) -> void
25
+
26
+ # The fastest of +samples+: the least CPU time and peak memory.
27
+ def self.fastest: (Array[sampled] samples) -> sampled
28
+
29
+ # Runs +command+ once, and returns its sample.
30
+ def run: (Array[String] command) -> sampled
31
+
32
+ # Ends the Ruby process of its own, if it started.
33
+ def stop: () -> void
34
+
35
+ # Runs a process for each line of +input+, and reports each on
36
+ # +output+.
37
+ def self.main: (String spec, ?input: IO, ?output: IO) -> void
38
+
39
+ # Runs +command+ once, and returns what it took.
40
+ def self.once: (Array[String] command, Hash[String, String?] env, String chdir, String log) -> ended
41
+
42
+ # The CPU time of the children waited for so far, in nanoseconds.
43
+ def self.cpu: () -> Integer
44
+
45
+ # How a process that failed ended.
46
+ def self.why: (Integer? code, Integer? signal) -> String
47
+
48
+ private
49
+
50
+ def start: () -> void
51
+
52
+ def kill: () -> void
53
+ end
54
+ end
@@ -0,0 +1,64 @@
1
+ module Duel
2
+ # Where a regression's time went: StackProf profiles of each version.
3
+ # @api private
4
+ class Profile
5
+ # A frame: its name, and its samples of its own and in total.
6
+ type frame = [String, Integer, Integer]
7
+
8
+ # A version's profile, as its worker reports it.
9
+ type profile = { "samples" => Integer, "frames" => Array[frame] }
10
+
11
+ # [frame, own, total]: what a frame gained, as a share of the
12
+ # baseline's samples.
13
+ type growth = [String, Float, Float]
14
+
15
+ # Its report: what grew, what garbage collection gained, and each
16
+ # version's profile (its top frames).
17
+ type report = { "grew" => Array[growth], "gc" => Float, "baseline" => profile, "candidate" => profile }
18
+
19
+ UNAVAILABLE: String
20
+ TOP: Integer
21
+ KEPT: Integer
22
+
23
+ # StackProf's frames for garbage collection.
24
+ GC: String
25
+ GC_FRAMES: Array[String]
26
+
27
+ @baseline: profile
28
+ @candidate: profile
29
+
30
+ # The frames that mark a benchmark's measured work: its iterations,
31
+ # what pauses them, and an iteration inside its around_iteration hooks.
32
+ ITERATE: String
33
+ PAUSE: String
34
+ ITERATION: String
35
+
36
+ # A version's profile, from StackProf's raw results: the benchmark's
37
+ # work, and garbage collection.
38
+ def self.of: (Hash[Symbol, untyped] results) -> profile
39
+
40
+ # Whether a sample, by its stack's frames' names, is kept.
41
+ def self.kept?: (Array[String] stack) -> bool
42
+
43
+ def initialize: (Hash[String, profile] profiles) -> void
44
+
45
+ # The TOP frames that gained the most samples of their own.
46
+ def grew: () -> Array[growth]
47
+
48
+ # What garbage collection gained, as a share of the baseline's samples.
49
+ def gc: () -> Float
50
+
51
+ def to_h: () -> report
52
+
53
+ private
54
+
55
+ def samples: () -> Float
56
+
57
+ # A profile's samples in garbage collection.
58
+ def collecting: (profile profile) -> Integer
59
+
60
+ def counts: (profile profile) -> Hash[String, [Integer, Integer]]
61
+
62
+ def trimmed: (profile profile) -> profile
63
+ end
64
+ end
@@ -0,0 +1,9 @@
1
+ module Duel
2
+ # A rake task running `duel` with +args+:
3
+ #
4
+ # Duel::RakeTask.new(:perf)
5
+ # Duel::RakeTask.new(:perf_since_release, "--base", "last-release")
6
+ class RakeTask
7
+ def initialize: (?Symbol | String name, *String args) -> void
8
+ end
9
+ end
@@ -0,0 +1,106 @@
1
+ module Duel
2
+ # What a prepare block (see Config#prepare) gets: a worker's, or a
3
+ # version's, own directory, and help running commands and starting a
4
+ # server there, and stopping it.
5
+ class Resource
6
+ # Seconds between looks while waiting.
7
+ # @api private
8
+ POLL: Float
9
+
10
+ # Seconds a process gets to end after its stop signal before it's
11
+ # killed.
12
+ # @api private
13
+ STOP_WAIT: Integer
14
+
15
+ # The lines of its log a failure shows.
16
+ # @api private
17
+ LOG_LINES: Integer
18
+
19
+ # What a cache key may be.
20
+ # @api private
21
+ KEY: Regexp
22
+
23
+ # The lock on building a cache.
24
+ # @api private
25
+ CACHE: Thread::Mutex
26
+
27
+ @name: String
28
+ @dir: String
29
+ @root: String
30
+ @ruby: String
31
+ @env: Hash[String, String?]
32
+ @timeout: Float
33
+ @cache: String
34
+ # Each process's program, log and stop signal, by pid.
35
+ @processes: Hash[Integer, [String, String, String]]
36
+ @stops: Array[^() -> void]
37
+
38
+ # +env+ with +vars+ too: two prepare blocks that set a variable differently
39
+ # fail.
40
+ # @api private
41
+ def self.environment: (String name, Hash[String, String?] env, Hash[String, String?] vars) -> Hash[String, String?]
42
+
43
+ # The worker it's for ("baseline-1", say).
44
+ attr_reader name: String
45
+
46
+ # The directory it has of its own.
47
+ attr_reader dir: String
48
+
49
+ # The tree of the version its worker runs.
50
+ attr_reader root: String
51
+
52
+ # The Ruby its version runs on.
53
+ attr_reader ruby: String
54
+
55
+ # What its commands get in their environment, under their own.
56
+ attr_reader env: Hash[String, String?]
57
+
58
+ # @api private
59
+ def initialize: (String dir, String name, String root, timeout: Float, cache: String, ?ruby: String,
60
+ ?env: Hash[String, String?]) -> void
61
+
62
+ # A path in its directory.
63
+ def path: (String file) -> String
64
+
65
+ # A directory built once, by the block, for every worker of this run
66
+ # and later ones. The first that asks for +key+ builds it.
67
+ def cached: (String key) { (String dir) -> void } -> String
68
+
69
+ # Starts a process (as Process.spawn takes it), its output added to a
70
+ # log in the directory (+log+.log: by default, its program's name), and
71
+ # stopped with the resource by +stop_signal+. Returns its pid.
72
+ def spawn: (*untyped command, ?log: String?, ?stop_signal: String, **untyped options) -> Integer
73
+
74
+ # Runs a command to its end, logged as #spawn's is. Fails, showing its
75
+ # log, unless it succeeds.
76
+ def run: (*untyped command, ?log: String?, **untyped options) -> void
77
+
78
+ # Waits until the block is true: fails if a process spawned dies first,
79
+ # or the run's timeout passes.
80
+ def wait_until: () { () -> boolish } -> void
81
+
82
+ # Runs the block when the resource stops, before its processes do.
83
+ def at_stop: () { () -> void } -> void
84
+
85
+ # Fails, saying it's this resource that did, with the end of +log+.
86
+ def failed: (String message, ?log: String?) -> bot
87
+
88
+ # Stops it.
89
+ # @api private
90
+ def stop: () -> void
91
+
92
+ private
93
+
94
+ # Builds +dir+ in a directory of its own, then renames it into place.
95
+ def build: (String dir) { (String building) -> void } -> void
96
+
97
+ # The log a command's output goes to.
98
+ def log_path: (Array[untyped] command, String? name) -> String
99
+
100
+ def start: (Array[untyped] command, String log, Hash[Symbol, untyped] options) -> Integer
101
+
102
+ def end_process: (Integer pid, String signal) -> void
103
+
104
+ def monotonic: () -> Float
105
+ end
106
+ end