gvl-tracing 1.4.0 → 1.5.1

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: d4945e5419655b2e69f310845e6214ff0bd6eb66ae820383f9cbe97855f15cdb
4
- data.tar.gz: dd95a9431c53c3904b054227a305da7433a789d8828b0340abc2b48864361735
3
+ metadata.gz: 47df73ea238560430f56d7b1ee2f0484e9e57b8f0784e065209cd5cfbe7b1084
4
+ data.tar.gz: 1517522202fd357b31db4a087f769991ccf9f0a0c56b38191f64976827b3e752
5
5
  SHA512:
6
- metadata.gz: eca1b710f5918e887f06c44dfa970100e77c99bbc63318321a72e76dfffc4b44022b055dae4de05992494fc40910a467f4aa3ed045ab891018303a15fe8169e1
7
- data.tar.gz: 59deb53b041211513979526345af62c3e94495e73deae38d1f1e7272a11469695ae5c2435331bcf811f1c943c18f9602c4e3199c97fef9963cb3e3e4cd25f5cd
6
+ metadata.gz: ed938fa078cb982b23efe5da8a1a0fcb6d367534e7eeb342b2ce4853bf82cb7f0bb7998fd1a0bfa3bc3fd61a9f5d049717e245235dcf3df5a072be0b54eb9c2b
7
+ data.tar.gz: c1f2db9dbde2a28321deee3b4186c08eb06f3da1f231f652d1822692e67e479e7be3a7b22daada77bc8130e9d6e016027b8470f9c84eceb827f4e06a17ac6d12
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);
146
+
147
+ #ifdef RUBY_3_2
148
+ uint32_t native_thread_id = 0;
91
149
 
92
- static inline void render_thread_metadata(void) {
93
- char native_thread_name_buffer[64] = "(unnamed)";
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
94
159
 
95
- #ifdef HAVE_PTHREAD_GETNAME_NP
96
- pthread_getname_np(pthread_self(), native_thread_name_buffer, sizeof(native_thread_name_buffer));
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,41 @@ 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
+
268
+ if (!state) return;
269
+
270
+ #ifdef RUBY_3_3_PLUS
271
+ if (!state->thread) state->thread = event_data->thread;
272
+ #endif
273
+ // In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
212
274
  // are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
213
275
  // timeline easier to see.
214
276
  //
215
277
  // I haven't observed other situations where we'd want to coalesce events, but we may apply this to all events in the
216
278
  // future. One annoying thing to remember when generalizing this is how to reset the `previous_state` across multiple
217
279
  // start/stop calls to GvlTracing.
218
- if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == previous_state) return;
219
- previous_state = event_id;
280
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
281
+ state->previous_state = event_id;
220
282
 
221
- if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && sleeping) {
222
- render_event("sleeping");
283
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && state->sleeping) {
284
+ render_event(state, "sleeping");
223
285
  return;
224
286
  } else {
225
- sleeping = false;
287
+ state->sleeping = false;
226
288
  }
227
289
 
228
290
  const char* event_name = "bug_unknown_event";
@@ -233,20 +295,109 @@ static void on_thread_event(rb_event_flag_t event_id, UNUSED_ARG const rb_intern
233
295
  case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
234
296
  case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "died"; break;
235
297
  };
236
- render_event(event_name);
298
+ render_event(state, event_name);
237
299
  }
238
300
 
239
301
  static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
240
302
  const char* event_name = "bug_unknown_event";
303
+ thread_local_state *state = GT_LOCAL_STATE(rb_thread_current(), false); // no alloc during GC
304
+
305
+ if (!state) return;
306
+
241
307
  switch (rb_tracearg_event_flag(rb_tracearg_from_tracepoint(tpval))) {
242
308
  case RUBY_INTERNAL_EVENT_GC_ENTER: event_name = "gc"; break;
243
309
  // TODO: is it possible the thread wasn't running? Might need to save the last state.
244
310
  case RUBY_INTERNAL_EVENT_GC_EXIT: event_name = "running"; break;
245
311
  }
