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 +4 -4
- data/CHANGELOG.md +26 -2
- data/ext/datadog_ci_native/ci.c +4 -0
- data/ext/datadog_ci_native/datadog_common.c +17 -6
- data/ext/datadog_ci_native/datadog_common.h +4 -3
- data/ext/datadog_ci_native/datadog_cov.c +182 -55
- data/ext/datadog_ci_native/file_serialization.c +238 -0
- data/ext/datadog_ci_native/file_serialization.h +3 -0
- data/lib/datadog/ci/configuration/components.rb +5 -2
- data/lib/datadog/ci/contrib/instrumentation.rb +14 -0
- data/lib/datadog/ci/contrib/minitest/ext.rb +4 -0
- data/lib/datadog/ci/contrib/minitest/test.rb +69 -4
- data/lib/datadog/ci/contrib/parallel_tests/cli.rb +8 -1
- data/lib/datadog/ci/contrib/rspec/example.rb +40 -3
- data/lib/datadog/ci/contrib/rspec/ext.rb +4 -0
- data/lib/datadog/ci/file_serialization.rb +12 -0
- data/lib/datadog/ci/source_code/path_filter.rb +12 -4
- data/lib/datadog/ci/span.rb +9 -5
- data/lib/datadog/ci/test_impact_analysis/component.rb +4 -7
- data/lib/datadog/ci/test_impact_analysis/coverage/event.rb +1 -6
- data/lib/datadog/ci/test_impact_analysis/coverage/files.rb +31 -2
- data/lib/datadog/ci/test_session.rb +6 -0
- data/lib/datadog/ci/test_tracing/component.rb +7 -4
- data/lib/datadog/ci/test_tracing/null_component.rb +1 -1
- data/lib/datadog/ci/utils/file_storage.rb +50 -10
- data/lib/datadog/ci/version.rb +2 -2
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a6bdfb71fe5140388ad8b1ea7a086977ba9a4a69c86fdc1b465746f287ba2ec
|
|
4
|
+
data.tar.gz: 27615bb724035bc22617b7495cac97cac343fac08da9363fba31e7a4fc21a84e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
data/ext/datadog_ci_native/ci.c
CHANGED
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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,
|
|
20
|
-
|
|
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
|
|
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:
|
|
44
|
-
//
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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->
|
|
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->
|
|
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),
|
|
135
|
-
dd_cov_data->
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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->
|
|
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
|
-
|
|
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
|
|
233
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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->
|
|
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
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
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->
|
|
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
|
}
|