gvl-tracing 1.3.0 → 1.5.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: a5ec59682543834d531647bea2601457de0d8f8d57fe039181375891ef29ae60
4
- data.tar.gz: ebfa80876458868eb9780bdf655c75ff7c3b649f0661799533e6dcd12f5b06ea
3
+ metadata.gz: 9640e68d58cfbda91dc7c034982b3e43eec6048729d0ca61c8498495ad8f4277
4
+ data.tar.gz: ab154e734bfb085aff5615837efda1b7836784019fd689051b21db483f86c62b
5
5
  SHA512:
6
- metadata.gz: 259ae5c91f2d750ed61ec938a3eb3f92fc813391393420648288626d824408f1678a534de2185c73a6ea8fc2a695aa46135f5529a8fa9bd32e00c0f878b5a538
7
- data.tar.gz: ead5ce15933a39684675705a60983dbcd5fc8afb7752822103799261d05d5e3b3f6da8562ca496b9929f7bebf9ed203c31ccc5b778be59bf2c724f7bab9c4d49
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,18 +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);
53
+ #ifdef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET
54
+ #define RUBY_3_3_PLUS
55
+ #else
56
+ #define RUBY_3_2
57
+ #endif
59
58
 
60
- // Thread-local state
61
- static _Thread_local bool current_thread_seen = false;
62
- static _Thread_local unsigned int current_thread_serial = 0;
63
- static _Thread_local uint64_t thread_id = 0;
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;
64
69
 
65
70
  // Global mutable state
66
71
  static rb_atomic_t thread_serial = 0;
@@ -69,46 +74,119 @@ static rb_internal_thread_event_hook_t *current_hook = NULL;
69
74
  static double started_tracing_at_microseconds = 0;
70
75
  static int64_t process_id = 0;
71
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
72
120
 
73
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
+
74
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");
75
132
 
76
133
  VALUE gvl_tracing_module = rb_define_module("GvlTracing");
77
134
 
135
+ rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
78
136
  rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 1);
79
137
  rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
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);
80
141
  }
81
142
 
82
- static inline void initialize_thread_id(void) {
83
- current_thread_seen = true;
84
- current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
85
- set_native_thread_id();
86
- }
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);
87
146
 
88
- static inline void render_thread_metadata(void) {
89
- char native_thread_name_buffer[64] = "(unnamed)";
147
+ #ifdef RUBY_3_2
148
+ uint32_t native_thread_id = 0;
90
149
 
91
- #ifdef HAVE_PTHREAD_GETNAME_NP
92
- 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;
93
161
  #endif
162
+ }
94
163
 
95
- fprintf(output_file,
96
- " {\"ph\": \"M\", \"pid\": %"PRId64", \"tid\": %"PRIu64", \"name\": \"thread_name\", \"args\": {\"name\": \"%s\"}},\n",
97
- 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;
98
172
  }
99
173
 
100
174
  static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
101
175
  Check_Type(output_path, T_STRING);
102
176
 
177
+ trim_all_seen_threads(Qnil);
178
+
103
179
  if (output_file != NULL) rb_raise(rb_eRuntimeError, "Already started");
104
180
  output_file = fopen(StringValuePtr(output_path), "w");
105
181
  if (output_file == NULL) rb_syserr_fail(errno, "Failed to open GvlTracing output file");
106
182
 
183
+ fprintf(output_file, "[\n");
184
+
185
+ thread_local_state *state = GT_CURRENT_THREAD_LOCAL_STATE();
107
186
  started_tracing_at_microseconds = timestamp_microseconds();
108
187
  process_id = getpid();
109
188
 
110
- fprintf(output_file, "[\n");
111
- render_event("started_tracing");
189
+ render_event(state, "started_tracing");
112
190
 
113
191
  current_hook = rb_internal_thread_add_event_hook(
114
192
  on_thread_event,
@@ -122,15 +200,8 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
122
200
  NULL
123
201
  );
124
202
 
125
- gc_tracepoint = rb_tracepoint_new(
126
- 0,
127
- (
128
- RUBY_INTERNAL_EVENT_GC_ENTER |
129
- RUBY_INTERNAL_EVENT_GC_EXIT
130
- ),
131
- on_gc_event,
132
- (void *) NULL
133
- );
203
+ gc_tracepoint = rb_tracepoint_new(0, (RUBY_INTERNAL_EVENT_GC_ENTER | RUBY_INTERNAL_EVENT_GC_EXIT), on_gc_event, NULL);
204
+
134
205
  rb_tracepoint_enable(gc_tracepoint);
135
206
 
136
207
  return Qtrue;
@@ -139,18 +210,23 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
139
210
  static VALUE tracing_stop(UNUSED_ARG VALUE _self) {
140
211
  if (output_file == NULL) rb_raise(rb_eRuntimeError, "Tracing not running");
141
212
 
213
+ thread_local_state *state = GT_CURRENT_THREAD_LOCAL_STATE();
142
214
  rb_internal_thread_remove_event_hook(current_hook);
143
215
  rb_tracepoint_disable(gc_tracepoint);
144
216
  gc_tracepoint = Qnil;
145
217
 
146
- render_event("stopped_tracing");
218
+ render_event(state, "stopped_tracing");
147
219
  // closing the json syntax in the output file is handled in GvlTracing.stop code
148
220
 
149
221
  if (fclose(output_file) != 0) rb_syserr_fail(errno, "Failed to close GvlTracing output file");
150
222
 
151
223
  output_file = NULL;
152
224
 
153
- 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
154
230
  }
155
231
 
