gvl-tracing 1.6.0 → 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: af27ef999bd0dba6070d9faf4e4465f77ae7dc440d77d99999be6ac0c1be07ec
4
- data.tar.gz: 2a0978b7135b22a9827e7f040f04ba7e261bd3ebb9f576218e19473d84560053
3
+ metadata.gz: d331bd3080954e6c98634d72ed6a0a7b0ed3aa622cb2455bb38aea773d5ed44b
4
+ data.tar.gz: 6ba87866b4bcf64463aa60dee238fb1000367d8028f360237c88ed9a22353625
5
5
  SHA512:
6
- metadata.gz: fc9bb3ac74a50d3103c0acc538ec7ef171324427f78ef62eadcc1afc5a2def15f3ef7063f3f74150aad20f3e93a406d2c3f2a0ca19aa63cff824d1e684637e19
7
- data.tar.gz: cca0048ff2181b6346e0628dcb9710ee83937d2b7a1014c0133b0b7747b2b5328e73d29d4164daedeaddde9cb6e879a52a73646e62ca95e0c8845c2dc044d3d0
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
@@ -84,7 +86,12 @@ 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
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);
87
93
 
94
+ static inline void initialize_timeslice_meta(void);
88
95
  static VALUE tracing_init_local_storage(VALUE, VALUE);
89
96
  static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_threads_view_enabled_arg);
90
97
  static VALUE tracing_stop(VALUE _self);
@@ -92,7 +99,6 @@ static double timestamp_microseconds(void);
92
99
  static double render_event(thread_local_state *, const char *event_name);
93
100
  static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
94
101
  static void on_gc_event(VALUE tpval, void *_unused1);
95
- static VALUE mark_sleeping(VALUE _self);
96
102
  static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused);
97
103
  static void thread_local_state_mark(void *data);
98
104
  static inline int32_t thread_id_for(thread_local_state *state);
@@ -137,14 +143,27 @@ void Init_gvl_tracing_native_extension(void) {
137
143
 
138
144
  all_seen_threads = rb_ary_new();
139
145
 
146
+ sleep_id = rb_intern("sleep");
147
+
140
148
  VALUE gvl_tracing_module = rb_define_module("GvlTracing");
141
149
 
142
150
  rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
143
151
  rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 2);
144
152
  rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
145
- rb_define_singleton_method(gvl_tracing_module, "mark_sleeping", mark_sleeping, 0);
146
153
  rb_define_singleton_method(gvl_tracing_module, "_thread_id_for", ruby_thread_id_for, 1);
147
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
+ }
148
167
  }
149
168
 
150
169
  static inline void initialize_thread_local_state(thread_local_state *state) {
@@ -184,10 +203,15 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_t
184
203
  VALUE ruby_version = rb_const_get(rb_cObject, rb_intern("RUBY_VERSION"));
185
204
  Check_Type(ruby_version, T_STRING);
186
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
+
187
211
  fprintf(output_file, "[\n");
188
212
  fprintf(output_file,
189
213
  " {\"ph\": \"M\", \"pid\": %"PRId64", \"name\": \"process_name\", \"args\": {\"name\": \"Ruby threads view (%s)\"}},\n",
190
- process_id, StringValuePtr(ruby_version)
214
+ process_id, StringValuePtr(metadata)
191
215
  );
192
216
 
193
217
  double now_microseconds = render_event(state, "started_tracing");
@@ -213,6 +237,8 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_t
213
237
 
214
238
  rb_tracepoint_enable(gc_tracepoint);
215
239
 
240
+ RB_GC_GUARD(metadata);
241
+
216
242
  return Qtrue;
217
243
  }
218
244
 
@@ -280,9 +306,13 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
280
306
 
281
307
  if (!state) return;
282
308
 
283
- #ifdef RUBY_3_3_PLUS
284
- if (!state->thread) state->thread = event_data->thread;
285
- #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
+ }
286
316
  // In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
287
317
  // are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
288
318
  // timeline easier to see.
@@ -293,11 +323,18 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
293
323
  if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
294
324
  state->previous_state = event_id;
295
325
 
296
- if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && state->sleeping) {
297
- render_event(state, "sleeping");
298
- return;
299
- } else {
300
- 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
+ }
301
338
  }
302
339
 
303
340
  const char* event_name = "bug_unknown_event";
@@ -333,11 +370,6 @@ static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
333
370
  render_event(state, event_name);
334
371
  }
335
372
 
336
- static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
337
- GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
338
- return Qnil;
339
- }
340
-
341
373
  static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
342
374
 
