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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47df73ea238560430f56d7b1ee2f0484e9e57b8f0784e065209cd5cfbe7b1084
4
- data.tar.gz: 1517522202fd357b31db4a087f769991ccf9f0a0c56b38191f64976827b3e752
3
+ metadata.gz: af27ef999bd0dba6070d9faf4e4465f77ae7dc440d77d99999be6ac0c1be07ec
4
+ data.tar.gz: 2a0978b7135b22a9827e7f040f04ba7e261bd3ebb9f576218e19473d84560053
5
5
  SHA512:
6
- metadata.gz: ed938fa078cb982b23efe5da8a1a0fcb6d367534e7eeb342b2ce4853bf82cb7f0bb7998fd1a0bfa3bc3fd61a9f5d049717e245235dcf3df5a072be0b54eb9c2b
7
- data.tar.gz: c1f2db9dbde2a28321deee3b4186c08eb06f3da1f231f652d1822692e67e479e7be3a7b22daada77bc8130e9d6e016027b8470f9c84eceb827f4e06a17ac6d12
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 <threads.h>
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 mtx_t all_seen_threads_mutex;
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 void render_event(thread_local_state *, const char *event_name);
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, 1);
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
- uint32_t native_thread_id = 0;
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 void render_event(thread_local_state *state, const char *event_name) {
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
- if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
372
+ all_seen_threads_mutex_lock();
342
373
  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");
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
- if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
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
- if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
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
@@ -9,6 +9,7 @@ gem "rake-compiler", "~> 1.2"
9
9
  gem "pry"
10
10
  gem "pry-byebug"
11
11
  gem "rspec"
12
- gem "standard", "~> 1.33"
12
+ gem "standard", "~> 1.41"
13
13
  gem "concurrent-ruby"
14
14
  gem "benchmark-ips", "~> 2.13"
15
+ gem "rubocop", ">= 1.66.0"
data/lib/gvl-tracing.rb CHANGED
@@ -34,8 +34,8 @@ module GvlTracing
34
34
  private :_start
35
35
  private :_stop
36
36
 
37
- def start(file)
38
- _start(file)
37
+ def start(file, os_threads_view_enabled: false)
38
+ _start(file, os_threads_view_enabled)
39
39
  _init_local_storage(Thread.list)
40
40
  @path = file
41
41
 
@@ -5,7 +5,7 @@
5
5
  module GvlTracing::SleepTracking
6
6
  def sleep(...)
7
7
  GvlTracing.mark_sleeping
8
- super(...)
8
+ super
9
9
  end
10
10
  end
11
11
 
@@ -26,5 +26,5 @@
26
26
  # frozen_string_literal: true
27
27
 
28
28
  module GvlTracing
29
- VERSION = "1.5.1"
29
+ VERSION = "1.6.0"
30
30
  end
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.5.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-03-29 00:00:00.000000000 Z
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.4.10
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: []