gvl-tracing 1.6.0 → 1.7.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/.rspec +1 -0
- data/README.adoc +24 -5
- data/ext/gvl_tracing_native_extension/direct-bind.c +175 -0
- data/ext/gvl_tracing_native_extension/direct-bind.h +61 -0
- data/ext/gvl_tracing_native_extension/gvl_tracing.c +49 -17
- data/gvl-tracing.gemspec +2 -1
- data/lib/gvl_tracing/sleep_tracking.rb +1 -12
- data/lib/gvl_tracing/version.rb +1 -1
- metadata +6 -12
- data/.editorconfig +0 -22
- data/.ruby-version +0 -1
- data/.standard.yml +0 -9
- data/Rakefile +0 -36
- data/gems.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d331bd3080954e6c98634d72ed6a0a7b0ed3aa622cb2455bb38aea773d5ed44b
|
4
|
+
data.tar.gz: 6ba87866b4bcf64463aa60dee238fb1000367d8028f360237c88ed9a22353625
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a6cb0429d039c6980c1df628e33d8f5275ecefb0bc5417e59af714ee48c505c24b3a099e8e0d1d57654c3afabdd199588a88d6f35b0f65eafa50aea18b3bfd3
|
7
|
+
data.tar.gz: fb4b359539bb519526e6232b72b383c2de3ee1c9bae171f302918c52a8646a8e19386012946ccecf5611e05f176fa8b250e2761e2bc32ad077508c9ffdc2b8d8
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/README.adoc
CHANGED
@@ -8,9 +8,14 @@ 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
|
+
There's a few blog posts and conference talks about what this gem is and how to read its results:
|
12
12
|
|
13
|
-
|
13
|
+
* https://ivoanjo.me/blog/2025/03/30/mn-scheduling-and-how-the-ruby-gvl-impacts-app-perf/[m:n scheduling and how the (ruby) global vm lock impacts app performance]
|
14
|
+
* https://ivoanjo.me/blog/2023/07/23/understanding-the-ruby-global-vm-lock-by-observing-it/[understanding the ruby global vm lock by observing it]
|
15
|
+
* https://ivoanjo.me/blog/2023/02/11/ruby-unexpected-io-vs-cpu-unfairness/[ruby’s unexpected i/o vs cpu unfairness]
|
16
|
+
* https://ivoanjo.me/blog/2022/07/17/tracing-ruby-global-vm-lock/[tracing ruby’s (global) vm lock] (Also available [https://techracho.bpsinc.jp/hachi8833/2022_09_02/120530[in Japanese] and https://velog.io/@heka1024/%EB%B2%88%EC%97%AD-tracing-rubys-global-vm-lock[in Korean])
|
17
|
+
|
18
|
+
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 this writing) currently work on Microsoft Windows.
|
14
19
|
|
15
20
|
== Quickest start
|
16
21
|
|
@@ -68,11 +73,25 @@ This gem only provides a single module (`GvlTracing`) with methods:
|
|
68
73
|
|
69
74
|
The resulting traces can be analyzed by going to https://ui.perfetto.dev/[Perfetto UI].
|
70
75
|
|
71
|
-
|
76
|
+
=== What do each of the events mean?
|
77
|
+
|
78
|
+
The following events are shown in the timeline:
|
72
79
|
|
73
|
-
|
80
|
+
* `started_tracing`: First event, when `GvlTracing` is enabled
|
81
|
+
* `stopped_tracing`: Last event, when `GvlTracing` is disabled
|
82
|
+
* `started`: Ruby thread created
|
83
|
+
* `died`: Ruby thread died
|
84
|
+
* `wants_gvl`: Ruby thread is ready to execute, but needs the GVL before it can do so
|
85
|
+
* `running`: Ruby thread is running code (and owns the GVL)
|
86
|
+
* `waiting`: Ruby thread is waiting to be waken up when some event happens (IO, timeout)
|
87
|
+
* `gc`: Doing garbage collection
|
88
|
+
* `sleeping`: Thread called `Kernel#sleep`
|
89
|
+
|
90
|
+
Note that not all events come from the GVL instrumentation API, and some events were renamed vs the "RUBY_INTERNAL_THREAD_EVENT" entries.
|
91
|
+
|
92
|
+
== Experimental features
|
74
93
|
|
75
|
-
|
94
|
+
1. 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
95
|
|
77
96
|
== Tips
|
78
97
|
|
@@ -0,0 +1,175 @@
|
|
1
|
+
// direct-bind: Ruby gem for getting direct access to function pointers
|
2
|
+
// Copyright (c) 2025 Ivo Anjo <ivo@ivoanjo.me>
|
3
|
+
//
|
4
|
+
// This file is part of direct-bind.
|
5
|
+
//
|
6
|
+
// MIT License
|
7
|
+
//
|
8
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
// of this software and associated documentation files (the "Software"), to deal
|
10
|
+
// in the Software without restriction, including without limitation the rights
|
11
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
// copies of the Software, and to permit persons to whom the Software is
|
13
|
+
// furnished to do so, subject to the following conditions:
|
14
|
+
//
|
15
|
+
// The above copyright notice and this permission notice shall be included in all
|
16
|
+
// copies or substantial portions of the Software.
|
17
|
+
//
|
18
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
24
|
+
// SOFTWARE.
|
25
|
+
|
26
|
+
// See direct-bind.h for details on using direct-bind and why you may be finding this file vendored inside another gem.
|
27
|
+
|
28
|
+
#include "direct-bind.h"
|
29
|
+
|
30
|
+
static bool direct_bind_self_test(bool raise_on_failure);
|
31
|
+
|
32
|
+
// # Initialization and version management
|
33
|
+
|
34
|
+
bool direct_bind_initialize(VALUE publish_version_under, bool raise_on_failure) {
|
35
|
+
if (!direct_bind_self_test(raise_on_failure)) return false;
|
36
|
+
|
37
|
+
if (publish_version_under != Qnil) {
|
38
|
+
rb_define_const(rb_define_module_under(publish_version_under, "DirectBind"), "VERSION", rb_str_new_lit(DIRECT_BIND_VERSION));
|
39
|
+
}
|
40
|
+
|
41
|
+
return true;
|
42
|
+
}
|
43
|
+
|
44
|
+
// # Self-test implementation
|
45
|
+
|
46
|
+
#define SELF_TEST_ARITY 3
|
47
|
+
|
48
|
+
static VALUE self_test_target_func(
|
49
|
+
__attribute__((unused)) VALUE _1,
|
50
|
+
__attribute__((unused)) VALUE _2,
|
51
|
+
__attribute__((unused)) VALUE _3,
|
52
|
+
__attribute__((unused)) VALUE _4
|
53
|
+
) {
|
54
|
+
return Qnil;
|
55
|
+
}
|
56
|
+
|
57
|
+
static bool direct_bind_self_test(bool raise_on_failure) {
|
58
|
+
VALUE anonymous_module = rb_module_new();
|
59
|
+
rb_define_method(anonymous_module, "direct_bind_self_test_target", self_test_target_func, SELF_TEST_ARITY);
|
60
|
+
|
61
|
+
ID self_test_id = rb_intern("direct_bind_self_test_target");
|
62
|
+
direct_bind_cfunc_result test_target = direct_bind_get_cfunc_with_arity(anonymous_module, self_test_id, SELF_TEST_ARITY, raise_on_failure);
|
63
|
+
|
64
|
+
return test_target.ok && test_target.func == self_test_target_func;
|
65
|
+
}
|
66
|
+
|
67
|
+
// # Structure layouts and exported symbol definitions from Ruby
|
68
|
+
|
69
|
+
// ## From internal/gc.h
|
70
|
+
void rb_objspace_each_objects(int (*callback)(void *start, void *end, size_t stride, void *data), void *data);
|
71
|
+
int rb_objspace_internal_object_p(VALUE obj);
|
72
|
+
|
73
|
+
// ## From method.h
|
74
|
+
typedef struct rb_method_entry_struct {
|
75
|
+
VALUE flags;
|
76
|
+
VALUE defined_class;
|
77
|
+
struct rb_method_definition_struct * const def;
|
78
|
+
ID called_id;
|
79
|
+
VALUE owner;
|
80
|
+
} rb_method_entry_t;
|
81
|
+
|
82
|
+
// ### This was simplified/inlined vs the original structure
|
83
|
+
struct rb_method_definition_struct {
|
84
|
+
unsigned int type: 4;
|
85
|
+
int _ignored;
|
86
|
+
struct {
|
87
|
+
VALUE (*func)(ANYARGS);
|
88
|
+
void *_ignored;
|
89
|
+
int argc;
|
90
|
+
} cfunc;
|
91
|
+
};
|
92
|
+
|
93
|
+
// # This is where the magic happens: Using objectspace to find the method entry and retrieve the cfunc
|
94
|
+
|
95
|
+
typedef struct {
|
96
|
+
VALUE target_klass;
|
97
|
+
ID target_id;
|
98
|
+
direct_bind_cfunc_result result;
|
99
|
+
} find_data_t;
|
100
|
+
|
101
|
+
static bool valid_method_entry(VALUE object);
|
102
|
+
static bool found_target_method_entry(rb_method_entry_t *method_entry, find_data_t *find_data);
|
103
|
+
static int find_cfunc(void *start, void *end, size_t stride, void *data);
|
104
|
+
|
105
|
+
direct_bind_cfunc_result direct_bind_get_cfunc(VALUE klass, ID method_name, bool raise_on_failure) {
|
106
|
+
VALUE definition_not_found = rb_sprintf("method %"PRIsVALUE".%"PRIsVALUE" not found", klass, ID2SYM(method_name));
|
107
|
+
|
108
|
+
find_data_t find_data = {.target_klass = klass, .target_id = method_name, .result = {.ok = false, .failure_reason = definition_not_found}};
|
109
|
+
rb_objspace_each_objects(find_cfunc, &find_data);
|
110
|
+
|
111
|
+
if (raise_on_failure && find_data.result.ok == false) {
|
112
|
+
rb_raise(rb_eRuntimeError, "direct_bind_get_cfunc failed: %"PRIsVALUE, find_data.result.failure_reason);
|
113
|
+
}
|
114
|
+
|
115
|
+
return find_data.result;
|
116
|
+
}
|
117
|
+
|
118
|
+
direct_bind_cfunc_result direct_bind_get_cfunc_with_arity(VALUE klass, ID method_name, int arity, bool raise_on_failure) {
|
119
|
+
direct_bind_cfunc_result result = direct_bind_get_cfunc(klass, method_name, raise_on_failure);
|
120
|
+
|
121
|
+
if (result.ok && result.arity != arity) {
|
122
|
+
VALUE unexpected_arity = rb_sprintf("method %"PRIsVALUE".%"PRIsVALUE" unexpected arity %d, expected %d", klass, ID2SYM(method_name), result.arity, arity);
|
123
|
+
|
124
|
+
if (raise_on_failure) rb_raise(rb_eRuntimeError, "direct_bind_get_cfunc_with_arity failed: %"PRIsVALUE, unexpected_arity);
|
125
|
+
else result = (direct_bind_cfunc_result) {.ok = false, .failure_reason = unexpected_arity};
|
126
|
+
}
|
127
|
+
|
128
|
+
return result;
|
129
|
+
}
|
130
|
+
|
131
|
+
// TODO: Maybe change this to use safe memory reads that can never segv (e.g. if structure layouts are off?)
|
132
|
+
static int find_cfunc(void *start, void *end, size_t stride, void *data) {
|
133
|
+
const int stop_iteration = 1;
|
134
|
+
const int continue_iteration = 0;
|
135
|
+
const int vm_method_type_cfunc = 1;
|
136
|
+
|
137
|
+
find_data_t *find_data = (find_data_t *) data;
|
138
|
+
|
139
|
+
for (VALUE v = (VALUE) start; v != (VALUE) end; v += stride) {
|
140
|
+
if (!valid_method_entry(v)) continue;
|
141
|
+
|
142
|
+
rb_method_entry_t *method_entry = (rb_method_entry_t*) v;
|
143
|
+
if (!found_target_method_entry(method_entry, find_data)) continue;
|
144
|
+
|
145
|
+
if (method_entry->def == NULL) {
|
146
|
+
find_data->result.failure_reason = rb_str_new_lit("method_entry->def is NULL");
|
147
|
+
} else if (method_entry->def->type != vm_method_type_cfunc) {
|
148
|
+
find_data->result.failure_reason = rb_str_new_lit("method_entry is not a cfunc");
|
149
|
+
} else {
|
150
|
+
find_data->result = (direct_bind_cfunc_result) {
|
151
|
+
.ok = true,
|
152
|
+
.failure_reason = Qnil,
|
153
|
+
.arity = method_entry->def->cfunc.argc,
|
154
|
+
.func = method_entry->def->cfunc.func,
|
155
|
+
};
|
156
|
+
}
|
157
|
+
return stop_iteration;
|
158
|
+
}
|
159
|
+
|
160
|
+
return continue_iteration;
|
161
|
+
}
|
162
|
+
|
163
|
+
static bool is_method_entry(VALUE imemo) {
|
164
|
+
const unsigned long method_entry_id = 6;
|
165
|
+
return ((RBASIC(imemo)->flags >> FL_USHIFT) & method_entry_id) == method_entry_id;
|
166
|
+
}
|
167
|
+
|
168
|
+
static bool valid_method_entry(VALUE object) {
|
169
|
+
return rb_objspace_internal_object_p(object) && RB_BUILTIN_TYPE(object) == RUBY_T_IMEMO && RB_TYPE_P(object, RUBY_T_IMEMO) && is_method_entry(object);
|
170
|
+
}
|
171
|
+
|
172
|
+
static bool found_target_method_entry(rb_method_entry_t *method_entry, find_data_t *find_data) {
|
173
|
+
VALUE method_klass = method_entry->defined_class ? method_entry->defined_class : method_entry->owner;
|
174
|
+
return method_klass == find_data->target_klass && method_entry->called_id == find_data->target_id;
|
175
|
+
}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
// direct-bind: Ruby gem for getting direct access to function pointers
|
2
|
+
// Copyright (c) 2025 Ivo Anjo <ivo@ivoanjo.me>
|
3
|
+
//
|
4
|
+
// This file is part of direct-bind.
|
5
|
+
//
|
6
|
+
// MIT License
|
7
|
+
//
|
8
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
// of this software and associated documentation files (the "Software"), to deal
|
10
|
+
// in the Software without restriction, including without limitation the rights
|
11
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
// copies of the Software, and to permit persons to whom the Software is
|
13
|
+
// furnished to do so, subject to the following conditions:
|
14
|
+
//
|
15
|
+
// The above copyright notice and this permission notice shall be included in all
|
16
|
+
// copies or substantial portions of the Software.
|
17
|
+
//
|
18
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
24
|
+
// SOFTWARE.
|
25
|
+
|
26
|
+
// The recommended way to consume the direct-bind gem is to always vendor it.
|
27
|
+
// That is, use its rake task to automatically copy direct-bind.h and direct-bind.c into another gem's native extension
|
28
|
+
// sources folder. (P.s.: There's also a test helper to make sure copying is working fine and the gem is up-to-date.)
|
29
|
+
//
|
30
|
+
// This makes the actual Ruby direct-bind gem only a development dependency, simplifying distribution for the gem
|
31
|
+
// that uses it.
|
32
|
+
//
|
33
|
+
// For more details, check the direct-bind gem's documentation.
|
34
|
+
|
35
|
+
#pragma once
|
36
|
+
|
37
|
+
#include <stdbool.h>
|
38
|
+
#include <ruby.h>
|
39
|
+
|
40
|
+
#define DIRECT_BIND_VERSION "1.0.0"
|
41
|
+
|
42
|
+
typedef struct {
|
43
|
+
bool ok;
|
44
|
+
VALUE failure_reason;
|
45
|
+
int arity;
|
46
|
+
VALUE (*func)(ANYARGS);
|
47
|
+
} direct_bind_cfunc_result;
|
48
|
+
|
49
|
+
// Recommended to call once during your gem's initialization, to validate that direct-bind's Ruby hacking is in good shape and
|
50
|
+
// to make it easy to (optionally) validate what version you're using
|
51
|
+
bool direct_bind_initialize(VALUE publish_version_under, bool raise_on_failure);
|
52
|
+
|
53
|
+
// Provides the reverse of `rb_define_method`: Given a class and a method_name, retrieves the arity and func previously
|
54
|
+
// passed to `rb_define_method`.
|
55
|
+
//
|
56
|
+
// Performance note: As of this writing, this method scans objspace to find the definition of the method, so you
|
57
|
+
// most probably want to cache its result, rather than calling it very often.
|
58
|
+
direct_bind_cfunc_result direct_bind_get_cfunc(VALUE klass, ID method_name, bool raise_on_failure);
|
59
|
+
|
60
|
+
// Same as above, but automatically fails if arity isn't the expected value
|
61
|
+
direct_bind_cfunc_result direct_bind_get_cfunc_with_arity(VALUE klass, ID method_name, int arity, bool raise_on_failure);
|
@@ -27,6 +27,7 @@
|
|
27
27
|
#include <ruby/debug.h>
|
28
28
|
#include <ruby/thread.h>
|
29
29
|
#include <ruby/atomic.h>
|
30
|
+
|
30
31
|
#include <errno.h>
|
31
32
|
#include <inttypes.h>
|
32
33
|
#include <stdbool.h>
|
@@ -34,6 +35,8 @@
|
|
34
35
|
#include <pthread.h>
|
35
36
|
#include <stdint.h>
|
36
37
|
|
38
|
+
#include "direct-bind.h"
|
39
|
+
|
37
40
|
#include "extconf.h"
|
38
41
|
|
39
42
|
#ifdef HAVE_PTHREAD_H
|
@@ -69,7 +72,6 @@ typedef struct {
|
|
69
72
|
#endif
|
70
73
|
VALUE thread;
|
71
74
|
rb_event_flag_t previous_state; // Used to coalesce similar events
|
72
|
-
bool sleeping; // Used to track when a thread is sleeping
|
73
75
|
} thread_local_state;
|
74
76
|
|
75
77
|
// Global mutable state
|
@@ -84,7 +86,12 @@ static int thread_storage_key = 0;
|
|
84
86
|
static VALUE all_seen_threads = Qnil;
|
85
87
|
static pthread_mutex_t all_seen_threads_mutex = PTHREAD_MUTEX_INITIALIZER;
|
86
88
|
static bool os_threads_view_enabled;
|
89
|
+
static uint32_t timeslice_meta_ms = 0;
|
90
|
+
|
91
|
+
static ID sleep_id;
|
92
|
+
static VALUE (*is_thread_alive)(VALUE thread);
|
87
93
|
|
94
|
+
static inline void initialize_timeslice_meta(void);
|
88
95
|
static VALUE tracing_init_local_storage(VALUE, VALUE);
|
89
96
|
static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_threads_view_enabled_arg);
|
90
97
|
static VALUE tracing_stop(VALUE _self);
|
@@ -92,7 +99,6 @@ static double timestamp_microseconds(void);
|
|
92
99
|
static double render_event(thread_local_state *, const char *event_name);
|
93
100
|
static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
|
94
101
|
static void on_gc_event(VALUE tpval, void *_unused1);
|
95
|
-
static VALUE mark_sleeping(VALUE _self);
|
96
102
|
static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused);
|
97
103
|
static void thread_local_state_mark(void *data);
|
98
104
|
static inline int32_t thread_id_for(thread_local_state *state);
|
@@ -137,14 +143,27 @@ void Init_gvl_tracing_native_extension(void) {
|
|
137
143
|
|
138
144
|
all_seen_threads = rb_ary_new();
|
139
145
|
|
146
|
+
sleep_id = rb_intern("sleep");
|
147
|
+
|
140
148
|
VALUE gvl_tracing_module = rb_define_module("GvlTracing");
|
141
149
|
|
142
150
|
rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
|
143
151
|
rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 2);
|
144
152
|
rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
|
145
|
-
rb_define_singleton_method(gvl_tracing_module, "mark_sleeping", mark_sleeping, 0);
|
146
153
|
rb_define_singleton_method(gvl_tracing_module, "_thread_id_for", ruby_thread_id_for, 1);
|
147
154
|
rb_define_singleton_method(gvl_tracing_module, "trim_all_seen_threads", trim_all_seen_threads, 0);
|
155
|
+
|
156
|
+
initialize_timeslice_meta();
|
157
|
+
|
158
|
+
direct_bind_initialize(gvl_tracing_module, true);
|
159
|
+
is_thread_alive = direct_bind_get_cfunc_with_arity(rb_cThread, rb_intern("alive?"), 0, true).func;
|
160
|
+
}
|
161
|
+
|
162
|
+
static inline void initialize_timeslice_meta(void) {
|
163
|
+
const char *timeslice = getenv("RUBY_THREAD_TIMESLICE");
|
164
|
+
if (timeslice) {
|
165
|
+
timeslice_meta_ms = (uint32_t) strtol(timeslice, NULL, 0);
|
166
|
+
}
|
148
167
|
}
|
149
168
|
|
150
169
|
static inline void initialize_thread_local_state(thread_local_state *state) {
|
@@ -184,10 +203,15 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_t
|
|
184
203
|
VALUE ruby_version = rb_const_get(rb_cObject, rb_intern("RUBY_VERSION"));
|
185
204
|
Check_Type(ruby_version, T_STRING);
|
186
205
|
|
206
|
+
VALUE metadata = rb_obj_dup(ruby_version);
|
207
|
+
if (timeslice_meta_ms > 0) {
|
208
|
+
rb_str_append(metadata, rb_sprintf(", %ums", timeslice_meta_ms));
|
209
|
+
}
|
210
|
+
|
187
211
|
fprintf(output_file, "[\n");
|
188
212
|
fprintf(output_file,
|
189
213
|
" {\"ph\": \"M\", \"pid\": %"PRId64", \"name\": \"process_name\", \"args\": {\"name\": \"Ruby threads view (%s)\"}},\n",
|
190
|
-
process_id, StringValuePtr(
|
214
|
+
process_id, StringValuePtr(metadata)
|
191
215
|
);
|
192
216
|
|
193
217
|
double now_microseconds = render_event(state, "started_tracing");
|
@@ -213,6 +237,8 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_t
|
|
213
237
|
|
214
238
|
rb_tracepoint_enable(gc_tracepoint);
|
215
239
|
|
240
|
+
RB_GC_GUARD(metadata);
|
241
|
+
|
216
242
|
return Qtrue;
|
217
243
|
}
|
218
244
|
|
@@ -280,9 +306,13 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
|
|
280
306
|
|
281
307
|
if (!state) return;
|
282
308
|
|
283
|
-
|
284
|
-
|
285
|
-
|
309
|
+
if (!state->thread) {
|
310
|
+
#ifdef RUBY_3_3_PLUS
|
311
|
+
state->thread = event_data->thread;
|
312
|
+
#else
|
313
|
+
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED) { state->thread = rb_thread_current(); }
|
314
|
+
#endif
|
315
|
+
}
|
286
316
|
// In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
|
287
317
|
// are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
|
288
318
|
// timeline easier to see.
|
@@ -293,11 +323,18 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
|
|
293
323
|
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
|
294
324
|
state->previous_state = event_id;
|
295
325
|
|
296
|
-
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED &&
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
326
|
+
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED &&
|
327
|
+
// Check that thread is not being shut down
|
328
|
+
(state->thread != Qnil && is_thread_alive(state->thread))
|
329
|
+
) {
|
330
|
+
ID current_method = 0;
|
331
|
+
VALUE current_method_owner = Qnil;
|
332
|
+
rb_frame_method_id_and_class(¤t_method, ¤t_method_owner);
|
333
|
+
|
334
|
+
if (current_method == sleep_id && current_method_owner == rb_mKernel) {
|
335
|
+
render_event(state, "sleeping");
|
336
|
+
return;
|
337
|
+
}
|
301
338
|
}
|
302
339
|
|
303
340
|
const char* event_name = "bug_unknown_event";
|
@@ -333,11 +370,6 @@ static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
|
|
333
370
|
render_event(state, event_name);
|
334
371
|
}
|
335
372
|
|
336
|
-
static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
|
337
|
-
GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
|
338
|
-
return Qnil;
|
339
|
-
}
|
340
|
-
|
341
373
|
static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
|
342
374
|
|
343
375
|
static void thread_local_state_mark(void *data) {
|
data/gvl-tracing.gemspec
CHANGED
@@ -42,7 +42,8 @@ Gem::Specification.new do |spec|
|
|
42
42
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
43
43
|
spec.files = Dir.chdir(__dir__) do
|
44
44
|
`git ls-files -z`.split("\x0").reject do |f|
|
45
|
-
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|examples)/|\.(?:git|travis|circleci)|appveyor)})
|
45
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|examples)/|\.(?:git|travis|circleci)|appveyor)}) ||
|
46
|
+
[".editorconfig", ".ruby-version", ".standard.yml", "gems.rb", "Rakefile"].include?(f)
|
46
47
|
end
|
47
48
|
end
|
48
49
|
spec.require_paths = ["lib", "ext"]
|
@@ -1,12 +1 @@
|
|
1
|
-
|
2
|
-
# regular "waiting". This can be useful to distinguish when waiting is happening based on time, vs for some event to
|
3
|
-
# happen.
|
4
|
-
|
5
|
-
module GvlTracing::SleepTracking
|
6
|
-
def sleep(...)
|
7
|
-
GvlTracing.mark_sleeping
|
8
|
-
super
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
include GvlTracing::SleepTracking
|
1
|
+
warn("GvlTracing::SleepTracking no longer requires loading this file. Please remove your require 'gvl_tracing/sleep_tracking'.")
|
data/lib/gvl_tracing/version.rb
CHANGED
metadata
CHANGED
@@ -1,16 +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.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivo Anjo
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies: []
|
13
|
-
description:
|
14
12
|
email:
|
15
13
|
- ivo@ivoanjo.me
|
16
14
|
executables: []
|
@@ -18,16 +16,14 @@ extensions:
|
|
18
16
|
- ext/gvl_tracing_native_extension/extconf.rb
|
19
17
|
extra_rdoc_files: []
|
20
18
|
files:
|
21
|
-
- ".
|
22
|
-
- ".ruby-version"
|
23
|
-
- ".standard.yml"
|
19
|
+
- ".rspec"
|
24
20
|
- CODE_OF_CONDUCT.adoc
|
25
21
|
- LICENSE
|
26
22
|
- README.adoc
|
27
|
-
-
|
23
|
+
- ext/gvl_tracing_native_extension/direct-bind.c
|
24
|
+
- ext/gvl_tracing_native_extension/direct-bind.h
|
28
25
|
- ext/gvl_tracing_native_extension/extconf.rb
|
29
26
|
- ext/gvl_tracing_native_extension/gvl_tracing.c
|
30
|
-
- gems.rb
|
31
27
|
- gvl-tracing.gemspec
|
32
28
|
- lib/gvl-tracing.rb
|
33
29
|
- lib/gvl_tracing/sleep_tracking.rb
|
@@ -37,7 +33,6 @@ homepage: https://github.com/ivoanjo/gvl-tracing
|
|
37
33
|
licenses:
|
38
34
|
- MIT
|
39
35
|
metadata: {}
|
40
|
-
post_install_message:
|
41
36
|
rdoc_options: []
|
42
37
|
require_paths:
|
43
38
|
- lib
|
@@ -53,8 +48,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
48
|
- !ruby/object:Gem::Version
|
54
49
|
version: '0'
|
55
50
|
requirements: []
|
56
|
-
rubygems_version: 3.
|
57
|
-
signing_key:
|
51
|
+
rubygems_version: 3.6.7
|
58
52
|
specification_version: 4
|
59
53
|
summary: Get a timeline view of Global VM Lock usage in your Ruby app
|
60
54
|
test_files: []
|
data/.editorconfig
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
# EditorConfig is awesome: https://EditorConfig.org
|
2
|
-
|
3
|
-
# top-most EditorConfig file
|
4
|
-
root = true
|
5
|
-
|
6
|
-
# Unix-style newlines with a newline ending every file
|
7
|
-
[*]
|
8
|
-
end_of_line = lf
|
9
|
-
insert_final_newline = true
|
10
|
-
trim_trailing_whitespace = true
|
11
|
-
|
12
|
-
[*.h]
|
13
|
-
indent_style = space
|
14
|
-
indent_size = 2
|
15
|
-
|
16
|
-
[*.c]
|
17
|
-
indent_style = space
|
18
|
-
indent_size = 2
|
19
|
-
|
20
|
-
[*.yml]
|
21
|
-
indent_style = space
|
22
|
-
indent_size = 2
|
data/.ruby-version
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
ruby-3.2.2
|
data/.standard.yml
DELETED
data/Rakefile
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
# gvl-tracing: Ruby gem for getting a timelinew view of GVL usage
|
2
|
-
# Copyright (c) 2022 Ivo Anjo <ivo@ivoanjo.me>
|
3
|
-
#
|
4
|
-
# This file is part of gvl-tracing.
|
5
|
-
#
|
6
|
-
# MIT License
|
7
|
-
#
|
8
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
-
# of this software and associated documentation files (the "Software"), to deal
|
10
|
-
# in the Software without restriction, including without limitation the rights
|
11
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
-
# copies of the Software, and to permit persons to whom the Software is
|
13
|
-
# furnished to do so, subject to the following conditions:
|
14
|
-
#
|
15
|
-
# The above copyright notice and this permission notice shall be included in all
|
16
|
-
# copies or substantial portions of the Software.
|
17
|
-
#
|
18
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
24
|
-
# SOFTWARE.
|
25
|
-
|
26
|
-
# frozen_string_literal: true
|
27
|
-
|
28
|
-
require "bundler/gem_tasks"
|
29
|
-
require "standard/rake"
|
30
|
-
require "rake/extensiontask"
|
31
|
-
require "rspec/core/rake_task"
|
32
|
-
|
33
|
-
Rake::ExtensionTask.new("gvl_tracing_native_extension")
|
34
|
-
RSpec::Core::RakeTask.new(:spec)
|
35
|
-
|
36
|
-
task default: [:compile, :"standard:fix", :spec]
|
data/gems.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
source "https://rubygems.org"
|
4
|
-
|
5
|
-
gemspec
|
6
|
-
|
7
|
-
gem "rake", "~> 13.0"
|
8
|
-
gem "rake-compiler", "~> 1.2"
|
9
|
-
gem "pry"
|
10
|
-
gem "pry-byebug"
|
11
|
-
gem "rspec"
|
12
|
-
gem "standard", "~> 1.41"
|
13
|
-
gem "concurrent-ruby"
|
14
|
-
gem "benchmark-ips", "~> 2.13"
|
15
|
-
gem "rubocop", ">= 1.66.0"
|