gvl-tracing 1.4.0 → 1.5.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: d4945e5419655b2e69f310845e6214ff0bd6eb66ae820383f9cbe97855f15cdb
4
- data.tar.gz: dd95a9431c53c3904b054227a305da7433a789d8828b0340abc2b48864361735
3
+ metadata.gz: 9640e68d58cfbda91dc7c034982b3e43eec6048729d0ca61c8498495ad8f4277
4
+ data.tar.gz: ab154e734bfb085aff5615837efda1b7836784019fd689051b21db483f86c62b
5
5
  SHA512:
6
- metadata.gz: eca1b710f5918e887f06c44dfa970100e77c99bbc63318321a72e76dfffc4b44022b055dae4de05992494fc40910a467f4aa3ed045ab891018303a15fe8169e1
7
- data.tar.gz: 59deb53b041211513979526345af62c3e94495e73deae38d1f1e7272a11469695ae5c2435331bcf811f1c943c18f9602c4e3199c97fef9963cb3e3e4cd25f5cd
6
+ metadata.gz: a87a874de0373814d27f9e567c19fa4d36f5c2cdb5db2f3a6d7a7ce3280917dd8ecf0fc85b130f3573aa496cc5633427b92780f48ab00dc9cac8e6649763502a
7
+ data.tar.gz: c5ef422b98a1cf2835f62d0769c0e0ea2b7d0f6af36888f05617795e4667a0bf4bbcf404e0e860fbdf319ae2be8e41fd84f09c2168282e6a0adfbee92a252840
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.2.0
1
+ ruby-3.2.2
data/.standard.yml CHANGED
@@ -3,3 +3,7 @@ ruby_version: 3.2
3
3
  ignore:
4
4
  - 'examples/example1.rb':
5
5
  - Style/InfiniteLoop
6
+ - 'examples/example5.rb':
7
+ - Style/InfiniteLoop
8
+ - 'lib/gvl_tracing/sleep_tracking.rb':
9
+ - Style/MixinUsage
data/README.adoc CHANGED
@@ -8,9 +8,9 @@ A Ruby gem for getting a timeline view of Global VM Lock usage in your Ruby app
8
8
 
9
9
  image::preview.png[]
10
10
 
11
- See my blog post https://ivoanjo.me/blog/2022/07/17/tracing-ruby-global-vm-lock/[tracing ruby's (global) vm lock] for more details!
11
+ For instructions and examples on how to use it, see my https://ivoanjo.me/blog/2023/07/23/understanding-the-ruby-global-vm-lock-by-observing-it/[RubyKaigi 2023 talk on "Understanding the Ruby Global VM Lock by observing it"].
12
12
 
13
- NOTE: This gem only works on Ruby 3.2 and above because it depends on the https://github.com/ruby/ruby/pull/5500[GVL Instrumentation API].
13
+ NOTE: This gem only works on Ruby 3.2 and above because it depends on the https://github.com/ruby/ruby/pull/5500[GVL Instrumentation API]. Furthermore, the GVL Instrumentation API does not (as of Ruby 3.2 and 3.3) currently work on Microsoft Windows.
14
14
 
15
15
  == Quickest start
16
16
 
@@ -76,7 +76,9 @@ This way you can actually link from your dashboards and similar pages directly t
76
76
 
77
77
  == Development
78
78
 
79
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to https://rubygems.org[rubygems.org].
79
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to https://rubygems.org[rubygems.org]. To run specs, run `bundle exec rake spec`.
80
+
81
+ To run all actions (build the extension, check linting, and run specs), run `bundle exec rake`.
80
82
 
81
83
  == Contributing
82
84
 
data/Rakefile CHANGED
@@ -28,7 +28,9 @@
28
28
  require "bundler/gem_tasks"
29
29
  require "standard/rake"
30
30
  require "rake/extensiontask"
31
+ require "rspec/core/rake_task"
31
32
 
32
33
  Rake::ExtensionTask.new("gvl_tracing_native_extension")
34
+ RSpec::Core::RakeTask.new(:spec)
33
35
 
