duel-gvl 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 262cd836d526942fc68b7994b37318ba4b4677b7a8b8b6cf562507171bdb6605
4
+ data.tar.gz: 15fbe625b54e163cf6778191fc8c7e7a177be3bbfc8017ad86cd1496adc71907
5
+ SHA512:
6
+ metadata.gz: d4198de77958ccc952464529998760db5ccb0e20622cd50e65ad951ef0416d1a166ded66f157d2b3f28b113bbe84cba5933a8428e38b5fccfbd9a4f78d911b30
7
+ data.tar.gz: 3777cd966a104dcba2b23a8f288fd38d02ce16c99d06e3f4c4462060ddbe02b7a74f541482da4894033022d06dfd4bdd148c7ff532dc67a845b6375911792c85
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-24
11
+
12
+ The first release. duel-gvl is a Duel plugin, open source under the MIT
13
+ license. It needs Duel 0.1 and CRuby 3.4 or later, where it builds a
14
+ small C extension (elsewhere it installs without it, and measures
15
+ nothing).
16
+
17
+ ### Added
18
+
19
+ - `c.use :gvl`: the `gvl_wait` metric, how long threads waited
20
+ for the GVL in each sample of a benchmark with threads of its own, from
21
+ Ruby's thread event hooks, in a C extension (CRuby), loaded in Duel's
22
+ workers. It's judged on samples that wait a tenth of their CPU time or
23
+ more on either version (`cpu_floor: 0.1`), against a limit of 1.5, and
24
+ not across the board (`across: false`). It's in nanoseconds
25
+ (`unit: "ns"`), so reports show it as time (`51.16µs → 817.9µs an
26
+ iteration`).
27
+
28
+ [Unreleased]: https://github.com/sferik/duel-ruby/compare/duel-gvl-v0.1.0...HEAD
29
+ [0.1.0]: https://github.com/sferik/duel-ruby/releases/tag/duel-gvl-v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erik Berlin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # duel-gvl
2
+
3
+ [![Duel](https://raw.githubusercontent.com/sferik/duel-ruby/main/assets/badge.svg)](https://github.com/sferik/duel-ruby)
4
+ [![duel-gvl](https://github.com/sferik/duel-ruby/actions/workflows/plugin-gvl.yml/badge.svg)](https://github.com/sferik/duel-ruby/actions/workflows/plugin-gvl.yml)
5
+ [![Gem Version](https://badge.fury.io/rb/duel-gvl.svg)](https://rubygems.org/gems/duel-gvl)
6
+
7
+ A [Duel](https://github.com/sferik/duel-ruby) plugin for code that runs
8
+ threads: every benchmark with threads of its own is judged on how long
9
+ they waited for the GVL too.
10
+
11
+ | Metric | What it is | A regression |
12
+ |---|---|---|
13
+ | `gvl_wait` | the time all threads spent waiting for the GVL, in ns | > 50% more, 99% confident (where it's a tenth of the sample's CPU time or more, on either version) |
14
+
15
+ A thread waiting for the GVL uses no CPU, so contention a change adds
16
+ doesn't show in CPU time: holding the GVL longer around I/O, a C call
17
+ that stops releasing it, more threads fighting over it. It shows here,
18
+ for two threads whose C call stopped releasing the GVL:
19
+
20
+ ```
21
+ ! wait 18.4s n=150 gvl_wait 34.093 [25.666, 41.575] slower cpu 1.023 [0.959, 1.238] same
22
+ ```
23
+
24
+ And under the regression, how long they waited an iteration on each
25
+ version (here, in another run):
26
+
27
+ ```
28
+ REGRESSION wait: worse gvl_wait in every measurement
29
+ gvl_wait: 51.16µs → 817.9µs an iteration (16.47x)
30
+ ```
31
+
32
+ ## Installation
33
+
34
+ ```ruby
35
+ # Gemfile
36
+ group :development do
37
+ gem "duel-gvl"
38
+ end
39
+ ```
40
+
41
+ > [!NOTE]
42
+ > It needs CRuby (3.4 or later), and builds a small C extension when it's
43
+ > installed. Elsewhere (JRuby and TruffleRuby, which have no GVL, Windows,
44
+ > or no C compiler) it installs without it, and there's no `gvl_wait`.
45
+
46
+ ## Usage
47
+
48
+ ```ruby
49
+ # .duel.rb
50
+ Duel.configure { |c| c.use :gvl }
51
+ ```
52
+
53
+ It measures only benchmarks marked `only: :timing`, as benchmarks with
54
+ threads of their own should be (their counts vary from run to run). The
55
+ rest have one thread and nothing to wait for:
56
+
57
+ ```ruby
58
+ # duel/pool_duel.rb
59
+ duel "pool: 4 threads", only: :timing do |n|
60
+ jobs = Thread::Queue.new(Array.new(n) { |i| i }).tap(&:close)
61
+ Array.new(4) { Thread.new { while (job = jobs.pop) do MyPool.handle(job) end } }.each(&:join)
62
+ end
63
+ ```
64
+
65
+ The threads are best started and joined in the benchmark, as here: one
66
+ that outlives a sample may be waiting in the next one.
67
+
68
+ ## How it measures
69
+
70
+ Ruby tells the extension when a thread starts waiting for the GVL and
71
+ when it gets it (its thread event hooks), and it adds up the waits. The
72
+ hook is added only for a sample of a benchmark it measures, and removed
73
+ after it. Then each time a thread takes the GVL costs two clock readings
74
+ more: some 40-70 ns on macOS (5-8% of a loop of one-byte pipe reads and
75
+ writes), more where reading the clock is slow (some virtual machines),
76
+ nothing in code that doesn't release the GVL, and alike on both
77
+ versions.
78
+
79
+ How long threads wait depends on when the operating system runs them,
80
+ so on what else the machine is doing: on a busy one, the same code's
81
+ samples can wait half or twice as long. What contention a change adds
82
+ makes them wait many times as long, so `gvl_wait` is slower only past
83
+ half as much again (limit 1.5). And it isn't judged across the board
84
+ (`across: false`), where Duel looks for a slowdown too small for any
85
+ one benchmark in all of them together, with a quarter of the margin: an
86
+ eighth more is well within what the machine alone does to it.
87
+
88
+ Threads that hardly wait for each other still wait a little now and
89
+ then, as the scheduler has it (starting and ending a thread are
90
+ handoffs too): some 0.3-2.5% of their CPU time, on a busy machine
91
+ too. Threads that fight for it wait about as long as they run, or
92
+ longer: a thread that wants the GVL waits until the one holding it lets
93
+ go, or its time slice runs out. (Threads that mostly wait on I/O, and
94
+ only now and then want the GVL at once, wait anything from a twentieth
95
+ as long as they run to more than that, as the machine is busy.) So a
96
+ benchmark is judged on `gvl_wait` only when its samples wait a tenth of
97
+ their CPU time or more on either version (`cpu_floor: 0.1`): code that hardly waits on
98
+ both isn't judged on the noise, and code that starts waiting where it
99
+ hardly did is (its ratio is many times over). It's a share and not a
100
+ time since samples are as long as their CPU time needs to settle (some
101
+ 10 ms, or more). `gvl_wait` doesn't size samples: CPU time does.
102
+
103
+ Duel's `c.measure` changes its settings, or leaves it out:
104
+
105
+ ```ruby
106
+ c.measure("gvl_wait", cpu_floor: 0.5, limit: 2.0) # judged from half the CPU time, slower past twice as long
107
+ c.measure("gvl_wait", skip: true)
108
+ ```
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Duel::GVL::Hook: the total time Ruby threads have spent waiting for
3
+ * the GVL while it's measuring.
4
+ *
5
+ * Ruby calls gvl_event when a thread starts waiting for the GVL (READY,
6
+ * without the GVL) and when it gets it (RESUMED, with it), possibly from
7
+ * several native threads at once, and not necessarily on the native
8
+ * thread that runs the Ruby thread: a thread woken by another (a new
9
+ * one, or one joining a thread that ends) is READY on the waker's, and
10
+ * with M:N threads any may be. So the start of a wait is kept with the
11
+ * Ruby thread, in a slot of the thread-specific storage Ruby keeps for
12
+ * tools like this one (in a native thread's variable, a joining thread's
13
+ * wait would be counted from a READY long before), and each wait is
14
+ * added to one total atomically.
15
+ *
16
+ * The hook is added by start and removed by stop: between them, each
17
+ * time a thread takes the GVL costs two clock readings and Ruby's
18
+ * calling the hook (some 25 ns): 40-70 ns on macOS, more where reading
19
+ * the clock is slow (some virtual machines); outside them, nothing.
20
+ */
21
+ #include <ruby.h>
22
+ #include <ruby/thread.h>
23
+ #include <stdint.h>
24
+ #include <time.h>
25
+
26
+ static rb_internal_thread_specific_key_t ready_key;
27
+ static rb_internal_thread_event_hook_t *hook;
28
+ static uint64_t waited_ns;
29
+ static uint64_t started_at;
30
+
31
+ /*
32
+ * Monotonic nanoseconds, as cheaply as the platform has them: each
33
+ * handoff of the GVL reads the clock twice (macOS's CLOCK_UPTIME_RAW
34
+ * skips the conversion its CLOCK_MONOTONIC makes, a third of the hook's
35
+ * cost there).
36
+ */
37
+ static uint64_t monotonic_ns(void) {
38
+ #ifdef __APPLE__
39
+ return clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
40
+ #else
41
+ struct timespec now;
42
+ clock_gettime(CLOCK_MONOTONIC, &now);
43
+ return ((uint64_t)now.tv_sec * 1000000000U) + (uint64_t)now.tv_nsec;
44
+ #endif
45
+ }
46
+
47
+ static void gvl_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *data, void *user_data) {
48
+ (void)user_data;
49
+ if (event == RUBY_INTERNAL_THREAD_EVENT_READY) {
50
+ rb_internal_thread_specific_set(data->thread, ready_key, (void *)(uintptr_t)monotonic_ns());
51
+ return;
52
+ }
53
+ uint64_t ready_at = (uint64_t)(uintptr_t)rb_internal_thread_specific_get(data->thread, ready_key);
54
+ /*
55
+ * A thread that was waiting already when measuring started has no
56
+ * start in this stretch: its stamp is older (or from an earlier
57
+ * stretch, its wait's end unseen), and its wait isn't counted.
58
+ */
59
+ if (ready_at >= __atomic_load_n(&started_at, __ATOMIC_RELAXED)) {
60
+ __atomic_fetch_add(&waited_ns, monotonic_ns() - ready_at, __ATOMIC_RELAXED);
61
+ }
62
+ rb_internal_thread_specific_set(data->thread, ready_key, NULL);
63
+ }
64
+
65
+ /* Starts measuring (again); does nothing while measuring. */
66
+ static VALUE native_start(VALUE self) {
67
+ (void)self;
68
+ if (hook == NULL) {
69
+ __atomic_store_n(&started_at, monotonic_ns(), __ATOMIC_RELAXED);
70
+ hook = rb_internal_thread_add_event_hook(
71
+ gvl_event, RUBY_INTERNAL_THREAD_EVENT_READY | RUBY_INTERNAL_THREAD_EVENT_RESUMED, NULL);
72
+ }
73
+ return Qnil;
74
+ }
75
+
76
+ /* Stops measuring; the total stays. */
77
+ static VALUE native_stop(VALUE self) {
78
+ (void)self;
79
+ if (hook != NULL) {
80
+ rb_internal_thread_remove_event_hook(hook);
81
+ hook = NULL;
82
+ }
83
+ return Qnil;
84
+ }
85
+
86
+ static VALUE native_running_p(VALUE self) {
87
+ (void)self;
88
+ return (hook != NULL) ? Qtrue : Qfalse;
89
+ }
90
+
91
+ /* Nanoseconds all threads have waited for the GVL while measuring. */
92
+ static VALUE native_wait_ns(VALUE self) {
93
+ (void)self;
94
+ return ULL2NUM(__atomic_load_n(&waited_ns, __ATOMIC_RELAXED));
95
+ }
96
+
97
+ /* Called by Ruby's `require`, so it must have external linkage. */
98
+ void Init_duel_gvl(void) {
99
+ VALUE duel = rb_define_module("Duel");
100
+ VALUE gvl = rb_define_module_under(duel, "GVL");
101
+ VALUE hook_module = rb_define_module_under(gvl, "Hook");
102
+
103
+ ready_key = rb_internal_thread_specific_key_create();
104
+ rb_define_module_function(hook_module, "start", native_start, 0);
105
+ rb_define_module_function(hook_module, "stop", native_stop, 0);
106
+ rb_define_module_function(hook_module, "running?", native_running_p, 0);
107
+ rb_define_module_function(hook_module, "wait_ns", native_wait_ns, 0);
108
+ }
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The extension is optional: without it, Duel::GVL leaves its metric out,
4
+ # so a gem install never fails over it. Where it can't be built (JRuby and
5
+ # TruffleRuby, which have no GVL to wait for; Windows, where Ruby's thread
6
+ # event hooks do nothing; 32-bit platforms, where a thread's slot can't
7
+ # hold a timestamp; no working C compiler), write a Makefile that does
8
+ # nothing.
9
+ def write_noop_makefile(reason)
10
+ warn "duel-gvl: not building the extension (#{reason}); there will be no gvl_wait metric"
11
+ File.write("Makefile", "all install clean distclean static:\n\t@:\n.PHONY: all install clean distclean static\n")
12
+ end
13
+
14
+ if RUBY_ENGINE != "ruby"
15
+ write_noop_makefile("#{RUBY_ENGINE} has no GVL")
16
+ elsif Gem.win_platform?
17
+ write_noop_makefile("Ruby's thread event hooks do nothing on Windows")
18
+ elsif [0].pack("J").bytesize < 8
19
+ write_noop_makefile("a 32-bit platform")
20
+ else
21
+ require "mkmf"
22
+
23
+ if try_compile("int main(void) { return 0; }") && have_func("rb_internal_thread_specific_get", "ruby/thread.h")
24
+ $CFLAGS << " -O2 -std=c99 -Wall -Wextra -Wno-unused-parameter -Wno-declaration-after-statement" # rubocop:disable Style/GlobalVars -- mkmf API
25
+ create_makefile("duel_gvl/duel_gvl")
26
+ else
27
+ write_noop_makefile("no working C compiler")
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Duel
4
+ module GVL
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/duel/gvl.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "duel"
4
+ require_relative "gvl/version"
5
+
6
+ module Duel
7
+ # How long threads waited for the GVL during each sample: the cost of
8
+ # the contention a change adds (holding the GVL longer around I/O, a C
9
+ # call that stops releasing it, more threads fighting for it), which
10
+ # CPU time doesn't show, since a thread waiting for the GVL uses none.
11
+ #
12
+ # # .duel.rb
13
+ # Duel.configure { |c| c.use :gvl }
14
+ #
15
+ # gvl_wait (a ratio metric, in nanoseconds) is measured for benchmarks with
16
+ # threads of their own (only: :timing), and left out of the rest, whose
17
+ # one thread has nothing to wait for. It needs CRuby and the gem's C
18
+ # extension (Duel::GVL::Hook); elsewhere it's left out.
19
+ module GVL
20
+ Duel.plugin(:gvl, self)
21
+
22
+ # Configures Duel for the plugin (c.use :gvl): the gvl_wait metric
23
+ # (where it's available), and the extension, loaded in workers.
24
+ # Returns the metric.
25
+ def self.configure(config)
26
+ config.in_worker { load_extension }
27
+ config.metric(Wait)
28
+ end
29
+
30
+ # Whether the extension is loaded to measure with.
31
+ def self.available? = const_defined?(:Hook, false)
32
+
33
+ # Loads the extension (Hook) where it can be: only on CRuby (another
34
+ # Ruby would try a build of CRuby's left in lib/), and only where it
35
+ # was built. Whether it's loaded. (Each worker loads it, before its
36
+ # metrics are made; the process that runs them needn't.)
37
+ def self.load_extension(engine: RUBY_ENGINE, feature: "duel_gvl/duel_gvl")
38
+ return false unless engine == "ruby"
39
+
40
+ require feature
41
+ available?
42
+ rescue LoadError
43
+ false
44
+ end
45
+
46
+ # gvl_wait: nanoseconds all threads waited for the GVL during a
47
+ # sample of a benchmark with threads of its own.
48
+ #
49
+ # Threads that hardly wait still wait a little now and then, as the
50
+ # scheduler has it (a thread's start and end are handoffs too), so a
51
+ # sample is judged on it only when it waited a tenth of its CPU time
52
+ # or more (CPU_FLOOR) on either version: code that starts waiting where it
53
+ # hardly did is judged (its ratio is many times over), code that
54
+ # hardly waits on both isn't judged on the noise. Threads that hardly
55
+ # wait wait some 0.3-2.5% of their CPU time, on a busy machine too;
56
+ # threads that fight for the GVL wait as long as they run, or longer
57
+ # (a thread wanting it waits out the holder's time slice), and those
58
+ # that only now and then want it at once, 5% to more than 100%, as
59
+ # busy as the machine is. A share of CPU time, and not a time, since
60
+ # a sample is as long as it takes for its CPU time to settle.
61
+ #
62
+ # How long threads wait depends on when the operating system runs
63
+ # them, so on everything else the machine is doing: on a busy one,
64
+ # the same code's samples can wait half or twice as long. What makes
65
+ # threads wait much longer is a change that holds the GVL where it
66
+ # didn't, which waits many times as long; so it's slower only past
67
+ # half as much again (LIMIT), and it isn't judged across the board,
68
+ # where the margin is a quarter of that (an eighth more).
69
+ #
70
+ # It doesn't size samples: four threads fighting for the GVL wait
71
+ # three times as long as they run, and samples sized by that would be
72
+ # too short for their CPU time to settle.
73
+ class Wait < Metric
74
+ CPU_FLOOR = 0.1
75
+
76
+ LIMIT = 1.5
77
+
78
+ def self.measures = {"gvl_wait" => {kind: :ratio, unit: "ns", cpu_floor: CPU_FLOOR, limit: LIMIT, sizes_samples: false, across: false}}
79
+
80
+ def self.available? = GVL.available?
81
+
82
+ def start_sample(sample)
83
+ @measuring = sample.benchmark.only == :timing
84
+ return unless @measuring
85
+
86
+ Hook.start
87
+ @waited = Hook.wait_ns
88
+ end
89
+
90
+ def stop_sample
91
+ return unless @measuring
92
+
93
+ Hook.stop
94
+ Hook.wait_ns - @waited
95
+ end
96
+ end
97
+ end
98
+ end
data/sig/duel/gvl.rbs ADDED
@@ -0,0 +1,52 @@
1
+ module Duel
2
+ # How long threads waited for the GVL during each sample of a benchmark
3
+ # with threads of its own (gvl_wait).
4
+ module GVL
5
+ VERSION: String
6
+
7
+ # Configures Duel for the plugin (c.use :gvl): the gvl_wait metric
8
+ # (where it's available), and the extension, loaded in workers.
9
+ # Returns the metric.
10
+ def self.configure: (Config config) -> singleton(Metric)
11
+
12
+ # Whether the extension is loaded to measure with.
13
+ def self.available?: () -> bool
14
+
15
+ # Loads the extension (Hook) where it can be; whether it's loaded.
16
+ def self.load_extension: (?engine: String, ?feature: String) -> bool
17
+
18
+ # The C extension: the total time threads have waited for the GVL
19
+ # while it's measuring.
20
+ module Hook
21
+ # Starts measuring (again); does nothing while measuring.
22
+ def self.start: () -> nil
23
+
24
+ # Stops measuring; the total stays.
25
+ def self.stop: () -> nil
26
+
27
+ def self.running?: () -> bool
28
+
29
+ # Nanoseconds all threads have waited for the GVL while measuring.
30
+ def self.wait_ns: () -> Integer
31
+ end
32
+
33
+ # gvl_wait: nanoseconds all threads waited for the GVL during a
34
+ # sample of a benchmark with threads of its own; judged at CPU_FLOOR
35
+ # of the sample's CPU time or more on either version, slower past
36
+ # LIMIT, and not across the board.
37
+ class Wait < Metric
38
+ CPU_FLOOR: Float
39
+
40
+ LIMIT: Float
41
+
42
+ @measuring: bool
43
+ @waited: Integer
44
+
45
+ def self.available?: () -> bool
46
+
47
+ def start_sample: (Sample sample) -> void
48
+
49
+ def stop_sample: () -> Integer?
50
+ end
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: duel-gvl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Erik Berlin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: duel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ description: 'A Duel plugin: benchmarks with threads of their own are judged on how
27
+ long their threads waited for the GVL too, so a change that adds contention fails
28
+ even when its CPU time doesn''t.'
29
+ email:
30
+ - sferik@gmail.com
31
+ executables: []
32
+ extensions:
33
+ - ext/duel_gvl/extconf.rb
34
+ extra_rdoc_files: []
35
+ files:
36
+ - CHANGELOG.md
37
+ - LICENSE
38
+ - README.md
39
+ - ext/duel_gvl/duel_gvl.c
40
+ - ext/duel_gvl/extconf.rb
41
+ - lib/duel/gvl.rb
42
+ - lib/duel/gvl/version.rb
43
+ - sig/duel/gvl.rbs
44
+ homepage: https://github.com/sferik/duel-ruby/tree/main/plugins/gvl
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ source_code_uri: https://github.com/sferik/duel-ruby/tree/main/plugins/gvl
49
+ changelog_uri: https://github.com/sferik/duel-ruby/blob/main/plugins/gvl/CHANGELOG.md
50
+ bug_tracker_uri: https://github.com/sferik/duel-ruby/issues
51
+ rubygems_mfa_required: 'true'
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '3.4'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 4.0.23
67
+ specification_version: 4
68
+ summary: How long threads waited for the GVL, for every Duel benchmark with threads
69
+ of its own
70
+ test_files: []