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/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
|
data/sig/duel/metric.rbs
ADDED
|
@@ -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
|
data/sig/duel/native.rbs
ADDED
|
@@ -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,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
|