156
232
  static double timestamp_microseconds(void) {
@@ -159,31 +235,12 @@ static double timestamp_microseconds(void) {
159
235
  return (current_monotonic.tv_nsec / 1000.0) + (current_monotonic.tv_sec * 1000.0 * 1000.0);
160
236
  }
161
237
 
162
- static void set_native_thread_id(void) {
163
- uint64_t native_thread_id = 0;
164
-
165
- #ifdef HAVE_PTHREAD_THREADID_NP
166
- pthread_threadid_np(pthread_self(), &native_thread_id);
167
- #elif HAVE_GETTID
168
- native_thread_id = gettid();
169
- #else
170
- native_thread_id = current_thread_serial; // TODO: Better fallback for Windows?
171
- #endif
172
-
173
- thread_id = native_thread_id;
174
- }
175
-
176
238
  // Render output using trace event format for perfetto:
177
239
  // https://chromium.googlesource.com/catapult/+/refs/heads/main/docs/trace-event-format.md
178
- static void render_event(const char *event_name) {
240
+ static void render_event(thread_local_state *state, const char *event_name) {
179
241
  // Event data
180
242
  double now_microseconds = timestamp_microseconds() - started_tracing_at_microseconds;
181
243
 
182
- if (!current_thread_seen) {
183
- initialize_thread_id();
184
- render_thread_metadata();
185
- }
186
-
187
244
  // Each event is converted into two events in the output: one that signals the end of the previous event
188
245
  // (whatever it was), and one that signals the start of the actual event we're processing.
189
246
  // Yes, this seems to be slightly bending the intention of the output format, but it seemed easier to do this way.
@@ -193,17 +250,41 @@ static void render_event(const char *event_name) {
193
250
 
194
251
  fprintf(output_file,
195
252
  // Finish previous duration
196
- " {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %"PRIu64", \"ts\": %f},\n" \
253
+ " {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f},\n" \
197
254
  // Current event
198
- " {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %"PRIu64", \"ts\": %f, \"name\": \"%s\"},\n",
255
+ " {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f, \"name\": \"%s\"},\n",
199
256
  // Args for first line
200
- process_id, thread_id, now_microseconds,
257
+ process_id, thread_id_for(state), now_microseconds,
201
258
  // Args for second line
202
- process_id, thread_id, now_microseconds, event_name
259
+ process_id, thread_id_for(state), now_microseconds, event_name
203
260
  );
204
261
  }
205
262
 
206
- 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) {
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)
272
+ // are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
273
+ // timeline easier to see.
274
+ //
275
+ // I haven't observed other situations where we'd want to coalesce events, but we may apply this to all events in the
276
+ // future. One annoying thing to remember when generalizing this is how to reset the `previous_state` across multiple
277
+ // start/stop calls to GvlTracing.
278
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
279
+ state->previous_state = event_id;
280
+
281
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && state->sleeping) {
282
+ render_event(state, "sleeping");
283
+ return;
284
+ } else {
285
+ state->sleeping = false;
286
+ }
287
+
207
288
  const char* event_name = "bug_unknown_event";
208
289
  switch (event_id) {
209
290
  case RUBY_INTERNAL_THREAD_EVENT_READY: event_name = "wants_gvl"; break;
@@ -212,15 +293,106 @@ static void on_thread_event(rb_event_flag_t event_id, UNUSED_ARG const rb_intern
212
293
  case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
213
294
  case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "died"; break;
214
295
  };
215
- render_event(event_name);
296
+ render_event(state, event_name);
216
297
  }
217
298
 
218
299
  static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
219
300
  const char* event_name = "bug_unknown_event";
301
+ thread_local_state *state = GT_LOCAL_STATE(rb_thread_current(), false); // no alloc during GC
220
302
  switch (rb_tracearg_event_flag(rb_tracearg_from_tracepoint(tpval))) {
221
303
  case RUBY_INTERNAL_EVENT_GC_ENTER: event_name = "gc"; break;
222
304
  // TODO: is it possible the thread wasn't running? Might need to save the last state.
223
305
  case RUBY_INTERNAL_EVENT_GC_EXIT: event_name = "running"; break;
224
306
  }
225
- render_event(event_name);
307
+ render_event(state, event_name);
308
+ }
309
+
310
+ static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
311
+ GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
312
+ return Qnil;
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;
226
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)
@@ -0,0 +1,12 @@
1
+ # Experimental: This monkey patch when loaded introduces a new state -- "sleeping" -- which is more specific than the
2
+ # regular "waiting". This can be useful to distinguish when waiting is happening based on time, vs for some event to
3
+ # happen.
4
+
5
+ module GvlTracing::SleepTracking
6
+ def sleep(...)
7
+ GvlTracing.mark_sleeping
8
+ super(...)
9
+ end
10
+ end
11
+
12
+ include GvlTracing::SleepTracking
@@ -26,5 +26,5 @@
26
26
  # frozen_string_literal: true
27
27
 
28
28
  module GvlTracing
29
- VERSION = "1.3.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.3.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-07-01 00:00:00.000000000 Z
11
+ date: 2024-03-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -30,6 +30,7 @@ files:
30
30
  - gems.rb
31
31
  - gvl-tracing.gemspec
32
32
  - lib/gvl-tracing.rb
33
+ - lib/gvl_tracing/sleep_tracking.rb
33
34
  - lib/gvl_tracing/version.rb
34
35
  - preview.png
35
36
  homepage: https://github.com/ivoanjo/gvl-tracing
@@ -52,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
53
  - !ruby/object:Gem::Version
53
54
  version: '0'
54
55
  requirements: []
55
- rubygems_version: 3.4.1
56
+ rubygems_version: 3.5.3
56
57
  signing_key:
57
58
  specification_version: 4
58
59
  summary: Get a timeline view of Global VM Lock usage in your Ruby app