ddtrace 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -16
  3. data/CHANGELOG.md +31 -2
  4. data/LICENSE-3rdparty.csv +3 -2
  5. data/README.md +2 -2
  6. data/ddtrace.gemspec +12 -3
  7. data/docs/GettingStarted.md +19 -2
  8. data/docs/ProfilingDevelopment.md +8 -8
  9. data/docs/UpgradeGuide.md +3 -3
  10. data/ext/ddtrace_profiling_loader/ddtrace_profiling_loader.c +118 -0
  11. data/ext/ddtrace_profiling_loader/extconf.rb +53 -0
  12. data/ext/ddtrace_profiling_native_extension/NativeExtensionDesign.md +31 -5
  13. data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +0 -8
  14. data/ext/ddtrace_profiling_native_extension/collectors_stack.c +278 -0
  15. data/ext/ddtrace_profiling_native_extension/extconf.rb +70 -100
  16. data/ext/ddtrace_profiling_native_extension/libddprof_helpers.h +13 -0
  17. data/ext/ddtrace_profiling_native_extension/native_extension_helpers.rb +186 -0
  18. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +579 -7
  19. data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +30 -0
  20. data/ext/ddtrace_profiling_native_extension/profiling.c +7 -0
  21. data/ext/ddtrace_profiling_native_extension/stack_recorder.c +139 -0
  22. data/ext/ddtrace_profiling_native_extension/stack_recorder.h +28 -0
  23. data/lib/datadog/appsec/autoload.rb +2 -2
  24. data/lib/datadog/appsec/configuration/settings.rb +19 -0
  25. data/lib/datadog/appsec/configuration.rb +8 -0
  26. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +76 -33
  27. data/lib/datadog/appsec/contrib/rack/integration.rb +1 -0
  28. data/lib/datadog/appsec/contrib/rack/patcher.rb +0 -1
  29. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +64 -0
  30. data/lib/datadog/appsec/contrib/rack/request.rb +6 -0
  31. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +41 -0
  32. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +60 -5
  33. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +81 -0
  34. data/lib/datadog/appsec/contrib/rails/patcher.rb +34 -1
  35. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +68 -0
  36. data/lib/datadog/appsec/contrib/rails/request.rb +33 -0
  37. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +124 -0
  38. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +69 -2
  39. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +63 -0
  40. data/lib/datadog/appsec/event.rb +33 -18
  41. data/lib/datadog/appsec/extensions.rb +0 -3
  42. data/lib/datadog/appsec/processor.rb +45 -2
  43. data/lib/datadog/appsec/rate_limiter.rb +5 -0
  44. data/lib/datadog/appsec/reactive/operation.rb +0 -1
  45. data/lib/datadog/ci/ext/environment.rb +21 -7
  46. data/lib/datadog/core/configuration/agent_settings_resolver.rb +1 -1
  47. data/lib/datadog/core/configuration/components.rb +22 -4
  48. data/lib/datadog/core/configuration/settings.rb +3 -3
  49. data/lib/datadog/core/configuration.rb +7 -5
  50. data/lib/datadog/core/environment/cgroup.rb +3 -1
  51. data/lib/datadog/core/environment/container.rb +2 -1
  52. data/lib/datadog/core/environment/variable_helpers.rb +26 -2
  53. data/lib/datadog/core/logging/ext.rb +11 -0
  54. data/lib/datadog/core/metrics/client.rb +15 -5
  55. data/lib/datadog/core/runtime/metrics.rb +1 -1
  56. data/lib/datadog/core/workers/async.rb +3 -1
  57. data/lib/datadog/core/workers/runtime_metrics.rb +0 -3
  58. data/lib/datadog/core.rb +6 -0
  59. data/lib/datadog/kit/enable_core_dumps.rb +50 -0
  60. data/lib/datadog/kit/identity.rb +63 -0
  61. data/lib/datadog/kit.rb +11 -0
  62. data/lib/datadog/opentracer/tracer.rb +0 -2
  63. data/lib/datadog/profiling/collectors/old_stack.rb +298 -0
  64. data/lib/datadog/profiling/collectors/stack.rb +6 -287
  65. data/lib/datadog/profiling/encoding/profile.rb +0 -1
  66. data/lib/datadog/profiling/ext.rb +1 -1
  67. data/lib/datadog/profiling/flush.rb +1 -1
  68. data/lib/datadog/profiling/load_native_extension.rb +22 -0
  69. data/lib/datadog/profiling/recorder.rb +1 -1
  70. data/lib/datadog/profiling/scheduler.rb +1 -1
  71. data/lib/datadog/profiling/stack_recorder.rb +33 -0
  72. data/lib/datadog/profiling/tag_builder.rb +48 -0
  73. data/lib/datadog/profiling/tasks/exec.rb +2 -2
  74. data/lib/datadog/profiling/tasks/setup.rb +6 -4
  75. data/lib/datadog/profiling.rb +29 -27
  76. data/lib/datadog/tracing/buffer.rb +9 -3
  77. data/lib/datadog/tracing/contrib/action_view/patcher.rb +0 -1
  78. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +2 -2
  79. data/lib/datadog/tracing/contrib/active_record/utils.rb +1 -1
  80. data/lib/datadog/tracing/contrib/active_record/vendor/connection_specification.rb +1 -1
  81. data/lib/datadog/tracing/contrib/active_support/notifications/subscription.rb +4 -2
  82. data/lib/datadog/tracing/contrib/concurrent_ruby/context_composite_executor_service.rb +10 -3
  83. data/lib/datadog/tracing/contrib/dalli/patcher.rb +0 -1
  84. data/lib/datadog/tracing/contrib/delayed_job/patcher.rb +0 -1
  85. data/lib/datadog/tracing/contrib/elasticsearch/integration.rb +9 -3
  86. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +38 -2
  87. data/lib/datadog/tracing/contrib/ethon/patcher.rb +0 -1
  88. data/lib/datadog/tracing/contrib/extensions.rb +0 -2
  89. data/lib/datadog/tracing/contrib/faraday/patcher.rb +0 -1
  90. data/lib/datadog/tracing/contrib/grape/patcher.rb +0 -1
  91. data/lib/datadog/tracing/contrib/graphql/patcher.rb +0 -1
  92. data/lib/datadog/tracing/contrib/grpc/patcher.rb +0 -1
  93. data/lib/datadog/tracing/contrib/kafka/patcher.rb +0 -1
  94. data/lib/datadog/tracing/contrib/lograge/instrumentation.rb +2 -1
  95. data/lib/datadog/tracing/contrib/qless/patcher.rb +0 -1
  96. data/lib/datadog/tracing/contrib/que/patcher.rb +0 -1
  97. data/lib/datadog/tracing/contrib/racecar/patcher.rb +0 -1
  98. data/lib/datadog/tracing/contrib/rails/log_injection.rb +3 -16
  99. data/lib/datadog/tracing/contrib/rake/instrumentation.rb +2 -2
  100. data/lib/datadog/tracing/contrib/rake/patcher.rb +0 -1
  101. data/lib/datadog/tracing/contrib/redis/patcher.rb +0 -1
  102. data/lib/datadog/tracing/contrib/resque/patcher.rb +0 -1
  103. data/lib/datadog/tracing/contrib/rest_client/patcher.rb +0 -1
  104. data/lib/datadog/tracing/contrib/semantic_logger/instrumentation.rb +2 -1
  105. data/lib/datadog/tracing/contrib/sidekiq/configuration/settings.rb +1 -0
  106. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +20 -1
  107. data/lib/datadog/tracing/contrib/sinatra/framework.rb +11 -0
  108. data/lib/datadog/tracing/contrib/sinatra/patcher.rb +0 -1
  109. data/lib/datadog/tracing/contrib/sneakers/patcher.rb +0 -1
  110. data/lib/datadog/tracing/contrib/sucker_punch/patcher.rb +0 -1
  111. data/lib/datadog/tracing/event.rb +2 -1
  112. data/lib/datadog/tracing/sampling/priority_sampler.rb +4 -5
  113. data/lib/datadog/tracing/sampling/rule.rb +12 -6
  114. data/lib/datadog/tracing/sampling/rule_sampler.rb +3 -5
  115. data/lib/datadog/tracing/span_operation.rb +2 -3
  116. data/lib/datadog/tracing/trace_operation.rb +0 -1
  117. data/lib/ddtrace/transport/http/client.rb +2 -1
  118. data/lib/ddtrace/transport/http/response.rb +34 -4
  119. data/lib/ddtrace/transport/io/client.rb +3 -1
  120. data/lib/ddtrace/version.rb +1 -1
  121. data/lib/ddtrace.rb +1 -0
  122. metadata +43 -6
