gvl-tracing 1.6.1 → 1.7.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: 2bf28c6665bf4afa02c13299947bb65d7c62c3af783858afdc3a5383e334fa47
4
- data.tar.gz: b1999e7e17d56e76ba732962708c14979e2690cd78204946d2f05e7146b05725
3
+ metadata.gz: d331bd3080954e6c98634d72ed6a0a7b0ed3aa622cb2455bb38aea773d5ed44b
4
+ data.tar.gz: 6ba87866b4bcf64463aa60dee238fb1000367d8028f360237c88ed9a22353625
5
5
  SHA512:
6
- metadata.gz: feb5fb173e07343af18c929608b20b2463fc0b676565b926242585f3fed31991e34ab5f549d7cbfe0e26c883d4a1e6eebf1d8da9e1586f0fa63d0890cdd3b108
7
- data.tar.gz: 615226e345e230b989d7738754f02a058935d67679b4ca16c610cbbb6fe34f82aff6de3299d9548af579a30d55bccaedf25cd52d4f631023cff93a683fe3c8bb
6
+ metadata.gz: 0a6cb0429d039c6980c1df628e33d8f5275ecefb0bc5417e59af714ee48c505c24b3a099e8e0d1d57654c3afabdd199588a88d6f35b0f65eafa50aea18b3bfd3
7
+ data.tar.gz: fb4b359539bb519526e6232b72b383c2de3ee1c9bae171f302918c52a8646a8e19386012946ccecf5611e05f176fa8b250e2761e2bc32ad077508c9ffdc2b8d8
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/README.adoc CHANGED
@@ -8,9 +8,14 @@ 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
- 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"].
11
+ There's a few blog posts and conference talks about what this gem is and how to read its results:
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]. Furthermore, the GVL Instrumentation API does not (as of Ruby 3.2 and 3.3) currently work on Microsoft Windows.
13
+ * https://ivoanjo.me/blog/2025/03/30/mn-scheduling-and-how-the-ruby-gvl-impacts-app-perf/[m:n scheduling and how the (ruby) global vm lock impacts app performance]
14
+ * https://ivoanjo.me/blog/2023/07/23/understanding-the-ruby-global-vm-lock-by-observing-it/[understanding the ruby global vm lock by observing it]
15
+ * https://ivoanjo.me/blog/2023/02/11/ruby-unexpected-io-vs-cpu-unfairness/[ruby’s unexpected i/o vs cpu unfairness]
16
+ * https://ivoanjo.me/blog/2022/07/17/tracing-ruby-global-vm-lock/[tracing ruby’s (global) vm lock] (Also available [https://techracho.bpsinc.jp/hachi8833/2022_09_02/120530[in Japanese] and https://velog.io/@heka1024/%EB%B2%88%EC%97%AD-tracing-rubys-global-vm-lock[in Korean])
17
+
18
+ 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 this writing) currently work on Microsoft Windows.
14
19
 
15
20
  == Quickest start
16
21
 
@@ -68,11 +73,25 @@ This gem only provides a single module (`GvlTracing`) with methods:
68
73
 
69
74
  The resulting traces can be analyzed by going to https://ui.perfetto.dev/[Perfetto UI].
70
75
 
71
- == Experimental features
76
+ === What do each of the events mean?
77
+
78
+ The following events are shown in the timeline:
72
79
 
73
- 1. Sleep tracking: Add `require 'gvl_tracing/sleep_tracking'` to add a more specific `sleeping` state for sleeps (which are otherwise rendered as `waiting` without this feature)
80
+ * `started_tracing`: First event, when `GvlTracing` is enabled
81
+ * `stopped_tracing`: Last event, when `GvlTracing` is disabled
82
+ * `started`: Ruby thread created
83
+ * `died`: Ruby thread died
84
+ * `wants_gvl`: Ruby thread is ready to execute, but needs the GVL before it can do so
85
+ * `running`: Ruby thread is running code (and owns the GVL)
86
+ * `waiting`: Ruby thread is waiting to be waken up when some event happens (IO, timeout)
87
+ * `gc`: Doing garbage collection
88
+ * `sleeping`: Thread called `Kernel#sleep`
89
+
90
+ Note that not all events come from the GVL instrumentation API, and some events were renamed vs the "RUBY_INTERNAL_THREAD_EVENT" entries.
91
+
92
+ == Experimental features
74
93
 
