gvl-tracing 1.4.0 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +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 +225 -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: 47df73ea238560430f56d7b1ee2f0484e9e57b8f0784e065209cd5cfbe7b1084
|
4
|
+
data.tar.gz: 1517522202fd357b31db4a087f769991ccf9f0a0c56b38191f64976827b3e752
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed938fa078cb982b23efe5da8a1a0fcb6d367534e7eeb342b2ce4853bf82cb7f0bb7998fd1a0bfa3bc3fd61a9f5d049717e245235dcf3df5a072be0b54eb9c2b
|
7
|
+
data.tar.gz: c1f2db9dbde2a28321deee3b4186c08eb06f3da1f231f652d1822692e67e479e7be3a7b22daada77bc8130e9d6e016027b8470f9c84eceb827f4e06a17ac6d12
|
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
|
-
|
90
|
-
|
143
|
+
static inline void initialize_thread_local_state(thread_local_state *state) {
|
144
|
+
state->initialized = true;
|
145
|
+
state->current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
|
146
|
+
|
147
|
+
#ifdef RUBY_3_2
|
148
|
+
uint32_t native_thread_id = 0;
|
91
149
|
|
92
|
-
|
93
|
-
|
150
|
+
#ifdef HAVE_PTHREAD_THREADID_NP
|
151
|
+
pthread_threadid_np(pthread_self(), &native_thread_id);
|
152
|
+
#elif HAVE_GETTID
|
153
|
+
native_thread_id = gettid();
|
154
|
+
#else
|
155
|
+
// Note: We could use the current_thread_serial as a crappy fallback, but this would make getting thread names
|
156
|
+
// not work very well
|
157
|
+
#error No native thread id available?
|
158
|
+
#endif
|
94
159
|
|
95
|
-
|
96
|
-
pthread_getname_np(pthread_self(), native_thread_name_buffer, sizeof(native_thread_name_buffer));
|
160
|
+
state->native_thread_id = native_thread_id;
|
97
161
|
#endif
|
162
|
+
}
|
98
163
|
|
99
|
-
|
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,41 @@ static void render_event(const char *event_name) {
|
|
197
250
|
|
198
251
|
fprintf(output_file,
|
199
252
|
// Finish previous duration
|
200
|
-
" {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %
|
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
|
+
|
268
|
+
if (!state) return;
|
269
|
+
|
270
|
+
#ifdef RUBY_3_3_PLUS
|
271
|
+
if (!state->thread) state->thread = event_data->thread;
|
272
|
+
#endif
|
273
|
+
// In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
|
212
274
|
// are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
|
213
275
|
// timeline easier to see.
|
214
276
|
//
|
215
277
|
// I haven't observed other situations where we'd want to coalesce events, but we may apply this to all events in the
|
216
278
|
// future. One annoying thing to remember when generalizing this is how to reset the `previous_state` across multiple
|
217
279
|
// start/stop calls to GvlTracing.
|
218
|
-
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == previous_state) return;
|
219
|
-
previous_state = event_id;
|
280
|
+
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
|
281
|
+
state->previous_state = event_id;
|
220
282
|
|
221
|
-
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && sleeping) {
|
222
|
-
render_event("sleeping");
|
283
|
+
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && state->sleeping) {
|
284
|
+
render_event(state, "sleeping");
|
223
285
|
return;
|
224
286
|
} else {
|
225
|
-
sleeping = false;
|
287
|
+
state->sleeping = false;
|
226
288
|
}
|
227
289
|
|
228
290
|
const char* event_name = "bug_unknown_event";
|
@@ -233,20 +295,109 @@ static void on_thread_event(rb_event_flag_t event_id, UNUSED_ARG const rb_intern
|
|
233
295
|
case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
|
234
296
|
case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "died"; break;
|
235
297
|
};
|
236
|
-
render_event(event_name);
|
298
|
+
render_event(state, event_name);
|
237
299
|
}
|
238
300
|
|
239
301
|
static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
|
240
302
|
const char* event_name = "bug_unknown_event";
|
303
|
+
thread_local_state *state = GT_LOCAL_STATE(rb_thread_current(), false); // no alloc during GC
|
304
|
+
|
305
|
+
if (!state) return;
|
306
|
+
|
241
307
|
switch (rb_tracearg_event_flag(rb_tracearg_from_tracepoint(tpval))) {
|
242
308
|
case RUBY_INTERNAL_EVENT_GC_ENTER: event_name = "gc"; break;
|
243
309
|
// TODO: is it possible the thread wasn't running? Might need to save the last state.
|
244
310
|
case RUBY_INTERNAL_EVENT_GC_EXIT: event_name = "running"; break;
|
245
311
|
}
|
246
|
-
render_event(event_name);
|
312
|
+
render_event(state, event_name);
|
247
313
|
}
|
248
314
|
|
249
|
-
static VALUE mark_sleeping(VALUE _self) {
|
250
|
-
sleeping = true;
|
315
|
+
static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
|
316
|
+
GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
|
251
317
|
return Qnil;
|
252
318
|
}
|
319
|
+
|
320
|
+
static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
|
321
|
+
|
322
|
+
static void thread_local_state_mark(void *data) {
|
323
|
+
thread_local_state *state = (thread_local_state *)data;
|
324
|
+
rb_gc_mark(state->thread); // Marking thread to make sure it stays pinned
|
325
|
+
}
|
326
|
+
|
327
|
+
#ifdef RUBY_3_3_PLUS
|
328
|
+
static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
|
329
|
+
thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
|
330
|
+
if (!state && allocate) {
|
331
|
+
VALUE wrapper = TypedData_Make_Struct(rb_cObject, thread_local_state, &thread_local_state_type, state);
|
332
|
+
state->thread = thread;
|
333
|
+
rb_thread_local_aset(thread, rb_intern("__gvl_tracing_local_state"), wrapper);
|
334
|
+
rb_internal_thread_specific_set(thread, thread_storage_key, state);
|
335
|
+
RB_GC_GUARD(wrapper);
|
336
|
+
initialize_thread_local_state(state);
|
337
|
+
|
338
|
+
// Keep thread around, to be able to extract names at the end
|
339
|
+
// We grab a lock here since thread creation can happen in multiple Ractors, and we want to make sure only one
|
340
|
+
// of them is mutating the array at a time. @ivoanjo: I think this is enough to make this safe....?
|
341
|
+
if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
|
342
|
+
rb_ary_push(all_seen_threads, thread);
|
343
|
+
if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
|
344
|
+
}
|
345
|
+
return state;
|
346
|
+
}
|
347
|
+
#endif
|
348
|
+
|
349
|
+
#ifdef RUBY_3_2
|
350
|
+
static inline thread_local_state *GT_CURRENT_THREAD_LOCAL_STATE(void) {
|
351
|
+
thread_local_state *state = &__thread_local_state;
|
352
|
+
if (!state->initialized) {
|
353
|
+
initialize_thread_local_state(state);
|
354
|
+
}
|
355
|
+
return state;
|
356
|
+
}
|
357
|
+
#endif
|
358
|
+
|
359
|
+
static inline int32_t thread_id_for(thread_local_state *state) {
|
360
|
+
// We use different strategies for 3.2 vs 3.3+ to identify threads. This is because:
|
361
|
+
//
|
362
|
+
// 1. On 3.2 we have no way of associating the actual thread VALUE object with the state/serial, so instead we identify
|
363
|
+
// threads by their native ids. This is not entirely correct, since Ruby can reuse native threads (e.g. if a thread
|
364
|
+
// dies and another immediately gets created) but it's good enough for our purposes. (Associating the thread VALUE
|
365
|
+
// object is useful to, e.g. get thread names later.)
|
366
|
+
//
|
367
|
+
// 2. On 3.3 we can associate the state/serial with the thread VALUE object AND additionally with the MN scheduler
|
368
|
+
// the same thread VALUE can end up executing on different native threads so using the native thread id as an
|
369
|
+
// identifier would be wrong.
|
370
|
+
#ifdef RUBY_3_3_PLUS
|
371
|
+
return state->current_thread_serial;
|
372
|
+
#else
|
373
|
+
return state->native_thread_id;
|
374
|
+
#endif
|
375
|
+
}
|
376
|
+
|
377
|
+
static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread) {
|
378
|
+
#ifdef RUBY_3_2
|
379
|
+
rb_raise(rb_eRuntimeError, "On Ruby 3.2 we should use the native thread id directly");
|
380
|
+
#endif
|
381
|
+
|
382
|
+
thread_local_state *state = GT_LOCAL_STATE(thread, true);
|
383
|
+
return INT2FIX(thread_id_for(state));
|
384
|
+
}
|
385
|
+
|
386
|
+
// Can only be called while GvlTracing is not active + while holding the GVL
|
387
|
+
static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
|
388
|
+
if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
|
389
|
+
|
390
|
+
VALUE alive_threads = rb_ary_new();
|
391
|
+
|
392
|
+
for (long i = 0, len = RARRAY_LEN(all_seen_threads); i < len; i++) {
|
393
|
+
VALUE thread = RARRAY_AREF(all_seen_threads, i);
|
394
|
+
if (rb_funcall(thread, rb_intern("alive?"), 0) == Qtrue) {
|
395
|
+
rb_ary_push(alive_threads, thread);
|
396
|
+
}
|
397
|
+
}
|
398
|
+
|
399
|
+
rb_ary_replace(all_seen_threads, alive_threads);
|
400
|
+
|
401
|
+
if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
|
402
|
+
return Qtrue;
|
403
|
+
}
|
data/gems.rb
CHANGED
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivo Anjo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -53,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
requirements: []
|
56
|
-
rubygems_version: 3.4.
|
56
|
+
rubygems_version: 3.4.10
|
57
57
|
signing_key:
|
58
58
|
specification_version: 4
|
59
59
|
summary: Get a timeline view of Global VM Lock usage in your Ruby app
|