datadog-ci 1.36.1 → 1.38.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70fc5fea2d200dd9654e43d303bc20ca67937ee3d6fdc03ff5595d5c22991424
4
- data.tar.gz: 0ad11c672983e445ef7baed4fcf15af2c4710871b90962076f3075b572383ca7
3
+ metadata.gz: 4a6bdfb71fe5140388ad8b1ea7a086977ba9a4a69c86fdc1b465746f287ba2ec
4
+ data.tar.gz: 27615bb724035bc22617b7495cac97cac343fac08da9363fba31e7a4fc21a84e
5
5
  SHA512:
6
- metadata.gz: c0df4d0e21c6bab928409176b1eee9b401532eac85b9522623c88dd89f54e4a75cb64c3c528cc508357cc77642e3c7e042bc6459edffea8423ef94d48dd6ee4d
7
- data.tar.gz: '07888f1e31d079859c4e3bcfde608dc6c5c56028857f5c4feb58cbdacac4ab7be6a9865076174cf27cc7d6f9d1868e3f4a7cc6a1e014cd74c34982e335151b55'
6
+ metadata.gz: 9ae087aef544b7775bb9784fb64a27a836e3181076229cac5c814d4263ecb02432f723d9e136350aba020d6ad67820a8658edcce62fb3dcccea5d5efff999820
7
+ data.tar.gz: aca348e24dba29c5a98fe60fe7e89879257e1466bce6b179c9efd43c40e4f3f1cffb2641f1d35fd54adbd111ba0c1f957db144848702fb51b0bf567282508ace
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.38.0] - 2026-08-28
4
+
5
+ ### Added
6
+ * Trace RSpec and Minitest lifecycle steps ([#595][])
7
+
8
+ ### Changed
9
+ * Optimize TIA performance ([#589][])
10
+
11
+ ### Fixed
12
+ * Isolate state between parallel_tests sessions ([#593][])
13
+ * Test Optimization telemetry shutdown order ([#586][])
14
+
15
+ ### Removed
16
+ * Disable LiveDebugger's SymDB upload in CI mode ([#594][])
17
+
18
+ ## [1.37.0] - 2026-08-19
19
+
3
20
  ## [1.36.1] - 2026-08-11
4
21
 
5
22
  ### Fixed
@@ -682,7 +699,9 @@ Currently test suite level visibility is not used by our instrumentation: it wil
682
699
 
683
700
  - Ruby versions < 2.7 no longer supported ([#8][])
684
701
 
685
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.36.1...main
702
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.38.0...main
703
+ [1.38.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.37.0...v1.38.0
704
+ [1.37.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.36.1...v1.37.0
686
705
  [1.36.1]: https://github.com/DataDog/datadog-ci-rb/compare/v1.36.0...v1.36.1
687
706
  [1.36.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.35.0...v1.36.0
688
707
  [1.35.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.34.0...v1.35.0
@@ -966,4 +985,9 @@ Currently test suite level visibility is not used by our instrumentation: it wil
966
985
  [#554]: https://github.com/DataDog/datadog-ci-rb/issues/554
967
986
  [#555]: https://github.com/DataDog/datadog-ci-rb/issues/555
968
987
  [#566]: https://github.com/DataDog/datadog-ci-rb/issues/566
969
- [#570]: https://github.com/DataDog/datadog-ci-rb/issues/570
988
+ [#570]: https://github.com/DataDog/datadog-ci-rb/issues/570
989
+ [#586]: https://github.com/DataDog/datadog-ci-rb/issues/586
990
+ [#589]: https://github.com/DataDog/datadog-ci-rb/issues/589
991
+ [#593]: https://github.com/DataDog/datadog-ci-rb/issues/593
992
+ [#594]: https://github.com/DataDog/datadog-ci-rb/issues/594
993
+ [#595]: https://github.com/DataDog/datadog-ci-rb/issues/595
@@ -1,11 +1,15 @@
1
1
  #include "datadog_cov.h"
2
2
  #include "datadog_method_inspect.h"
3
+ #include "file_serialization.h"
3
4
  #include "iseq_collector.h"
4
5
 
5
6
  void Init_datadog_ci_native(void) {
6
7
  // Coverage::DDCov
7
8
  Init_datadog_cov();
8
9
 
10
+ // FileSerialization
11
+ Init_file_serialization();
12
+
9
13
  // SourceCode
10
14
  Init_datadog_method_inspect();
11
15
  Init_dd_ci_iseq_collector();
@@ -2,14 +2,25 @@
2
2
  #include <ruby.h>
3
3
  #include <string.h>
4
4
 
5
- bool dd_ci_is_path_included(const char *path, const char *root_path,
6
- long root_path_len, const char *ignored_path,
7
- long ignored_path_len) {
8
- if (strncmp(root_path, path, root_path_len) != 0) {
5
+ static bool has_directory_prefix(const char *path, long path_len,
6
+ const char *prefix, long prefix_len) {
7
+ if (prefix_len > path_len || memcmp(prefix, path, prefix_len) != 0) {
9
8
  return false;
10
9
  }
11
- if (ignored_path_len > 0 &&
12
- strncmp(ignored_path, path, ignored_path_len) == 0) {
10
+
11
+ return prefix_len == path_len || prefix[prefix_len - 1] == '/' ||
12
+ path[prefix_len] == '/';
13
+ }
14
+
15
+ bool dd_ci_is_path_included(const char *path, long path_len,
16
+ const char *root_path, long root_path_len,
17
+ const char *ignored_path, long ignored_path_len) {
18
+ if (!has_directory_prefix(path, path_len, root_path, root_path_len)) {
19
+ return false;
20
+ }
21
+ if (ignored_path_len > 0 && has_directory_prefix(
22
+ path, path_len, ignored_path,
23
+ ignored_path_len)) {
13
24
  return false;
14
25
  }
15
26
  return true;
@@ -11,14 +11,15 @@
11
11
  * Returns true if the path should be included, false otherwise.
12
12
  *
13
13
  * @param path The file path to check
14
+ * @param path_len Length of path
14
15
  * @param root_path The root path prefix (required)
15
16
  * @param root_path_len Length of root_path
16
17
  * @param ignored_path Path prefix to exclude (can be NULL)
17
18
  * @param ignored_path_len Length of ignored_path (0 if not set)
18
19
  */
19
- bool dd_ci_is_path_included(const char *path, const char *root_path,
20
- long root_path_len, const char *ignored_path,
21
- long ignored_path_len);
20
+ bool dd_ci_is_path_included(const char *path, long path_len,
21
+ const char *root_path, long root_path_len,
22
+ const char *ignored_path, long ignored_path_len);
22
23
 
23
24
  /* ---- Utility functions -------------------------------------------------- */
24
25
 
@@ -3,6 +3,7 @@
3
3
  #include <ruby/st.h>
4
4
 
5
5
  #include <stdbool.h>
6
+ #include <string.h>
6
7
 
7
8
  #include "datadog_common.h"
8
9
 
@@ -11,12 +12,26 @@
11
12
  // running only the tests that are affected by the changes.
12
13
 
13
14
  #define PROFILE_FRAMES_BUFFER_SIZE 1
15
+ #define SEEN_FILENAME_CACHE_SIZE 1024
16
+ #define SEEN_ALLOCATED_CLASS_CACHE_SIZE 4096
17
+ #define KLASS_FILES_CACHE_SIZE 100000
18
+
19
+ #if SEEN_FILENAME_CACHE_SIZE == 0 || \
20
+ (SEEN_FILENAME_CACHE_SIZE & (SEEN_FILENAME_CACHE_SIZE - 1)) != 0
21
+ #error "SEEN_FILENAME_CACHE_SIZE must be a power of two"
22
+ #endif
23
+
24
+ #if SEEN_ALLOCATED_CLASS_CACHE_SIZE == 0 || \
25
+ (SEEN_ALLOCATED_CLASS_CACHE_SIZE & \
26
+ (SEEN_ALLOCATED_CLASS_CACHE_SIZE - 1)) != 0
27
+ #error "SEEN_ALLOCATED_CLASS_CACHE_SIZE must be a power of two"
28
+ #endif
14
29
 
15
30
  // threading modes
16
31
  enum threading_mode { single, multi };
17
32
 
18
33
  // functions declarations
19
- static void on_newobj_event(VALUE tracepoint_data, void *data);
34
+ static void on_newobj_event(VALUE self, const rb_trace_arg_t *tracearg);
20
35
 
21
36
  static int mark_key_for_gc_i(st_data_t key, st_data_t _value, st_data_t _data) {
22
37
  VALUE klass = (VALUE)key;
@@ -25,6 +40,15 @@ static int mark_key_for_gc_i(st_data_t key, st_data_t _value, st_data_t _data) {
25
40
  return ST_CONTINUE;
26
41
  }
27
42
 
43
+ static int mark_klass_files_for_gc_i(st_data_t key, st_data_t value,
44
+ st_data_t _data) {
45
+ // Both the class key and its cached filenames must remain at stable addresses
46
+ // because they are stored outside Ruby's object containers.
47
+ rb_gc_mark((VALUE)key);
48
+ rb_gc_mark((VALUE)value);
49
+ return ST_CONTINUE;
50
+ }
51
+
28
52
  // Data structure
29
53
  struct dd_cov_data {
30
54
  // Ruby hash with filenames impacted by the test.
@@ -40,9 +64,12 @@ struct dd_cov_data {
40
64
  char *ignored_path;
41
65
  long ignored_path_len;
42
66
 
43
- // Line tracepoint optimisation: cache last seen filename pointer to avoid
44
- // unnecessary string comparison if we stay in the same file.
45
- uintptr_t last_filename_ptr;
67
+ // Line tracepoint optimisation: make consecutive events from the same file a
68
+ // single comparison, then use a direct-mapped cache for later revisits. Keep
69
+ // the Ruby strings alive so their pointers cannot be reused for other paths.
70
+ // A collision only repeats path processing; it can never suppress coverage.
71
+ VALUE last_filename;
72
+ VALUE seen_filenames[SEEN_FILENAME_CACHE_SIZE];
46
73
 
47
74
  // Line tracepoint can work in two modes: single threaded and multi threaded
48
75
  //
@@ -61,22 +88,38 @@ struct dd_cov_data {
61
88
  // contain any methods that could be covered by line tracepoint.
62
89
  //
63
90
  // Allocation tracing works only in multi threaded mode.
64
- VALUE object_allocation_tracepoint;
91
+ bool allocation_tracing_enabled;
92
+ bool allocation_hook_active;
65
93
  st_table *klasses_table; // { (VALUE) -> int } hashmap with class names that
66
94
  // were covered by allocation during the test run
95
+ VALUE last_allocated_klass;
96
+ VALUE seen_allocated_klasses[SEEN_ALLOCATED_CLASS_CACHE_SIZE];
97
+ st_table *klass_files_cache; // { (VALUE) -> Array<String> } resolved files
98
+ size_t klass_files_cache_size;
67
99
  };
68
100
 
69
101
  static void dd_cov_mark(void *ptr) {
70
102
  struct dd_cov_data *dd_cov_data = ptr;
71
103
  rb_gc_mark_movable(dd_cov_data->impacted_files);
72
104
  rb_gc_mark_movable(dd_cov_data->th_covered);
73
- rb_gc_mark_movable(dd_cov_data->object_allocation_tracepoint);
105
+
106
+ if (dd_cov_data->last_filename != Qnil) {
107
+ rb_gc_mark(dd_cov_data->last_filename);
108
+ }
109
+ for (size_t i = 0; i < SEEN_FILENAME_CACHE_SIZE; i++) {
110
+ if (dd_cov_data->seen_filenames[i] != Qnil) {
111
+ rb_gc_mark(dd_cov_data->seen_filenames[i]);
112
+ }
113
+ }
74
114
 
75
115
  // if GC starts withing dd_cov_allocate() call, klasses_table might not be
76
116
  // initialized yet
77
117
  if (dd_cov_data->klasses_table != NULL) {
78
118
  st_foreach(dd_cov_data->klasses_table, mark_key_for_gc_i, 0);
79
119
  }
120
+ if (dd_cov_data->klass_files_cache != NULL) {
121
+ st_foreach(dd_cov_data->klass_files_cache, mark_klass_files_for_gc_i, 0);
122
+ }
80
123
  }
81
124
 
82
125
  static void dd_cov_free(void *ptr) {
@@ -84,6 +127,7 @@ static void dd_cov_free(void *ptr) {
84
127
  xfree(dd_cov_data->root);
85
128
  xfree(dd_cov_data->ignored_path);
86
129
  st_free_table(dd_cov_data->klasses_table);
130
+ st_free_table(dd_cov_data->klass_files_cache);
87
131
  xfree(dd_cov_data);
88
132
  }
89
133
 
@@ -91,8 +135,6 @@ static void dd_cov_compact(void *ptr) {
91
135
  struct dd_cov_data *dd_cov_data = ptr;
92
136
  dd_cov_data->impacted_files = rb_gc_location(dd_cov_data->impacted_files);
93
137
  dd_cov_data->th_covered = rb_gc_location(dd_cov_data->th_covered);
94
- dd_cov_data->object_allocation_tracepoint =
95
- rb_gc_location(dd_cov_data->object_allocation_tracepoint);
96
138
  // keys for dd_cov_data->klasses_table are not moved by GC, so we don't need
97
139
  // to update them
98
140
  }
@@ -115,12 +157,21 @@ static VALUE dd_cov_allocate(VALUE klass) {
115
157
  dd_cov_data->root_len = 0;
116
158
  dd_cov_data->ignored_path = NULL;
117
159
  dd_cov_data->ignored_path_len = 0;
118
- dd_cov_data->last_filename_ptr = 0;
160
+ dd_cov_data->last_filename = Qnil;
161
+ for (size_t i = 0; i < SEEN_FILENAME_CACHE_SIZE; i++) {
162
+ dd_cov_data->seen_filenames[i] = Qnil;
163
+ }
119
164
  dd_cov_data->threading_mode = multi;
120
165
 
121
- dd_cov_data->object_allocation_tracepoint = Qnil;
166
+ dd_cov_data->allocation_tracing_enabled = false;
167
+ dd_cov_data->allocation_hook_active = false;
168
+ dd_cov_data->last_allocated_klass = Qnil;
169
+ memset(dd_cov_data->seen_allocated_klasses, 0,
170
+ sizeof(dd_cov_data->seen_allocated_klasses));
122
171
  // numtable type is needed to store VALUE as a key
123
172
  dd_cov_data->klasses_table = st_init_numtable();
173
+ dd_cov_data->klass_files_cache = st_init_numtable();
174
+ dd_cov_data->klass_files_cache_size = 0;
124
175
 
125
176
  return dd_cov;
126
177
  }
@@ -131,8 +182,9 @@ static VALUE dd_cov_allocate(VALUE klass) {
131
182
  // not in the ignored folder) and adds it to the impacted_files hash.
132
183
  static bool record_impacted_file(struct dd_cov_data *dd_cov_data,
133
184
  VALUE filename) {
134
- if (!dd_ci_is_path_included(RSTRING_PTR(filename), dd_cov_data->root,
135
- dd_cov_data->root_len, dd_cov_data->ignored_path,
185
+ if (!dd_ci_is_path_included(RSTRING_PTR(filename), RSTRING_LEN(filename),
186
+ dd_cov_data->root, dd_cov_data->root_len,
187
+ dd_cov_data->ignored_path,
136
188
  dd_cov_data->ignored_path_len)) {
137
189
  return false;
138
190
  }
@@ -145,18 +197,29 @@ static bool record_impacted_file(struct dd_cov_data *dd_cov_data,
145
197
  // rb_profile_frames.
146
198
  static void on_line_event(rb_event_flag_t event, VALUE data, VALUE self, ID id,
147
199
  VALUE klass) {
148
- struct dd_cov_data *dd_cov_data;
149
- TypedData_Get_Struct(data, struct dd_cov_data, &dd_cov_data_type,
150
- dd_cov_data);
200
+ // The hook is registered only with DDCov instances, so the full typed-data
201
+ // type check on every Ruby line is unnecessary.
202
+ struct dd_cov_data *dd_cov_data = RTYPEDDATA_DATA(data);
151
203
 
152
204
  const char *c_filename = rb_sourcefile();
205
+ if (c_filename == NULL) {
206
+ return;
207
+ }
153
208
 
154
- // skip if we cover the same file again
155
209
  uintptr_t current_filename_ptr = (uintptr_t)c_filename;
156
- if (dd_cov_data->last_filename_ptr == current_filename_ptr) {
210
+ if (dd_cov_data->last_filename != Qnil &&
211
+ RSTRING_PTR(dd_cov_data->last_filename) == c_filename) {
212
+ return;
213
+ }
214
+
215
+ size_t cache_index =
216
+ ((current_filename_ptr >> 4) ^ (current_filename_ptr >> 12)) &
217
+ (SEEN_FILENAME_CACHE_SIZE - 1);
218
+ VALUE cached_filename = dd_cov_data->seen_filenames[cache_index];
219
+ if (cached_filename != Qnil && RSTRING_PTR(cached_filename) == c_filename) {
220
+ dd_cov_data->last_filename = cached_filename;
157
221
  return;
158
222
  }
159
- dd_cov_data->last_filename_ptr = current_filename_ptr;
160
223
 
161
224
  VALUE top_frame;
162
225
  int captured_frames =
@@ -172,6 +235,8 @@ static void on_line_event(rb_event_flag_t event, VALUE data, VALUE self, ID id,
172
235
  return;
173
236
  }
174
237
 
238
+ dd_cov_data->last_filename = filename;
239
+ dd_cov_data->seen_filenames[cache_index] = filename;
175
240
  record_impacted_file(dd_cov_data, filename);
176
241
  }
177
242
 
@@ -185,21 +250,6 @@ static VALUE safely_get_mod_ancestors(VALUE klass) {
185
250
  return dd_ci_rescue_nil(rb_mod_ancestors, klass);
186
251
  }
187
252
 
188
- static bool record_impacted_klass(struct dd_cov_data *dd_cov_data,
189
- VALUE klass) {
190
- VALUE klass_name = safely_get_class_name(klass);
191
- if (klass_name == Qnil) {
192
- return false;
193
- }
194
-
195
- VALUE filename = dd_ci_resolve_const_to_file(klass_name);
196
- if (filename == Qnil) {
197
- return false;
198
- }
199
-
200
- return record_impacted_file(dd_cov_data, filename);
201
- }
202
-
203
253
  // This function is called for each class that was instantiated during the test
204
254
  // run.
205
255
  static int each_instantiated_klass(st_data_t key, st_data_t _value,
@@ -207,6 +257,18 @@ static int each_instantiated_klass(st_data_t key, st_data_t _value,
207
257
  VALUE klass = (VALUE)key;
208
258
  struct dd_cov_data *dd_cov_data = (struct dd_cov_data *)data;
209
259
 
260
+ st_data_t cached_files;
261
+ if (st_lookup(dd_cov_data->klass_files_cache, key, &cached_files)) {
262
+ VALUE files = (VALUE)cached_files;
263
+ long files_len = RARRAY_LEN(files);
264
+ for (long i = 0; i < files_len; i++) {
265
+ rb_hash_aset(dd_cov_data->impacted_files, rb_ary_entry(files, i), Qtrue);
266
+ }
267
+ return ST_CONTINUE;
268
+ }
269
+
270
+ VALUE files = rb_ary_new();
271
+
210
272
  // rb_mod_ancestors returns an array containing the "klass" itself
211
273
  // and all the parent classes and/or included/prepended modules
212
274
  VALUE ancestors = safely_get_mod_ancestors(klass);
@@ -221,17 +283,36 @@ static int each_instantiated_klass(st_data_t key, st_data_t _value,
221
283
  continue;
222
284
  }
223
285
 
224
- record_impacted_klass(dd_cov_data, mod);
286
+ VALUE klass_name = safely_get_class_name(mod);
287
+ if (klass_name == Qnil) {
288
+ continue;
289
+ }
290
+
291
+ VALUE filename = dd_ci_resolve_const_to_file(klass_name);
292
+ if (filename == Qnil || !record_impacted_file(dd_cov_data, filename)) {
293
+ continue;
294
+ }
295
+
296
+ rb_ary_push(files, filename);
225
297
  }
226
298
 
299
+ rb_obj_freeze(files);
300
+ // Bound cross-test retention without adding eviction work to cache hits.
301
+ // Reaching the limit starts a fresh cache generation.
302
+ if (dd_cov_data->klass_files_cache_size >= KLASS_FILES_CACHE_SIZE) {
303
+ st_clear(dd_cov_data->klass_files_cache);
304
+ dd_cov_data->klass_files_cache_size = 0;
305
+ }
306
+ st_insert(dd_cov_data->klass_files_cache, key, (st_data_t)files);
307
+ dd_cov_data->klass_files_cache_size++;
308
+
227
309
  return ST_CONTINUE;
228
310
  }
229
311
 
230
312
  // Executed on RUBY_INTERNAL_EVENT_NEWOBJ event and captures the source file for
231
313
  // the allocated object's class.
232
- static void on_newobj_event(VALUE tracepoint_data, void *data) {
233
- rb_trace_arg_t *tracearg = rb_tracearg_from_tracepoint(tracepoint_data);
234
- VALUE new_object = rb_tracearg_object(tracearg);
314
+ static void on_newobj_event(VALUE self, const rb_trace_arg_t *tracearg) {
315
+ VALUE new_object = rb_tracearg_object((rb_trace_arg_t *)tracearg);
235
316
 
236
317
  // To keep things fast and practical, we only care about objects that extend
237
318
  // either Object or Struct.
@@ -244,20 +325,41 @@ static void on_newobj_event(VALUE tracepoint_data, void *data) {
244
325
  if (klass == Qnil || klass == 0) {
245
326
  return;
246
327
  }
247
- // Skip anonymous classes starting with "#<Class".
248
- // it allows us to skip the source location lookup that will always fail
249
- //
250
- // rb_mod_name returns nil for anonymous classes
251
- if (rb_mod_name(klass) == Qnil) {
328
+
329
+ struct dd_cov_data *dd_cov_data = RTYPEDDATA_DATA(self);
330
+
331
+ if (dd_cov_data->last_allocated_klass == klass) {
252
332
  return;
253
333
  }
254
334
 
255
- struct dd_cov_data *dd_cov_data = (struct dd_cov_data *)data;
335
+ uintptr_t klass_ptr = (uintptr_t)klass;
336
+ size_t cache_index =
337
+ ((klass_ptr >> 4) ^ (klass_ptr >> 12)) &
338
+ (SEEN_ALLOCATED_CLASS_CACHE_SIZE - 1);
339
+ if (dd_cov_data->seen_allocated_klasses[cache_index] == klass) {
340
+ dd_cov_data->last_allocated_klass = klass;
341
+ return;
342
+ }
343
+
344
+ // A direct-cache collision must not make us repeat name resolution or table
345
+ // insertion for a class that this test already observed.
346
+ if (st_is_member(dd_cov_data->klasses_table, (st_data_t)klass)) {
347
+ dd_cov_data->last_allocated_klass = klass;
348
+ dd_cov_data->seen_allocated_klasses[cache_index] = klass;
349
+ return;
350
+ }
351
+
352
+ // rb_mod_name returns nil for anonymous classes and is safe during NEWOBJ.
353
+ if (rb_mod_name(klass) == Qnil) {
354
+ return;
355
+ }
256
356
 
257
357
  // We use VALUE directly as a key for the hashmap
258
358
  // Ruby itself does it too:
259
359
  // https://github.com/ruby/ruby/blob/94b87084a689a3bc732dcaee744508a708223d6c/ext/objspace/object_tracing.c#L113
260
360
  st_insert(dd_cov_data->klasses_table, (st_data_t)klass, 1);
361
+ dd_cov_data->last_allocated_klass = klass;
362
+ dd_cov_data->seen_allocated_klasses[cache_index] = klass;
261
363
  }
262
364
 
263
365
  // DDCov instance methods available in Ruby
@@ -265,12 +367,22 @@ static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self) {
265
367
  VALUE opt;
266
368
 
267
369
  rb_scan_args(argc, argv, "10", &opt);
370
+ Check_Type(opt, T_HASH);
371
+
268
372
  VALUE rb_root = rb_hash_lookup(opt, ID2SYM(rb_intern("root")));
269
373
  if (!RTEST(rb_root)) {
270
374
  rb_raise(rb_eArgError, "root is required");
271
375
  }
376
+ Check_Type(rb_root, T_STRING);
377
+ const char *root = StringValueCStr(rb_root);
378
+
272
379
  VALUE rb_ignored_path =
273
380
  rb_hash_lookup(opt, ID2SYM(rb_intern("ignored_path")));
381
+ const char *ignored_path = NULL;
382
+ if (RTEST(rb_ignored_path)) {
383
+ Check_Type(rb_ignored_path, T_STRING);
384
+ ignored_path = StringValueCStr(rb_ignored_path);
385
+ }
274
386
 
275
387
  VALUE rb_threading_mode =
276
388
  rb_hash_lookup(opt, ID2SYM(rb_intern("threading_mode")));
@@ -296,18 +408,16 @@ static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self) {
296
408
 
297
409
  dd_cov_data->threading_mode = threading_mode;
298
410
  dd_cov_data->root_len = RSTRING_LEN(rb_root);
299
- dd_cov_data->root =
300
- dd_ci_ruby_strndup(RSTRING_PTR(rb_root), dd_cov_data->root_len);
411
+ dd_cov_data->root = dd_ci_ruby_strndup(root, dd_cov_data->root_len);
301
412
 
302
413
  if (RTEST(rb_ignored_path)) {
303
414
  dd_cov_data->ignored_path_len = RSTRING_LEN(rb_ignored_path);
304
415
  dd_cov_data->ignored_path = dd_ci_ruby_strndup(
305
- RSTRING_PTR(rb_ignored_path), dd_cov_data->ignored_path_len);
416
+ ignored_path, dd_cov_data->ignored_path_len);
306
417
  }
307
418
 
308
419
  if (rb_allocation_tracing_enabled == Qtrue) {
309
- dd_cov_data->object_allocation_tracepoint = rb_tracepoint_new(
310
- Qnil, RUBY_INTERNAL_EVENT_NEWOBJ, on_newobj_event, (void *)dd_cov_data);
420
+ dd_cov_data->allocation_tracing_enabled = true;
311
421
  }
312
422
 
313
423
  return Qnil;
@@ -332,9 +442,18 @@ static VALUE dd_cov_start(VALUE self) {
332
442
  rb_add_event_hook(on_line_event, RUBY_EVENT_LINE, self);
333
443
  }
334
444
 
335
- // add object allocation tracepoint
336
- if (dd_cov_data->object_allocation_tracepoint != Qnil) {
337
- rb_tracepoint_enable(dd_cov_data->object_allocation_tracepoint);
445
+ // Register the raw hook that TracePoint would wrap and dispatch directly to
446
+ // the allocation callback. NEWOBJ permits no general Ruby API; the callback
447
+ // is limited to the existing event-safe accessors plus rb_mod_name, which is
448
+ // explicitly safe here. Each collector owns its hook, preserving concurrent
449
+ // collector behavior.
450
+ if (dd_cov_data->allocation_tracing_enabled &&
451
+ !dd_cov_data->allocation_hook_active) {
452
+ rb_add_event_hook2((rb_event_hook_func_t)on_newobj_event,
453
+ RUBY_INTERNAL_EVENT_NEWOBJ, self,
454
+ RUBY_EVENT_HOOK_FLAG_SAFE |
455
+ RUBY_EVENT_HOOK_FLAG_RAW_ARG);
456
+ dd_cov_data->allocation_hook_active = true;
338
457
  }
339
458
 
340
459
  return self;
@@ -360,20 +479,28 @@ static VALUE dd_cov_stop(VALUE self) {
360
479
  rb_remove_event_hook(on_line_event);
361
480
  }
362
481
 
363
- // stop object allocation tracepoint
364
- if (dd_cov_data->object_allocation_tracepoint != Qnil) {
365
- rb_tracepoint_disable(dd_cov_data->object_allocation_tracepoint);
482
+ // Remove only this collector's hook; other concurrently active collectors
483
+ // continue to receive allocation events.
484
+ if (dd_cov_data->allocation_hook_active) {
485
+ rb_remove_event_hook_with_data((rb_event_hook_func_t)on_newobj_event, self);
486
+ dd_cov_data->allocation_hook_active = false;
366
487
  }
367
488
 
368
489
  // process classes covered by allocation tracing
369
490
  st_foreach(dd_cov_data->klasses_table, each_instantiated_klass,
370
491
  (st_data_t)dd_cov_data);
492
+ dd_cov_data->last_allocated_klass = Qnil;
493
+ memset(dd_cov_data->seen_allocated_klasses, 0,
494
+ sizeof(dd_cov_data->seen_allocated_klasses));
371
495
  st_clear(dd_cov_data->klasses_table);
372
496
 
373
497
  VALUE res = dd_cov_data->impacted_files;
374
498
 
375
499
  dd_cov_data->impacted_files = rb_hash_new();
376
- dd_cov_data->last_filename_ptr = 0;
500
+ dd_cov_data->last_filename = Qnil;
501
+ for (size_t i = 0; i < SEEN_FILENAME_CACHE_SIZE; i++) {
502
+ dd_cov_data->seen_filenames[i] = Qnil;
503
+ }
377
504
 
378
505
  return res;
379
506
  }