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 +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE +21 -0
- data/README.md +108 -0
- data/ext/duel_gvl/duel_gvl.c +108 -0
- data/ext/duel_gvl/extconf.rb +29 -0
- data/lib/duel/gvl/version.rb +7 -0
- data/lib/duel/gvl.rb +98 -0
- data/sig/duel/gvl.rbs +52 -0
- metadata +70 -0
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
|
+
[](https://github.com/sferik/duel-ruby)
|
|
4
|
+
[](https://github.com/sferik/duel-ruby/actions/workflows/plugin-gvl.yml)
|
|
5
|
+
[](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
|
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: []
|