@@ -0,0 +1,278 @@
1
+ #include <ruby.h>
2
+ #include <ruby/debug.h>
3
+ #include "extconf.h"
4
+ #include "libddprof_helpers.h"
5
+ #include "private_vm_api_access.h"
6
+ #include "stack_recorder.h"
7
+
8
+ // Gathers stack traces from running threads, storing them in a StackRecorder instance
9
+ // This file implements the native bits of the Datadog::Profiling::Collectors::Stack class
10
+
11
+ #define MAX_FRAMES_LIMIT 10000
12
+ #define MAX_FRAMES_LIMIT_AS_STRING "10000"
13
+
14
+ static VALUE missing_string = Qnil;
15
+
16
+ // Used as scratch space during sampling
17
+ typedef struct sampling_buffer {
18
+ unsigned int max_frames;
19
+ VALUE *stack_buffer;
20
+ int *lines_buffer;
21
+ bool *is_ruby_frame;
22
+ ddprof_ffi_Location *locations;
23
+ ddprof_ffi_Line *lines;
24
+ } sampling_buffer;
25
+
26
+ static VALUE _native_sample(VALUE self, VALUE thread, VALUE recorder_instance, VALUE metric_values_hash, VALUE labels_array, VALUE max_frames);
27
+ void sample(VALUE thread, sampling_buffer* buffer, VALUE recorder_instance, ddprof_ffi_Slice_i64 metric_values, ddprof_ffi_Slice_label labels);
28
+ void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer* buffer, char *frames_omitted_message, int frames_omitted_message_size);
29
+ void record_placeholder_stack_in_native_code(VALUE recorder_instance, ddprof_ffi_Slice_i64 metric_values, ddprof_ffi_Slice_label labels);
30
+ sampling_buffer *sampling_buffer_new(unsigned int max_frames);
31
+ void sampling_buffer_free(sampling_buffer *buffer);
32
+
33
+ void collectors_stack_init(VALUE profiling_module) {
34
+ VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
35
+ VALUE collectors_stack_class = rb_define_class_under(collectors_module, "Stack", rb_cObject);
36
+
37
+ rb_define_singleton_method(collectors_stack_class, "_native_sample", _native_sample, 5);
38
+
39
+ missing_string = rb_str_new2("");
40
+ rb_global_variable(&missing_string);
41
+ }
42
+
43
+ // This method exists only to enable testing Collectors::Stack behavior using RSpec.
44
+ // It SHOULD NOT be used for other purposes.
45
+ static VALUE _native_sample(VALUE self, VALUE thread, VALUE recorder_instance, VALUE metric_values_hash, VALUE labels_array, VALUE max_frames) {
46
+ Check_Type(metric_values_hash, T_HASH);
47
+ Check_Type(labels_array, T_ARRAY);
48
+
49
+ if (RHASH_SIZE(metric_values_hash) != ENABLED_VALUE_TYPES_COUNT) {
50
+ rb_raise(
51
+ rb_eArgError,
52
+ "Mismatched values for metrics; expected %lu values and got %lu instead",
53
+ ENABLED_VALUE_TYPES_COUNT,
54
+ RHASH_SIZE(metric_values_hash)
55
+ );
56
+ }
57
+
58
+ int64_t metric_values[ENABLED_VALUE_TYPES_COUNT];
59
+ for (unsigned int i = 0; i < ENABLED_VALUE_TYPES_COUNT; i++) {
60
+ VALUE metric_value = rb_hash_fetch(metric_values_hash, rb_str_new_cstr(enabled_value_types[i].type_.ptr));
61
+ metric_values[i] = NUM2LONG(metric_value);
62
+ }
63
+
64
+ long labels_count = RARRAY_LEN(labels_array);
65
+ ddprof_ffi_Label labels[labels_count];
66
+
67
+ for (int i = 0; i < labels_count; i++) {
68
+ VALUE key_str_pair = rb_ary_entry(labels_array, i);
69
+
70
+ labels[i] = (ddprof_ffi_Label) {
71
+ .key = char_slice_from_ruby_string(rb_ary_entry(key_str_pair, 0)),
72
+ .str = char_slice_from_ruby_string(rb_ary_entry(key_str_pair, 1))
73
+ };
74
+ }
75
+
76
+ int max_frames_requested = NUM2INT(max_frames);
77
+ if (max_frames_requested < 0) rb_raise(rb_eArgError, "Invalid max_frames: value must not be negative");
78
+
79
+ sampling_buffer *buffer = sampling_buffer_new(max_frames_requested);
80
+
81
+ sample(
82
+ thread,
83
+ buffer,
84
+ recorder_instance,
85
+ (ddprof_ffi_Slice_i64) {.ptr = metric_values, .len = ENABLED_VALUE_TYPES_COUNT},
86
+ (ddprof_ffi_Slice_label) {.ptr = labels, .len = labels_count}
87
+ );
88
+
89
+ sampling_buffer_free(buffer);
90
+
91
+ return Qtrue;
92
+ }
93
+
94
+ void sample(VALUE thread, sampling_buffer* buffer, VALUE recorder_instance, ddprof_ffi_Slice_i64 metric_values, ddprof_ffi_Slice_label labels) {
95
+ int captured_frames = ddtrace_rb_profile_frames(
96
+ thread,
97
+ 0 /* stack starting depth */,
98
+ buffer->max_frames,
99
+ buffer->stack_buffer,
100
+ buffer->lines_buffer,
101
+ buffer->is_ruby_frame
102
+ );
103
+
104
+ // Idea: Should we release the global vm lock (GVL) after we get the data from `rb_profile_frames`? That way other Ruby threads
105
+ // could continue making progress while the sample was ingested into the profile.
106
+ //
107
+ // Other things to take into consideration if we go in that direction:
108
+ // * Is it safe to call `rb_profile_frame_...` methods on things from the `stack_buffer` without the GVL acquired?
109
+ // * We need to make `VALUE` references in the `stack_buffer` visible to the Ruby GC
110
+ // * Should we move this into a different thread entirely?
111
+ // * If we don't move it into a different thread, does releasing the GVL on a Ruby thread mean that we're introducing
112
+ // a new thread switch point where there previously was none?
113
+
114
+ // Ruby does not give us path and line number for methods implemented using native code.
115
+ // The convention in Kernel#caller_locations is to instead use the path and line number of the first Ruby frame
116
+ // on the stack that is below (e.g. directly or indirectly has called) the native method.
117
+ // Thus, we keep that frame here to able to replicate that behavior.
118
+ // (This is why we also iterate the sampling buffers backwards below -- so that it's easier to keep the last_ruby_frame)
119
+ VALUE last_ruby_frame = Qnil;
120
+ int last_ruby_line = 0;
121
+
122
+ if (captured_frames == PLACEHOLDER_STACK_IN_NATIVE_CODE) {
123
+ record_placeholder_stack_in_native_code(recorder_instance, metric_values, labels);
124
+ return;
125
+ }
126
+
127
+ for (int i = captured_frames - 1; i >= 0; i--) {
128
+ VALUE name, filename;
129
+ int line;
130
+
131
+ if (buffer->is_ruby_frame[i]) {
132
+ last_ruby_frame = buffer->stack_buffer[i];
133
+ last_ruby_line = buffer->lines_buffer[i];
134
+
135
+ name = rb_profile_frame_base_label(buffer->stack_buffer[i]);
136
+ filename = rb_profile_frame_path(buffer->stack_buffer[i]);
137
+ line = buffer->lines_buffer[i];
138
+ } else {
139
+ // **IMPORTANT**: Be very careful when calling any `rb_profile_frame_...` API with a non-Ruby frame, as legacy
140
+ // Rubies may assume that what's in a buffer will lead to a Ruby frame.
141
+ //
142
+ // In particular for Ruby 2.2 and below the buffer contains a Ruby string (see the notes on our custom
143
+ // rb_profile_frames for Ruby 2.2 and below) and CALLING **ANY** OF THOSE APIs ON IT WILL CAUSE INSTANT VM CRASHES
144
+
145
+ #ifndef USE_LEGACY_RB_PROFILE_FRAMES // Modern Rubies
146
+ name = ddtrace_rb_profile_frame_method_name(buffer->stack_buffer[i]);
147
+ #else // Ruby < 2.3
148
+ name = buffer->stack_buffer[i];
149
+ #endif
150
+
151
+ filename = NIL_P(last_ruby_frame) ? Qnil : rb_profile_frame_path(last_ruby_frame);
152
+ line = last_ruby_line;
153
+ }
154
+
155
+ name = NIL_P(name) ? missing_string : name;
156
+ filename = NIL_P(filename) ? missing_string : filename;
157
+
158
+ buffer->lines[i] = (ddprof_ffi_Line) {
159
+ .function = (ddprof_ffi_Function) {
160
+ .name = char_slice_from_ruby_string(name),
161
+ .filename = char_slice_from_ruby_string(filename)
162
+ },
163
+ .line = line,
164
+ };
165
+
166
+ buffer->locations[i] = (ddprof_ffi_Location) {.lines = (ddprof_ffi_Slice_line) {.ptr = &buffer->lines[i], .len = 1}};
167
+ }
168
+
169
+ // Used below; since we want to stack-allocate this, we must do it here rather than in maybe_add_placeholder_frames_omitted
170
+ const int frames_omitted_message_size = sizeof(MAX_FRAMES_LIMIT_AS_STRING " frames omitted");
171
+ char frames_omitted_message[frames_omitted_message_size];
172
+
173
+ // If we filled up the buffer, some frames may have been omitted. In that case, we'll add a placeholder frame
174
+ // with that info.
175
+ if (captured_frames == (long) buffer->max_frames) {
176
+ maybe_add_placeholder_frames_omitted(thread, buffer, frames_omitted_message, frames_omitted_message_size);
177
+ }
178
+
179
+ record_sample(
180
+ recorder_instance,
181
+ (ddprof_ffi_Sample) {
182
+ .locations = (ddprof_ffi_Slice_location) {.ptr = buffer->locations, .len = captured_frames},
183
+ .values = metric_values,
184
+ .labels = labels,
185
+ }
186
+ );
187
+ }
188
+
189
+ void maybe_add_placeholder_frames_omitted(VALUE thread, sampling_buffer* buffer, char *frames_omitted_message, int frames_omitted_message_size) {
190
+ ptrdiff_t frames_omitted = stack_depth_for(thread) - buffer->max_frames;
191
+
192
+ if (frames_omitted == 0) return; // Perfect fit!
193
+
194
+ // The placeholder frame takes over a space, so if 10 frames were left out and we consume one other space for the
195
+ // placeholder, then 11 frames are omitted in total
196
+ frames_omitted++;
197
+
198
+ snprintf(frames_omitted_message, frames_omitted_message_size, "%td frames omitted", frames_omitted);
199
+
200
+ // Important note: `frames_omitted_message` MUST have a lifetime that is at least as long as the call to
201
+ // `record_sample`. So be careful where it gets allocated. (We do have tests for this, at least!)
202
+ buffer->lines[buffer->max_frames - 1] = (ddprof_ffi_Line) {
203
+ .function = (ddprof_ffi_Function) {
204
+ .name = DDPROF_FFI_CHARSLICE_C(""),
205
+ .filename = ((ddprof_ffi_CharSlice) {.ptr = frames_omitted_message, .len = strlen(frames_omitted_message)})
206
+ },
207
+ .line = 0,
208
+ };
209
+ }
210
+
211
+ // Our custom rb_profile_frames returning PLACEHOLDER_STACK_IN_NATIVE_CODE is equivalent to when the
212
+ // Ruby `Thread#backtrace` API returns an empty array: we know that a thread is alive but we don't know what it's doing:
213
+ //
214
+ // 1. It can be starting up
215
+ // ```
216
+ // > Thread.new { sleep }.backtrace
217
+ // => [] # <-- note the thread hasn't actually started running sleep yet, we got there first
218
+ // ```
219
+ // 2. It can be running native code
220
+ // ```
221
+ // > t = Process.detach(fork { sleep })
222
+ // => #<Process::Waiter:0x00007ffe7285f7a0 run>
223
+ // > t.backtrace
224
+ // => [] # <-- this can happen even minutes later, e.g. it's not a race as in 1.
225
+ // ```
226
+ // This effect has been observed in threads created by the Iodine web server and the ffi gem,
227
+ // see for instance https://github.com/ffi/ffi/pull/883 and https://github.com/DataDog/dd-trace-rb/pull/1719 .
228
+ //
229
+ // To give customers visibility into these threads, rather than reporting an empty stack, we replace the empty stack
230
+ // with one containing a placeholder frame, so that these threads are properly represented in the UX.
231
+ void record_placeholder_stack_in_native_code(VALUE recorder_instance, ddprof_ffi_Slice_i64 metric_values, ddprof_ffi_Slice_label labels) {
232
+ ddprof_ffi_Line placeholder_stack_in_native_code_line = {
233
+ .function = (ddprof_ffi_Function) {
234
+ .name = DDPROF_FFI_CHARSLICE_C(""),
235
+ .filename = DDPROF_FFI_CHARSLICE_C("In native code")
236
+ },
237
+ .line = 0
238
+ };
239
+ ddprof_ffi_Location placeholder_stack_in_native_code_location =
240
+ {.lines = (ddprof_ffi_Slice_line) {.ptr = &placeholder_stack_in_native_code_line, .len = 1}};
241
+
242
+ record_sample(
243
+ recorder_instance,
244
+ (ddprof_ffi_Sample) {
245
+ .locations = (ddprof_ffi_Slice_location) {.ptr = &placeholder_stack_in_native_code_location, .len = 1},
246
+ .values = metric_values,
247
+ .labels = labels,
248
+ }
249
+ );
250
+ }
251
+
252
+ sampling_buffer *sampling_buffer_new(unsigned int max_frames) {
253
+ if (max_frames < 5) rb_raise(rb_eArgError, "Invalid max_frames: value must be >= 5");
254
+ if (max_frames > MAX_FRAMES_LIMIT) rb_raise(rb_eArgError, "Invalid max_frames: value must be <= " MAX_FRAMES_LIMIT_AS_STRING);
255
+
256
+ // Note: never returns NULL; if out of memory, it calls the Ruby out-of-memory handlers
257
+ sampling_buffer* buffer = ruby_xcalloc(1, sizeof(sampling_buffer));
258
+
259
+ buffer->max_frames = max_frames;
260
+
261
+ buffer->stack_buffer = ruby_xcalloc(max_frames, sizeof(VALUE));
262
+ buffer->lines_buffer = ruby_xcalloc(max_frames, sizeof(int));
263
+ buffer->is_ruby_frame = ruby_xcalloc(max_frames, sizeof(bool));
264
+ buffer->locations = ruby_xcalloc(max_frames, sizeof(ddprof_ffi_Location));
265
+ buffer->lines = ruby_xcalloc(max_frames, sizeof(ddprof_ffi_Line));
266
+
267
+ return buffer;
268
+ }
269
+
270
+ void sampling_buffer_free(sampling_buffer *buffer) {
271
+ ruby_xfree(buffer->stack_buffer);
272
+ ruby_xfree(buffer->lines_buffer);
273
+ ruby_xfree(buffer->is_ruby_frame);
274
+ ruby_xfree(buffer->locations);
275
+ ruby_xfree(buffer->lines);
276
+
277
+ ruby_xfree(buffer);
278
+ }
@@ -1,92 +1,40 @@
1
1
  # typed: ignore
