gvltools 0.2.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: ed2a6e0154120c8b048e3b92db8e6f6c4a87d2b653b105b90a06a171ca61895b
4
- data.tar.gz: a976aab0ebf0da04af47d53587c144b7e89eeeef8f078c5b115d037db0fe3f6b
3
+ metadata.gz: 5b6af92049391130d2248c5d14ec3c93e859f4ab92d6af9842e2a88c289b3e50
4
+ data.tar.gz: 4681a32780409d180b6727a791cdc122a9a03d21eba7f28f2070c8de6e52ddb1
5
5
  SHA512:
6
- metadata.gz: baca86f153a68a90ca36213775bd22d5263325e98fdf3cd3332c4b3db953014c7c9a8e14c9ddee27ad0b19de5300065fd77d79d795d7ad8f931d45838f797921
7
- data.tar.gz: be738a183964c4b6e0ef4f9ff5b9aac5e1a9b983d3fffe38034a338416260dc8be5fd95fcc23fe35697c2cc9d79ab8b1e8689deb3907e07468fe8e9985247911
6
+ metadata.gz: 005a90c02765c716c97f9165f777dd6b26b3853037e6c64f13e3255278db5a7c5d378855850758a7e391d9957389d18db7edb24a3658a46a5ec6105668238fa7
7
+ data.tar.gz: e16793a8f452f958cd0dde468c5aff337a78719485c8657bc9f5e1e91bd393b0bc1a523e56b5a1e06246c963e8661ed34707ef49e05cb15d6e94e57865c9932c
data/.rubocop.yml CHANGED
@@ -22,6 +22,15 @@ Style/GlobalVars:
22
22
  Exclude:
23
23
  - ext/**/extconf.rb
24
24
 
25
+ Style/EmptyMethod:
26
+ Enabled: false
27
+
28
+ Style/IfUnlessModifier:
29
+ Enabled: false
30
+
31
+ Style/GuardClause:
32
+ Enabled: false
33
+
25
34
  Style/Documentation:
26
35
  Enabled: false
27
36
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
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
+
6
+ ## [0.3.0] - 2023-04-11
7
+
8
+ - Automatically reset the `WaitingThreads` counter when enabling it (#7).
9
+ - Disallow resetting the `WaitingThreads` counter when it is active (#7).
10
+
3
11
  ## [0.2.0] - 2023-03-28
4
12
 
5
13
  - Use C11 atomics instead of MRI's `rb_atomic_t`.
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.2.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,68 +126,106 @@ 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);
131
+ }
132
+
133
+ static VALUE local_timer_m_reset(VALUE module) {
134
+ GT_CURRENT_THREAD_LOCAL_STATE()->timer_total = 0;
135
+ return Qtrue;
136
+ }
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);
87
148
  }
88
149
 
89
- static VALUE local_timer_reset(VALUE module) {
90
- local_timer_total = 0;
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;
91
154
  return Qtrue;
92
155
  }
156
+ #endif
93
157
 
94
158
  // Thread counts
95
159
  static _Atomic counter_t waiting_threads_total = 0;
160
+ static _Atomic counter_t waiting_threads_current_generation = 1;
96
161
 
97
162
  static VALUE waiting_threads_count(VALUE module) {
98
163
  return ULL2NUM(waiting_threads_total);
99
164
  }
100
165
 
101
166
  static VALUE waiting_threads_reset(VALUE module) {
167
+ waiting_threads_current_generation++;
102
168
  waiting_threads_total = 0;
103
169
  return Qtrue;
104
170
  }
105
171
 
106
172
  // General callback
