gvl-tracing 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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: []