2
2
 
3
3
  # rubocop:disable Style/StderrPuts
4
+ # rubocop:disable Style/GlobalVars
4
5
 
5
- # Older Rubies don't have the MJIT header, used by the JIT compiler, so we need to use a different approach
6
- CAN_USE_MJIT_HEADER = RUBY_VERSION >= '2.6'
6
+ require_relative 'native_extension_helpers'
7
7
 
8
- def on_jruby?
9
- # We don't support JRuby for profiling, and JRuby doesn't support native extensions, so let's just skip this entire
10
- # thing so that JRuby users of dd-trace-rb aren't impacted.
11
- RUBY_ENGINE == 'jruby'
12
- end
13
-
14
- def on_truffleruby?
15
- # We don't officially support TruffleRuby for dd-trace-rb at all BUT let's not break adventurous customers that
16
- # want to give it a try.
17
- RUBY_ENGINE == 'truffleruby'
18
- end
19
-
20
- def on_windows?
21
- # Microsoft Windows is unsupported, so let's not build the extension there.
22
- Gem.win_platform?
23
- end
24
-
25
- def expected_to_use_mjit_but_mjit_is_disabled?
26
- # On some Rubies, we require the mjit header to be present. If Ruby was installed without MJIT support, we also skip
27
- # building the extension.
28
- mjit_disabled = CAN_USE_MJIT_HEADER && RbConfig::CONFIG['MJIT_SUPPORT'] != 'yes'
29
-
30
- if mjit_disabled
31
- $stderr.puts(%(
32
- +------------------------------------------------------------------------------+
33
- | Your Ruby has been compiled without JIT support (--disable-jit-support). |
34
- | The profiling native extension requires a Ruby compiled with JIT support, |
35
- | even if the JIT is not in use by the application itself. |
36
- | |
37
- | WARNING: Without the profiling native extension, some profiling features |
38
- | will not be available. |
39
- +------------------------------------------------------------------------------+
40
-
41
- ))
42
- end
43
-
44
- mjit_disabled
45
- end
46
-
47
- def disabled_via_env?
48
- # Experimental toggle to disable building the extension.
49
- # Disabling the extension will lead to the profiler not working in future releases.
50
- # If you needed to use this, please tell us why on <https://github.com/DataDog/dd-trace-rb/issues/new>.
51
- ENV['DD_PROFILING_NO_EXTENSION'].to_s.downcase == 'true'
52
- end
53
-
54
- def skip_building_extension?
55
- disabled_via_env? || on_jruby? || on_truffleruby? || on_windows? || expected_to_use_mjit_but_mjit_is_disabled?
56
- end
8
+ SKIPPED_REASON_FILE = "#{__dir__}/skipped_reason.txt".freeze
9
+ # Not a problem if the file doesn't exist or we can't delete it
10
+ File.delete(SKIPPED_REASON_FILE) rescue nil
57
11
 
