gvl-tracing 1.4.0 → 1.5.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/.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 +220 -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: 9640e68d58cfbda91dc7c034982b3e43eec6048729d0ca61c8498495ad8f4277
         | 
| 4 | 
            +
              data.tar.gz: ab154e734bfb085aff5615837efda1b7836784019fd689051b21db483f86c62b
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: a87a874de0373814d27f9e567c19fa4d36f5c2cdb5db2f3a6d7a7ce3280917dd8ecf0fc85b130f3573aa496cc5633427b92780f48ab00dc9cac8e6649763502a
         | 
| 7 | 
            +
              data.tar.gz: c5ef422b98a1cf2835f62d0769c0e0ea2b7d0f6af36888f05617795e4667a0bf4bbcf404e0e860fbdf319ae2be8e41fd84f09c2168282e6a0adfbee92a252840
         | 
    
        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 | 
            -
              set_native_thread_id();
         | 
| 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);
         | 
| 91 146 |  | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 147 | 
            +
              #ifdef RUBY_3_2
         | 
| 148 | 
            +
                uint32_t native_thread_id = 0;
         | 
| 94 149 |  | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 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;
         | 
| 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,39 @@ 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 | 
            +
              if (!state) return;
         | 
| 268 | 
            +
              #ifdef RUBY_3_3_PLUS
         | 
| 269 | 
            +
                if (!state->thread) state->thread = event_data->thread;
         | 
| 270 | 
            +
              #endif
         | 
| 271 | 
            +
              // In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
         | 
| 212 272 | 
             
              // are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
         | 
| 213 273 | 
             
              // timeline easier to see.
         | 
| 214 274 | 
             
              //
         | 
| 215 275 | 
             
              // I haven't observed other situations where we'd want to coalesce events, but we may apply this to all events in the
         | 
| 216 276 | 
             
              // future. One annoying thing to remember when generalizing this is how to reset the `previous_state` across multiple
         | 
| 217 277 | 
             
              // start/stop calls to GvlTracing.
         | 
| 218 | 
            -
              if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == previous_state) return;
         | 
| 219 | 
            -
              previous_state = event_id;
         | 
| 278 | 
            +
              if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
         | 
| 279 | 
            +
              state->previous_state = event_id;
         | 
| 220 280 |  | 
| 221 | 
            -
              if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && sleeping) {
         | 
| 222 | 
            -
                render_event("sleeping");
         | 
| 281 | 
            +
              if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && state->sleeping) {
         | 
| 282 | 
            +
                render_event(state, "sleeping");
         | 
| 223 283 | 
             
                return;
         | 
| 224 284 | 
             
              } else {
         | 
| 225 | 
            -
                sleeping = false;
         | 
| 285 | 
            +
                state->sleeping = false;
         | 
| 226 286 | 
             
              }
         | 
| 227 287 |  | 
| 228 288 | 
             
              const char* event_name = "bug_unknown_event";
         | 
| @@ -233,20 +293,106 @@ static void on_thread_event(rb_event_flag_t event_id, UNUSED_ARG const rb_intern | |
| 233 293 | 
             
                case RUBY_INTERNAL_THREAD_EVENT_STARTED:   event_name = "started";   break;
         | 
| 234 294 | 
             
                case RUBY_INTERNAL_THREAD_EVENT_EXITED:    event_name = "died";      break;
         | 
| 235 295 | 
             
              };
         | 
| 236 | 
            -
              render_event(event_name);
         | 
| 296 | 
            +
              render_event(state, event_name);
         | 
| 237 297 | 
             
            }
         | 
| 238 298 |  | 
| 239 299 | 
             
            static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
         | 
| 240 300 | 
             
              const char* event_name = "bug_unknown_event";
         | 
| 301 | 
            +
              thread_local_state *state = GT_LOCAL_STATE(rb_thread_current(), false); // no alloc during GC
         | 
| 241 302 | 
             
              switch (rb_tracearg_event_flag(rb_tracearg_from_tracepoint(tpval))) {
         | 
| 242 303 | 
             
                case RUBY_INTERNAL_EVENT_GC_ENTER: event_name = "gc"; break;
         | 
| 243 304 | 
             
                // TODO: is it possible the thread wasn't running? Might need to save the last state.
         | 
| 244 305 | 
             
                case RUBY_INTERNAL_EVENT_GC_EXIT: event_name = "running"; break;
         | 
| 245 306 | 
             
              }
         | 
| 246 | 
            -
              render_event(event_name);
         | 
| 307 | 
            +
              render_event(state, event_name);
         | 
| 247 308 | 
             
            }
         | 
| 248 309 |  | 
| 249 | 
            -
            static VALUE mark_sleeping(VALUE _self) {
         | 
| 250 | 
            -
              sleeping = true;
         | 
| 310 | 
            +
            static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
         | 
| 311 | 
            +
              GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
         | 
| 251 312 | 
             
              return Qnil;
         | 
| 252 313 | 
             
            }
         | 
| 314 | 
            +
             | 
| 315 | 
            +
            static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
         | 
| 316 | 
            +
             | 
| 317 | 
            +
            static void thread_local_state_mark(void *data) {
         | 
| 318 | 
            +
              thread_local_state *state = (thread_local_state *)data;
         | 
| 319 | 
            +
              rb_gc_mark(state->thread); // Marking thread to make sure it stays pinned
         | 
| 320 | 
            +
            }
         | 
| 321 | 
            +
             | 
| 322 | 
            +
            #ifdef RUBY_3_3_PLUS
         | 
| 323 | 
            +
              static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
         | 
| 324 | 
            +
                thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
         | 