246
- render_event(event_name);
312
+ render_event(state, event_name);
247
313
  }
248
314
 
249
- static VALUE mark_sleeping(VALUE _self) {
250
- sleeping = true;
315
+ static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
316
+ GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
251
317
  return Qnil;
252
318
  }
319
+
320
+ static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
321
+
322
+ static void thread_local_state_mark(void *data) {
323
+ thread_local_state *state = (thread_local_state *)data;
324
+ rb_gc_mark(state->thread); // Marking thread to make sure it stays pinned
325
+ }
326
+
327
+ #ifdef RUBY_3_3_PLUS
328
+ static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
329
+ thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
330
+ if (!state && allocate) {
331
+ VALUE wrapper = TypedData_Make_Struct(rb_cObject, thread_local_state, &thread_local_state_type, state);
332
+ state->thread = thread;
333
+ rb_thread_local_aset(thread, rb_intern("__gvl_tracing_local_state"), wrapper);
334
+ rb_internal_thread_specific_set(thread, thread_storage_key, state);
335
+ RB_GC_GUARD(wrapper);
336
+ initialize_thread_local_state(state);
337
+
338
+ // Keep thread around, to be able to extract names at the end
339
+ // We grab a lock here since thread creation can happen in multiple Ractors, and we want to make sure only one
340
+ // of them is mutating the array at a time. @ivoanjo: I think this is enough to make this safe....?
341
+ if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
342
+ rb_ary_push(all_seen_threads, thread);
343
+ if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
344
+ }
345
+ return state;
346
+ }
347
+ #endif
348
+
349
+ #ifdef RUBY_3_2
350
+ static inline thread_local_state *GT_CURRENT_THREAD_LOCAL_STATE(void) {
351
+ thread_local_state *state = &__thread_local_state;
352
+ if (!state->initialized) {
353
+ initialize_thread_local_state(state);
354
+ }
355
+ return state;
356
+ }
357
+ #endif
358
+
359
+ static inline int32_t thread_id_for(thread_local_state *state) {
360
+ // We use different strategies for 3.2 vs 3.3+ to identify threads. This is because:
361
+ //
362
+ // 1. On 3.2 we have no way of associating the actual thread VALUE object with the state/serial, so instead we identify
363
+ // threads by their native ids. This is not entirely correct, since Ruby can reuse native threads (e.g. if a thread
364
+ // dies and another immediately gets created) but it's good enough for our purposes. (Associating the thread VALUE
365
+ // object is useful to, e.g. get thread names later.)
366
+ //
367
+ // 2. On 3.3 we can associate the state/serial with the thread VALUE object AND additionally with the MN scheduler
368
+ // the same thread VALUE can end up executing on different native threads so using the native thread id as an
369
+ // identifier would be wrong.
370
+ #ifdef RUBY_3_3_PLUS
371
+ return state->current_thread_serial;
372
+ #else
373
+ return state->native_thread_id;
374
+ #endif
375
+ }
376
+
377
+ static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread) {
378
+ #ifdef RUBY_3_2
379
+ rb_raise(rb_eRuntimeError, "On Ruby 3.2 we should use the native thread id directly");
380
+ #endif
381
+
382
+ thread_local_state *state = GT_LOCAL_STATE(thread, true);
383
+ return INT2FIX(thread_id_for(state));
384
+ }
385
+
386
+ // Can only be called while GvlTracing is not active + while holding the GVL
387
+ static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
388
+ if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
389
+
390
+ VALUE alive_threads = rb_ary_new();
391
+
392
+ for (long i = 0, len = RARRAY_LEN(all_seen_threads); i < len; i++) {
393
+ VALUE thread = RARRAY_AREF(all_seen_threads, i);
394
+ if (rb_funcall(thread, rb_intern("alive?"), 0) == Qtrue) {
395
+ rb_ary_push(alive_threads, thread);
396
+ }
397
+ }
398
+
399
+ rb_ary_replace(all_seen_threads, alive_threads);
400
+
401
+ if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
402
+ return Qtrue;
403
+ }
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.1"
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.1
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.4.10
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