58
- # IMPORTANT: When adding flags, remember that our customers compile with a wide range of gcc/clang versions, so
59
- # doublecheck that what you're adding can be reasonably expected to work on their systems.
60
- def add_compiler_flag(flag)
61
- $CFLAGS << ' ' << flag
62
- end
12
+ def skip_building_extension!(reason)
13
+ $stderr.puts(Datadog::Profiling::NativeExtensionHelpers::Supported.failure_banner_for(**reason))
14
+ File.write(
15
+ SKIPPED_REASON_FILE,
16
+ Datadog::Profiling::NativeExtensionHelpers::Supported.render_skipped_reason_file(**reason),
17
+ )
63
18
 
64
- def skip_building_extension!
65
19
  File.write('Makefile', 'all install clean: # dummy makefile that does nothing')
66
20
  exit
67
21
  end
68
22
 
69
- if skip_building_extension?
70
- $stderr.puts(%(
71
- +------------------------------------------------------------------------------+
72
- | Skipping build of profiling native extension and replacing it with a no-op |
73
- | Makefile |
74
- +------------------------------------------------------------------------------+
75
-
76
- ))
77
- skip_building_extension!
23
+ unless Datadog::Profiling::NativeExtensionHelpers::Supported.supported?
24
+ skip_building_extension!(Datadog::Profiling::NativeExtensionHelpers::Supported.unsupported_reason)
78
25
  end
