gvl-tracing 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.standard.yml +4 -0
- data/README.adoc +5 -3
- data/Rakefile +3 -1
- data/ext/gvl_tracing_native_extension/extconf.rb +10 -1
- data/ext/gvl_tracing_native_extension/gvl_tracing.c +220 -74
- data/gems.rb +4 -1
- data/lib/gvl-tracing.rb +17 -9
- data/lib/gvl_tracing/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9640e68d58cfbda91dc7c034982b3e43eec6048729d0ca61c8498495ad8f4277
|
4
|
+
data.tar.gz: ab154e734bfb085aff5615837efda1b7836784019fd689051b21db483f86c62b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a87a874de0373814d27f9e567c19fa4d36f5c2cdb5db2f3a6d7a7ce3280917dd8ecf0fc85b130f3573aa496cc5633427b92780f48ab00dc9cac8e6649763502a
|
7
|
+
data.tar.gz: c5ef422b98a1cf2835f62d0769c0e0ea2b7d0f6af36888f05617795e4667a0bf4bbcf404e0e860fbdf319ae2be8e41fd84f09c2168282e6a0adfbee92a252840
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-3.2.
|
1
|
+
ruby-3.2.2
|
data/.standard.yml
CHANGED
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
|
-
|
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[
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
87
|
-
|
88
|
-
current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
|
89
|
-
set_native_thread_id();
|
90
|
-
}
|
143
|
+
static inline void initialize_thread_local_state(thread_local_state *state) {
|
144
|
+
state->initialized = true;
|
145
|
+
state->current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
|
91
146
|
|
92
|
-
|
93
|
-
|
147
|
+
#ifdef RUBY_3_2
|
148
|
+
uint32_t native_thread_id = 0;
|
94
149
|
|
95
|
-
|
96
|
-
|
150
|
+
#ifdef HAVE_PTHREAD_THREADID_NP
|
151
|
+
pthread_threadid_np(pthread_self(), &native_thread_id);
|
152
|
+
#elif HAVE_GETTID
|
153
|
+
native_thread_id = gettid();
|
154
|
+
#else
|
155
|
+
// Note: We could use the current_thread_serial as a crappy fallback, but this would make getting thread names
|
156
|
+
// not work very well
|
157
|
+
#error No native thread id available?
|
158
|
+
#endif
|
159
|
+
|
160
|
+
state->native_thread_id = native_thread_id;
|
97
161
|
#endif
|
162
|
+
}
|
98
163
|
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
225
|
+
#ifdef RUBY_3_3_PLUS
|
226
|
+
return all_seen_threads;
|
227
|
+
#else
|
228
|
+
return rb_funcall(rb_cThread, rb_intern("list"), 0);
|
229
|
+
#endif
|
158
230
|
}
|
159
231
|
|
160
232
|
static double timestamp_microseconds(void) {
|
@@ -163,31 +235,12 @@ static double timestamp_microseconds(void) {
|
|
163
235
|
return (current_monotonic.tv_nsec / 1000.0) + (current_monotonic.tv_sec * 1000.0 * 1000.0);
|
164
236
|
}
|
165
237
|
|
166
|
-
static void set_native_thread_id(void) {
|
167
|
-
uint64_t native_thread_id = 0;
|
168
|
-
|
169
|
-
#ifdef HAVE_PTHREAD_THREADID_NP
|
170
|
-
pthread_threadid_np(pthread_self(), &native_thread_id);
|
171
|
-
#elif HAVE_GETTID
|
172
|
-
native_thread_id = gettid();
|
173
|
-
#else
|
174
|
-
native_thread_id = current_thread_serial; // TODO: Better fallback for Windows?
|
175
|
-
#endif
|
176
|
-
|
177
|
-
thread_id = native_thread_id;
|
178
|
-
}
|
179
|
-
|
180
238
|
// Render output using trace event format for perfetto:
|
181
239
|
// https://chromium.googlesource.com/catapult/+/refs/heads/main/docs/trace-event-format.md
|
182
|
-
static void render_event(const char *event_name) {
|
240
|
+
static void render_event(thread_local_state *state, const char *event_name) {
|
183
241
|
// Event data
|
184
242
|
double now_microseconds = timestamp_microseconds() - started_tracing_at_microseconds;
|
185
243
|
|
186
|
-
if (!current_thread_seen) {
|
187
|
-
initialize_thread_id();
|
188
|
-
render_thread_metadata();
|
189
|
-
}
|
190
|
-
|
191
244
|
// Each event is converted into two events in the output: one that signals the end of the previous event
|
192
245
|
// (whatever it was), and one that signals the start of the actual event we're processing.
|
193
246
|
// Yes, this seems to be slightly bending the intention of the output format, but it seemed easier to do this way.
|
@@ -197,32 +250,39 @@ static void render_event(const char *event_name) {
|
|
197
250
|
|
198
251
|
fprintf(output_file,
|
199
252
|
// Finish previous duration
|
200
|
-
" {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %
|
253
|
+
" {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f},\n" \
|
201
254
|
// Current event
|
202
|
-
" {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %
|
255
|
+
" {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f, \"name\": \"%s\"},\n",
|
203
256
|
// Args for first line
|
204
|
-
process_id,
|
257
|
+
process_id, thread_id_for(state), now_microseconds,
|
205
258
|
// Args for second line
|
206
|
-
process_id,
|
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,
|
211
|
-
|
263
|
+
static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *event_data, UNUSED_ARG void *_unused2) {
|
264
|
+
thread_local_state *state = GT_EVENT_LOCAL_STATE(event_data,
|
265
|
+
// These events are guaranteed to hold the GVL, so they can allocate
|
266
|
+
event_id & (RUBY_INTERNAL_THREAD_EVENT_STARTED | RUBY_INTERNAL_THREAD_EVENT_RESUMED));
|
267
|
+
if (!state) return;
|
268
|
+
#ifdef RUBY_3_3_PLUS
|
269
|
+
if (!state->thread) state->thread = event_data->thread;
|
270
|
+
#endif
|
271
|
+
// In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
|
212
272
|
// are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
|
213
273
|
// timeline easier to see.
|
214
274
|
//
|
215
275
|
// I haven't observed other situations where we'd want to coalesce events, but we may apply this to all events in the
|
216
276
|
// future. One annoying thing to remember when generalizing this is how to reset the `previous_state` across multiple
|
217
277
|
// start/stop calls to GvlTracing.
|
218
|
-
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == previous_state) return;
|
219
|
-
previous_state = event_id;
|
278
|
+
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
|
279
|
+
state->previous_state = event_id;
|
220
280
|
|
221
|
-
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && sleeping) {
|
222
|
-
render_event("sleeping");
|
281
|
+
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && state->sleeping) {
|
282
|
+
render_event(state, "sleeping");
|
223
283
|
return;
|
224
284
|
} else {
|
225
|
-
sleeping = false;
|
285
|
+
state->sleeping = false;
|
226
286
|
}
|
227
287
|
|
228
288
|
const char* event_name = "bug_unknown_event";
|
@@ -233,20 +293,106 @@ static void on_thread_event(rb_event_flag_t event_id, UNUSED_ARG const rb_intern
|
|
233
293
|
case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
|
234
294
|
case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "died"; break;
|
235
295
|
};
|
236
|
-
render_event(event_name);
|
296
|
+
render_event(state, event_name);
|
237
297
|
}
|
238
298
|
|
239
299
|
static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
|
240
300
|
const char* event_name = "bug_unknown_event";
|
301
|
+
thread_local_state *state = GT_LOCAL_STATE(rb_thread_current(), false); // no alloc during GC
|
241
302
|
switch (rb_tracearg_event_flag(rb_tracearg_from_tracepoint(tpval))) {
|
242
303
|
case RUBY_INTERNAL_EVENT_GC_ENTER: event_name = "gc"; break;
|
243
304
|
// TODO: is it possible the thread wasn't running? Might need to save the last state.
|
244
305
|
case RUBY_INTERNAL_EVENT_GC_EXIT: event_name = "running"; break;
|
245
306
|
}
|
246
|
-
render_event(event_name);
|
307
|
+
render_event(state, event_name);
|
247
308
|
}
|
248
309
|
|
249
|
-
static VALUE mark_sleeping(VALUE _self) {
|
250
|
-
sleeping = true;
|
310
|
+
static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
|
311
|
+
GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
|
251
312
|
return Qnil;
|
252
313
|
}
|
314
|
+
|
315
|
+
static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
|
316
|
+
|
317
|
+
static void thread_local_state_mark(void *data) {
|
318
|
+
thread_local_state *state = (thread_local_state *)data;
|
319
|
+
rb_gc_mark(state->thread); // Marking thread to make sure it stays pinned
|
320
|
+
}
|
321
|
+
|
322
|
+
#ifdef RUBY_3_3_PLUS
|
323
|
+
static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
|
324
|
+
thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
|
325
|
+
if (!state && allocate) {
|
326
|
+
VALUE wrapper = TypedData_Make_Struct(rb_cObject, thread_local_state, &thread_local_state_type, state);
|
327
|
+
state->thread = thread;
|
328
|
+
rb_thread_local_aset(thread, rb_intern("__gvl_tracing_local_state"), wrapper);
|
329
|
+
rb_internal_thread_specific_set(thread, thread_storage_key, state);
|
330
|
+
RB_GC_GUARD(wrapper);
|
331
|
+
initialize_thread_local_state(state);
|
332
|
+
|
333
|
+
// Keep thread around, to be able to extract names at the end
|
334
|
+
// We grab a lock here since thread creation can happen in multiple Ractors, and we want to make sure only one
|
335
|
+
// of them is mutating the array at a time. @ivoanjo: I think this is enough to make this safe....?
|
336
|
+
if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
|
337
|
+
rb_ary_push(all_seen_threads, thread);
|
338
|
+
if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
|
339
|
+
}
|
340
|
+
return state;
|
341
|
+
}
|
342
|
+
#endif
|
343
|
+
|
344
|
+
#ifdef RUBY_3_2
|
345
|
+
static inline thread_local_state *GT_CURRENT_THREAD_LOCAL_STATE(void) {
|
346
|
+
thread_local_state *state = &__thread_local_state;
|
347
|
+
if (!state->initialized) {
|
348
|
+
initialize_thread_local_state(state);
|
349
|
+
}
|
350
|
+
return state;
|
351
|
+
}
|
352
|
+
#endif
|
353
|
+
|
354
|
+
static inline int32_t thread_id_for(thread_local_state *state) {
|
355
|
+
// We use different strategies for 3.2 vs 3.3+ to identify threads. This is because:
|
356
|
+
//
|
357
|
+
// 1. On 3.2 we have no way of associating the actual thread VALUE object with the state/serial, so instead we identify
|
358
|
+
// threads by their native ids. This is not entirely correct, since Ruby can reuse native threads (e.g. if a thread
|
359
|
+
// dies and another immediately gets created) but it's good enough for our purposes. (Associating the thread VALUE
|
360
|
+
// object is useful to, e.g. get thread names later.)
|
361
|
+
//
|
362
|
+
// 2. On 3.3 we can associate the state/serial with the thread VALUE object AND additionally with the MN scheduler
|
363
|
+
// the same thread VALUE can end up executing on different native threads so using the native thread id as an
|
364
|
+
// identifier would be wrong.
|
365
|
+
#ifdef RUBY_3_3_PLUS
|
366
|
+
return state->current_thread_serial;
|
367
|
+
#else
|
368
|
+
return state->native_thread_id;
|
369
|
+
#endif
|
370
|
+
}
|
371
|
+
|
372
|
+
static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread) {
|
373
|
+
#ifdef RUBY_3_2
|
374
|
+
rb_raise(rb_eRuntimeError, "On Ruby 3.2 we should use the native thread id directly");
|
375
|
+
#endif
|
376
|
+
|
377
|
+
thread_local_state *state = GT_LOCAL_STATE(thread, true);
|
378
|
+
return INT2FIX(thread_id_for(state));
|
379
|
+
}
|
380
|
+
|
381
|
+
// Can only be called while GvlTracing is not active + while holding the GVL
|
382
|
+
static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
|
383
|
+
if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
|
384
|
+
|
385
|
+
VALUE alive_threads = rb_ary_new();
|
386
|
+
|
387
|
+
for (long i = 0, len = RARRAY_LEN(all_seen_threads); i < len; i++) {
|
388
|
+
VALUE thread = RARRAY_AREF(all_seen_threads, i);
|
389
|
+
if (rb_funcall(thread, rb_intern("alive?"), 0) == Qtrue) {
|
390
|
+
rb_ary_push(alive_threads, thread);
|
391
|
+
}
|
392
|
+
}
|
393
|
+
|
394
|
+
rb_ary_replace(all_seen_threads, alive_threads);
|
395
|
+
|
396
|
+
if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
|
397
|
+
return Qtrue;
|
398
|
+
}
|
data/gems.rb
CHANGED
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
|
-
|
47
|
+
stop
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
50
51
|
def stop
|
51
|
-
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
|
-
|
62
|
-
File.open(@path,
|
63
|
-
f.puts(
|
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
|
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
|
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)
|
data/lib/gvl_tracing/version.rb
CHANGED
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
|
+
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:
|
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.
|
56
|
+
rubygems_version: 3.5.3
|
57
57
|
signing_key:
|
58
58
|
specification_version: 4
|
59
59
|
summary: Get a timeline view of Global VM Lock usage in your Ruby app
|