34
- task default: [:compile, :"standard:fix"]
36
+ task default: [:compile, :"standard:fix", :spec]
@@ -30,18 +30,27 @@ if ["jruby", "truffleruby"].include?(RUBY_ENGINE)
30
30
  "Perhaps a #{RUBY_ENGINE} equivalent could be created -- help is welcome! :)\n#{"-" * 80}"
31
31
  end
32
32
 
33
+ if Gem.win_platform?
34
+ raise \
35
+ "\n#{"-" * 80}\nSorry! This gem is currently unsupported on Microsoft Windows. That's because Ruby's GVL " \
36
+ "instrumentation API, which it relies on, also doesn't work on Windows.\n" \
37
+ "Hint: This gem does work on WSL."
38
+ end
39
+
33
40
  require "mkmf"
34
41
 
35
42
  have_func("gettid", "unistd.h")
36
43
  have_header("pthread.h")
37
44
  have_func("pthread_getname_np", "pthread.h")
38
45
  have_func("pthread_threadid_np", "pthread.h")
46
+ have_func("rb_internal_thread_specific_get", "ruby/thread.h") # 3.3+
47
+
39
48
  append_cflags("-Werror-implicit-function-declaration")
40
49
  append_cflags("-Wunused-parameter")
41
50
  append_cflags("-Wold-style-definition")
42
51
  append_cflags("-Wall")
43
52
  append_cflags("-Wextra")
44
- append_cflags("-Werror") if ENV['ENABLE_WERROR'] == 'true'
53
+ append_cflags("-Werror") if ENV["ENABLE_WERROR"] == "true"
45
54
 
46
55
  create_header
47
56
  create_makefile "gvl_tracing_native_extension"
@@ -31,6 +31,7 @@
31
31
  #include <inttypes.h>
32
32
  #include <stdbool.h>
33
33
  #include <sys/types.h>
34
+ #include <threads.h>
34
35
 
35
36
  #include "extconf.h"
36
37
 
@@ -49,21 +50,22 @@
49
50
  #define UNUSED_ARG
50
51
  #endif
51
52
 
52
- static VALUE tracing_start(VALUE _self, VALUE output_path);
53
- static VALUE tracing_stop(VALUE _self);
54
- static double timestamp_microseconds(void);
55
- static void set_native_thread_id(void);
56
- static void render_event(const char *event_name);
57
- static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
58
- static void on_gc_event(VALUE tpval, void *_unused1);
59
- static VALUE mark_sleeping(VALUE _self);
53
+ #ifdef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET
54
+ #define RUBY_3_3_PLUS
55
+ #else
56
+ #define RUBY_3_2
57
+ #endif
60
58
 
61
- // Thread-local state
62
- static _Thread_local bool current_thread_seen = false;
63
- static _Thread_local unsigned int current_thread_serial = 0;
64
- static _Thread_local uint64_t thread_id = 0;
65
- static _Thread_local rb_event_flag_t previous_state = 0; // Used to coalesce similar events
66
- static _Thread_local bool sleeping = false; // Used to track when a thread is sleeping
59
+ typedef struct {
60
+ bool initialized;
61
+ int32_t current_thread_serial;
62
+ #ifdef RUBY_3_2
63
+ int32_t native_thread_id;
64
+ #endif
65
+ VALUE thread;
66
+ rb_event_flag_t previous_state; // Used to coalesce similar events
67
+ bool sleeping; // Used to track when a thread is sleeping
68
+ } thread_local_state;
67
69
 
68
70
  // Global mutable state
69
71
  static rb_atomic_t thread_serial = 0;
@@ -72,47 +74,119 @@ static rb_internal_thread_event_hook_t *current_hook = NULL;
72
74
  static double started_tracing_at_microseconds = 0;
73
75
  static int64_t process_id = 0;
74
76
  static VALUE gc_tracepoint = Qnil;
