gvltools 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/Gemfile +2 -3
- data/Gemfile.lock +22 -17
- data/README.md +20 -0
- data/ext/gvltools/extconf.rb +2 -1
- data/ext/gvltools/instrumentation.c +116 -29
- data/gvltools.gemspec +0 -2
- data/lib/gvltools/version.rb +1 -1
- data/lib/gvltools.rb +1 -1
- metadata +4 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b6af92049391130d2248c5d14ec3c93e859f4ab92d6af9842e2a88c289b3e50
|
4
|
+
data.tar.gz: 4681a32780409d180b6727a791cdc122a9a03d21eba7f28f2070c8de6e52ddb1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 005a90c02765c716c97f9165f777dd6b26b3853037e6c64f13e3255278db5a7c5d378855850758a7e391d9957389d18db7edb24a3658a46a5ec6105668238fa7
|
7
|
+
data.tar.gz: e16793a8f452f958cd0dde468c5aff337a78719485c8657bc9f5e1e91bd393b0bc1a523e56b5a1e06246c963e8661ed34707ef49e05cb15d6e94e57865c9932c
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,46 +1,51 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gvltools (0.
|
4
|
+
gvltools (0.4.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
9
|
ast (2.4.2)
|
10
|
-
|
11
|
-
|
12
|
-
|
10
|
+
json (2.7.1)
|
11
|
+
language_server-protocol (3.17.0.3)
|
12
|
+
minitest (5.21.2)
|
13
|
+
parallel (1.24.0)
|
14
|
+
parser (3.3.0.2)
|
13
15
|
ast (~> 2.4.1)
|
16
|
+
racc
|
17
|
+
racc (1.7.3)
|
14
18
|
rainbow (3.1.1)
|
15
19
|
rake (13.0.6)
|
16
20
|
rake-compiler (1.2.0)
|
17
21
|
rake
|
18
|
-
regexp_parser (2.
|
19
|
-
rexml (3.2.
|
20
|
-
rubocop (1.
|
22
|
+
regexp_parser (2.9.0)
|
23
|
+
rexml (3.2.6)
|
24
|
+
rubocop (1.59.0)
|
25
|
+
json (~> 2.3)
|
26
|
+
language_server-protocol (>= 3.17.0)
|
21
27
|
parallel (~> 1.10)
|
22
|
-
parser (>= 3.
|
28
|
+
parser (>= 3.2.2.4)
|
23
29
|
rainbow (>= 2.2.2, < 4.0)
|
24
30
|
regexp_parser (>= 1.8, < 3.0)
|
25
31
|
rexml (>= 3.2.5, < 4.0)
|
26
|
-
rubocop-ast (>= 1.
|
32
|
+
rubocop-ast (>= 1.30.0, < 2.0)
|
27
33
|
ruby-progressbar (~> 1.7)
|
28
|
-
unicode-display_width (>=
|
29
|
-
rubocop-ast (1.
|
30
|
-
parser (>= 3.
|
31
|
-
ruby-progressbar (1.
|
32
|
-
unicode-display_width (2.
|
34
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
35
|
+
rubocop-ast (1.30.0)
|
36
|
+
parser (>= 3.2.1.0)
|
37
|
+
ruby-progressbar (1.13.0)
|
38
|
+
unicode-display_width (2.5.0)
|
33
39
|
|
34
40
|
PLATFORMS
|
35
41
|
aarch64-linux
|
36
|
-
arm64-darwin
|
37
|
-
arm64-darwin-22
|
42
|
+
arm64-darwin
|
38
43
|
x86_64-linux
|
39
44
|
|
40
45
|
DEPENDENCIES
|
41
46
|
gvltools!
|
42
47
|
minitest (~> 5.0)
|
43
|
-
rake
|
48
|
+
rake
|
44
49
|
rake-compiler
|
45
50
|
rubocop (~> 1.21)
|
46
51
|
|
data/README.md
CHANGED
@@ -59,6 +59,26 @@ class GVLInstrumentationMiddleware
|
|
59
59
|
end
|
60
60
|
```
|
61
61
|
|
62
|
+
Starting from Ruby 3.3, a thread local timer can be accessed from another thread:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
def fibonacci(n)
|
66
|
+
if n < 2
|
67
|
+
n
|
68
|
+
else
|
69
|
+
fibonacci(n - 1) + fibonacci(n - 2)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
GVLTools::LocalTimer.enable
|
74
|
+
thread = Thread.new do
|
75
|
+
fibonacci(20)
|
76
|
+
end
|
77
|
+
thread.join(1)
|
78
|
+
local_timer = GVLTools::LocalTimer.for(thread)
|
79
|
+
local_timer.monotonic_time # => 127000
|
80
|
+
```
|
81
|
+
|
62
82
|
### GlobalTimer
|
63
83
|
|
64
84
|
`GlobalTimer` records the overall time spent waiting on the GVL by all threads combined.
|
data/ext/gvltools/extconf.rb
CHANGED
@@ -3,9 +3,10 @@
|
|
3
3
|
require "mkmf"
|
4
4
|
if RUBY_ENGINE == "ruby" &&
|
5
5
|
have_header("stdatomic.h") &&
|
6
|
-
have_func("rb_internal_thread_add_event_hook", ["ruby/thread.h"])
|
6
|
+
have_func("rb_internal_thread_add_event_hook", ["ruby/thread.h"]) # 3.1+
|
7
7
|
|
8
8
|
$CFLAGS << " -O3 -Wall "
|
9
|
+
have_func("rb_internal_thread_specific_get", "ruby/thread.h") # 3.3+
|
9
10
|
create_makefile("gvltools/instrumentation")
|
10
11
|
else
|
11
12
|
File.write("Makefile", dummy_makefile($srcdir).join)
|
@@ -5,6 +5,8 @@
|
|
5
5
|
|
6
6
|
typedef unsigned long long counter_t;
|
7
7
|
|
8
|
+
VALUE rb_cLocalTimer = Qnil;
|
9
|
+
|
8
10
|
// Metrics
|
9
11
|
static rb_internal_thread_event_hook_t *gt_hook = NULL;
|
10
12
|
|
@@ -15,6 +17,44 @@ static unsigned int enabled_mask = 0;
|
|
15
17
|
|
16
18
|
#define ENABLED(metric) (enabled_mask & (metric))
|
17
19
|
|
20
|
+
typedef struct {
|
21
|
+
bool was_ready;
|
22
|
+
_Atomic counter_t timer_total;
|
23
|
+
_Atomic counter_t waiting_threads_ready_generation;
|
24
|
+
struct timespec timer_ready_at;
|
25
|
+
} thread_local_state;
|
26
|
+
|
27
|
+
#ifdef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET // 3.3+
|
28
|
+
static int thread_storage_key = 0;
|
29
|
+
|
30
|
+
static size_t thread_local_state_memsize(const void *data) {
|
31
|
+
return sizeof(thread_local_state);
|
32
|
+
}
|
33
|
+
|
34
|
+
static const rb_data_type_t thread_local_state_type = {
|
35
|
+
.wrap_struct_name = "GVLTools::ThreadLocal",
|
36
|
+
.function = {
|
37
|
+
.dmark = NULL,
|
38
|
+
.dfree = RUBY_DEFAULT_FREE,
|
39
|
+
.dsize = thread_local_state_memsize,
|
40
|
+
},
|
41
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,
|
42
|
+
};
|
43
|
+
|
44
|
+
static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
|
45
|
+
thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
|
46
|
+
if (!state && allocate) {
|
47
|
+
VALUE wrapper = TypedData_Make_Struct(rb_cLocalTimer, thread_local_state, &thread_local_state_type, state);
|
48
|
+
rb_thread_local_aset(thread, rb_intern("__gvltools_local_state"), wrapper);
|
49
|
+
RB_GC_GUARD(wrapper);
|
50
|
+
rb_internal_thread_specific_set(thread, thread_storage_key, state);
|
51
|
+
}
|
52
|
+
return state;
|
53
|
+
}
|
54
|
+
|
55
|
+
#define GT_EVENT_LOCAL_STATE(event_data, allocate) GT_LOCAL_STATE(event_data->thread, allocate)
|
56
|
+
#define GT_CURRENT_THREAD_LOCAL_STATE() GT_LOCAL_STATE(rb_thread_current(), true)
|
57
|
+
#else
|
18
58
|
#if __STDC_VERSION__ >= 201112
|
19
59
|
#define THREAD_LOCAL_SPECIFIER _Thread_local
|
20
60
|
#elif defined(__GNUC__) && !defined(RB_THREAD_LOCAL_SPECIFIER_IS_UNSUPPORTED)
|
@@ -22,11 +62,17 @@ static unsigned int enabled_mask = 0;
|
|
22
62
|
#define THREAD_LOCAL_SPECIFIER __thread
|
23
63
|
#endif
|
24
64
|
|
65
|
+
static THREAD_LOCAL_SPECIFIER thread_local_state __thread_local_state = {0};
|
66
|
+
#undef THREAD_LOCAL_SPECIFIER
|
67
|
+
|
68
|
+
#define GT_LOCAL_STATE(thread) (&__thread_local_state)
|
69
|
+
#define GT_EVENT_LOCAL_STATE(event_data, allocate) (&__thread_local_state)
|
70
|
+
#define GT_CURRENT_THREAD_LOCAL_STATE() (&__thread_local_state)
|
71
|
+
#endif
|
72
|
+
|
25
73
|
// Common
|
26
74
|
#define SECONDS_TO_NANOSECONDS (1000 * 1000 * 1000)
|
27
75
|
|
28
|
-
static THREAD_LOCAL_SPECIFIER bool was_ready = 0;
|
29
|
-
|
30
76
|
static inline void gt_gettime(struct timespec *time) {
|
31
77
|
if (clock_gettime(CLOCK_MONOTONIC, time) == -1) {
|
32
78
|
rb_sys_fail("clock_gettime");
|
@@ -50,7 +96,7 @@ static VALUE gt_enable_metric(VALUE module, VALUE metric) {
|
|
50
96
|
if (!gt_hook) {
|
51
97
|
gt_hook = rb_internal_thread_add_event_hook(
|
52
98
|
gt_thread_callback,
|
53
|
-
RUBY_INTERNAL_THREAD_EVENT_EXITED | RUBY_INTERNAL_THREAD_EVENT_READY | RUBY_INTERNAL_THREAD_EVENT_RESUMED,
|
99
|
+
RUBY_INTERNAL_THREAD_EVENT_STARTED | RUBY_INTERNAL_THREAD_EVENT_EXITED | RUBY_INTERNAL_THREAD_EVENT_READY | RUBY_INTERNAL_THREAD_EVENT_RESUMED,
|
54
100
|
NULL
|
55
101
|
);
|
56
102
|
}
|
@@ -70,8 +116,6 @@ static VALUE gt_disable_metric(VALUE module, VALUE metric) {
|
|
70
116
|
|
71
117
|
// GVLTools::LocalTimer and GVLTools::GlobalTimer
|
72
118
|
static _Atomic counter_t global_timer_total = 0;
|
73
|
-
static THREAD_LOCAL_SPECIFIER counter_t local_timer_total = 0;
|
74
|
-
static THREAD_LOCAL_SPECIFIER struct timespec timer_ready_at = {0};
|
75
119
|
|
76
120
|
static VALUE global_timer_monotonic_time(VALUE module) {
|
77
121
|
return ULL2NUM(global_timer_total);
|
@@ -82,19 +126,38 @@ static VALUE global_timer_reset(VALUE module) {
|
|
82
126
|
return Qtrue;
|
83
127
|
}
|
84
128
|
|
85
|
-
static VALUE
|
86
|
-
return ULL2NUM(
|
129
|
+
static VALUE local_timer_m_monotonic_time(VALUE module) {
|
130
|
+
return ULL2NUM(GT_CURRENT_THREAD_LOCAL_STATE()->timer_total);
|
87
131
|
}
|
88
132
|
|
89
|
-
static VALUE
|
90
|
-
|
133
|
+
static VALUE local_timer_m_reset(VALUE module) {
|
134
|
+
GT_CURRENT_THREAD_LOCAL_STATE()->timer_total = 0;
|
91
135
|
return Qtrue;
|
92
136
|
}
|
93
137
|
|
138
|
+
#ifdef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET
|
139
|
+
static VALUE local_timer_for(VALUE module, VALUE thread) {
|
140
|
+
GT_LOCAL_STATE(thread, true);
|
141
|
+
return rb_thread_local_aref(thread, rb_intern("__gvltools_local_state"));
|
142
|
+
}
|
143
|
+
|
144
|
+
static VALUE local_timer_monotonic_time(VALUE timer) {
|
145
|
+
thread_local_state *state;
|
146
|
+
TypedData_Get_Struct(timer, thread_local_state, &thread_local_state_type, state);
|
147
|
+
return ULL2NUM(state->timer_total);
|
148
|
+
}
|
149
|
+
|
150
|
+
static VALUE local_timer_reset(VALUE timer) {
|
151
|
+
thread_local_state *state;
|
152
|
+
TypedData_Get_Struct(timer, thread_local_state, &thread_local_state_type, state);
|
153
|
+
state->timer_total = 0;
|
154
|
+
return Qtrue;
|
155
|
+
}
|
156
|
+
#endif
|
157
|
+
|
94
158
|
// Thread counts
|
95
159
|
static _Atomic counter_t waiting_threads_total = 0;
|
96
160
|
static _Atomic counter_t waiting_threads_current_generation = 1;
|
97
|
-
static THREAD_LOCAL_SPECIFIER counter_t waiting_threads_ready_generation = 0;
|
98
161
|
|
99
162
|
static VALUE waiting_threads_count(VALUE module) {
|
100
163
|
return ULL2NUM(waiting_threads_total);
|
@@ -107,38 +170,51 @@ static VALUE waiting_threads_reset(VALUE module) {
|
|
107
170
|
}
|
108
171
|
|
109
172
|
// General callback
|
110
|
-
static void gt_reset_thread_local_state(void) {
|
111
|
-
// MRI can re-use native threads, so we need to reset thread local state,
|
112
|
-
// otherwise it will leak from one Ruby thread from another.
|
113
|
-
was_ready = false;
|
114
|
-
local_timer_total = 0;
|
115
|
-
}
|
116
|
-
|
117
173
|
static void gt_thread_callback(rb_event_flag_t event, const rb_internal_thread_event_data_t *event_data, void *user_data) {
|
118
174
|
switch(event) {
|
119
|
-
case RUBY_INTERNAL_THREAD_EVENT_STARTED:
|
175
|
+
case RUBY_INTERNAL_THREAD_EVENT_STARTED: {
|
176
|
+
// The STARTED event is triggered from the parent thread with the GVL held
|
177
|
+
// so we can allocate the struct.
|
178
|
+
GT_EVENT_LOCAL_STATE(event_data, true);
|
179
|
+
}
|
180
|
+
break;
|
120
181
|
case RUBY_INTERNAL_THREAD_EVENT_EXITED: {
|
121
|
-
|
182
|
+
#ifndef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET
|
183
|
+
thread_local_state *state = GT_EVENT_LOCAL_STATE(event_data, false);
|
184
|
+
if (state) {
|
185
|
+
// MRI can re-use native threads, so we need to reset thread local state,
|
186
|
+
// otherwise it will leak from one Ruby thread from another.
|
187
|
+
state->was_ready = false;
|
188
|
+
state->timer_total = 0;
|
189
|
+
}
|
190
|
+
#endif
|
122
191
|
}
|
123
192
|
break;
|
124
193
|
case RUBY_INTERNAL_THREAD_EVENT_READY: {
|
125
|
-
|
194
|
+
thread_local_state *state = GT_EVENT_LOCAL_STATE(event_data, false);
|
195
|
+
if (!state) {
|
196
|
+
break;
|
197
|
+
}
|
198
|
+
state->was_ready = true;
|
126
199
|
|
127
200
|
if (ENABLED(WAITING_THREADS)) {
|
128
|
-
waiting_threads_ready_generation = waiting_threads_current_generation;
|
201
|
+
state->waiting_threads_ready_generation = waiting_threads_current_generation;
|
129
202
|
waiting_threads_total++;
|
130
203
|
}
|
131
204
|
|
132
205
|
if (ENABLED(TIMER_GLOBAL | TIMER_LOCAL)) {
|
133
|
-
gt_gettime(&timer_ready_at);
|
206
|
+
gt_gettime(&state->timer_ready_at);
|
134
207
|
}
|
135
208
|
}
|
136
209
|
break;
|
137
210
|
case RUBY_INTERNAL_THREAD_EVENT_RESUMED: {
|
138
|
-
|
211
|
+
thread_local_state *state = GT_EVENT_LOCAL_STATE(event_data, true);
|
212
|
+
if (!state->was_ready) {
|
213
|
+
break; // In case we registered the hook while some threads were already waiting on the GVL
|
214
|
+
}
|
139
215
|
|
140
216
|
if (ENABLED(WAITING_THREADS)) {
|
141
|
-
if (waiting_threads_ready_generation == waiting_threads_current_generation) {
|
217
|
+
if (state->waiting_threads_ready_generation == waiting_threads_current_generation) {
|
142
218
|
waiting_threads_total--;
|
143
219
|
}
|
144
220
|
}
|
@@ -146,10 +222,10 @@ static void gt_thread_callback(rb_event_flag_t event, const rb_internal_thread_e
|
|
146
222
|
if (ENABLED(TIMER_GLOBAL | TIMER_LOCAL)) {
|
147
223
|
struct timespec current_time;
|
148
224
|
gt_gettime(¤t_time);
|
149
|
-
counter_t diff = gt_time_diff_ns(timer_ready_at, current_time);
|
225
|
+
counter_t diff = gt_time_diff_ns(state->timer_ready_at, current_time);
|
150
226
|
|
151
227
|
if (ENABLED(TIMER_LOCAL)) {
|
152
|
-
|
228
|
+
state->timer_total += diff;
|
153
229
|
}
|
154
230
|
|
155
231
|
if (ENABLED(TIMER_GLOBAL)) {
|
@@ -162,6 +238,10 @@ static void gt_thread_callback(rb_event_flag_t event, const rb_internal_thread_e
|
|
162
238
|
}
|
163
239
|
|
164
240
|
void Init_instrumentation(void) {
|
241
|
+
#ifdef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET // 3.3+
|
242
|
+
thread_storage_key = rb_internal_thread_specific_key_create();
|
243
|
+
#endif
|
244
|
+
|
165
245
|
VALUE rb_mGVLTools = rb_const_get(rb_cObject, rb_intern("GVLTools"));
|
166
246
|
|
167
247
|
VALUE rb_mNative = rb_const_get(rb_mGVLTools, rb_intern("Native"));
|
@@ -173,9 +253,16 @@ void Init_instrumentation(void) {
|
|
173
253
|
rb_define_singleton_method(rb_mGlobalTimer, "reset", global_timer_reset, 0);
|
174
254
|
rb_define_singleton_method(rb_mGlobalTimer, "monotonic_time", global_timer_monotonic_time, 0);
|
175
255
|
|
176
|
-
|
177
|
-
|
178
|
-
|
256
|
+
rb_global_variable(&rb_cLocalTimer);
|
257
|
+
rb_cLocalTimer = rb_const_get(rb_mGVLTools, rb_intern("LocalTimer"));
|
258
|
+
rb_undef_alloc_func(rb_cLocalTimer);
|
259
|
+
rb_define_singleton_method(rb_cLocalTimer, "reset", local_timer_m_reset, 0);
|
260
|
+
rb_define_singleton_method(rb_cLocalTimer, "monotonic_time", local_timer_m_monotonic_time, 0);
|
261
|
+
#ifdef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET
|
262
|
+
rb_define_singleton_method(rb_cLocalTimer, "for", local_timer_for, 1);
|
263
|
+
rb_define_method(rb_cLocalTimer, "reset", local_timer_reset, 0);
|
264
|
+
rb_define_method(rb_cLocalTimer, "monotonic_time", local_timer_monotonic_time, 0);
|
265
|
+
#endif
|
179
266
|
|
180
267
|
VALUE rb_mWaitingThreads = rb_const_get(rb_mGVLTools, rb_intern("WaitingThreads"));
|
181
268
|
rb_define_singleton_method(rb_mWaitingThreads, "_reset", waiting_threads_reset, 0);
|
data/gvltools.gemspec
CHANGED
data/lib/gvltools/version.rb
CHANGED
data/lib/gvltools.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gvltools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: rake-compiler
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
11
|
+
date: 2024-01-19 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
27
13
|
description:
|
28
14
|
email:
|
29
15
|
- jean.boussier@gmail.com
|
@@ -67,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
53
|
- !ruby/object:Gem::Version
|
68
54
|
version: '0'
|
69
55
|
requirements: []
|
70
|
-
rubygems_version: 3.
|
56
|
+
rubygems_version: 3.5.3
|
71
57
|
signing_key:
|
72
58
|
specification_version: 4
|
73
59
|
summary: Set of GVL instrumentation tools
|