gvl-tracing 1.5.1 → 1.6.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/README.adoc +7 -1
- data/ext/gvl_tracing_native_extension/gvl_tracing.c +107 -31
- data/gems.rb +2 -1
- data/lib/gvl-tracing.rb +2 -2
- data/lib/gvl_tracing/sleep_tracking.rb +1 -1
- data/lib/gvl_tracing/version.rb +1 -1
- data/preview.png +0 -0
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af27ef999bd0dba6070d9faf4e4465f77ae7dc440d77d99999be6ac0c1be07ec
|
4
|
+
data.tar.gz: 2a0978b7135b22a9827e7f040f04ba7e261bd3ebb9f576218e19473d84560053
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc9bb3ac74a50d3103c0acc538ec7ef171324427f78ef62eadcc1afc5a2def15f3ef7063f3f74150aad20f3e93a406d2c3f2a0ca19aa63cff824d1e684637e19
|
7
|
+
data.tar.gz: cca0048ff2181b6346e0628dcb9710ee83937d2b7a1014c0133b0b7747b2b5328e73d29d4164daedeaddde9cb6e879a52a73646e62ca95e0c8845c2dc044d3d0
|
data/README.adoc
CHANGED
@@ -25,7 +25,7 @@ def fib(n)
|
|
25
25
|
fib(n - 1) + fib(n - 2)
|
26
26
|
end
|
27
27
|
|
28
|
-
GvlTracing.start("example1.json") do
|
28
|
+
GvlTracing.start("example1.json", os_threads_view_enabled: false) do
|
29
29
|
Thread.new { sleep(0.05) while true }
|
30
30
|
|
31
31
|
sleep(0.05)
|
@@ -68,6 +68,12 @@ This gem only provides a single module (`GvlTracing`) with methods:
|
|
68
68
|
|
69
69
|
The resulting traces can be analyzed by going to https://ui.perfetto.dev/[Perfetto UI].
|
70
70
|
|
71
|
+
== Experimental features
|
72
|
+
|
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)
|
74
|
+
|
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.
|
76
|
+
|
71
77
|
== Tips
|
72
78
|
|
73
79
|
You can "embed" links to the perfetto UI which trigger loading of a trace by following the instructions on https://perfetto.dev/docs/visualization/deep-linking-to-perfetto-ui .
|
@@ -31,7 +31,8 @@
|
|
31
31
|
#include <inttypes.h>
|
32
32
|
#include <stdbool.h>
|
33
33
|
#include <sys/types.h>
|
34
|
-
#include <
|
34
|
+
#include <pthread.h>
|
35
|
+
#include <stdint.h>
|
35
36
|
|
36
37
|
#include "extconf.h"
|
37
38
|
|
@@ -56,6 +57,10 @@
|
|
56
57
|
#define RUBY_3_2
|
57
58
|
#endif
|
58
59
|
|
60
|
+
// For the OS threads view, we emit data as if it was for another pid so it gets grouped separately in perfetto.
|
61
|
+
// This is a really big hack, but I couldn't think of a better way?
|
62
|
+
#define OS_THREADS_VIEW_PID (INT64_C(0))
|
63
|
+
|
59
64
|
typedef struct {
|
60
65
|
bool initialized;
|
61
66
|
int32_t current_thread_serial;
|
@@ -77,13 +82,14 @@ static VALUE gc_tracepoint = Qnil;
|
|
77
82
|
#pragma GCC diagnostic ignored "-Wunused-variable"
|
78
83
|
static int thread_storage_key = 0;
|
79
84
|
static VALUE all_seen_threads = Qnil;
|
80
|
-
static
|
85
|
+
static pthread_mutex_t all_seen_threads_mutex = PTHREAD_MUTEX_INITIALIZER;
|
86
|
+
static bool os_threads_view_enabled;
|
81
87
|
|
82
88
|
static VALUE tracing_init_local_storage(VALUE, VALUE);
|
83
|
-
static VALUE tracing_start(VALUE _self, VALUE output_path);
|
89
|
+
static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_threads_view_enabled_arg);
|
84
90
|
static VALUE tracing_stop(VALUE _self);
|
85
91
|
static double timestamp_microseconds(void);
|
86
|
-
static
|
92
|
+
static double render_event(thread_local_state *, const char *event_name);
|
87
93
|
static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
|
88
94
|
static void on_gc_event(VALUE tpval, void *_unused1);
|
89
95
|
static VALUE mark_sleeping(VALUE _self);
|
@@ -92,6 +98,9 @@ static void thread_local_state_mark(void *data);
|
|
92
98
|
static inline int32_t thread_id_for(thread_local_state *state);
|
93
99
|
static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread);
|
94
100
|
static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self);
|
101
|
+
static void render_os_thread_event(thread_local_state *state, double now_microseconds);
|
102
|
+
static void finish_previous_os_thread_event(double now_microseconds);
|
103
|
+
static inline uint32_t current_native_thread_id(void);
|
95
104
|
|
96
105
|
#pragma GCC diagnostic ignored "-Wunused-const-variable"
|
97
106
|
static const rb_data_type_t thread_local_state_type = {
|
@@ -128,12 +137,10 @@ void Init_gvl_tracing_native_extension(void) {
|
|
128
137
|
|
129
138
|
all_seen_threads = rb_ary_new();
|
130
139
|
|
131
|
-
if (mtx_init(&all_seen_threads_mutex, mtx_plain) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to initialize GvlTracing mutex");
|
132
|
-
|
133
140
|
VALUE gvl_tracing_module = rb_define_module("GvlTracing");
|
134
141
|
|
135
142
|
rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
|
136
|
-
rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start,
|
143
|
+
rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 2);
|
137
144
|
rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
|
138
145
|
rb_define_singleton_method(gvl_tracing_module, "mark_sleeping", mark_sleeping, 0);
|
139
146
|
rb_define_singleton_method(gvl_tracing_module, "_thread_id_for", ruby_thread_id_for, 1);
|
@@ -145,19 +152,7 @@ static inline void initialize_thread_local_state(thread_local_state *state) {
|
|
145
152
|
state->current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
|
146
153
|
|
147
154
|
#ifdef RUBY_3_2
|
148
|
-
|
149
|
-
|
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;
|
155
|
+
state->native_thread_id = current_native_thread_id();
|
161
156
|
#endif
|
162
157
|
}
|
163
158
|
|
@@ -171,8 +166,9 @@ static VALUE tracing_init_local_storage(UNUSED_ARG VALUE _self, VALUE threads) {
|
|
171
166
|
return Qtrue;
|
172
167
|
}
|
173
168
|
|
174
|
-
static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
|
169
|
+
static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_threads_view_enabled_arg) {
|
175
170
|
Check_Type(output_path, T_STRING);
|
171
|
+
if (os_threads_view_enabled_arg != Qtrue && os_threads_view_enabled_arg != Qfalse) rb_raise(rb_eArgError, "os_threads_view_enabled must be true/false");
|
176
172
|
|
177
173
|
trim_all_seen_threads(Qnil);
|
178
174
|
|
@@ -180,13 +176,26 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
|
|
180
176
|
output_file = fopen(StringValuePtr(output_path), "w");
|
181
177
|
if (output_file == NULL) rb_syserr_fail(errno, "Failed to open GvlTracing output file");
|
182
178
|
|
183
|
-
fprintf(output_file, "[\n");
|
184
|
-
|
185
179
|
thread_local_state *state = GT_CURRENT_THREAD_LOCAL_STATE();
|
186
180
|
started_tracing_at_microseconds = timestamp_microseconds();
|
187
181
|
process_id = getpid();
|
182
|
+
os_threads_view_enabled = (os_threads_view_enabled_arg == Qtrue);
|
183
|
+
|
184
|
+
VALUE ruby_version = rb_const_get(rb_cObject, rb_intern("RUBY_VERSION"));
|
185
|
+
Check_Type(ruby_version, T_STRING);
|
186
|
+
|
187
|
+
fprintf(output_file, "[\n");
|
188
|
+
fprintf(output_file,
|
189
|
+
" {\"ph\": \"M\", \"pid\": %"PRId64", \"name\": \"process_name\", \"args\": {\"name\": \"Ruby threads view (%s)\"}},\n",
|
190
|
+
process_id, StringValuePtr(ruby_version)
|
191
|
+
);
|
188
192
|
|
189
|
-
render_event(state, "started_tracing");
|
193
|
+
double now_microseconds = render_event(state, "started_tracing");
|
194
|
+
|
195
|
+
if (os_threads_view_enabled) {
|
196
|
+
fprintf(output_file, " {\"ph\": \"M\", \"pid\": %"PRId64", \"name\": \"process_name\", \"args\": {\"name\": \"OS threads view\"}},\n", OS_THREADS_VIEW_PID);
|
197
|
+
render_os_thread_event(state, now_microseconds);
|
198
|
+
}
|
190
199
|
|
191
200
|
current_hook = rb_internal_thread_add_event_hook(
|
192
201
|
on_thread_event,
|
@@ -215,7 +224,9 @@ static VALUE tracing_stop(UNUSED_ARG VALUE _self) {
|
|
215
224
|
rb_tracepoint_disable(gc_tracepoint);
|
216
225
|
gc_tracepoint = Qnil;
|
217
226
|
|
218
|
-
render_event(state, "stopped_tracing");
|
227
|
+
double now_microseconds = render_event(state, "stopped_tracing");
|
228
|
+
if (os_threads_view_enabled) finish_previous_os_thread_event(now_microseconds);
|
229
|
+
|
219
230
|
// closing the json syntax in the output file is handled in GvlTracing.stop code
|
220
231
|
|
221
232
|
if (fclose(output_file) != 0) rb_syserr_fail(errno, "Failed to close GvlTracing output file");
|
@@ -237,7 +248,7 @@ static double timestamp_microseconds(void) {
|
|
237
248
|
|
238
249
|
// Render output using trace event format for perfetto:
|
239
250
|
// https://chromium.googlesource.com/catapult/+/refs/heads/main/docs/trace-event-format.md
|
240
|
-
static
|
251
|
+
static double render_event(thread_local_state *state, const char *event_name) {
|
241
252
|
// Event data
|
242
253
|
double now_microseconds = timestamp_microseconds() - started_tracing_at_microseconds;
|
243
254
|
|
@@ -258,6 +269,8 @@ static void render_event(thread_local_state *state, const char *event_name) {
|
|
258
269
|
// Args for second line
|
259
270
|
process_id, thread_id_for(state), now_microseconds, event_name
|
260
271
|
);
|
272
|
+
|
273
|
+
return now_microseconds;
|
261
274
|
}
|
262
275
|
|
263
276
|
static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *event_data, UNUSED_ARG void *_unused2) {
|
@@ -295,7 +308,15 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
|
|
295
308
|
case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
|
296
309
|
case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "died"; break;
|
297
310
|
};
|
298
|
-
render_event(state, event_name);
|
311
|
+
double now_microseconds = render_event(state, event_name);
|
312
|
+
|
313
|
+
if (os_threads_view_enabled) {
|
314
|
+
if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) {
|
315
|
+
render_os_thread_event(state, now_microseconds);
|
316
|
+
} else if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED || event_id == RUBY_INTERNAL_THREAD_EVENT_EXITED) {
|
317
|
+
finish_previous_os_thread_event(now_microseconds);
|
318
|
+
}
|
319
|
+
}
|
299
320
|
}
|
300
321
|
|
301
322
|
static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
|
@@ -324,6 +345,16 @@ static void thread_local_state_mark(void *data) {
|
|
324
345
|
rb_gc_mark(state->thread); // Marking thread to make sure it stays pinned
|
325
346
|
}
|
326
347
|
|
348
|
+
static inline void all_seen_threads_mutex_lock(void) {
|
349
|
+
int error = pthread_mutex_lock(&all_seen_threads_mutex);
|
350
|
+
if (error) rb_syserr_fail(error, "Failed to lock GvlTracing mutex");
|
351
|
+
}
|
352
|
+
|
353
|
+
static inline void all_seen_threads_mutex_unlock(void) {
|
354
|
+
int error = pthread_mutex_unlock(&all_seen_threads_mutex);
|
355
|
+
if (error) rb_syserr_fail(error, "Failed to unlock GvlTracing mutex");
|
356
|
+
}
|
357
|
+
|
327
358
|
#ifdef RUBY_3_3_PLUS
|
328
359
|
static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
|
329
360
|
thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
|
@@ -338,9 +369,9 @@ static void thread_local_state_mark(void *data) {
|
|
338
369
|
// Keep thread around, to be able to extract names at the end
|
339
370
|
// We grab a lock here since thread creation can happen in multiple Ractors, and we want to make sure only one
|
340
371
|
// of them is mutating the array at a time. @ivoanjo: I think this is enough to make this safe....?
|
341
|
-
|
372
|
+
all_seen_threads_mutex_lock();
|
342
373
|
rb_ary_push(all_seen_threads, thread);
|
343
|
-
|
374
|
+
all_seen_threads_mutex_unlock();
|
344
375
|
}
|
345
376
|
return state;
|
346
377
|
}
|
@@ -385,7 +416,7 @@ static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread) {
|
|
385
416
|
|
386
417
|
// Can only be called while GvlTracing is not active + while holding the GVL
|
387
418
|
static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
|
388
|
-
|
419
|
+
all_seen_threads_mutex_lock();
|
389
420
|
|
390
421
|
VALUE alive_threads = rb_ary_new();
|
391
422
|
|
@@ -398,6 +429,51 @@ static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
|
|
398
429
|
|
399
430
|
rb_ary_replace(all_seen_threads, alive_threads);
|
400
431
|
|
401
|
-
|
432
|
+
all_seen_threads_mutex_unlock();
|
402
433
|
return Qtrue;
|
403
434
|
}
|
435
|
+
|
436
|
+
// Creates an event that follows the current native thread. Note that this assumes that whatever event
|
437
|
+
// made us call `render_os_thread_event` is an event about the current (native) thread; if the event is not about the
|
438
|
+
// current thread, the results will be incorrect.
|
439
|
+
static void render_os_thread_event(thread_local_state *state, double now_microseconds) {
|
440
|
+
finish_previous_os_thread_event(now_microseconds);
|
441
|
+
|
442
|
+
// Hack: If we name threads as "Thread N", perfetto seems to color them all with the same color, which looks awful.
|
443
|
+
// I did not check the code, but in practice perfetto seems to be doing some kind of hashing based only on regular
|
444
|
+
// chars, so here we append a different letter to each thread to cause the color hashing to differ.
|
445
|
+
char color_suffix_hack = ('a' + (thread_id_for(state) % 26));
|
446
|
+
|
447
|
+
fprintf(output_file,
|
448
|
+
" {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %u, \"ts\": %f, \"name\": \"Thread %d (%c)\"},\n",
|
449
|
+
OS_THREADS_VIEW_PID, current_native_thread_id(), now_microseconds, thread_id_for(state), color_suffix_hack
|
450
|
+
);
|
451
|
+
}
|
452
|
+
|
453
|
+
static void finish_previous_os_thread_event(double now_microseconds) {
|
454
|
+
fprintf(output_file,
|
455
|
+
" {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %u, \"ts\": %f},\n",
|
456
|
+
OS_THREADS_VIEW_PID, current_native_thread_id(), now_microseconds
|
457
|
+
);
|
458
|
+
}
|
459
|
+
|
460
|
+
static inline uint32_t current_native_thread_id(void) {
|
461
|
+
uint32_t native_thread_id = 0;
|
462
|
+
|
463
|
+
#ifdef HAVE_PTHREAD_THREADID_NP
|
464
|
+
uint64_t full_native_thread_id;
|
465
|
+
pthread_threadid_np(pthread_self(), &full_native_thread_id);
|
466
|
+
// Note: `pthread_threadid_np` is declared as taking in a `uint64_t` but I don't think macOS uses such really
|
467
|
+
// high thread ids, and anyway perfetto doesn't like full 64-bit ids for threads so let's go with a simplification
|
468
|
+
// for now.
|
469
|
+
native_thread_id = (uint32_t) full_native_thread_id;
|
470
|
+
#elif HAVE_GETTID
|
471
|
+
native_thread_id = gettid();
|
472
|
+
#else
|
473
|
+
// Note: We could use a native thread-local crappy fallback, but I think the two above alternatives are available
|
474
|
+
// on all OSs that support the GVL tracing API.
|
475
|
+
#error No native thread id available?
|
476
|
+
#endif
|
477
|
+
|
478
|
+
return native_thread_id;
|
479
|
+
}
|
data/gems.rb
CHANGED
data/lib/gvl-tracing.rb
CHANGED
data/lib/gvl_tracing/version.rb
CHANGED
data/preview.png
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gvl-tracing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivo Anjo
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
13
|
+
description:
|
14
14
|
email:
|
15
15
|
- ivo@ivoanjo.me
|
16
16
|
executables: []
|
@@ -37,7 +37,7 @@ homepage: https://github.com/ivoanjo/gvl-tracing
|
|
37
37
|
licenses:
|
38
38
|
- MIT
|
39
39
|
metadata: {}
|
40
|
-
post_install_message:
|
40
|
+
post_install_message:
|
41
41
|
rdoc_options: []
|
42
42
|
require_paths:
|
43
43
|
- lib
|
@@ -53,8 +53,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
requirements: []
|
56
|
-
rubygems_version: 3.
|
57
|
-
signing_key:
|
56
|
+
rubygems_version: 3.5.11
|
57
|
+
signing_key:
|
58
58
|
specification_version: 4
|
59
59
|
summary: Get a timeline view of Global VM Lock usage in your Ruby app
|
60
60
|
test_files: []
|