77
+ #pragma GCC diagnostic ignored "-Wunused-variable"
78
+ static int thread_storage_key = 0;
79
+ static VALUE all_seen_threads = Qnil;
80
+ static mtx_t all_seen_threads_mutex;
81
+
82
+ static VALUE tracing_init_local_storage(VALUE, VALUE);
83
+ static VALUE tracing_start(VALUE _self, VALUE output_path);
84
+ static VALUE tracing_stop(VALUE _self);
85
+ static double timestamp_microseconds(void);
86
+ static void render_event(thread_local_state *, const char *event_name);
87
+ static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
88
+ static void on_gc_event(VALUE tpval, void *_unused1);
89
+ static VALUE mark_sleeping(VALUE _self);
90
+ static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused);
91
+ static void thread_local_state_mark(void *data);
92
+ static inline int32_t thread_id_for(thread_local_state *state);
93
+ static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread);
94
+ static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self);
95
+
96
+ #pragma GCC diagnostic ignored "-Wunused-const-variable"
97
+ static const rb_data_type_t thread_local_state_type = {
98
+ .wrap_struct_name = "GvlTracing::__threadLocal",
99
+ .function = {
100
+ .dmark = thread_local_state_mark,
101
+ .dfree = RUBY_DEFAULT_FREE,
102
+ .dsize = thread_local_state_memsize,
103
+ },
104
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED,
105
+ };
106
+
107
+ #ifdef RUBY_3_3_PLUS
108
+ static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate);
109
+ #define GT_EVENT_LOCAL_STATE(event_data, allocate) GT_LOCAL_STATE(event_data->thread, allocate)
110
+ // Must only be called from a thread holding the GVL
111
+ #define GT_CURRENT_THREAD_LOCAL_STATE() GT_LOCAL_STATE(rb_thread_current(), true)
112
+ #else
113
+ // Thread-local state
114
+ static _Thread_local thread_local_state __thread_local_state = { 0 };
115
+
116
+ static inline thread_local_state *GT_CURRENT_THREAD_LOCAL_STATE(void);
117
+ #define GT_LOCAL_STATE(thread, allocate) GT_CURRENT_THREAD_LOCAL_STATE()
118
+ #define GT_EVENT_LOCAL_STATE(event_data, allocate) GT_CURRENT_THREAD_LOCAL_STATE()
119
+ #endif
75
120
 
76
121
  void Init_gvl_tracing_native_extension(void) {
122
+ #ifdef RUBY_3_3_PLUS
123
+ thread_storage_key = rb_internal_thread_specific_key_create();
124
+ #endif
125
+
77
126
  rb_global_variable(&gc_tracepoint);
127
+ rb_global_variable(&all_seen_threads);
128
+
129
+ all_seen_threads = rb_ary_new();
130
+
131
+ if (mtx_init(&all_seen_threads_mutex, mtx_plain) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to initialize GvlTracing mutex");
78
132
 
79
133
  VALUE gvl_tracing_module = rb_define_module("GvlTracing");
80
134
 
135
+ rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
81
136
  rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 1);
82
137
  rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
83
138
  rb_define_singleton_method(gvl_tracing_module, "mark_sleeping", mark_sleeping, 0);
139
+ rb_define_singleton_method(gvl_tracing_module, "_thread_id_for", ruby_thread_id_for, 1);
140
+ rb_define_singleton_method(gvl_tracing_module, "trim_all_seen_threads", trim_all_seen_threads, 0);
84
141
  }
85
142
 