75
- 2. OS threads view: Pass in `os_threads_view_enabled: true` to `GvlTracing.start` to also render a view of Ruby thread activity from the OS native threads point-of-view. This is useful when using M:N thread scheduling, which is used on Ruby 3.3+ Ractors, and when using the `RUBY_MN_THREADS=1` setting.
94
+ 1. OS threads view: Pass in `os_threads_view_enabled: true` to `GvlTracing.start` to also render a view of Ruby thread activity from the OS native threads point-of-view. This is useful when using M:N thread scheduling, which is used on Ruby 3.3+ Ractors, and when using the `RUBY_MN_THREADS=1` setting.
76
95
 
77
96
  == Tips
78
97
 
@@ -0,0 +1,175 @@
1
+ // direct-bind: Ruby gem for getting direct access to function pointers
2
+ // Copyright (c) 2025 Ivo Anjo <ivo@ivoanjo.me>
3
+ //
4
+ // This file is part of direct-bind.
5
+ //
6
+ // MIT License
7
+ //
8
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ // of this software and associated documentation files (the "Software"), to deal
10
+ // in the Software without restriction, including without limitation the rights
11
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ // copies of the Software, and to permit persons to whom the Software is
13
+ // furnished to do so, subject to the following conditions:
14
+ //
15
+ // The above copyright notice and this permission notice shall be included in all
16
+ // copies or substantial portions of the Software.
17
+ //
18
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ // SOFTWARE.
25
+
26
+ // See direct-bind.h for details on using direct-bind and why you may be finding this file vendored inside another gem.
27
+
28
+ #include "direct-bind.h"
29
+
30
+ static bool direct_bind_self_test(bool raise_on_failure);
31
+
32
+ // # Initialization and version management
33
+
34
+ bool direct_bind_initialize(VALUE publish_version_under, bool raise_on_failure) {
35
+ if (!direct_bind_self_test(raise_on_failure)) return false;
36
+
37
+ if (publish_version_under != Qnil) {
38
+ rb_define_const(rb_define_module_under(publish_version_under, "DirectBind"), "VERSION", rb_str_new_lit(DIRECT_BIND_VERSION));
39
+ }
40
+
41
+ return true;
42
+ }
43
+
44
+ // # Self-test implementation
45
+
46
+ #define SELF_TEST_ARITY 3
47
+
48
+ static VALUE self_test_target_func(
49
+ __attribute__((unused)) VALUE _1,
50
+ __attribute__((unused)) VALUE _2,
51
+ __attribute__((unused)) VALUE _3,
52
+ __attribute__((unused)) VALUE _4
53
+ ) {
54
+ return Qnil;
55
+ }
56
+
57
+ static bool direct_bind_self_test(bool raise_on_failure) {
58
+ VALUE anonymous_module = rb_module_new();
59
+ rb_define_method(anonymous_module, "direct_bind_self_test_target", self_test_target_func, SELF_TEST_ARITY);
60
+
61
+ ID self_test_id = rb_intern("direct_bind_self_test_target");
62
+ direct_bind_cfunc_result test_target = direct_bind_get_cfunc_with_arity(anonymous_module, self_test_id, SELF_TEST_ARITY, raise_on_failure);
63
+
64
+ return test_target.ok && test_target.func == self_test_target_func;
65
+ }
66
+
67
+ // # Structure layouts and exported symbol definitions from Ruby
68
+
69
+ // ## From internal/gc.h
70
+ void rb_objspace_each_objects(int (*callback)(void *start, void *end, size_t stride, void *data), void *data);
71
+ int rb_objspace_internal_object_p(VALUE obj);
72
+
73
+ // ## From method.h
74
+ typedef struct rb_method_entry_struct {
75
+ VALUE flags;
76
+ VALUE defined_class;
77
+ struct rb_method_definition_struct * const def;
78
+ ID called_id;
79
+ VALUE owner;
80
+ } rb_method_entry_t;
81
+
82
+ // ### This was simplified/inlined vs the original structure
83
+ struct rb_method_definition_struct {
84
+ unsigned int type: 4;
85
+ int _ignored;
86
+ struct {
87
+ VALUE (*func)(ANYARGS);
88
+ void *_ignored;
89
+ int argc;
90
+ } cfunc;
91
+ };
92
+
93
+ // # This is where the magic happens: Using objectspace to find the method entry and retrieve the cfunc
94
+
95
+ typedef struct {
96
+ VALUE target_klass;
97
+ ID target_id;
98
+ direct_bind_cfunc_result result;
99
+ } find_data_t;
100
+
101
+ static bool valid_method_entry(VALUE object);
102
+ static bool found_target_method_entry(rb_method_entry_t *method_entry, find_data_t *find_data);
103
+ static int find_cfunc(void *start, void *end, size_t stride, void *data);
104
+
105
+ direct_bind_cfunc_result direct_bind_get_cfunc(VALUE klass, ID method_name, bool raise_on_failure) {
106
+ VALUE definition_not_found = rb_sprintf("method %"PRIsVALUE".%"PRIsVALUE" not found", klass, ID2SYM(method_name));
107
+
108
+ find_data_t find_data = {.target_klass = klass, .target_id = method_name, .result = {.ok = false, .failure_reason = definition_not_found}};
109
+ rb_objspace_each_objects(find_cfunc, &find_data);
110
+
111
+ if (raise_on_failure && find_data.result.ok == false) {
112
+ rb_raise(rb_eRuntimeError, "direct_bind_get_cfunc failed: %"PRIsVALUE, find_data.result.failure_reason);
113
+ }
114
+
115
+ return find_data.result;
116
+ }
117
+
118
+ direct_bind_cfunc_result direct_bind_get_cfunc_with_arity(VALUE klass, ID method_name, int arity, bool raise_on_failure) {
119
+ direct_bind_cfunc_result result = direct_bind_get_cfunc(klass, method_name, raise_on_failure);
120
+
121
+ if (result.ok && result.arity != arity) {
122
+ VALUE unexpected_arity = rb_sprintf("method %"PRIsVALUE".%"PRIsVALUE" unexpected arity %d, expected %d", klass, ID2SYM(method_name), result.arity, arity);
123
+
124
+ if (raise_on_failure) rb_raise(rb_eRuntimeError, "direct_bind_get_cfunc_with_arity failed: %"PRIsVALUE, unexpected_arity);
125
+ else result = (direct_bind_cfunc_result) {.ok = false, .failure_reason = unexpected_arity};
126
+ }
127
+
128
+ return result;
129
+ }
130
+
131
+ // TODO: Maybe change this to use safe memory reads that can never segv (e.g. if structure layouts are off?)
132
+ static int find_cfunc(void *start, void *end, size_t stride, void *data) {
133
+ const int stop_iteration = 1;
134
+ const int continue_iteration = 0;
135
+ const int vm_method_type_cfunc = 1;
136
+
137
+ find_data_t *find_data = (find_data_t *) data;
138
+
139
+ for (VALUE v = (VALUE) start; v != (VALUE) end; v += stride) {
140
+ if (!valid_method_entry(v)) continue;
141
+
142
+ rb_method_entry_t *method_entry = (rb_method_entry_t*) v;
143
+ if (!found_target_method_entry(method_entry, find_data)) continue;
144
+
145
+ if (method_entry->def == NULL) {
146
+ find_data->result.failure_reason = rb_str_new_lit("method_entry->def is NULL");
147
+ } else if (method_entry->def->type != vm_method_type_cfunc) {
148
+ find_data->result.failure_reason = rb_str_new_lit("method_entry is not a cfunc");
149
+ } else {
150
+ find_data->result = (direct_bind_cfunc_result) {
151
+ .ok = true,
152
+ .failure_reason = Qnil,
153
+ .arity = method_entry->def->cfunc.argc,
154
+ .func = method_entry->def->cfunc.func,
155
+ };
156
+ }
157
+ return stop_iteration;
158
+ }
159
+
160
+ return continue_iteration;
161
+ }
162
+
163
+ static bool is_method_entry(VALUE imemo) {
164
+ const unsigned long method_entry_id = 6;
165
+ return ((RBASIC(imemo)->flags >> FL_USHIFT) & method_entry_id) == method_entry_id;
166
+ }
167
+
168
+ static bool valid_method_entry(VALUE object) {
169
+ return rb_objspace_internal_object_p(object) && RB_BUILTIN_TYPE(object) == RUBY_T_IMEMO && RB_TYPE_P(object, RUBY_T_IMEMO) && is_method_entry(object);
170
+ }
171
+
172
+ static bool found_target_method_entry(rb_method_entry_t *method_entry, find_data_t *find_data) {
173
+ VALUE method_klass = method_entry->defined_class ? method_entry->defined_class : method_entry->owner;
174
+ return method_klass == find_data->target_klass && method_entry->called_id == find_data->target_id;
175
+ }
@@ -0,0 +1,61 @@
1
+ // direct-bind: Ruby gem for getting direct access to function pointers
2
+ // Copyright (c) 2025 Ivo Anjo <ivo@ivoanjo.me>
3
+ //
4
+ // This file is part of direct-bind.
5
+ //
6
+ // MIT License
7
+ //
8
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ // of this software and associated documentation files (the "Software"), to deal
10
+ // in the Software without restriction, including without limitation the rights
11
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ // copies of the Software, and to permit persons to whom the Software is
13
+ // furnished to do so, subject to the following conditions:
14
+ //
15
+ // The above copyright notice and this permission notice shall be included in all
16
+ // copies or substantial portions of the Software.
17
+ //
18
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ // SOFTWARE.
25
+
26
+ // The recommended way to consume the direct-bind gem is to always vendor it.
27
+ // That is, use its rake task to automatically copy direct-bind.h and direct-bind.c into another gem's native extension
28
+ // sources folder. (P.s.: There's also a test helper to make sure copying is working fine and the gem is up-to-date.)
29
+ //
30
+ // This makes the actual Ruby direct-bind gem only a development dependency, simplifying distribution for the gem
31
+ // that uses it.
32
+ //
33
+ // For more details, check the direct-bind gem's documentation.
34
+
35
+ #pragma once
36
+
37
+ #include <stdbool.h>
38
+ #include <ruby.h>
39
+
40
+ #define DIRECT_BIND_VERSION "1.0.0"
41
+
42
+ typedef struct {
43
+ bool ok;
44
+ VALUE failure_reason;
45
+ int arity;
46
+ VALUE (*func)(ANYARGS);
47
+ } direct_bind_cfunc_result;
48
+
49
+ // Recommended to call once during your gem's initialization, to validate that direct-bind's Ruby hacking is in good shape and
50
+ // to make it easy to (optionally) validate what version you're using
51
+ bool direct_bind_initialize(VALUE publish_version_under, bool raise_on_failure);
52
+
53
+ // Provides the reverse of `rb_define_method`: Given a class and a method_name, retrieves the arity and func previously
54
+ // passed to `rb_define_method`.
55
+ //
56
+ // Performance note: As of this writing, this method scans objspace to find the definition of the method, so you
57
+ // most probably want to cache its result, rather than calling it very often.
58
+ direct_bind_cfunc_result direct_bind_get_cfunc(VALUE klass, ID method_name, bool raise_on_failure);
59
+
60
+ // Same as above, but automatically fails if arity isn't the expected value
61
+ direct_bind_cfunc_result direct_bind_get_cfunc_with_arity(VALUE klass, ID method_name, int arity, bool raise_on_failure);
@@ -27,6 +27,7 @@
27
27
  #include <ruby/debug.h>
