gvl-tracing 1.5.2 → 1.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f2845379f02ef9e8cd317a293b0c24c92dbd9aebad15ccb9b378ea1c41cf894
4
- data.tar.gz: ce639fb1bffb10262e1dee5f5bf7a80f4425d9db7b690a494b6cefedae507265
3
+ metadata.gz: 2bf28c6665bf4afa02c13299947bb65d7c62c3af783858afdc3a5383e334fa47
4
+ data.tar.gz: b1999e7e17d56e76ba732962708c14979e2690cd78204946d2f05e7146b05725
5
5
  SHA512:
6
- metadata.gz: '01659c02734923610f4c771afcd1a863de369781a7c21cf42c46e5d956f897c65643f0ae6a5a1eebb7fcb81895556271f57f7e164b3ac3581177509d31381db7'
7
- data.tar.gz: f0e0894e7d3dcc5c7f00485b8e741be31f832142ef0436524f26e5996da9f901dc17b46080b26f2ecf5bcfc0ca37b6bdaae48b014eaeb00518dfa56b6c892167
6
+ metadata.gz: feb5fb173e07343af18c929608b20b2463fc0b676565b926242585f3fed31991e34ab5f549d7cbfe0e26c883d4a1e6eebf1d8da9e1586f0fa63d0890cdd3b108
7
+ data.tar.gz: 615226e345e230b989d7738754f02a058935d67679b4ca16c610cbbb6fe34f82aff6de3299d9548af579a30d55bccaedf25cd52d4f631023cff93a683fe3c8bb
data/README.adoc CHANGED
@@ -25,7 +25,7 @@ def fib(n)
25
25
  fib(n - 1) + fib(n - 2)
26
26
  end
27
27
 
28
- GvlTracing.start("example1.json") do
28
+ GvlTracing.start("example1.json", os_threads_view_enabled: false) do
29
29
  Thread.new { sleep(0.05) while true }
30
30
 
31
31
  sleep(0.05)
@@ -68,6 +68,12 @@ This gem only provides a single module (`GvlTracing`) with methods:
68
68
 
69
69
  The resulting traces can be analyzed by going to https://ui.perfetto.dev/[Perfetto UI].
70
70
 
71
+ == Experimental features
72
+
73
+ 1. Sleep tracking: Add `require 'gvl_tracing/sleep_tracking'` to add a more specific `sleeping` state for sleeps (which are otherwise rendered as `waiting` without this feature)
74
+
75
+ 2. 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
+
71
77
  == Tips
72
78
 
73
79
  You can "embed" links to the perfetto UI which trigger loading of a trace by following the instructions on https://perfetto.dev/docs/visualization/deep-linking-to-perfetto-ui .
@@ -32,6 +32,7 @@
32
32
  #include <stdbool.h>
33
33
  #include <sys/types.h>
34
34
  #include <pthread.h>
35
+ #include <stdint.h>
35
36
 
36
37
  #include "extconf.h"
37
38
 
@@ -56,6 +57,10 @@
56
57
  #define RUBY_3_2
57
58
  #endif
58
59
 
60
+ // For the OS threads view, we emit data as if it was for another pid so it gets grouped separately in perfetto.
61
+ // This is a really big hack, but I couldn't think of a better way?
62
+ #define OS_THREADS_VIEW_PID (INT64_C(0))
63
+
59
64
  typedef struct {
60
65
  bool initialized;
61
66
  int32_t current_thread_serial;
@@ -78,20 +83,27 @@ static VALUE gc_tracepoint = Qnil;
78
83
  static int thread_storage_key = 0;
79
84
  static VALUE all_seen_threads = Qnil;
80
85
  static pthread_mutex_t all_seen_threads_mutex = PTHREAD_MUTEX_INITIALIZER;
86
+ static pthread_mutex_t output_mutex = PTHREAD_MUTEX_INITIALIZER;
87
+ static bool os_threads_view_enabled;
81
88
 
82
89
  static VALUE tracing_init_local_storage(VALUE, VALUE);
83
- static VALUE tracing_start(VALUE _self, VALUE output_path);
90
+ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_threads_view_enabled_arg);
84
91
  static VALUE tracing_stop(VALUE _self);
85
92
  static double timestamp_microseconds(void);
86
- static void render_event(thread_local_state *, const char *event_name);
93
+ static double render_event(thread_local_state *, const char *event_name);
87
94
  static void on_thread_event(rb_event_flag_t event, const rb_internal_thread_event_data_t *_unused1, void *_unused2);
88
95
  static void on_gc_event(VALUE tpval, void *_unused1);
89
96
  static VALUE mark_sleeping(VALUE _self);
90
97
  static size_t thread_local_state_memsize(UNUSED_ARG const void *_unused);
91
98
  static void thread_local_state_mark(void *data);