| 325 | 
            +
                if (!state && allocate) {
         | 
| 326 | 
            +
                  VALUE wrapper = TypedData_Make_Struct(rb_cObject, thread_local_state, &thread_local_state_type, state);
         | 
| 327 | 
            +
                  state->thread = thread;
         | 
| 328 | 
            +
                  rb_thread_local_aset(thread, rb_intern("__gvl_tracing_local_state"), wrapper);
         | 
| 329 | 
            +
                  rb_internal_thread_specific_set(thread, thread_storage_key, state);
         | 
| 330 | 
            +
                  RB_GC_GUARD(wrapper);
         | 
| 331 | 
            +
                  initialize_thread_local_state(state);
         | 
| 332 | 
            +
             | 
| 333 | 
            +
                  // Keep thread around, to be able to extract names at the end
         | 
| 334 | 
            +
                  // We grab a lock here since thread creation can happen in multiple Ractors, and we want to make sure only one
         | 
| 335 | 
            +
                  // of them is mutating the array at a time. @ivoanjo: I think this is enough to make this safe....?
         | 
| 336 | 
            +
                  if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
         | 
| 337 | 
            +
                  rb_ary_push(all_seen_threads, thread);
         | 
| 338 | 
            +
                  if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
         | 
| 339 | 
            +
                }
         | 
| 340 | 
            +
                return state;
         | 
| 341 | 
            +
              }
         | 
| 342 | 
            +
            #endif
         | 
| 343 | 
            +
             | 
| 344 | 
            +
            #ifdef RUBY_3_2
         | 
| 345 | 
            +
              static inline thread_local_state *GT_CURRENT_THREAD_LOCAL_STATE(void) {
         | 
| 346 | 
            +
                thread_local_state *state = &__thread_local_state;
         | 
| 347 | 
            +
                if (!state->initialized) {
         | 
| 348 | 
            +
                  initialize_thread_local_state(state);
         | 
| 349 | 
            +
                }
         | 
| 350 | 
            +
                return state;
         | 
| 351 | 
            +
              }
         | 
| 352 | 
            +
            #endif
         | 
| 353 | 
            +
             | 
| 354 | 
            +
            static inline int32_t thread_id_for(thread_local_state *state) {
         | 
| 355 | 
            +
              // We use different strategies for 3.2 vs 3.3+ to identify threads. This is because:
         | 
| 356 | 
            +
              //
         | 
| 357 | 
            +
              // 1. On 3.2 we have no way of associating the actual thread VALUE object with the state/serial, so instead we identify
         | 
| 358 | 
            +
              //    threads by their native ids. This is not entirely correct, since Ruby can reuse native threads (e.g. if a thread
         | 
| 359 | 
            +
              //    dies and another immediately gets created) but it's good enough for our purposes. (Associating the thread VALUE
         | 
| 360 | 
            +
              //    object is useful to, e.g. get thread names later.)
         | 
| 361 | 
            +
              //
         | 
| 362 | 
            +
              // 2. On 3.3 we can associate the state/serial with the thread VALUE object AND additionally with the MN scheduler
         | 
| 363 | 
            +
              //    the same thread VALUE can end up executing on different native threads so using the native thread id as an
         | 
| 364 | 
            +
              //    identifier would be wrong.
         | 
| 365 | 
            +
              #ifdef RUBY_3_3_PLUS
         | 
| 366 | 
            +
                return state->current_thread_serial;
         | 
| 367 | 
            +
              #else
         | 
| 368 | 
            +
                return state->native_thread_id;
         | 
| 369 | 
            +
              #endif
         | 
| 370 | 
            +
            }
         | 
| 371 | 
            +
             | 
| 372 | 
            +
            static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread) {
         | 
| 373 | 
            +
              #ifdef RUBY_3_2
         | 
| 374 | 
            +
                rb_raise(rb_eRuntimeError, "On Ruby 3.2 we should use the native thread id directly");
         | 
| 375 | 
            +
              #endif
         | 
| 376 | 
            +
             | 
| 377 | 
            +
              thread_local_state *state = GT_LOCAL_STATE(thread, true);
         | 
| 378 | 
            +
              return INT2FIX(thread_id_for(state));
         | 
| 379 | 
            +
            }
         | 
| 380 | 
            +
             | 
| 381 | 
            +
            // Can only be called while GvlTracing is not active + while holding the GVL
         | 
| 382 | 
            +
            static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
         | 
| 383 | 
            +
              if (mtx_lock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to lock GvlTracing mutex");
         | 
| 384 | 
            +
             | 
| 385 | 
            +
              VALUE alive_threads = rb_ary_new();
         | 
| 386 | 
            +
             | 
| 387 | 
            +
              for (long i = 0, len = RARRAY_LEN(all_seen_threads); i < len; i++) {
         | 
| 388 | 
            +
                VALUE thread = RARRAY_AREF(all_seen_threads, i);
         | 
| 389 | 
            +
                if (rb_funcall(thread, rb_intern("alive?"), 0) == Qtrue) {
         | 
| 390 | 
            +
                  rb_ary_push(alive_threads, thread);
         | 
| 391 | 
            +
                }
         | 
| 392 | 
            +
              }
         | 
| 393 | 
            +
             | 
| 394 | 
            +
              rb_ary_replace(all_seen_threads, alive_threads);
         | 
| 395 | 
            +
             | 
| 396 | 
            +
              if (mtx_unlock(&all_seen_threads_mutex) != thrd_success) rb_raise(rb_eRuntimeError, "Failed to unlock GvlTracing mutex");
         | 
| 397 | 
            +
              return Qtrue;
         | 
| 398 | 
            +
            }
         | 
    
        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.0
         | 
| 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. | 
| 56 | 
            +
            rubygems_version: 3.5.3
         | 
| 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
         |