gvltools 0.3.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ffb320bec5fdc8a0d4940d8b370af1607a20383e1a9a8f939cc759588ca6627
4
- data.tar.gz: 223bee0c124c17296b8386bbdda94406f91b84ad0adcf3acd09921424bb8eaf8
3
+ metadata.gz: 5b6af92049391130d2248c5d14ec3c93e859f4ab92d6af9842e2a88c289b3e50
4
+ data.tar.gz: 4681a32780409d180b6727a791cdc122a9a03d21eba7f28f2070c8de6e52ddb1
5
5
  SHA512:
6
- metadata.gz: 1c988c2e9d2c17925b178e4201f99530e75328cda648b3e3b50664e1d751ff5ad086139293a22dab5a54ae18e86bdd7d6e14064f4a4833eae18d3d948f134f20
7
- data.tar.gz: 3b56ad91bf7784d0c61e4d321964eeaaae8ca97ec6c6c86c1934e8e95ad9d720b00933a7b0f7e91ec45e7ffd2a07665800c6f616250b7251318dc889a5ba94ca
6
+ metadata.gz: 005a90c02765c716c97f9165f777dd6b26b3853037e6c64f13e3255278db5a7c5d378855850758a7e391d9957389d18db7edb24a3658a46a5ec6105668238fa7
7
+ data.tar.gz: e16793a8f452f958cd0dde468c5aff337a78719485c8657bc9f5e1e91bd393b0bc1a523e56b5a1e06246c963e8661ed34707ef49e05cb15d6e94e57865c9932c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - Fixed compatibility with Ruby 3.3.0.
4
+ - Added `GVLTools::LocalTimer.for(thread)` to access another thread counter (Ruby 3.3+ only).
5
+
3
6
  ## [0.3.0] - 2023-04-11
4
7
 
5
8
  - Automatically reset the `WaitingThreads` counter when enabling it (#7).
data/Gemfile CHANGED
@@ -2,11 +2,10 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- # Specify your gem's dependencies in gvltools.gemspec
6
5
  gemspec
7
6
 
8
- gem "rake", "~> 13.0"
7
+ gem "rake"
8
+ gem "rake-compiler"
9
9
 
10
10
  gem "minitest", "~> 5.0"
11
-
12
11
  gem "rubocop", "~> 1.21"
data/Gemfile.lock CHANGED
@@ -1,46 +1,51 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gvltools (0.3.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
- minitest (5.15.0)
11
- parallel (1.22.1)
12
- parser (3.1.2.0)
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.5.0)
19
- rexml (3.2.5)
20
- rubocop (1.30.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.1.0.0)
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.18.0, < 2.0)
32
+ rubocop-ast (>= 1.30.0, < 2.0)
27
33
  ruby-progressbar (~> 1.7)
28
- unicode-display_width (>= 1.4.0, < 3.0)
29
- rubocop-ast (1.18.0)
30
- parser (>= 3.1.1.0)
31
- ruby-progressbar (1.11.0)
32
- unicode-display_width (2.1.0)
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-21
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 (~> 13.0)
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.
@@ -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 local_timer_monotonic_time(VALUE module) {
86
- return ULL2NUM(local_timer_total);
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 local_timer_reset(VALUE module) {
90
- local_timer_total = 0;
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
- gt_reset_thread_local_state();
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
- if (!was_ready) was_ready = true;
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
- if (!was_ready) break; // In case we registered the hook while some threads were already waiting on the GVL
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(&current_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
- local_timer_total += diff;
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
- VALUE rb_mLocalTimer = rb_const_get(rb_mGVLTools, rb_intern("LocalTimer"));
177
- rb_define_singleton_method(rb_mLocalTimer, "reset", local_timer_reset, 0);
178
- rb_define_singleton_method(rb_mLocalTimer, "monotonic_time", local_timer_monotonic_time, 0);
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
@@ -30,6 +30,4 @@ Gem::Specification.new do |spec|
30
30
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
31
  spec.require_paths = ["lib"]
32
32
  spec.extensions = ["ext/gvltools/extconf.rb"]
33
-
34
- spec.add_development_dependency "rake-compiler"
35
33
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GVLTools
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/gvltools.rb CHANGED
@@ -70,7 +70,7 @@ module GVLTools
70
70
  end
71
71
  end
72
72
 
73
- module LocalTimer
73
+ class LocalTimer
74
74
  extend AbstractInstrumenter
75
75
 
76
76
  class << self
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.3.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: 2023-04-11 00:00:00.000000000 Z
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.4.10
56
+ rubygems_version: 3.5.3
71
57
  signing_key:
72
58
  specification_version: 4
73
59
  summary: Set of GVL instrumentation tools