86
- static inline void initialize_thread_id(void) {
87
- current_thread_seen = true;
88
- current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
89
- set_native_thread_id();
90
- }
143
+ static inline void initialize_thread_local_state(thread_local_state *state) {
144
+ state->initialized = true;
145
+ state->current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
91
146
 
92
- static inline void render_thread_metadata(void) {
93
- char native_thread_name_buffer[64] = "(unnamed)";
147
+ #ifdef RUBY_3_2
148
+ uint32_t native_thread_id = 0;
94
149
 
95
- #ifdef HAVE_PTHREAD_GETNAME_NP
96
- pthread_getname_np(pthread_self(), native_thread_name_buffer, sizeof(native_thread_name_buffer));
150
+ #ifdef HAVE_PTHREAD_THREADID_NP
151
+ pthread_threadid_np(pthread_self(), &native_thread_id);
152
+ #elif HAVE_GETTID
153
+ native_thread_id = gettid();
154
+ #else
155
+ // Note: We could use the current_thread_serial as a crappy fallback, but this would make getting thread names
156
+ // not work very well
157
+ #error No native thread id available?
158
+ #endif
159
+
160
+ state->native_thread_id = native_thread_id;
97
161
  #endif
162
+ }
98
163
 
99
- fprintf(output_file,
100
- " {\"ph\": \"M\", \"pid\": %"PRId64", \"tid\": %"PRIu64", \"name\": \"thread_name\", \"args\": {\"name\": \"%s\"}},\n",
101
- process_id, thread_id, native_thread_name_buffer);
164
+ static VALUE tracing_init_local_storage(UNUSED_ARG VALUE _self, VALUE threads) {
165
+ #ifdef RUBY_3_3_PLUS
166
+ for (long i = 0, len = RARRAY_LEN(threads); i < len; i++) {
167
+ VALUE thread = RARRAY_AREF(threads, i);
168
+ GT_LOCAL_STATE(thread, true);
169
+ }
170
+ #endif
171
+ return Qtrue;
102
172
  }
103
173
 
104
174
  static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
105
175
  Check_Type(output_path, T_STRING);
106
176
 
177
+ trim_all_seen_threads(Qnil);
178
+
107
179
  if (output_file != NULL) rb_raise(rb_eRuntimeError, "Already started");
108
180
  output_file = fopen(StringValuePtr(output_path), "w");
109
181
  if (output_file == NULL) rb_syserr_fail(errno, "Failed to open GvlTracing output file");
110
182
 
183
+ fprintf(output_file, "[\n");
184
+
185
+ thread_local_state *state = GT_CURRENT_THREAD_LOCAL_STATE();
111
186
  started_tracing_at_microseconds = timestamp_microseconds();
112
187
  process_id = getpid();
113
188
 
114
- fprintf(output_file, "[\n");
115
- render_event("started_tracing");
189
+ render_event(state, "started_tracing");
116
190
 
117
191
  current_hook = rb_internal_thread_add_event_hook(
118
192
  on_thread_event,
@@ -126,15 +200,8 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
126
200
  NULL
127
201
  );
128
202
 
129
- gc_tracepoint = rb_tracepoint_new(
130
- 0,
131
- (
132
- RUBY_INTERNAL_EVENT_GC_ENTER |
133
- RUBY_INTERNAL_EVENT_GC_EXIT
134
- ),
135
- on_gc_event,
136
- (void *) NULL
137
- );
203
+ gc_tracepoint = rb_tracepoint_new(0, (RUBY_INTERNAL_EVENT_GC_ENTER | RUBY_INTERNAL_EVENT_GC_EXIT), on_gc_event, NULL);
204
+
138
205
  rb_tracepoint_enable(gc_tracepoint);
139
206
 
140
207
  return Qtrue;
@@ -143,18 +210,23 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
143
210
  static VALUE tracing_stop(UNUSED_ARG VALUE _self) {
144
211
  if (output_file == NULL) rb_raise(rb_eRuntimeError, "Tracing not running");
145
212
 
213
+ thread_local_state *state = GT_CURRENT_THREAD_LOCAL_STATE();
146
214
  rb_internal_thread_remove_event_hook(current_hook);
147
215
  rb_tracepoint_disable(gc_tracepoint);
148
216
  gc_tracepoint = Qnil;
149
217
 
150
- render_event("stopped_tracing");
218
+ render_event(state, "stopped_tracing");
151
219
  // closing the json syntax in the output file is handled in GvlTracing.stop code
152
220
 
153
221
  if (fclose(output_file) != 0) rb_syserr_fail(errno, "Failed to close GvlTracing output file");
154
222
 
155
223
  output_file = NULL;
156
224
 
157
- return Qtrue;
225
+ #ifdef RUBY_3_3_PLUS
226
+ return all_seen_threads;
227
+ #else
228
+ return rb_funcall(rb_cThread, rb_intern("list"), 0);
229
+ #endif
158
230
  }
159
231
 
160
232
  static double timestamp_microseconds(void) {
@@ -163,31 +235,12 @@ static double timestamp_microseconds(void) {
163
235
  return (current_monotonic.tv_nsec / 1000.0) + (current_monotonic.tv_sec * 1000.0 * 1000.0);
164
236
  }
165
237
 
166
- static void set_native_thread_id(void) {
167
- uint64_t native_thread_id = 0;
168
-
169
- #ifdef HAVE_PTHREAD_THREADID_NP
170
- pthread_threadid_np(pthread_self(), &native_thread_id);
171
- #elif HAVE_GETTID
172
- native_thread_id = gettid();
173
- #else
174
- native_thread_id = current_thread_serial; // TODO: Better fallback for Windows?
175
- #endif
176
-
177
- thread_id = native_thread_id;
178
- }
179
-
180
238
  // Render output using trace event format for perfetto:
181
239
  // https://chromium.googlesource.com/catapult/+/refs/heads/main/docs/trace-event-format.md
182
- static void render_event(const char *event_name) {
240
+ static void render_event(thread_local_state *state, const char *event_name) {
183
241
  // Event data
184
242
  double now_microseconds = timestamp_microseconds() - started_tracing_at_microseconds;
185
243
 
186
- if (!current_thread_seen) {
187
- initialize_thread_id();
188
- render_thread_metadata();
189
- }
190
-
191
244
  // Each event is converted into two events in the output: one that signals the end of the previous event
192
245
  // (whatever it was), and one that signals the start of the actual event we're processing.
193
246
  // Yes, this seems to be slightly bending the intention of the output format, but it seemed easier to do this way.
@@ -197,32 +250,39 @@ static void render_event(const char *event_name) {
197
250
 
198
251
  fprintf(output_file,
199
252
  // Finish previous duration
200
- " {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %"PRIu64", \"ts\": %f},\n" \
253
+ " {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f},\n" \
201
254
  // Current event
202
- " {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %"PRIu64", \"ts\": %f, \"name\": \"%s\"},\n",
255
+ " {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f, \"name\": \"%s\"},\n",
203
256
  // Args for first line
204
- process_id, thread_id, now_microseconds,
257
+ process_id, thread_id_for(state), now_microseconds,
205
258
  // Args for second line
206
- process_id, thread_id, now_microseconds, event_name
259
+ process_id, thread_id_for(state), now_microseconds, event_name
207
260
  );
208
261
  }
209
262
 
210
- static void on_thread_event(rb_event_flag_t event_id, UNUSED_ARG const rb_internal_thread_event_data_t *_unused1, UNUSED_ARG void *_unused2) {
211
- // In some cases, Ruby seems to even multiple suspended events for the same thread in a row (e.g. when multiple threads)
263
+ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *event_data, UNUSED_ARG void *_unused2) {
264
+ thread_local_state *state = GT_EVENT_LOCAL_STATE(event_data,
265
+ // These events are guaranteed to hold the GVL, so they can allocate
266
+ event_id & (RUBY_INTERNAL_THREAD_EVENT_STARTED | RUBY_INTERNAL_THREAD_EVENT_RESUMED));
267
+ if (!state) return;
268
+ #ifdef RUBY_3_3_PLUS
269
+ if (!state->thread) state->thread = event_data->thread;
270
+ #endif
271
+ // In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
212
272
  // are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
213
273
  // timeline easier to see.
214
274
  //
215
275
  // I haven't observed other situations where we'd want to coalesce events, but we may apply this to all events in the
216
276
  // future. One annoying thing to remember when generalizing this is how to reset the `previous_state` across multiple
217
277
  // start/stop calls to GvlTracing.
218
- if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == previous_state) return;
219
- previous_state = event_id;
278
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
279
+ state->previous_state = event_id;
220
280
 
221
- if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && sleeping) {
222
- render_event("sleeping");
281
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && state->sleeping) {
282
+ render_event(state, "sleeping");
223
283
  return;
224
284
  } else {
225
- sleeping = false;
285
+ state->sleeping = false;
226
286
  }
227
287
 
228
288
  const char* event_name = "bug_unknown_event";
@@ -233,20 +293,106 @@ static void on_thread_event(rb_event_flag_t event_id, UNUSED_ARG const rb_intern
233
293
  case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
234
294
  case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "died"; break;
235
295
  };
236
- render_event(event_name);
296
+ render_event(state, event_name);
237
297
  }
238
298
 
239
299
  static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
240
300
  const char* event_name = "bug_unknown_event";
301
+ thread_local_state *state = GT_LOCAL_STATE(rb_thread_current(), false); // no alloc during GC
241
302
  switch (rb_tracearg_event_flag(rb_tracearg_from_tracepoint(tpval))) {
242
303
  case RUBY_INTERNAL_EVENT_GC_ENTER: event_name = "gc"; break;
243
304
  // TODO: is it possible the thread wasn't running? Might need to save the last state.
244
305
  case RUBY_INTERNAL_EVENT_GC_EXIT: event_name = "running"; break;
245
306
  }
246
- render_event(event_name);
307
+ render_event(state, event_name);
247
308
  }
248
309
 
249
- static VALUE mark_sleeping(VALUE _self) {
250
- sleeping = true;
310
+ static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
311
+ GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
251
312
  return Qnil;
252
313
  }
314
+
315
+ static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
316
+
317
+ static void thread_local_state_mark(void *data) {
318
+ thread_local_state *state = (thread_local_state *)data;
319
+ rb_gc_mark(state->thread); // Marking thread to make sure it stays pinned
320
+ }
321
+
322
+ #ifdef RUBY_3_3_PLUS
323
+ static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
324
+ thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
325
+ if (!state && allocate) {
326
+ VALUE wrapper = TypedData_Make_Struct(rb_cObject, thread_local_state, &thread_local_state_type, state);
327
+ state->thread = thread;
328
+ rb_thread_local_aset(thread, rb_intern("__gvl_tracing_local_state"), wrapper);
329
+ rb_internal_thread_specific_set(thread, thread_storage_key, state);
330
+ RB_GC_GUARD(wrapper);
331
+ initialize_thread_local_state(state);
332
+
333
+ // Keep thread around, to be able to extract names at the end
334
+ // We grab a lock here since thread creation can happen in multiple Ractors, and we want to make sure only one
335
+ // of them is mutating the array at a time. @ivoanjo: I think this is enough to make this safe....?
336
+ if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
337
+ rb_ary_push(all_seen_threads, thread);
338
+ if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
339
+ }
340
+ return state;
341
+ }
342
+ #endif
343
+
344
+ #ifdef RUBY_3_2
345
+ static inline thread_local_state *GT_CURRENT_THREAD_LOCAL_STATE(void) {
346
+ thread_local_state *state = &__thread_local_state;
347
+ if (!state->initialized) {
348
+ initialize_thread_local_state(state);
349
+ }
350
+ return state;
351
+ }
352
+ #endif
353
+
354
+ static inline int32_t thread_id_for(thread_local_state *state) {
355
+ // We use different strategies for 3.2 vs 3.3+ to identify threads. This is because:
356
+ //
357
+ // 1. On 3.2 we have no way of associating the actual thread VALUE object with the state/serial, so instead we identify
358
+ // threads by their native ids. This is not entirely correct, since Ruby can reuse native threads (e.g. if a thread
359
+ // dies and another immediately gets created) but it's good enough for our purposes. (Associating the thread VALUE
360
+ // object is useful to, e.g. get thread names later.)
361
+ //
362
+ // 2. On 3.3 we can associate the state/serial with the thread VALUE object AND additionally with the MN scheduler
363
+ // the same thread VALUE can end up executing on different native threads so using the native thread id as an
364
+ // identifier would be wrong.
365
+ #ifdef RUBY_3_3_PLUS
366
+ return state->current_thread_serial;
367
+ #else
368
+ return state->native_thread_id;
369
+ #endif
370
+ }
371
+
372
+ static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread) {
373
+ #ifdef RUBY_3_2
374
+ rb_raise(rb_eRuntimeError, "On Ruby 3.2 we should use the native thread id directly");
375
+ #endif
376
+
377
+ thread_local_state *state = GT_LOCAL_STATE(thread, true);
378
+ return INT2FIX(thread_id_for(state));
379
+ }
380
+
381
+ // Can only be called while GvlTracing is not active + while holding the GVL
382
+ static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
383
+ if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
384
+
385
+ VALUE alive_threads = rb_ary_new();
386
+
387
+ for (long i = 0, len = RARRAY_LEN(all_seen_threads); i < len; i++) {
388
+ VALUE thread = RARRAY_AREF(all_seen_threads, i);
389
+ if (rb_funcall(thread, rb_intern("alive?"), 0) == Qtrue) {
390
+ rb_ary_push(alive_threads, thread);
391
+ }
392
+ }
393
+
394
+ rb_ary_replace(all_seen_threads, alive_threads);
395
+
396
+ if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
397
+ return Qtrue;
398
+ }
data/gems.rb CHANGED
@@ -7,5 +7,8 @@ gemspec
7
7
  gem "rake", "~> 13.0"