28
28
  #include <ruby/thread.h>
29
29
  #include <ruby/atomic.h>
30
+
30
31
  #include <errno.h>
31
32
  #include <inttypes.h>
32
33
  #include <stdbool.h>
@@ -34,6 +35,8 @@
34
35
  #include <pthread.h>
35
36
  #include <stdint.h>
36
37
 
38
+ #include "direct-bind.h"
39
+
37
40
  #include "extconf.h"
38
41
 
39
42
  #ifdef HAVE_PTHREAD_H
@@ -69,7 +72,6 @@ typedef struct {
69
72
  #endif
70
73
  VALUE thread;
71
74
  rb_event_flag_t previous_state; // Used to coalesce similar events
72
- bool sleeping; // Used to track when a thread is sleeping
73
75
  } thread_local_state;
74
76
 
75
77
  // Global mutable state
@@ -83,9 +85,13 @@ static VALUE gc_tracepoint = Qnil;
83
85
  static int thread_storage_key = 0;
84
86
  static VALUE all_seen_threads = Qnil;
85
87
  static pthread_mutex_t all_seen_threads_mutex = PTHREAD_MUTEX_INITIALIZER;
86
- static pthread_mutex_t output_mutex = PTHREAD_MUTEX_INITIALIZER;
87
88
  static bool os_threads_view_enabled;
