gvl-tracing 0.1.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be2bff3cbad042f549ffee919cb659fe201d49714ae64924ae3fa30aba79d331
4
- data.tar.gz: d138181409369aed6507b63efa33da6d4ccc261c7e0aae469fe250bcb2b062ae
3
+ metadata.gz: 64d9b437af5f15e4a865a580486df67974e42b22b9836fa37e4de608597bf8d8
4
+ data.tar.gz: 52e7ecb9f89e613acd619a4225cfa2ac1f83cbf42e9173d59ccb91db09a1e7c6
5
5
  SHA512:
6
- metadata.gz: 544ada893bf0c5b3152bd36a2a8fe6496b0609910d5b4e75044ea00c7c15acab999bc5708e8efff46f4b7022621108237bc700990d54c67c9a206401ce18cb56
7
- data.tar.gz: 61de24e198a80220fe2d408aaa267b515032ddda686452501c445dd1a6af754c7fded4bb43ca9ba25091bcc6493e63b547d543bd2f0b5af5bfaf2fe1ca2dd755
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 **FIXME** for more details!
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 new https://github.com/ruby/ruby/pull/5500[GVL Instrumentation API]. See below for an easy way to run Ruby 3.2 inside docker.
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
- #include <unistd.h>
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 on_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
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
- on_event,
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
- pid_t thread_id = gettid();
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 is seems to be bending a bit the intention of the output format, but it seemed easier to do this way.
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 on_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *_unused1, void *_unused2) {
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 = "resumed"; break;
129
- case RUBY_INTERNAL_THREAD_EVENT_SUSPENDED: event_name = "suspended"; break;
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.dev"
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"]
@@ -26,5 +26,5 @@
26
26
  # frozen_string_literal: true
27
27
 
28
28
  module GvlTracing
29
- VERSION = "0.1.1"
29
+ VERSION = "1.0.0"
30
30
  end
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.1.1
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: 2022-07-24 00:00:00.000000000 Z
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.dev
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.0.dev
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
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