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 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