89
+ static uint32_t timeslice_meta_ms = 0;
90
+
91
+ static ID sleep_id;
92
+ static VALUE (*is_thread_alive)(VALUE thread);
88
93
 
94
+ static inline void initialize_timeslice_meta(void);
89
95
  static VALUE tracing_init_local_storage(VALUE, VALUE);
90
96
  static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_threads_view_enabled_arg);
91
97
  static VALUE tracing_stop(VALUE _self);
@@ -93,11 +99,8 @@ static double timestamp_microseconds(void);
93
99
  static double render_event(thread_local_state *, const char *event_name);
94
100
  static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
95
101
  static void on_gc_event(VALUE tpval, void *_unused1);
96
- static VALUE mark_sleeping(VALUE _self);
97
102
  static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused);
98
103
  static void thread_local_state_mark(void *data);
99
- static inline void output_mutex_lock(void);
100
- static inline void output_mutex_unlock(void);
101
104
  static inline int32_t thread_id_for(thread_local_state *state);
102
105
  static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread);
103
106
  static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self);
@@ -140,14 +143,27 @@ void Init_gvl_tracing_native_extension(void) {
140
143
 
141
144
  all_seen_threads = rb_ary_new();
142
145
 
146
+ sleep_id = rb_intern("sleep");
147
+
143
148
  VALUE gvl_tracing_module = rb_define_module("GvlTracing");
144
149
 
145
150
  rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
146
151
  rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 2);