343
375
  static void thread_local_state_mark(void *data) {
data/gvl-tracing.gemspec CHANGED
@@ -42,7 +42,8 @@ Gem::Specification.new do |spec|
42
42
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
43
43
  spec.files = Dir.chdir(__dir__) do
44
44
  `git ls-files -z`.split("\x0").reject do |f|
45
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|examples)/|\.(?:git|travis|circleci)|appveyor)})
45
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|examples)/|\.(?:git|travis|circleci)|appveyor)}) ||
46
+ [".editorconfig", ".ruby-version", ".standard.yml", "gems.rb", "Rakefile"].include?(f)
46
47
  end
47
48
  end
48
49
  spec.require_paths = ["lib", "ext"]
@@ -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.0"
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.0
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-10-30 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,16 +16,14 @@ extensions:
18
16
  - ext/gvl_tracing_native_extension/extconf.rb
19
17
  extra_rdoc_files: []
20
18
  files:
21
- - ".editorconfig"
22
- - ".ruby-version"
23
- - ".standard.yml"
19
+ - ".rspec"
24
20
  - CODE_OF_CONDUCT.adoc
25
21
  - LICENSE
26
22
  - README.adoc
27
- - Rakefile
23
+ - ext/gvl_tracing_native_extension/direct-bind.c
24
+ - ext/gvl_tracing_native_extension/direct-bind.h
28
25
  - ext/gvl_tracing_native_extension/extconf.rb
29
26
  - ext/gvl_tracing_native_extension/gvl_tracing.c
30
- - gems.rb
31
27
  - gvl-tracing.gemspec
32
28
  - lib/gvl-tracing.rb
33
29
  - lib/gvl_tracing/sleep_tracking.rb
@@ -37,7 +33,6 @@ homepage: https://github.com/ivoanjo/gvl-tracing
37
33
  licenses:
38
34
  - MIT
39
35
  metadata: {}
40
- post_install_message:
41
36
  rdoc_options: []
42
37
  require_paths:
43
38
  - lib
@@ -53,8 +48,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
48
  - !ruby/object:Gem::Version
54
49
  version: '0'
55
50
  requirements: []
56
- rubygems_version: 3.5.11
57
- signing_key:
51
+ rubygems_version: 3.6.7
58
52
  specification_version: 4
59
53
  summary: Get a timeline view of Global VM Lock usage in your Ruby app
60
54
  test_files: []
data/.editorconfig DELETED
@@ -1,22 +0,0 @@
1
- # EditorConfig is awesome: https://EditorConfig.org
2
-
3
- # top-most EditorConfig file
4
- root = true
5
-
6
- # Unix-style newlines with a newline ending every file
7
- [*]
8
- end_of_line = lf
9
- insert_final_newline = true
10
- trim_trailing_whitespace = true
11
-
12
- [*.h]
13
- indent_style = space
14
- indent_size = 2
15
-
16
- [*.c]
17
- indent_style = space
18
- indent_size = 2
19
-
20
- [*.yml]
21
- indent_style = space
22
- indent_size = 2
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- ruby-3.2.2
data/.standard.yml DELETED
@@ -1,9 +0,0 @@
1
- ruby_version: 3.2
2
-
3
- ignore:
4
- - 'examples/example1.rb':
5
- - Style/InfiniteLoop
6
- - 'examples/example5.rb':
7
- - Style/InfiniteLoop
8
- - 'lib/gvl_tracing/sleep_tracking.rb':
9
- - Style/MixinUsage
data/Rakefile DELETED
@@ -1,36 +0,0 @@
1
- # gvl-tracing: Ruby gem for getting a timelinew view of GVL usage
2
- # Copyright (c) 2022 Ivo Anjo <ivo@ivoanjo.me>
3
- #
4
- # This file is part of gvl-tracing.
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
- # frozen_string_literal: true
27
-
28
- require "bundler/gem_tasks"
29
- require "standard/rake"
30
- require "rake/extensiontask"
31
- require "rspec/core/rake_task"
32
-
33
- Rake::ExtensionTask.new("gvl_tracing_native_extension")
34
- RSpec::Core::RakeTask.new(:spec)
35
-
36
- task default: [:compile, :"standard:fix", :spec]
data/gems.rb DELETED
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- gemspec
6
-
7
- gem "rake", "~> 13.0"
8
- gem "rake-compiler", "~> 1.2"
9
- gem "pry"
10
- gem "pry-byebug"
11
- gem "rspec"
12
- gem "standard", "~> 1.41"
13
- gem "concurrent-ruby"
14
- gem "benchmark-ips", "~> 2.13"
15
- gem "rubocop", ">= 1.66.0"