8
8
  gem "rake-compiler", "~> 1.2"
9
9
  gem "pry"
10
- gem "standard", "~> 1.12"
10
+ gem "pry-byebug"
11
+ gem "rspec"
12
+ gem "standard", "~> 1.33"
11
13
  gem "concurrent-ruby"
14
+ gem "benchmark-ips", "~> 2.13"
data/lib/gvl-tracing.rb CHANGED
@@ -36,6 +36,7 @@ module GvlTracing
36
36
 
37
37
  def start(file)
38
38
  _start(file)
39
+ _init_local_storage(Thread.list)
39
40
  @path = file
40
41
 
41
42
  return unless block_given?
@@ -43,24 +44,24 @@ module GvlTracing
43
44
  begin
44
45
  yield
45
46
  ensure
46
- _stop
47
+ stop
47
48
  end
48
49
  end
49
50
 
50
51
  def stop
51
- thread_list = Thread.list
52
-
53
- _stop
52
+ thread_list = _stop
54
53
 
55
54
  append_thread_names(thread_list)
55
+
56
+ trim_all_seen_threads
56
57
  end
57
58
 
58
59
  private
59
60
 
60
61
  def append_thread_names(list)
61
- threads_name = aggreate_thread_list(list).join(",\n")
62
- File.open(@path, 'a') do |f|
63
- f.puts(threads_name)
62
+ thread_names = aggreate_thread_list(list).join(",\n")
63
+ File.open(@path, "a") do |f|
64
+ f.puts(thread_names)
64
65
  f.puts("]")
