gvl-tracing 1.6.1 → 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 -38
- data/lib/gvl_tracing/sleep_tracking.rb +1 -12
- data/lib/gvl_tracing/version.rb +1 -1
- metadata +6 -7
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
|
@@ -83,9 +85,13 @@ static VALUE gc_tracepoint = Qnil;
|
|
83
85
|
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
|
-
static pthread_mutex_t output_mutex = PTHREAD_MUTEX_INITIALIZER;
|
87
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);
|
88
93
|
|
94
|
+
static inline void initialize_timeslice_meta(void);
|
89
95
|
static VALUE tracing_init_local_storage(VALUE, VALUE);
|
90
96
|
static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_threads_view_enabled_arg);
|
91
97
|
static VALUE tracing_stop(VALUE _self);
|
@@ -93,11 +99,8 @@ static double timestamp_microseconds(void);
|
|
93
99
|
static double render_event(thread_local_state *, const char *event_name);
|
94
100
|
static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
|
95
101
|
static void on_gc_event(VALUE tpval, void *_unused1);
|
96
|
-
static VALUE mark_sleeping(VALUE _self);
|
97
102
|
static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused);
|
98
103
|
static void thread_local_state_mark(void *data);
|
99
|
-
static inline void output_mutex_lock(void);
|
100
|
-
static inline void output_mutex_unlock(void);
|
101
104
|
static inline int32_t thread_id_for(thread_local_state *state);
|
102
105
|
static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread);
|
103
106
|
static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self);
|
@@ -140,14 +143,27 @@ void Init_gvl_tracing_native_extension(void) {
|
|
140
143
|
|
141
144
|
all_seen_threads = rb_ary_new();
|
142
145
|
|
146
|
+
sleep_id = rb_intern("sleep");
|
147
|
+
|
143
148
|
VALUE gvl_tracing_module = rb_define_module("GvlTracing");
|
144
149
|
|
145
150
|
rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
|
146
151
|
rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 2);
|
147
152
|
rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
|
148
|
-
rb_define_singleton_method(gvl_tracing_module, "mark_sleeping", mark_sleeping, 0);
|
149
153
|
rb_define_singleton_method(gvl_tracing_module, "_thread_id_for", ruby_thread_id_for, 1);
|
150
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
|
+
}
|
151
167
|
}
|
152
168
|
|
153
169
|
static inline void initialize_thread_local_state(thread_local_state *state) {
|
@@ -187,10 +203,15 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_t
|
|
187
203
|
VALUE ruby_version = rb_const_get(rb_cObject, rb_intern("RUBY_VERSION"));
|
188
204
|
Check_Type(ruby_version, T_STRING);
|
189
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
|
+
|
190
211
|
fprintf(output_file, "[\n");
|
191
212
|
fprintf(output_file,
|
192
213
|
" {\"ph\": \"M\", \"pid\": %"PRId64", \"name\": \"process_name\", \"args\": {\"name\": \"Ruby threads view (%s)\"}},\n",
|
193
|
-
process_id, StringValuePtr(
|
214
|
+
process_id, StringValuePtr(metadata)
|
194
215
|
);
|
195
216
|
|
196
217
|
double now_microseconds = render_event(state, "started_tracing");
|
@@ -216,6 +237,8 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_t
|
|
216
237
|
|
217
238
|
rb_tracepoint_enable(gc_tracepoint);
|
218
239
|
|
240
|
+
RB_GC_GUARD(metadata);
|
241
|
+
|
219
242
|
return Qtrue;
|
220
243
|
}
|
221
244
|
|
@@ -262,7 +285,6 @@ static double render_event(thread_local_state *state, const char *event_name) {
|
|
262
285
|
// Important note: We've observed some rendering issues in perfetto if the tid or pid are numbers that are "too big",
|
263
286
|
// see https://github.com/ivoanjo/gvl-tracing/pull/4#issuecomment-1196463364 for an example.
|
264
287
|
|
265
|
-
output_mutex_lock();
|
266
288
|
fprintf(output_file,
|
267
289
|
// Finish previous duration
|
268
290
|
" {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f},\n" \
|
@@ -273,7 +295,6 @@ static double render_event(thread_local_state *state, const char *event_name) {
|
|
273
295
|
// Args for second line
|
274
296
|
process_id, thread_id_for(state), now_microseconds, event_name
|
275
297
|
);
|
276
|
-
output_mutex_unlock();
|
277
298
|
|
278
299
|
return now_microseconds;
|
279
300
|
}
|
@@ -285,9 +306,13 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
|
|
285
306
|
|
286
307
|
if (!state) return;
|
287
308
|
|
288
|
-
|
289
|
-
|
290
|
-
|
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
|
+
}
|
291
316
|
// In some cases, Ruby seems to emit multiple suspended events for the same thread in a row (e.g. when multiple threads)
|
292
317
|
// are waiting on a Thread::ConditionVariable.new that gets signaled. We coalesce these events to make the resulting
|
293
318
|
// timeline easier to see.
|
@@ -298,11 +323,18 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
|
|
298
323
|
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED && event_id == state->previous_state) return;
|
299
324
|
state->previous_state = event_id;
|
300
325
|
|
301
|
-
if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED &&
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
+
}
|
306
338
|
}
|
307
339
|
|
308
340
|
const char* event_name = "bug_unknown_event";
|
@@ -338,11 +370,6 @@ static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
|
|
338
370
|
render_event(state, event_name);
|
339
371
|
}
|
340
372
|
|
341
|
-
static VALUE mark_sleeping(UNUSED_ARG VALUE _self) {
|
342
|
-
GT_CURRENT_THREAD_LOCAL_STATE()->sleeping = true;
|
343
|
-
return Qnil;
|
344
|
-
}
|
345
|
-
|
346
373
|
static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused) { return sizeof(thread_local_state); }
|
347
374
|
|
348
375
|
static void thread_local_state_mark(void *data) {
|
@@ -360,18 +387,6 @@ static inline void all_seen_threads_mutex_unlock(void) {
|
|
360
387
|
if (error) rb_syserr_fail(error, "Failed to unlock GvlTracing mutex");
|
361
388
|
}
|
362
389
|
|
363
|
-
static inline void output_mutex_lock(void) {
|
364
|
-
int error = pthread_mutex_lock(&output_mutex);
|
365
|
-
// Can't raise exceptions on error since it gets used from outside the GVL
|
366
|
-
if (error) fprintf(stderr, "Failed to lock the GvlTracing output_mutex");
|
367
|
-
}
|
368
|
-
|
369
|
-
static inline void output_mutex_unlock(void) {
|
370
|
-
int error = pthread_mutex_unlock(&output_mutex);
|
371
|
-
// Can't raise exceptions on error since it gets used from outside the GVL
|
372
|
-
if (error) fprintf(stderr, "Failed to unlock the GvlTracing output_mutex");
|
373
|
-
}
|
374
|
-
|
375
390
|
#ifdef RUBY_3_3_PLUS
|
376
391
|
static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
|
377
392
|
thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
|
@@ -461,21 +476,17 @@ static void render_os_thread_event(thread_local_state *state, double now_microse
|
|
461
476
|
// chars, so here we append a different letter to each thread to cause the color hashing to differ.
|
462
477
|
char color_suffix_hack = ('a' + (thread_id_for(state) % 26));
|
463
478
|
|
464
|
-
output_mutex_lock();
|
465
479
|
fprintf(output_file,
|
466
480
|
" {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %u, \"ts\": %f, \"name\": \"Thread %d (%c)\"},\n",
|
467
481
|
OS_THREADS_VIEW_PID, current_native_thread_id(), now_microseconds, thread_id_for(state), color_suffix_hack
|
468
482
|
);
|
469
|
-
output_mutex_unlock();
|
470
483
|
}
|
471
484
|
|
472
485
|
static void finish_previous_os_thread_event(double now_microseconds) {
|
473
|
-
output_mutex_lock();
|
474
486
|
fprintf(output_file,
|
475
487
|
" {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %u, \"ts\": %f},\n",
|
476
488
|
OS_THREADS_VIEW_PID, current_native_thread_id(), now_microseconds
|
477
489
|
);
|
478
|
-
output_mutex_unlock();
|
479
490
|
}
|
480
491
|
|
481
492
|
static inline uint32_t current_native_thread_id(void) {
|
@@ -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,9 +16,12 @@ extensions:
|
|
18
16
|
- ext/gvl_tracing_native_extension/extconf.rb
|
19
17
|
extra_rdoc_files: []
|
20
18
|
files:
|
19
|
+
- ".rspec"
|
21
20
|
- CODE_OF_CONDUCT.adoc
|
22
21
|
- LICENSE
|
23
22
|
- README.adoc
|
23
|
+
- ext/gvl_tracing_native_extension/direct-bind.c
|
24
|
+
- ext/gvl_tracing_native_extension/direct-bind.h
|
24
25
|
- ext/gvl_tracing_native_extension/extconf.rb
|
25
26
|
- ext/gvl_tracing_native_extension/gvl_tracing.c
|
26
27
|
- gvl-tracing.gemspec
|
@@ -32,7 +33,6 @@ homepage: https://github.com/ivoanjo/gvl-tracing
|
|
32
33
|
licenses:
|
33
34
|
- MIT
|
34
35
|
metadata: {}
|
35
|
-
post_install_message:
|
36
36
|
rdoc_options: []
|
37
37
|
require_paths:
|
38
38
|
- lib
|
@@ -48,8 +48,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
48
|
- !ruby/object:Gem::Version
|
49
49
|
version: '0'
|
50
50
|
requirements: []
|
51
|
-
rubygems_version: 3.
|
52
|
-
signing_key:
|
51
|
+
rubygems_version: 3.6.7
|
53
52
|
specification_version: 4
|
54
53
|
summary: Get a timeline view of Global VM Lock usage in your Ruby app
|
55
54
|
test_files: []
|