147
152
  rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
148
- rb_define_singleton_method(gvl_tracing_module, "mark_sleeping", mark_sleeping, 0);
149
153
  rb_define_singleton_method(gvl_tracing_module, "_thread_id_for", ruby_thread_id_for, 1);
150
154
  rb_define_singleton_method(gvl_tracing_module, "trim_all_seen_threads", trim_all_seen_threads, 0);
155
+
156
+ initialize_timeslice_meta();
157
+
158
+ direct_bind_initialize(gvl_tracing_module, true);
159
+ is_thread_alive = direct_bind_get_cfunc_with_arity(rb_cThread, rb_intern("alive?"), 0, true).func;
160
+ }
161
+
162
+ static inline void initialize_timeslice_meta(void) {
163
+ const char *timeslice = getenv("RUBY_THREAD_TIMESLICE");
164
+ if (timeslice) {
165
+ timeslice_meta_ms = (uint32_t) strtol(timeslice, NULL, 0);
166
+ }
151
167
  }
152
168
 
153
169
  static inline void initialize_thread_local_state(thread_local_state *state) {
@@ -187,10 +203,15 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_t
187
203
  VALUE ruby_version = rb_const_get(rb_cObject, rb_intern("RUBY_VERSION"));
188
204
  Check_Type(ruby_version, T_STRING);
189
205
 
206
+ VALUE metadata = rb_obj_dup(ruby_version);
207
+ if (timeslice_meta_ms > 0) {
208
+ rb_str_append(metadata, rb_sprintf(", %ums", timeslice_meta_ms));
209
+ }
210
+
190
211
  fprintf(output_file, "[\n");
191
212
  fprintf(output_file,
192
213
  " {\"ph\": \"M\", \"pid\": %"PRId64", \"name\": \"process_name\", \"args\": {\"name\": \"Ruby threads view (%s)\"}},\n",
193
- process_id, StringValuePtr(ruby_version)
214
+ process_id, StringValuePtr(metadata)
194
215
  );
195
216
 
196
217
  double now_microseconds = render_event(state, "started_tracing");
@@ -216,6 +237,8 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_t
216
237
 
217
238
  rb_tracepoint_enable(gc_tracepoint);
218
239
 
240
+ RB_GC_GUARD(metadata);
241
+
219
242
  return Qtrue;
220
243
  }