107
- static void gt_reset_thread_local_state(void) {
108
- // MRI can re-use native threads, so we need to reset thread local state,
109
- // otherwise it will leak from one Ruby thread from another.
110
- was_ready = false;
111
- local_timer_total = 0;
112
- }
113
-
114
173
  static void gt_thread_callback(rb_event_flag_t event, const rb_internal_thread_event_data_t *event_data, void *user_data) {
115
174
  switch(event) {
116
- 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;
117
181
  case RUBY_INTERNAL_THREAD_EVENT_EXITED: {
118
- 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
119
191
  }
120
192
  break;
121
193
  case RUBY_INTERNAL_THREAD_EVENT_READY: {
122
- 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;
123
199
 
124
200
  if (ENABLED(WAITING_THREADS)) {
201
+ state->waiting_threads_ready_generation = waiting_threads_current_generation;
125
202
  waiting_threads_total++;
126
203
  }
127
204
 
128
205
  if (ENABLED(TIMER_GLOBAL | TIMER_LOCAL)) {
129
- gt_gettime(&timer_ready_at);
206
+ gt_gettime(&state->timer_ready_at);
130
207
  }
131
208
  }
132
209
  break;
133
210
  case RUBY_INTERNAL_THREAD_EVENT_RESUMED: {
134
- 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
+ }
135
215
 
136
216
  if (ENABLED(WAITING_THREADS)) {
137
- waiting_threads_total--;
217
+ if (state->waiting_threads_ready_generation == waiting_threads_current_generation) {
218
+ waiting_threads_total--;
219
+ }
138
220
  }
139
221
 
140
222
  if (ENABLED(TIMER_GLOBAL | TIMER_LOCAL)) {
141
223
  struct timespec current_time;
142
224
  gt_gettime(&current_time);
143
- 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);
144
226
 
145
227
  if (ENABLED(TIMER_LOCAL)) {
146
- local_timer_total += diff;
228
+ state->timer_total += diff;
147
229
  }
148
230
 
149
231
  if (ENABLED(TIMER_GLOBAL)) {
@@ -156,6 +238,10 @@ static void gt_thread_callback(rb_event_flag_t event, const rb_internal_thread_e
156
238
  }
157
239
 
158
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
+
159
245
  VALUE rb_mGVLTools = rb_const_get(rb_cObject, rb_intern("GVLTools"));
160
246
 
161
247
  VALUE rb_mNative = rb_const_get(rb_mGVLTools, rb_intern("Native"));
@@ -167,11 +253,18 @@ void Init_instrumentation(void) {
167
253
  rb_define_singleton_method(rb_mGlobalTimer, "reset", global_timer_reset, 0);
168
254
  rb_define_singleton_method(rb_mGlobalTimer, "monotonic_time", global_timer_monotonic_time, 0);
169
255
 
170
- VALUE rb_mLocalTimer = rb_const_get(rb_mGVLTools, rb_intern("LocalTimer"));
171
- rb_define_singleton_method(rb_mLocalTimer, "reset", local_timer_reset, 0);
172
- 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
173
266
 
174
267
  VALUE rb_mWaitingThreads = rb_const_get(rb_mGVLTools, rb_intern("WaitingThreads"));
175
- rb_define_singleton_method(rb_mWaitingThreads, "reset", waiting_threads_reset, 0);
268
+ rb_define_singleton_method(rb_mWaitingThreads, "_reset", waiting_threads_reset, 0);
176
269
  rb_define_singleton_method(rb_mWaitingThreads, "count", waiting_threads_count, 0);
177
270
  }
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.2.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
@@ -96,8 +96,27 @@ module GVLTools
96
96
  end
97
97
  alias_method :count, :count
98
98
 
99
+ def enable
100
+ unless enabled?
101
+ reset
102
+ end
103
+ super
104
+ end
105
+
106
+ def reset
107
+ if enabled?
108
+ raise Error, "can't reset WaitingThreads counter while it is active"
109
+ else
110
+ _reset
111
+ end
112
+ end
113
+
99
114
  private
100
115
 
116
+ def _reset
117
+ end
118
+ alias_method :_reset, :_reset # to be redefined from C.
119
+
101
120
  def metric
102
121
  WAITING_THREADS
103
122
  end
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.2.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-03-28 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.6
56
+ rubygems_version: 3.5.3
71
57
  signing_key:
72
58
  specification_version: 4
73
59
  summary: Set of GVL instrumentation tools