79
26
 
80
27
  $stderr.puts(%(
81
28
  +------------------------------------------------------------------------------+
82
- | ** Preparing to build the ddtrace native extension... ** |
29
+ | ** Preparing to build the ddtrace profiling native extension... ** |
83
30
  | |
84
31
  | If you run into any failures during this step, you can set the |
85
32
  | `DD_PROFILING_NO_EXTENSION` environment variable to `true` e.g. |
86
33
  | `$ DD_PROFILING_NO_EXTENSION=true bundle install` to skip this step. |
87
34
  | |
88
- | Disabling the extension will lead to the ddtrace profiling features not |
89
- | working in future releases. |
35
+ | If you disable this extension, the Datadog Continuous Profiler will |
36
+ | not be available, but all other ddtrace features will work fine! |
37
+ | |
90
38
  | If you needed to use this, please tell us why on |
91
39
  | <https://github.com/DataDog/dd-trace-rb/issues/new> so we can fix it :\) |
92
40
  | |
@@ -99,6 +47,22 @@ $stderr.puts(%(
99
47
  # that may fail on an environment not properly setup for building Ruby extensions.
100
48
  require 'mkmf'
101
49
 
50
+ # mkmf on modern Rubies actually has an append_cflags that does something similar
51
+ # (see https://github.com/ruby/ruby/pull/5760), but as usual we need a bit more boilerplate to deal with legacy Rubies
52
+ def add_compiler_flag(flag)
53
+ if try_cflags(flag)
54
+ $CFLAGS << ' ' << flag
55
+ else
56
+ $stderr.puts("WARNING: '#{flag}' not accepted by compiler, skipping it")
57
+ end
58
+ end
59
+
60
+ # Older gcc releases may not default to C99 and we need to ask for this. This is also used:
61
+ # * by upstream Ruby -- search for gnu99 in the codebase
62
+ # * by msgpack, another ddtrace dependency
63
+ # (https://github.com/msgpack/msgpack-ruby/blob/18ce08f6d612fe973843c366ac9a0b74c4e50599/ext/msgpack/extconf.rb#L8)
64
+ add_compiler_flag '-std=gnu99'
65
+
102
66
  # Gets really noisy when we include the MJIT header, let's omit it
103
67
  add_compiler_flag '-Wno-unused-function'
104
68
 
@@ -128,13 +92,41 @@ if RUBY_PLATFORM.include?('linux')
128
92
  $defs << '-DHAVE_PTHREAD_GETCPUCLOCKID'
129
93
  end
130
94
 
95
+ # On older Rubies, we need to use a backported version of this function. See private_vm_api_access.h for details.
96
+ $defs << '-DUSE_BACKPORTED_RB_PROFILE_FRAME_METHOD_NAME' if RUBY_VERSION < '3'
97
+
98
+ # On older Rubies, we need to use rb_thread_t instead of rb_execution_context_t
99
+ $defs << '-DUSE_THREAD_INSTEAD_OF_EXECUTION_CONTEXT' if RUBY_VERSION < '2.5'
100
+
101
+ # On older Rubies...
102
+ if RUBY_VERSION < '2.4'
103
+ # ...we need to use RUBY_VM_NORMAL_ISEQ_P instead of VM_FRAME_RUBYFRAME_P
104
+ $defs << '-DUSE_ISEQ_P_INSTEAD_OF_RUBYFRAME_P'
105
+ # ...we use a legacy copy of rb_vm_frame_method_entry
106
+ $defs << '-DUSE_LEGACY_RB_VM_FRAME_METHOD_ENTRY'
107
+ end
108
+
109
+ # For REALLY OLD Rubies...
110
+ if RUBY_VERSION < '2.3'
111
+ # ...there was no rb_time_timespec_new function
112
+ $defs << '-DNO_RB_TIME_TIMESPEC_NEW'
113
+ # ...the VM changed enough that we need an alternative legacy rb_profile_frames
114
+ $defs << '-DUSE_LEGACY_RB_PROFILE_FRAMES'
115
+ end
116
+
117
+ # If we got here, libddprof is available and loaded
118
+ ENV['PKG_CONFIG_PATH'] = "#{ENV['PKG_CONFIG_PATH']}:#{Libddprof.pkgconfig_folder}"
119
+ unless pkg_config('ddprof_ffi_with_rpath')
120
+ skip_building_extension!(Datadog::Profiling::NativeExtensionHelpers::Supported::FAILED_TO_CONFIGURE_LIBDDPROF)
121
+ end
122
+
131
123
  # Tag the native extension library with the Ruby version and Ruby platform.
132
124
  # This makes it easier for development (avoids "oops I forgot to rebuild when I switched my Ruby") and ensures that
133
125
  # the wrong library is never loaded.
134
126
  # When requiring, we need to use the exact same string, including the version and the platform.
135
127
  EXTENSION_NAME = "ddtrace_profiling_native_extension.#{RUBY_VERSION}_#{RUBY_PLATFORM}".freeze
136
128
 
137
- if CAN_USE_MJIT_HEADER
129
+ if Datadog::Profiling::NativeExtensionHelpers::CAN_USE_MJIT_HEADER
138
130
  mjit_header_file_name = "rb_mjit_min_header-#{RUBY_VERSION}.h"
139
131
 
140
132
  # Validate that the mjit header can actually be compiled on this system. We learned via
@@ -146,42 +138,15 @@ if CAN_USE_MJIT_HEADER
146
138
  original_common_headers = MakeMakefile::COMMON_HEADERS
147
139
  MakeMakefile::COMMON_HEADERS = ''.freeze
148
140
  unless have_macro('RUBY_MJIT_H', mjit_header_file_name)
149
- $stderr.puts(%(
150
- +------------------------------------------------------------------------------+
151
- | WARNING: Unable to compile a needed component for ddtrace native extension. |
152
- | Your C compiler or Ruby VM just-in-time compiler seems to be broken. |
153
- | |
154
- | You will be NOT be able to use ddtrace profiling features, |
155
- | but all other features will work fine! |
156
- | |
157
- | For help solving this issue, please contact Datadog support at |
158
- | <https://docs.datadoghq.com/help/>. |
159
- +------------------------------------------------------------------------------+
160
-
161
- ))
162
- skip_building_extension!
141
+ skip_building_extension!(Datadog::Profiling::NativeExtensionHelpers::Supported::COMPILATION_BROKEN)
163
142
  end
164
143
  MakeMakefile::COMMON_HEADERS = original_common_headers
165
144
 
166
- $defs << '-DUSE_MJIT_HEADER'
145
+ $defs << "-DRUBY_MJIT_HEADER='\"#{mjit_header_file_name}\"'"
167
146
 
168
147
  # NOTE: This needs to come after all changes to $defs
169
148
  create_header
170
149
 
171
- # The MJIT header is always (afaik?) suffixed with the exact Ruby VM version,
172
- # including patch (e.g. 2.7.2). Thus, we add to the header file a definition
173
- # containing the exact file, so that it can be used in a #include in the C code.
174
- header_contents =
175
- File.read($extconf_h)
176
- .sub('#endif',
177
- <<-EXTCONF_H.strip
178
- #define RUBY_MJIT_HEADER "#{mjit_header_file_name}"
179
-
180
- #endif
181
- EXTCONF_H
182
- )
183
- File.open($extconf_h, 'w') { |file| file.puts(header_contents) }
184
-
185
150
  create_makefile EXTENSION_NAME
186
151
  else
187
152
  # On older Rubies, we use the debase-ruby_core_source gem to get access to private VM headers.
@@ -201,6 +166,11 @@ else
201
166
  dir_config('ruby') # allow user to pass in non-standard core include directory
202
167
 
203
168
  Debase::RubyCoreSource
204
- .create_makefile_with_core(proc { have_header('vm_core.h') && thread_native_for_ruby_2_1.call }, EXTENSION_NAME)
169
+ .create_makefile_with_core(
170
+ proc { have_header('vm_core.h') && have_header('iseq.h') && thread_native_for_ruby_2_1.call },
171
+ EXTENSION_NAME,
172
+ )
205
173
  end
174
+
175
+ # rubocop:enable Style/GlobalVars
206
176
  # rubocop:enable Style/StderrPuts
@@ -0,0 +1,13 @@
1
+ #pragma once
2
+
3
+ #include <ddprof/ffi.h>
4
+
5
+ inline static ddprof_ffi_CharSlice char_slice_from_ruby_string(VALUE string) {
6
+ Check_Type(string, T_STRING);
7
+ ddprof_ffi_CharSlice char_slice = {.ptr = StringValuePtr(string), .len = RSTRING_LEN(string)};
8
+ return char_slice;
9
+ }
10
+
11
+ inline static VALUE ruby_string_from_vec_u8(ddprof_ffi_Vec_u8 string) {
12
+ return rb_str_new((char *) string.ptr, string.len);
13
+ }