221
244
 
@@ -262,7 +285,6 @@ static double render_event(thread_local_state *state, const char *event_name) {
262
285
  // Important note: We've observed some rendering issues in perfetto if the tid or pid are numbers that are "too big",
263
286
  // see https://github.com/ivoanjo/gvl-tracing/pull/4#issuecomment-1196463364 for an example.
264
287
 
265
- output_mutex_lock();
266
288
  fprintf(output_file,
267
289
  // Finish previous duration
268
290
  " {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f},\n" \
@@ -273,7 +295,6 @@ static double render_event(thread_local_state *state, const char *event_name) {
273
295
  // Args for second line
274
296
  process_id, thread_id_for(state), now_microseconds, event_name
275
297
  );
276
- output_mutex_unlock();
277
298
 
278
299
  return now_microseconds;
279
300
  }
@@ -285,9 +306,13 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
285
306
 
286
307
  if (!state) return;
287
308
 
288
- #ifdef RUBY_3_3_PLUS
289
- if (!state->thread) state->thread = event_data->thread;
290
- #endif
309
+ if (!state->thread) {
310
+ #ifdef RUBY_3_3_PLUS
311
+ state->thread = event_data->thread;
312
+ #else
313
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED) { state->thread = rb_thread_current(); }
314
+ #endif
315
+ }
291
316
  // In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
292
317
  // are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
293
318
  // timeline easier to see.
@@ -298,11 +323,18 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
298
323
  if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
299
324
  state->previous_state = event_id;
300
325
 
301
- if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && state->sleeping) {
302
- render_event(state, "sleeping");
303
- return;
304
- } else {
305
- state->sleeping = false;
326
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED &&
327
+ // Check that thread is not being shut down
328
+ (state->thread != Qnil && is_thread_alive(state->thread))
329
+ ) {
330
+ ID current_method = 0;
331
+ VALUE current_method_owner = Qnil;
332
+ rb_frame_method_id_and_class(&current_method, &current_method_owner);
333
+
334
+ if (current_method == sleep_id && current_method_owner == rb_mKernel) {
335
+ render_event(state, "sleeping");
336
+ return;
337
+ }
306
338
  }
307
339
 
308
340
  const char* event_name = "bug_unknown_event";
@@ -338,11 +370,6 @@ static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
338
370
  render_event(state, event_name);
339
371
  }
340
372
 
341
- static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
342
- GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
343
- return Qnil;
344
- }
345
-
346
373
  static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
347
374
 
