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,91 @@
1
+ module Duel
2
+ # What a plugin's tests need: its prepare blocks run as a worker's would
3
+ # be, its metrics measuring a block, and whole comparisons of two small
4
+ # projects.
5
+ module Testing
6
+ extend Testing
7
+
8
+ # What Testing.compare found.
9
+ class Comparison < Data
10
+ # A Result for each benchmark.
11
+ attr_reader results: Array[Result]
12
+
13
+ # What the run printed.
14
+ attr_reader output: String
15
+
16
+ # As `duel` would have it: no benchmark worse, nothing across the board
17
+ # (see #passed?).
18
+ # @api private
19
+ attr_reader passed: bool
20
+
21
+ # @api private
22
+ def self.new: (Array[Result] results, String output, bool passed) -> Comparison
23
+
24
+ # Whether it passed, as `duel` would have it.
25
+ def passed?: () -> bool
26
+
27
+ # The benchmark's Result.
28
+ def []: (String name) -> Result?
29
+ end
30
+
31
+ # What #measure measured, by name, and what metrics said of it.
32
+ class Measurement < Hash[String, Metric::value]
33
+ # What metrics said of it, by measure.
34
+ attr_reader details: Hash[String, String]
35
+
36
+ # @api private
37
+ attr_writer details: Hash[String, String]
38
+ end
39
+
40
+ # Where prepare blocks' builds are kept, for every test of this process.
41
+ # @api private
42
+ CACHE: String
43
+
44
+ # Each config's metrics.
45
+ # @api private
46
+ MADE: Hash[Config, [Hash[String, Hash[Symbol, untyped]], Array[Metric]]]
47
+
48
+ # Runs +config+'s prepare blocks as they'd prepare a worker, sets the
49
+ # environment they give while the block runs, yields it, and stops
50
+ # what they started after.
51
+ def prepare: [T] (Config config, ?name: String, ?root: String, ?cache: String) { (Hash[String, String?] env) -> T } -> T
52
+
53
+ # What +config+'s metrics measure of the block, as one counted sample
54
+ # of +n+ iterations of a benchmark would, with its hooks (each
55
+ # iteration inside around_iteration hooks, if it has them). The
56
+ # block gets the benchmark's Context.
57
+ def measure: (Config config, ?n: Integer, ?only: (:count | :timing)?, **untyped hooks) { (Context context) -> untyped } -> Measurement
58
+
59
+ # Runs the block with +env+ set, and the environment as it was after.
60
+ # @api private
61
+ def with_env: [T] (Hash[String, String?] env) { () -> T } -> T
62
+
63
+ # Each config's worker's Stops.
64
+ # @api private
65
+ STOPS: Hash[Config, Stops]
66
+
67
+ # Runs the block as a worker of +config+ would, then ends it.
68
+ def in_worker: [T] (Config config) { () -> T } -> T
69
+
70
+ # +config+'s metrics, made once (its in_worker blocks run first).
71
+ def metrics: (Config config) -> Array[Metric]
72
+
73
+ # Compares the project at +base+ with the one at +candidate+, in
74
+ # worker processes, as `duel` does, with +settings+ (as `duel`'s
75
+ # options name them: processes:, timeout:, filter:, limit:...).
76
+ def compare: (String base, String candidate, **untyped settings) -> Comparison
77
+
78
+ # A stand-in Sample of +n+ iterations of a benchmark, measuring
79
+ # nothing.
80
+ def sample: (?n: Integer, ?name: String, ?only: only?, ?peak_memory: bool) -> Sample
81
+
82
+ # A stand-in reading of running totals, and what was read with them.
83
+ def reading: [T, C] (?Snapshot::totals[T] totals, ?C? context) -> Snapshot::Reading[T, C?]
84
+
85
+ # What running totals' details block says of +stretches+ of +sample+.
86
+ def details: (singleton(Snapshot) metric, Snapshot::stretches[Metric::value, untyped] stretches, ?Sample sample) -> Snapshot::explained
87
+
88
+ # Writes a project into +dir+: +files+, by path, with their contents.
89
+ def project: (String dir, Hash[String, String] files) -> String
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module Duel
2
+ VERSION: String
3
+ end
@@ -0,0 +1,74 @@
1
+ module Duel
2
+ # A worker process: loads one version of the code and the candidate's
3
+ # benchmarks, and answers the runner's requests, one JSON object a line.
4
+ # @api private
5
+ class Worker
6
+ type hello = { "benchmarks" => Array[Definition::summary], "metrics" => Array[Metric::spec], "ruby" => String,
7
+ "memory" => bool, "profiler" => bool, "yjit" => bool, "extensions" => Hash[String, String],
8
+ "gem_dirs" => Array[String] }
9
+
10
+ type ok = { "ok" => true }
11
+
12
+ type sampled = { "timed" => Hash[String, Metric::value], "counted" => Hash[String, Metric::value], "heap" => Integer,
13
+ ?"details" => Hash[String, String] }
14
+
15
+ type memory = { "peak" => Integer }
16
+
17
+ type error = { "error" => String, "backtrace" => Array[String] }
18
+
19
+ type reply = ok | sampled | memory | Profile::profile | error
20
+
21
+ BUILT_IN: Array[singleton(Metric)]
22
+
23
+ @metrics: Array[Metric]
24
+ @specs: Array[Metric::spec]
25
+ @benchmarks: Hash[String, Definition]
26
+ @hooks: Hash[Symbol, Array[hook]]
27
+ @harnesses: Hash[String, Harness]
28
+
29
+ def self.main: (String version_root, String candidate_root, String control, ?String? config_path) -> void
30
+
31
+ # The native extensions loaded, by feature, at their real paths.
32
+ def self.extensions: (?Array[String] features, ?Array[String] load_path) -> Hash[String, String]
33
+
34
+ # The directories gems are installed in, as real paths ending in "/".
35
+ def self.gem_dirs: (?Array[String] dirs) -> Array[String]
36
+
37
+ # Whether StackProf is installed, for profiles: on CRuby only.
38
+ def self.profiler?: (?String engine) -> bool
39
+
40
+ # Whether YJIT is on, by Ruby.
41
+ YJIT: ^() -> bool
42
+
43
+ # Whether YJIT is on, for a longer warm-up.
44
+ def self.yjit?: () -> bool
45
+
46
+ def initialize: (?Array[singleton(Metric)] plugin_metrics, ?Hash[String, Hash[Symbol, untyped]] measures,
47
+ ?hooks: Hash[Symbol, Array[hook]]) -> void
48
+
49
+ # Closes its metrics, last first, as it ends.
50
+ def close: () -> void
51
+
52
+ # +metrics+ made, and their measures' specs, with +measures+' changes.
53
+ def self.made: (Array[singleton(Metric)] metrics, Hash[String, Hash[Symbol, untyped]] measures) -> [Array[Metric], Array[Metric::spec]]
54
+
55
+ # What the runner learns first.
56
+ def hello: () -> hello
57
+
58
+ # Answers a request (a parsed JSON object). Errors are answered, not
59
+ # raised.
60
+ def handle: (Hash[String, untyped] request) -> reply
61
+
62
+ private
63
+
64
+ def setup: (String name, bool trial) -> ok
65
+
66
+ def sample: (String name, Integer n, bool count, Integer runs) -> sampled
67
+
68
+ def memory: (String name, Integer n) -> memory
69
+
70
+ def profile: (String name, Integer n, Integer runs) -> Profile::profile
71
+
72
+ def teardown: (String name) -> ok
73
+ end
74
+ end
data/sig/duel.rbs ADDED
@@ -0,0 +1,276 @@
1
+ # Performance regression guards: every benchmark runs on two versions of
2
+ # a codebase side by side, the baseline and the candidate, and the
3
+ # candidate fails if it's slower at any of them.
4
+ module Duel
5
+ # What Duel raises when something is used wrong, or fails (see Failed).
6
+ class Error < StandardError
7
+ end
8
+
9
+ # What a benchmark is judged on, if not both its timings and its
10
+ # counts (see DSL#duel).
11
+ type only = :count | :timing
12
+
13
+ # A benchmark's body, run in its Context: given the iterations to do
14
+ # when it takes a parameter, else run once an iteration.
15
+ # @api private
16
+ type body = ^() [self: Context] -> void | ^(Integer n) [self: Context] -> void
17
+
18
+ # A hook (see HOOKS): a block, or a lambda, given the benchmark (setup,
19
+ # teardown and before_sample, if it takes it), or the Iteration to run
20
+ # (around_iteration), or nothing (after_iterations). A file's, or a
21
+ # benchmark's own, runs in each benchmark's Context.
22
+ type hook = ^(?untyped) [self: Context] -> untyped
23
+
24
+ # A benchmark's own hooks, by name (see DSL#duel).
25
+ # @api private
26
+ type hooks = Hash[Symbol, hook]
27
+
28
+ # A benchmark (see DSL#duel), defined in +file+, whose hooks (its
29
+ # file's, and its own) it gets. Or one of a whole process (see
30
+ # DSL#duel_process). What a metric sees of it (Sample#benchmark): its
31
+ # name, only and peak_memory.
32
+ class Definition < Struct[String | only | bool | body | hooks | Definition::process | nil]
33
+ # A process benchmark's process: its command, or Ruby code.
34
+ # @api private
35
+ type process = { "command" => Array[String]?, "ruby" => String?, "runs" => Integer? }
36
+
37
+ # What the runner learns of it (see #to_h).
38
+ # @api private
39
+ type summary = { "name" => String, "only" => String?, "peak_memory" => bool, "process" => process? }
40
+
41
+ # Its name, as DSL#duel was given it.
42
+ attr_reader name: String
43
+
44
+ # @api private
45
+ attr_writer name: String
46
+
47
+ # What it's judged on (DSL#duel's only:): nil for both its timings
48
+ # and its counts.
49
+ attr_reader only: only?
50
+
51
+ # @api private
52
+ attr_writer only: only?
53
+
54
+ # Whether its peak memory is judged too.
55
+ attr_reader peak_memory: bool
56
+
57
+ # @api private
58
+ attr_writer peak_memory: bool
59
+
60
+ # @api private
61
+ attr_accessor file: String
62
+
63
+ # (None, for a process benchmark, which is never run in a worker.)
64
+ # @api private
65
+ attr_accessor body: body
66
+
67
+ # Its own hooks, by name (see DSL#duel).
68
+ # @api private
69
+ attr_accessor hooks: hooks
70
+
71
+ # Its process, for a process benchmark.
72
+ # @api private
73
+ attr_accessor process: process?
74
+
75
+ # @api private
76
+ def self.new: (String name, only? only, bool peak_memory, String file, body? body, ?hooks? hooks, ?process? process) -> instance
77
+
78
+ # Whose CPU time it takes: the benchmark thread's, or the process's.
79
+ # @api private
80
+ def cpu: () -> (:thread | :process)
81
+
82
+ # What the runner learns of it.
83
+ # @api private
84
+ def to_h: () -> summary
85
+ end
86
+
87
+ # The iterations of a benchmark judged on its counts alone.
88
+ # @api private
89
+ COUNTS_ONLY: Integer
90
+
91
+ # What a benchmark may be judged on: nil for both.
92
+ # @api private
93
+ ONLY: Array[only?]
94
+
95
+ # The hooks, by name, in the order a benchmark's life runs them.
96
+ HOOKS: Array[Symbol]
97
+
98
+ # No hooks, by name.
99
+ # @api private
100
+ NO_HOOKS: Hash[Symbol, Array[hook]]
101
+
102
+ # What a worker's blocks (Config#in_worker) are given: blocks to run
103
+ # when the worker ends.
104
+ class Stops
105
+ @blocks: Array[^() -> void]
106
+
107
+ # @api private
108
+ def initialize: () -> void
109
+
110
+ # Runs the block when the worker ends.
111
+ def at_stop: () { () -> void } -> void
112
+
113
+ # Runs them, last first.
114
+ # @api private
115
+ def run: () -> void
116
+ end
117
+
118
+ # What Config#in_worker returns: what its block returned, in the
119
+ # worker.
120
+ class InWorker[T]
121
+ # The value, boxed (so a nil or false one is made too), or nil outside
122
+ # a worker.
123
+ @made: [T]?
124
+
125
+ # The worker's. Outside a worker, it fails.
126
+ def value: () -> T
127
+
128
+ # @api private
129
+ def made: (T value) -> void
130
+
131
+ # @api private
132
+ def forget: () -> void
133
+ end
134
+
135
+ self.@config: Config?
136
+ self.@benchmarks: Hash[String, Definition]?
137
+ self.@file_hooks: Hash[String, Hash[Symbol, Array[hook]]]?
138
+ self.@plugins: Hash[Symbol, Module & _Plugin]?
139
+
140
+ # A plugin (see Duel.plugin): what configures Duel for it, given the
141
+ # configuration and the options it's used with.
142
+ interface _Plugin
143
+ def configure: (Config config, **untyped options) -> untyped
144
+ end
145
+
146
+ # The configuration: the project's .duel.rb, or the conventions.
147
+ def self.config: () -> Config
148
+
149
+ # @api private
150
+ def self.config=: (Config config) -> Config
151
+
152
+ # Yields the configuration, as .duel.rb does.
153
+ def self.configure: [T] () { (Config config) -> T } -> T
154
+
155
+ self.@measuring: bool?
156
+
157
+ # Whether a benchmark is being measured right now: in a worker, inside a
158
+ # sample, but not in its untimed blocks (or in setup).
159
+ def self.measuring?: () -> bool
160
+
161
+ # Fails while a benchmark is being measured: for a helper, +name+d,
162
+ # meant for setting up.
163
+ def self.unmeasured!: (String name) -> void
164
+
165
+ # Runs the block with measuring? as +measuring+.
166
+ # @api private
167
+ def self.measured: [T] (bool measuring) { () -> T } -> T
168
+
169
+ # Loads a config file (.duel.rb): a fresh configuration, rooted where
170
+ # the file is.
171
+ # @api private
172
+ def self.load_config: (String path) -> Config
173
+
174
+ # Where +method+ is defined ("file:line"), or whose it is.
175
+ # @api private
176
+ def self.where: (Method | UnboundMethod method) -> String
177
+
178
+ # Every benchmark defined, by name.
179
+ # @api private
180
+ def self.benchmarks: () -> Hash[String, Definition]
181
+
182
+ # Each file's hooks, by its path, then by name.
183
+ # @api private
184
+ def self.file_hooks: () -> Hash[String, Hash[Symbol, Array[hook]]]
185
+
186
+ # Adds +block+ as a hook of the file of benchmarks running, or of
187
+ # +file+, the one that gave it.
188
+ # @api private
189
+ def self.file_hook: (Symbol name, Proc? block, String file) -> void
190
+
191
+ # +block+, a hook +name+d, or a failure if it isn't one.
192
+ # @api private
193
+ def self.hook: (Symbol name, untyped block) -> untyped
194
+
195
+ # Registers a plugin, +name+d as its file is required (see
196
+ # Config#use): a module whose configure Config#use calls, and whose
197
+ # DSL module, if it has one, has its DSL methods. Returns its name.
198
+ def self.plugin: (Symbol name, Module & _Plugin plugin) -> Symbol
199
+
200
+ # Every plugin registered, by name.
201
+ # @api private
202
+ def self.plugins: () -> Hash[Symbol, Module & _Plugin]
203
+
204
+ self.@defining_file: String?
205
+
206
+ # The file of benchmarks that called the DSL method running, if one is.
207
+ # @api private
208
+ def self.defining_file: () -> String?
209
+
210
+ # Runs the block with +file+ as the defining file, unless one is already.
211
+ # @api private
212
+ def self.defining: [T] (String? file) { () -> T } -> T
213
+
214
+ # What files of benchmarks call (the worker gives it to its main
215
+ # object).
216
+ module DSL
217
+ # Adds the public methods of a plugin's DSL module to the DSL (a name
218
+ # it has, or every object has, fails), as .duel.rb uses the plugin.
219
+ # @api private
220
+ def self.add: (Module methods) -> Array[Symbol]
221
+
222
+ # Adds one of a plugin's DSL module's methods to the DSL, unless it's
223
+ # there already.
224
+ # @api private
225
+ def self.define: (Symbol name, Module methods) -> void
226
+
227
+ # The plugin's DSL module each method DSL.add added is from.
228
+ # @api private
229
+ DEFINED: Hash[Symbol, Module]
230
+
231
+ # Defines a benchmark: its block is timed, run as many times as a
232
+ # sample needs or, with a parameter, once a sample, given how many
233
+ # iterations to do. Its own hooks (see HOOKS) run after its file's:
234
+ # an around_iteration one runs around each iteration, given it to
235
+ # run, and only the iteration is measured.
236
+ def duel: (String name, ?only: only?, ?peak_memory: bool, **hook hooks) { (?Integer n) [self: Context] -> void } -> void
237
+
238
+ # A benchmark's own hooks, checked.
239
+ # @api private
240
+ def self.hooks: (String name, Hash[Symbol, untyped] hooks, ^(?) [self: Context] -> untyped body) -> hooks
241
+
242
+ # Defines a benchmark of a whole process: a command (as Process.spawn
243
+ # takes it), or Ruby code, run fresh for each version in every sample
244
+ # (the fastest of +runs+).
245
+ def duel_process: (String name, *String command, ?ruby: String?, ?runs: Integer?, **untyped options) -> void
246
+
247
+ # En garde.
248
+ # @api private
249
+ alias `⚔️` duel
250
+
251
+ # @api private
252
+ alias ⚔ duel
253
+
254
+ # The file's hooks (see HOOKS), for every benchmark in it, run in its
255
+ # context: setup once in each worker before a benchmark's samples,
256
+ # teardown after them, and before_sample before every sample, not
257
+ # measured, each given the benchmark if it takes it.
258
+ def setup: () { (?Definition benchmark) [self: Context] -> void } -> void
259
+
260
+ def teardown: () { (?Definition benchmark) [self: Context] -> void } -> void
261
+
262
+ def before_sample: () { (?Definition benchmark) [self: Context] -> void } -> void
263
+
264
+ # Runs the block around each iteration of every benchmark in the
265
+ # file, given the iteration to run: only the iteration is measured.
266
+ def around_iteration: () { (Iteration iteration) [self: Context] -> void } -> void
267
+
268
+ # Runs the block after each sample's iterations of every benchmark in
269
+ # the file, measured with them.
270
+ def after_iterations: () { () [self: Context] -> void } -> void
271
+
272
+ # The file that called the DSL method calling this.
273
+ # @api private
274
+ def self.caller_file: () -> String
275
+ end
276
+ end
data/sig/manifest.yaml ADDED
@@ -0,0 +1,4 @@
1
+ # The standard libraries Duel's signatures use, for rbs collection.
2
+ dependencies:
3
+ - name: optparse
4
+ - name: socket
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: duel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Erik Berlin
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fiddle
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.1'
26
+ description: Duel runs your benchmarks on two versions of your code side by side,
27
+ the baseline (where your branch left main, say) and the candidate, and fails if
28
+ the candidate is slower at any of them. Timings are judged by a distribution-free
29
+ confidence interval for paired CPU-time ratios and confirmed in fresh processes,
30
+ so it fails on real regressions, not noise.
31
+ email:
32
+ - sferik@gmail.com
33
+ executables:
34
+ - duel
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - AGENT_PROMPT.md
39
+ - CHANGELOG.md
40
+ - LICENSE
41
+ - README.md
42
+ - docs/README.md
43
+ - docs/benchmarks.md
44
+ - docs/ci.md
45
+ - docs/configuration.md
46
+ - docs/how-it-works.md
47
+ - docs/json.md
48
+ - docs/plugins.md
49
+ - docs/troubleshooting.md
50
+ - exe/duel
51
+ - lib/duel.rb
52
+ - lib/duel/across.rb
53
+ - lib/duel/baseline.rb
54
+ - lib/duel/cli.rb
55
+ - lib/duel/config.rb
56
+ - lib/duel/context.rb
57
+ - lib/duel/init.rb
58
+ - lib/duel/json_report.rb
59
+ - lib/duel/lock.rb
60
+ - lib/duel/metric.rb
61
+ - lib/duel/native.rb
62
+ - lib/duel/process_sample.rb
63
+ - lib/duel/profile.rb
64
+ - lib/duel/rake_task.rb
65
+ - lib/duel/resource.rb
66
+ - lib/duel/rubies.rb
67
+ - lib/duel/runner.rb
68
+ - lib/duel/stats.rb
69
+ - lib/duel/summary.rb
70
+ - lib/duel/templates/benchmark_duel.rb.erb
71
+ - lib/duel/templates/duel.rb.erb
72
+ - lib/duel/templates/perf.yml.erb
73
+ - lib/duel/testing.rb
74
+ - lib/duel/version.rb
75
+ - lib/duel/worker.rb
76
+ - sig/duel.rbs
77
+ - sig/duel/across.rbs
78
+ - sig/duel/baseline.rbs
79
+ - sig/duel/cli.rbs
80
+ - sig/duel/config.rbs
81
+ - sig/duel/context.rbs
82
+ - sig/duel/init.rbs
83
+ - sig/duel/json_report.rbs
84
+ - sig/duel/lock.rbs
85
+ - sig/duel/metric.rbs
86
+ - sig/duel/native.rbs
87
+ - sig/duel/process_sample.rbs
88
+ - sig/duel/profile.rbs
89
+ - sig/duel/rake_task.rbs
90
+ - sig/duel/resource.rbs
91
+ - sig/duel/rubies.rbs
92
+ - sig/duel/runner.rbs
93
+ - sig/duel/stats.rbs
94
+ - sig/duel/summary.rbs
95
+ - sig/duel/testing.rbs
96
+ - sig/duel/version.rbs
97
+ - sig/duel/worker.rbs
98
+ - sig/manifest.yaml
99
+ homepage: https://github.com/sferik/duel-ruby
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ source_code_uri: https://github.com/sferik/duel-ruby
104
+ changelog_uri: https://github.com/sferik/duel-ruby/blob/main/CHANGELOG.md
105
+ bug_tracker_uri: https://github.com/sferik/duel-ruby/issues
106
+ rubygems_mfa_required: 'true'
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '3.4'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 4.0.23
122
+ specification_version: 4
123
+ summary: 'Performance regression guards: fail when a change makes your code slower'
124
+ test_files: []