gvl-tracing 1.5.2 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.adoc +7 -1
- data/ext/gvl_tracing_native_extension/gvl_tracing.c +91 -28
- 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 .
|
@@ -32,6 +32,7 @@
|
|
32
32
|
#include <stdbool.h>
|
33
33
|
#include <sys/types.h>
|
34
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;
|
@@ -78,12 +83,13 @@ static VALUE gc_tracepoint = Qnil;
|
|
78
83
|
static int thread_storage_key = 0;
|
79
84
|
static VALUE all_seen_threads = Qnil;
|
80
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 = {
|
@@ -131,7 +140,7 @@ void Init_gvl_tracing_native_extension(void) {
|
|
131
140
|
VALUE gvl_tracing_module = rb_define_module("GvlTracing");
|
132
141
|
|
133
142
|
rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
|
134
|
-
rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start,
|
143
|
+
rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 2);
|
135
144
|
rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
|
136
145
|
rb_define_singleton_method(gvl_tracing_module, "mark_sleeping", mark_sleeping, 0);
|
137
146
|
rb_define_singleton_method(gvl_tracing_module, "_thread_id_for", ruby_thread_id_for, 1);
|
@@ -143,24 +152,7 @@ static inline void initialize_thread_local_state(thread_local_state *state) {
|
|
143
152
|
state->current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
|
144
153
|
|
145
154
|
#ifdef RUBY_3_2
|
146
|
-
|
147
|
-
|
148
|
-
#ifdef HAVE_PTHREAD_THREADID_NP
|
149
|
-
uint64_t full_native_thread_id;
|
150
|
-
pthread_threadid_np(pthread_self(), &full_native_thread_id);
|
151
|
-
// Note: `pthread_threadid_np` is declared as taking in a `uint64_t` but I don't think macOS uses such really
|
152
|
-
// high thread ids, and anyway perfetto doesn't like full 64-bit ids for threads so let's go with a simplification
|
153
|
-
// for now.
|
154
|
-
native_thread_id = (uint32_t) full_native_thread_id;
|
155
|
-
#elif HAVE_GETTID
|
156
|
-
native_thread_id = gettid();
|
157
|
-
#else
|
158
|
-
// Note: We could use the current_thread_serial as a crappy fallback, but this would make getting thread names
|
159
|
-
// not work very well
|
160
|
-
#error No native thread id available?
|
161
|
-
#endif
|
162
|
-
|
163
|
-
state->native_thread_id = native_thread_id;
|
155
|
+
state->native_thread_id = current_native_thread_id();
|
164
156
|
#endif
|
165
157
|
}
|
166
158
|
|
@@ -174,8 +166,9 @@ static VALUE tracing_init_local_storage(UNUSED_ARG VALUE _self, VALUE threads) {
|
|
174
166
|
return Qtrue;
|
175
167
|
}
|
176
168
|
|
177
|
-
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) {
|
178
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");
|
179
172
|
|
180
173
|
trim_all_seen_threads(Qnil);
|
181
174
|
|
@@ -183,13 +176,26 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
|
|
183
176
|
output_file = fopen(StringValuePtr(output_path), "w");
|
184
177
|
if (output_file == NULL) rb_syserr_fail(errno, "Failed to open GvlTracing output file");
|
185
178
|
|
186
|
-
fprintf(output_file, "[\n");
|
187
|
-
|
188
179
|
thread_local_state *state = GT_CURRENT_THREAD_LOCAL_STATE();
|
189
180
|
started_tracing_at_microseconds = timestamp_microseconds();
|
190
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
|
+
);
|
191
192
|
|
192
|
-
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
|
+
}
|
193
199
|
|
194
200
|
current_hook = rb_internal_thread_add_event_hook(
|
195
201
|
on_thread_event,
|
@@ -218,7 +224,9 @@ static VALUE tracing_stop(UNUSED_ARG VALUE _self) {
|
|
218
224
|
rb_tracepoint_disable(gc_tracepoint);
|
219
225
|
gc_tracepoint = Qnil;
|
220
226
|
|
221
|
-
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
|
+
|
222
230
|
// closing the json syntax in the output file is handled in GvlTracing.stop code
|
223
231
|
|
224
232
|
if (fclose(output_file) != 0) rb_syserr_fail(errno, "Failed to close GvlTracing output file");
|
@@ -240,7 +248,7 @@ static double timestamp_microseconds(void) {
|
|
240
248
|
|
241
249
|
// Render output using trace event format for perfetto:
|
242
250
|
// https://chromium.googlesource.com/catapult/+/refs/heads/main/docs/trace-event-format.md
|
243
|
-
static
|
251
|
+
static double render_event(thread_local_state *state, const char *event_name) {
|
244
252
|
// Event data
|
245
253
|
double now_microseconds = timestamp_microseconds() - started_tracing_at_microseconds;
|
246
254
|
|
@@ -261,6 +269,8 @@ static void render_event(thread_local_state *state, const char *event_name) {
|
|
261
269
|
// Args for second line
|
262
270
|
process_id, thread_id_for(state), now_microseconds, event_name
|
263
271
|
);
|
272
|
+
|
273
|
+
return now_microseconds;
|
264
274
|
}
|
265
275
|
|
266
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) {
|
@@ -298,7 +308,15 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
|
|
298
308
|
case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
|
299
309
|
case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "died"; break;
|
300
310
|
};
|
301
|
-
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
|
+
}
|
302
320
|
}
|
303
321
|
|
304
322
|
static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
|
@@ -414,3 +432,48 @@ static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
|
|
414
432
|
all_seen_threads_mutex_unlock();
|
415
433
|
return Qtrue;
|
416
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.5.
|
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: []
|