348
375
  static void thread_local_state_mark(void *data) {
@@ -360,18 +387,6 @@ static inline void all_seen_threads_mutex_unlock(void) {
360
387
  if (error) rb_syserr_fail(error, "Failed to unlock GvlTracing mutex");
361
388
  }
362
389
 
363
- static inline void output_mutex_lock(void) {
364
- int error = pthread_mutex_lock(&output_mutex);
365
- // Can't raise exceptions on error since it gets used from outside the GVL
366
- if (error) fprintf(stderr, "Failed to lock the GvlTracing output_mutex");
367
- }
368
-
369
- static inline void output_mutex_unlock(void) {
370
- int error = pthread_mutex_unlock(&output_mutex);
371
- // Can't raise exceptions on error since it gets used from outside the GVL
372
- if (error) fprintf(stderr, "Failed to unlock the GvlTracing output_mutex");
373
- }
374
-
375
390
  #ifdef RUBY_3_3_PLUS
376
391
  static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
377
392
  thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
@@ -461,21 +476,17 @@ static void render_os_thread_event(thread_local_state *state, double now_microse
461
476
  // chars, so here we append a different letter to each thread to cause the color hashing to differ.
462
477
  char color_suffix_hack = ('a' + (thread_id_for(state) % 26));
463
478
 
464
- output_mutex_lock();
465
479
  fprintf(output_file,
466
480
  " {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %u, \"ts\": %f, \"name\": \"Thread %d (%c)\"},\n",
467
481
  OS_THREADS_VIEW_PID, current_native_thread_id(), now_microseconds, thread_id_for(state), color_suffix_hack
468
482
  );
469
- output_mutex_unlock();
470
483
  }
471
484
 
472
485
  static void finish_previous_os_thread_event(double now_microseconds) {
473
- output_mutex_lock();
474
486
  fprintf(output_file,
475
487
  " {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %u, \"ts\": %f},\n",
476
488
  OS_THREADS_VIEW_PID, current_native_thread_id(), now_microseconds
477
489
  );
478
- output_mutex_unlock();
479
490
  }
480
491
 
481
492
  static inline uint32_t current_native_thread_id(void) {
@@ -1,12 +1 @@
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
1
+ warn("GvlTracing::SleepTracking no longer requires loading this file. Please remove your require 'gvl_tracing/sleep_tracking'.")
@@ -26,5 +26,5 @@
26
26
  # frozen_string_literal: true
27
27
 
28
28
  module GvlTracing
29
- VERSION = "1.6.1"
29
+ VERSION = "1.7.0"
30
30
  end
metadata CHANGED
@@ -1,16 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gvl-tracing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivo Anjo
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-08 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description:
14
12
  email:
15
13
  - ivo@ivoanjo.me
16
14
  executables: []
@@ -18,9 +16,12 @@ extensions:
18
16
  - ext/gvl_tracing_native_extension/extconf.rb
19
17
  extra_rdoc_files: []
20
18
  files:
19
+ - ".rspec"
21
20
  - CODE_OF_CONDUCT.adoc
22
21
  - LICENSE
23
22
  - README.adoc
23
+ - ext/gvl_tracing_native_extension/direct-bind.c
24
+ - ext/gvl_tracing_native_extension/direct-bind.h
24
25
  - ext/gvl_tracing_native_extension/extconf.rb
25
26
  - ext/gvl_tracing_native_extension/gvl_tracing.c
26
27
  - gvl-tracing.gemspec
@@ -32,7 +33,6 @@ homepage: https://github.com/ivoanjo/gvl-tracing
32
33
  licenses:
33
34
  - MIT
34
35
  metadata: {}
35
- post_install_message:
36
36
  rdoc_options: []
37
37
  require_paths:
38
38
  - lib
@@ -48,8 +48,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
48
48
  - !ruby/object:Gem::Version
49
49
  version: '0'
50
50
  requirements: []
51
- rubygems_version: 3.4.1
52
- signing_key:
51
+ rubygems_version: 3.6.7
53
52
  specification_version: 4
54
53
  summary: Get a timeline view of Global VM Lock usage in your Ruby app
55
54
  test_files: []