99
+ static inline void output_mutex_lock(void);
100
+ static inline void output_mutex_unlock(void);
92
101
  static inline int32_t thread_id_for(thread_local_state *state);
93
102
  static VALUE ruby_thread_id_for(UNUSED_ARG VALUE _self, VALUE thread);
94
103
  static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self);
104
+ static void render_os_thread_event(thread_local_state *state, double now_microseconds);
105
+ static void finish_previous_os_thread_event(double now_microseconds);
106
+ static inline uint32_t current_native_thread_id(void);
95
107
 
96
108
  #pragma GCC diagnostic ignored "-Wunused-const-variable"
97
109
  static const rb_data_type_t thread_local_state_type = {
@@ -131,7 +143,7 @@ void Init_gvl_tracing_native_extension(void) {
131
143
  VALUE gvl_tracing_module = rb_define_module("GvlTracing");
132
144
 
133
145
  rb_define_singleton_method(gvl_tracing_module, "_init_local_storage", tracing_init_local_storage, 1);
134
- rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 1);
146
+ rb_define_singleton_method(gvl_tracing_module, "_start", tracing_start, 2);
135
147
  rb_define_singleton_method(gvl_tracing_module, "_stop", tracing_stop, 0);
136
148
  rb_define_singleton_method(gvl_tracing_module, "mark_sleeping", mark_sleeping, 0);
137
149
  rb_define_singleton_method(gvl_tracing_module, "_thread_id_for", ruby_thread_id_for, 1);
@@ -143,24 +155,7 @@ static inline void initialize_thread_local_state(thread_local_state *state) {
143
155
  state->current_thread_serial = RUBY_ATOMIC_FETCH_ADD(thread_serial, 1);
144
156
 
145
157
  #ifdef RUBY_3_2
146
- uint32_t native_thread_id = 0;
147
-
148
- #ifdef HAVE_PTHREAD_THREADID_NP
149
- uint64_t full_native_thread_id;
150
- pthread_threadid_np(pthread_self(), &full_native_thread_id);
151
- // Note: `pthread_threadid_np` is declared as taking in a `uint64_t` but I don't think macOS uses such really
152
- // high thread ids, and anyway perfetto doesn't like full 64-bit ids for threads so let's go with a simplification
153
- // for now.
154
- native_thread_id = (uint32_t) full_native_thread_id;
155
- #elif HAVE_GETTID
156
- native_thread_id = gettid();
157
- #else
158
- // Note: We could use the current_thread_serial as a crappy fallback, but this would make getting thread names
159
- // not work very well
160
- #error No native thread id available?
161
- #endif
162
-
163
- state->native_thread_id = native_thread_id;
158
+ state->native_thread_id = current_native_thread_id();
164
159
  #endif
165
160
  }
166
161
 
@@ -174,8 +169,9 @@ static VALUE tracing_init_local_storage(UNUSED_ARG VALUE _self, VALUE threads) {
174
169
  return Qtrue;
175
170
  }
176
171
 
177
- static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
172
+ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path, VALUE os_threads_view_enabled_arg) {
178
173
  Check_Type(output_path, T_STRING);
174
+ if (os_threads_view_enabled_arg != Qtrue && os_threads_view_enabled_arg != Qfalse) rb_raise(rb_eArgError, "os_threads_view_enabled must be true/false");
179
175
 
180
176
  trim_all_seen_threads(Qnil);
181
177
 
@@ -183,13 +179,26 @@ static VALUE tracing_start(UNUSED_ARG VALUE _self, VALUE output_path) {
183
179
  output_file = fopen(StringValuePtr(output_path), "w");
184
180
  if (output_file == NULL) rb_syserr_fail(errno, "Failed to open GvlTracing output file");
185
181
 
186
- fprintf(output_file, "[\n");
187
-
188
182
  thread_local_state *state = GT_CURRENT_THREAD_LOCAL_STATE();
189
183
  started_tracing_at_microseconds = timestamp_microseconds();
190
184
  process_id = getpid();
185
+ os_threads_view_enabled = (os_threads_view_enabled_arg == Qtrue);
186
+
187
+ VALUE ruby_version = rb_const_get(rb_cObject, rb_intern("RUBY_VERSION"));
188
+ Check_Type(ruby_version, T_STRING);
189
+
190
+ fprintf(output_file, "[\n");
191
+ fprintf(output_file,
192
+ " {\"ph\": \"M\", \"pid\": %"PRId64", \"name\": \"process_name\", \"args\": {\"name\": \"Ruby threads view (%s)\"}},\n",
193
+ process_id, StringValuePtr(ruby_version)
194
+ );
195
+
196
+ double now_microseconds = render_event(state, "started_tracing");
191
197
 
192
- render_event(state, "started_tracing");
198
+ if (os_threads_view_enabled) {
199
+ fprintf(output_file, " {\"ph\": \"M\", \"pid\": %"PRId64", \"name\": \"process_name\", \"args\": {\"name\": \"OS threads view\"}},\n", OS_THREADS_VIEW_PID);
200
+ render_os_thread_event(state, now_microseconds);
201
+ }
193
202
 
194
203
  current_hook = rb_internal_thread_add_event_hook(
195
204
  on_thread_event,
@@ -218,7 +227,9 @@ static VALUE tracing_stop(UNUSED_ARG VALUE _self) {
218
227
  rb_tracepoint_disable(gc_tracepoint);
219
228
  gc_tracepoint = Qnil;
220
229
 
221
- render_event(state, "stopped_tracing");
230
+ double now_microseconds = render_event(state, "stopped_tracing");
231
+ if (os_threads_view_enabled) finish_previous_os_thread_event(now_microseconds);
232
+
222
233
  // closing the json syntax in the output file is handled in GvlTracing.stop code
223
234
 
224
235
  if (fclose(output_file) != 0) rb_syserr_fail(errno, "Failed to close GvlTracing output file");
@@ -240,7 +251,7 @@ static double timestamp_microseconds(void) {
240
251
 
241
252
  // Render output using trace event format for perfetto:
242
253
  // https://chromium.googlesource.com/catapult/+/refs/heads/main/docs/trace-event-format.md
243
- static void render_event(thread_local_state *state, const char *event_name) {
254
+ static double render_event(thread_local_state *state, const char *event_name) {
244
255
  // Event data
245
256
  double now_microseconds = timestamp_microseconds() - started_tracing_at_microseconds;
246
257
 
@@ -251,6 +262,7 @@ static void render_event(thread_local_state *state, const char *event_name) {
251
262
  // Important note: We've observed some rendering issues in perfetto if the tid or pid are numbers that are "too big",
252
263
  // see https://github.com/ivoanjo/gvl-tracing/pull/4#issuecomment-1196463364 for an example.
253
264
 
265
+ output_mutex_lock();
254
266
  fprintf(output_file,
255
267
  // Finish previous duration
256
268
  " {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %d, \"ts\": %f},\n" \
@@ -261,6 +273,9 @@ static void render_event(thread_local_state *state, const char *event_name) {
261
273
  // Args for second line
262
274
  process_id, thread_id_for(state), now_microseconds, event_name
263
275
  );
276
+ output_mutex_unlock();
277
+
278
+ return now_microseconds;
264
279
  }
265
280
 
266
281
  static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_event_data_t *event_data, UNUSED_ARG void *_unused2) {
@@ -298,7 +313,15 @@ static void on_thread_event(rb_event_flag_t event_id, const rb_internal_thread_e
298
313
  case RUBY_INTERNAL_THREAD_EVENT_STARTED: event_name = "started"; break;
299
314
  case RUBY_INTERNAL_THREAD_EVENT_EXITED: event_name = "died"; break;
300
315
  };
301
- render_event(state, event_name);
316
+ double now_microseconds = render_event(state, event_name);
317
+
318
+ if (os_threads_view_enabled) {
319
+ if (event_id == RUBY_INTERNAL_THREAD_EVENT_RESUMED) {
320
+ render_os_thread_event(state, now_microseconds);
321
+ } else if (event_id == RUBY_INTERNAL_THREAD_EVENT_SUSPENDED || event_id == RUBY_INTERNAL_THREAD_EVENT_EXITED) {
322
+ finish_previous_os_thread_event(now_microseconds);
323
+ }
324
+ }
302
325
  }
303
326
 
304
327
  static void on_gc_event(VALUE tpval, UNUSED_ARG void *_unused1) {
@@ -337,6 +360,18 @@ static inline void all_seen_threads_mutex_unlock(void) {
337
360
  if (error) rb_syserr_fail(error, "Failed to unlock GvlTracing mutex");
338
361
  }
339
362
 
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
+
340
375
  #ifdef RUBY_3_3_PLUS
341
376
  static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
342
377
  thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
@@ -414,3 +449,52 @@ static VALUE trim_all_seen_threads(UNUSED_ARG VALUE _self) {
414
449
  all_seen_threads_mutex_unlock();
415
450
  return Qtrue;
416
451
  }
452
+
453
+ // Creates an event that follows the current native thread. Note that this assumes that whatever event
454
+ // made us call `render_os_thread_event` is an event about the current (native) thread; if the event is not about the
455
+ // current thread, the results will be incorrect.
456
+ static void render_os_thread_event(thread_local_state *state, double now_microseconds) {
457
+ finish_previous_os_thread_event(now_microseconds);
458
+
459
+ // Hack: If we name threads as "Thread N", perfetto seems to color them all with the same color, which looks awful.
460
+ // I did not check the code, but in practice perfetto seems to be doing some kind of hashing based only on regular
461
+ // chars, so here we append a different letter to each thread to cause the color hashing to differ.
462
+ char color_suffix_hack = ('a' + (thread_id_for(state) % 26));
463
+
464
+ output_mutex_lock();
465
+ fprintf(output_file,
466
+ " {\"ph\": \"B\", \"pid\": %"PRId64", \"tid\": %u, \"ts\": %f, \"name\": \"Thread %d (%c)\"},\n",
467
+ OS_THREADS_VIEW_PID, current_native_thread_id(), now_microseconds, thread_id_for(state), color_suffix_hack
468
+ );
469
+ output_mutex_unlock();
470
+ }
471
+
472
+ static void finish_previous_os_thread_event(double now_microseconds) {
473
+ output_mutex_lock();
474
+ fprintf(output_file,
475
+ " {\"ph\": \"E\", \"pid\": %"PRId64", \"tid\": %u, \"ts\": %f},\n",
476
+ OS_THREADS_VIEW_PID, current_native_thread_id(), now_microseconds
477
+ );
478
+ output_mutex_unlock();
479
+ }
480
+
481
+ static inline uint32_t current_native_thread_id(void) {
482
+ uint32_t native_thread_id = 0;
483
+
484
+ #ifdef HAVE_PTHREAD_THREADID_NP
485
+ uint64_t full_native_thread_id;
486
+ pthread_threadid_np(pthread_self(), &full_native_thread_id);
487
+ // Note: `pthread_threadid_np` is declared as taking in a `uint64_t` but I don't think macOS uses such really
488
+ // high thread ids, and anyway perfetto doesn't like full 64-bit ids for threads so let's go with a simplification
489
+ // for now.
490
+ native_thread_id = (uint32_t) full_native_thread_id;
491
+ #elif HAVE_GETTID
492
+ native_thread_id = gettid();
493
+ #else
494
+ // Note: We could use a native thread-local crappy fallback, but I think the two above alternatives are available
495
+ // on all OSs that support the GVL tracing API.
496
+ #error No native thread id available?
497
+ #endif
498
+
499
+ return native_thread_id;
500
+ }
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"]
data/lib/gvl-tracing.rb CHANGED
@@ -34,8 +34,8 @@ module GvlTracing
34
34
  private :_start
35
35
  private :_stop
36
36
 
37
- def start(file)
38
- _start(file)
37
+ def start(file, os_threads_view_enabled: false)
38
+ _start(file, os_threads_view_enabled)
39
39
  _init_local_storage(Thread.list)
40
40
  @path = file
41
41
 
@@ -5,7 +5,7 @@
5
5
  module GvlTracing::SleepTracking
6
6
  def sleep(...)
7
7
  GvlTracing.mark_sleeping
8
- super(...)
8
+ super
9
9
  end
10
10
  end
11
11
 
@@ -26,5 +26,5 @@
26
26
  # frozen_string_literal: true
27
27
 
28
28
  module GvlTracing
29
- VERSION = "1.5.2"
29
+ VERSION = "1.6.1"
30
30
  end
data/preview.png CHANGED
Binary file
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.5.2
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivo Anjo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-06 00:00:00.000000000 Z
11
+ date: 2024-12-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -18,16 +18,11 @@ extensions:
18
18
  - ext/gvl_tracing_native_extension/extconf.rb
19
19
  extra_rdoc_files: []
20
20
  files:
21
- - ".editorconfig"
22
- - ".ruby-version"
23
- - ".standard.yml"
24
21
  - CODE_OF_CONDUCT.adoc
25
22
  - LICENSE
26
23
  - README.adoc
27
- - Rakefile
28
24
  - ext/gvl_tracing_native_extension/extconf.rb
29
25
  - ext/gvl_tracing_native_extension/gvl_tracing.c
30
- - gems.rb
31
26
  - gvl-tracing.gemspec
32
27
  - lib/gvl-tracing.rb
33
28
  - lib/gvl_tracing/sleep_tracking.rb
@@ -53,7 +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.5.3
51
+ rubygems_version: 3.4.1
57
52
  signing_key:
58
53
  specification_version: 4
59
54
  summary: Get a timeline view of Global VM Lock usage in your Ruby app
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
@@ -1,9 +0,0 @@
1
- ruby_version: 3.2
2
-
3
- ignore:
4
- - 'examples/example1.rb':
5
- - Style/InfiniteLoop
6
- - 'examples/example5.rb':
7
- - Style/InfiniteLoop
8
- - 'lib/gvl_tracing/sleep_tracking.rb':
9
- - Style/MixinUsage
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,14 +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.33"
13
- gem "concurrent-ruby"
14
- gem "benchmark-ips", "~> 2.13"