gvl-tracing 0.1.1 → 1.0.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/README.adoc +2 -6
- data/ext/gvl_tracing_native_extension/extconf.rb +6 -0
- data/ext/gvl_tracing_native_extension/gvl_tracing.c +87 -8
- data/gvl-tracing.gemspec +2 -2
- data/lib/gvl_tracing/version.rb +1 -1
- metadata +4 -6
- data/examples/example1.json.gz +0 -0
- data/examples/example1.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 64d9b437af5f15e4a865a580486df67974e42b22b9836fa37e4de608597bf8d8
|
4
|
+
data.tar.gz: 52e7ecb9f89e613acd619a4225cfa2ac1f83cbf42e9173d59ccb91db09a1e7c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 529be0892156b7060427359d3e4386ed4f2bf0d96a3671896a6fcde4d347370e061567b8f88718ec894520ff9dc4b3bc667810da6d16b3ed171ef004ea3ecaf3
|
7
|
+
data.tar.gz: 8702d542f6962be8f0b471ed15e66c6ff4da1a41eadd121edacb3ade280109eeb219b4b7a6c1786ba13518948406f5182102af1fc363c7327e9a3c5bfe5e2cd4
|
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
|
-
See my blog post
|
11
|
+
See my blog post https://ivoanjo.me/blog/2022/07/17/tracing-ruby-global-vm-lock/[tracing ruby's (global) vm lock] for more details!
|
12
12
|
|
13
|
-
NOTE: This gem only works on Ruby 3.2 and above because it depends on the
|
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].
|
14
14
|
|
15
15
|
== Quickest start
|
16
16
|
|
@@ -43,10 +43,6 @@ To do so:
|
|
43
43
|
1. Download link:https://github.com/ivoanjo/gvl-tracing/blob/master/examples/example1.json.gz?raw=true[`examples/example1.json.gz`]
|
44
44
|
2. Navigate to https://ui.perfetto.dev/ and use the **Open trace file** option to load the file
|
45
45
|
|
46
|
-
== Quick start using docker
|
47
|
-
|
48
|
-
**FIXME: TODO**
|
49
|
-
|
50
46
|
== Installation
|
51
47
|
|
52
48
|
Install the gem and add to the application's `Gemfile` or `gems.rb` file by executing:
|
@@ -32,4 +32,10 @@ end
|
|
32
32
|
|
33
33
|
require "mkmf"
|
34
34
|
|
35
|
+
have_func("gettid", "unistd.h")
|
36
|
+
have_header("pthread.h")
|
37
|
+
have_func("pthread_getname_np", "pthread.h")
|
38
|
+
have_func("pthread_threadid_np", "pthread.h")
|
39
|
+
|
40
|
+
create_header
|
35
41
|
create_makefile "gvl_tracing_native_extension"
|
@@ -24,31 +24,78 @@
|
|
24
24
|
// SOFTWARE.
|
25
25
|
|
26
26
|
#include <ruby/ruby.h>
|
27
|
+
#include <ruby/debug.h>
|
27
28
|
#include <ruby/thread.h>
|
29
|
+
#include <ruby/atomic.h>
|
28
30
|
#include <errno.h>
|
29
31
|
#include <stdbool.h>
|
30
32
|
#include <sys/types.h>
|
31
|
-
|
33
|
+
|
34
|
+
#include "extconf.h"
|
35
|
+
|
36
|
+
#ifdef HAVE_PTHREAD_H
|
37
|
+
#include <pthread.h>
|
38
|
+
#endif
|
39
|
+
|
40
|
+
#ifdef HAVE_GETTID
|
41
|
+
#include <unistd.h>
|
42
|
+
#endif
|
32
43
|
|
33
44
|
static VALUE tracing_start(VALUE _self, VALUE output_path);
|
34
45
|
static VALUE tracing_stop(VALUE _self);
|
35
46
|
static double timestamp_microseconds(void);
|
36
47
|
static void render_event(const char *event_name);
|
37
|
-
static void
|
48
|
+
static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
|
49
|
+
static void on_gc_event(VALUE tpval, void *_unused1);
|
50
|
+
|
51
|
+
// Thread-local state
|
52
|
+
static _Thread_local bool current_thread_serial_set = false;
|
53
|
+
static _Thread_local unsigned int current_thread_serial = 0;
|
38
54
|
|
39
55
|
// Global mutable state
|
56
|
+
static rb_atomic_t thread_serial = 0;
|
40
57
|
static FILE *output_file = NULL;
|
41
58
|
static rb_internal_thread_event_hook_t *current_hook = NULL;
|
42
59
|
static double started_tracing_at_microseconds = 0;
|
43
60
|
static pid_t process_id = 0;
|
61
|
+
static VALUE gc_tracepoint = Qnil;
|
44
62
|
|
45
63
|
void Init_gvl_tracing_native_extension(void) {
|
64
|
+
rb_global_variable(&gc_tracepoint);
|
65
|
+
|
46
66
|
VALUE gvl_tracing_module = rb_define_module("GvlTracing");
|
47
67
|
|
48
68
|
rb_define_singleton_method(gvl_tracing_module, "start", tracing_start, 1);
|
49
69
|
rb_define_singleton_method(gvl_tracing_module, "stop", tracing_stop, 0);
|
50
70
|
}
|
51
71
|
|
72
|
+
static inline void initialize_thread_id(void) {
|
73
|
+
current_thread_serial_set = true;
|
74
|
+
current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
|
75
|
+
}
|
76
|
+
|
77
|
+
static inline void render_thread_metadata(void) {
|
78
|
+
uint64_t native_thread_id = 0;
|
79
|
+
#ifdef HAVE_GETTID
|
80
|
+
native_thread_id = gettid();
|
81
|
+
#elif HAVE_PTHREAD_THREADID_NP
|
82
|
+
pthread_threadid_np(pthread_self(), &native_thread_id);
|
83
|
+
#else
|
84
|
+
native_thread_id = current_thread_serial; // TODO: Better fallback for Windows?
|
85
|
+
#endif
|
86
|
+
|
87
|
+
char native_thread_name_buffer[64] = "(unnamed)";
|
88
|
+
|
89
|
+
#ifdef HAVE_PTHREAD_GETNAME_NP
|
90
|
+
pthread_getname_np(pthread_self(), native_thread_name_buffer, sizeof(native_thread_name_buffer));
|
91
|
+
#endif
|
92
|
+
|
93
|
+
fprintf(output_file,
|
94
|
+
" {\"ph\": \"M\", \"pid\": %u, \"tid\": %u, \"name\": \"thread_name\", \"args\": {\"name\": \"%lu %s\"}},\n",
|
95
|
+
process_id, current_thread_serial, native_thread_id, native_thread_name_buffer
|
96
|
+
);
|
97
|
+
}
|
98
|
+
|
52
99
|
static VALUE tracing_start(VALUE _self, VALUE output_path) {
|
53
100
|
Check_Type(output_path, T_STRING);
|
54
101
|
|
@@ -63,7 +110,7 @@ static VALUE tracing_start(VALUE _self, VALUE output_path) {
|
|
63
110
|
render_event("started_tracing");
|
64
111
|
|
65
112
|
current_hook = rb_internal_thread_add_event_hook(
|
66
|
-
|
113
|
+
on_thread_event,
|
67
114
|
(
|
68
115
|
RUBY_INTERNAL_THREAD_EVENT_READY |
|
69
116
|
RUBY_INTERNAL_THREAD_EVENT_RESUMED |
|
@@ -74,6 +121,17 @@ static VALUE tracing_start(VALUE _self, VALUE output_path) {
|
|
74
121
|
NULL
|
75
122
|
);
|
76
123
|
|
124
|
+
gc_tracepoint = rb_tracepoint_new(
|
125
|
+
0,
|
126
|
+
(
|
127
|
+
RUBY_INTERNAL_EVENT_GC_ENTER |
|
128
|
+
RUBY_INTERNAL_EVENT_GC_EXIT
|
129
|
+
),
|
130
|
+
on_gc_event,
|
131
|
+
(void *) NULL
|
132
|
+
);
|
133
|
+
rb_tracepoint_enable(gc_tracepoint);
|
134
|
+
|
77
135
|
return Qtrue;
|
78
136
|
}
|
79
137
|
|
@@ -81,6 +139,8 @@ static VALUE tracing_stop(VALUE _self) {
|
|
81
139
|
if (output_file == NULL) rb_raise(rb_eRuntimeError, "Tracing not running");
|
82
140
|
|
83
141
|
rb_internal_thread_remove_event_hook(current_hook);
|
142
|
+
rb_tracepoint_disable(gc_tracepoint);
|
143
|
+
gc_tracepoint = Qnil;
|
84
144
|
|
85
145
|
render_event("stopped_tracing");
|
86
146
|
fprintf(output_file, "]\n");
|
@@ -103,11 +163,20 @@ static double timestamp_microseconds(void) {
|
|
103
163
|
static void render_event(const char *event_name) {
|
104
164
|
// Event data
|
105
165
|
double now_microseconds = timestamp_microseconds() - started_tracing_at_microseconds;
|
106
|
-
|
166
|
+
|
167
|
+
if (!current_thread_serial_set) {
|
168
|
+
initialize_thread_id();
|
169
|
+
render_thread_metadata();
|
170
|
+
}
|
171
|
+
|
172
|
+
unsigned int thread_id = current_thread_serial;
|
107
173
|
|
108
174
|
// Each event is converted into two events in the output: one that signals the end of the previous event
|
109
175
|
// (whatever it was), and one that signals the start of the actual event we're processing.
|
110
|
-
// Yes this
|
176
|
+
// Yes, this seems to be slightly bending the intention of the output format, but it seemed easier to do this way.
|
177
|
+
|
178
|
+
// Important note: We've observed some rendering issues in perfetto if the tid or pid are numbers that are "too big",
|
179
|
+
// see https://github.com/ivoanjo/gvl-tracing/pull/4#issuecomment-1196463364 for an example.
|
111
180
|
|
112
181
|
fprintf(output_file,
|
113
182
|
// Finish previous duration
|
@@ -121,14 +190,24 @@ static void render_event(const char *event_name) {
|
|
121
190
|
);
|
122
191
|
}
|
123
192
|
|
124
|
-
static void
|
193
|
+
static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *_unused1, void *_unused2) {
|
125
194
|
const char* event_name = "bug_unknown_event";
|
126
195
|
switch (event_id) {
|
127
196
|
case RUBY_INTERNAL_THREAD_EVENT_READY: event_name = "ready"; break;
|
128
|
-
case RUBY_INTERNAL_THREAD_EVENT_RESUMED: event_name = "
|
129
|
-
case RUBY_INTERNAL_THREAD_EVENT_SUSPENDED: event_name = "
|
197
|
+
case RUBY_INTERNAL_THREAD_EVENT_RESUMED: event_name = "running"; break;
|
198
|
+
case RUBY_INTERNAL_THREAD_EVENT_SUSPENDED: event_name = "waiting"; break;
|
130
199
|
case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
|
131
200
|
case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "exited"; break;
|
132
201
|
};
|
133
202
|
render_event(event_name);
|
134
203
|
}
|
204
|
+
|
205
|
+
static void on_gc_event(VALUE tpval, void *_unused1) {
|
206
|
+
const char* event_name = "bug_unknown_event";
|
207
|
+
switch (rb_tracearg_event_flag(rb_tracearg_from_tracepoint(tpval))) {
|
208
|
+
case RUBY_INTERNAL_EVENT_GC_ENTER: event_name = "gc"; break;
|
209
|
+
// TODO: is it possible the thread wasn't running? Might need to save the last state.
|
210
|
+
case RUBY_INTERNAL_EVENT_GC_EXIT: event_name = "running"; break;
|
211
|
+
}
|
212
|
+
render_event(event_name);
|
213
|
+
}
|
data/gvl-tracing.gemspec
CHANGED
@@ -36,13 +36,13 @@ Gem::Specification.new do |spec|
|
|
36
36
|
spec.summary = "Get a timeline view of Global VM Lock usage in your Ruby app"
|
37
37
|
spec.homepage = "https://github.com/ivoanjo/gvl-tracing"
|
38
38
|
spec.license = "MIT"
|
39
|
-
spec.required_ruby_version = ">= 3.2.0
|
39
|
+
spec.required_ruby_version = ">= 3.2.0"
|
40
40
|
|
41
41
|
# Specify which files should be added to the gem when it is released.
|
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)/|\.(?:git|travis|circleci)|appveyor)})
|
45
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|examples)/|\.(?:git|travis|circleci)|appveyor)})
|
46
46
|
end
|
47
47
|
end
|
48
48
|
spec.require_paths = ["lib", "ext"]
|
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: 0.
|
4
|
+
version: 1.0.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: 2023-02-11 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email:
|
@@ -24,8 +24,6 @@ files:
|
|
24
24
|
- LICENSE
|
25
25
|
- README.adoc
|
26
26
|
- Rakefile
|
27
|
-
- examples/example1.json.gz
|
28
|
-
- examples/example1.rb
|
29
27
|
- ext/gvl_tracing_native_extension/extconf.rb
|
30
28
|
- ext/gvl_tracing_native_extension/gvl_tracing.c
|
31
29
|
- gems.rb
|
@@ -46,14 +44,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
44
|
requirements:
|
47
45
|
- - ">="
|
48
46
|
- !ruby/object:Gem::Version
|
49
|
-
version: 3.2.0
|
47
|
+
version: 3.2.0
|
50
48
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
49
|
requirements:
|
52
50
|
- - ">="
|
53
51
|
- !ruby/object:Gem::Version
|
54
52
|
version: '0'
|
55
53
|
requirements: []
|
56
|
-
rubygems_version: 3.4.
|
54
|
+
rubygems_version: 3.4.1
|
57
55
|
signing_key:
|
58
56
|
specification_version: 4
|
59
57
|
summary: Get a timeline view of Global VM Lock usage in your Ruby app
|
data/examples/example1.json.gz
DELETED
Binary file
|
data/examples/example1.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
require "gvl-tracing"
|
2
|
-
|
3
|
-
def fib(n)
|
4
|
-
return n if n <= 1
|
5
|
-
fib(n - 1) + fib(n - 2)
|
6
|
-
end
|
7
|
-
|
8
|
-
GvlTracing.start("example1.json")
|
9
|
-
|
10
|
-
Thread.new { sleep(0.05) while true }
|
11
|
-
|
12
|
-
sleep(0.05)
|
13
|
-
|
14
|
-
3.times.map { Thread.new { fib(37) } }.map(&:join)
|
15
|
-
|
16
|
-
sleep(0.05)
|
17
|
-
|
18
|
-
GvlTracing.stop
|