65
66
  end
66
67
  end
@@ -69,14 +70,14 @@ module GvlTracing
69
70
  list.each_with_object([]) do |t, acc|
70
71
  next unless t.name || t == Thread.main
71
72
 
72
- acc << " {\"ph\": \"M\", \"pid\": #{Process.pid}, \"tid\": #{t.native_thread_id}, \"name\": \"thread_name\", \"args\": {\"name\": \"#{thread_label(t)}\"}}"
73
+ acc << " {\"ph\": \"M\", \"pid\": #{Process.pid}, \"tid\": #{thread_id_for(t)}, \"name\": \"thread_name\", \"args\": {\"name\": \"#{thread_label(t)}\"}}"
73
74
  end
74
75
  end
75
76
 
76
77
  REGEX = /lib(?!.*lib)\/([a-zA-Z-]+)/
77
78
  def thread_label(thread)
78
79
  if thread == Thread.main
79
- return thread.name ? thread.name : "Main Thread"
80
+ return thread.name || "Main Thread"
80
81
  end
81
82
 
82
83
  lib_name = thread.to_s.match(REGEX)
@@ -85,5 +86,12 @@ module GvlTracing
85
86
 
86
87
  "#{thread.name} from #{lib_name[1]}"
87
88
  end
89
+
90
+ def thread_id_for(t)
91
+ RUBY_VERSION.start_with?("3.2.") ? t.native_thread_id : _thread_id_for(t)
92
+ end
88
93
  end
89
94
  end
95
+
96
+ # Eagerly initialize context for main thread
97
+ GvlTracing.send(:thread_id_for, Thread.main)
@@ -26,5 +26,5 @@
26
26
  # frozen_string_literal: true
27
27
 
28
28
  module GvlTracing
29
- VERSION = "1.4.0"
29
+ VERSION = "1.5.0"
30
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gvl-tracing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivo Anjo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-05 00:00:00.000000000 Z
11
+ date: 2024-03-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -53,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  requirements: []
56
- rubygems_version: 3.4.1
56
+ rubygems_version: 3.5.3
57
57
  signing_key:
58
58
  specification_version: 4
59
59
  summary: Get a timeline view of Global VM Lock usage in your Ruby app