ddtrace 1.0.0 → 1.1.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/.gitignore +4 -16
- data/CHANGELOG.md +31 -2
- data/LICENSE-3rdparty.csv +3 -2
- data/README.md +2 -2
- data/ddtrace.gemspec +12 -3
- data/docs/GettingStarted.md +19 -2
- data/docs/ProfilingDevelopment.md +8 -8
- data/docs/UpgradeGuide.md +3 -3
- data/ext/ddtrace_profiling_loader/ddtrace_profiling_loader.c +118 -0
- data/ext/ddtrace_profiling_loader/extconf.rb +53 -0
- data/ext/ddtrace_profiling_native_extension/NativeExtensionDesign.md +31 -5
- data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +0 -8
- data/ext/ddtrace_profiling_native_extension/collectors_stack.c +278 -0
- data/ext/ddtrace_profiling_native_extension/extconf.rb +70 -100
- data/ext/ddtrace_profiling_native_extension/libddprof_helpers.h +13 -0
- data/ext/ddtrace_profiling_native_extension/native_extension_helpers.rb +186 -0
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +579 -7
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +30 -0
- data/ext/ddtrace_profiling_native_extension/profiling.c +7 -0
- data/ext/ddtrace_profiling_native_extension/stack_recorder.c +139 -0
- data/ext/ddtrace_profiling_native_extension/stack_recorder.h +28 -0
- data/lib/datadog/appsec/autoload.rb +2 -2
- data/lib/datadog/appsec/configuration/settings.rb +19 -0
- data/lib/datadog/appsec/configuration.rb +8 -0
- data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +76 -33
- data/lib/datadog/appsec/contrib/rack/integration.rb +1 -0
- data/lib/datadog/appsec/contrib/rack/patcher.rb +0 -1
- data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +64 -0
- data/lib/datadog/appsec/contrib/rack/request.rb +6 -0
- data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +41 -0
- data/lib/datadog/appsec/contrib/rack/request_middleware.rb +60 -5
- data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +81 -0
- data/lib/datadog/appsec/contrib/rails/patcher.rb +34 -1
- data/lib/datadog/appsec/contrib/rails/reactive/action.rb +68 -0
- data/lib/datadog/appsec/contrib/rails/request.rb +33 -0
- data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +124 -0
- data/lib/datadog/appsec/contrib/sinatra/patcher.rb +69 -2
- data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +63 -0
- data/lib/datadog/appsec/event.rb +33 -18
- data/lib/datadog/appsec/extensions.rb +0 -3
- data/lib/datadog/appsec/processor.rb +45 -2
- data/lib/datadog/appsec/rate_limiter.rb +5 -0
- data/lib/datadog/appsec/reactive/operation.rb +0 -1
- data/lib/datadog/ci/ext/environment.rb +21 -7
- data/lib/datadog/core/configuration/agent_settings_resolver.rb +1 -1
- data/lib/datadog/core/configuration/components.rb +22 -4
- data/lib/datadog/core/configuration/settings.rb +3 -3
- data/lib/datadog/core/configuration.rb +7 -5
- data/lib/datadog/core/environment/cgroup.rb +3 -1
- data/lib/datadog/core/environment/container.rb +2 -1
- data/lib/datadog/core/environment/variable_helpers.rb +26 -2
- data/lib/datadog/core/logging/ext.rb +11 -0
- data/lib/datadog/core/metrics/client.rb +15 -5
- data/lib/datadog/core/runtime/metrics.rb +1 -1
- data/lib/datadog/core/workers/async.rb +3 -1
- data/lib/datadog/core/workers/runtime_metrics.rb +0 -3
- data/lib/datadog/core.rb +6 -0
- data/lib/datadog/kit/enable_core_dumps.rb +50 -0
- data/lib/datadog/kit/identity.rb +63 -0
- data/lib/datadog/kit.rb +11 -0
- data/lib/datadog/opentracer/tracer.rb +0 -2
- data/lib/datadog/profiling/collectors/old_stack.rb +298 -0
- data/lib/datadog/profiling/collectors/stack.rb +6 -287
- data/lib/datadog/profiling/encoding/profile.rb +0 -1
- data/lib/datadog/profiling/ext.rb +1 -1
- data/lib/datadog/profiling/flush.rb +1 -1
- data/lib/datadog/profiling/load_native_extension.rb +22 -0
- data/lib/datadog/profiling/recorder.rb +1 -1
- data/lib/datadog/profiling/scheduler.rb +1 -1
- data/lib/datadog/profiling/stack_recorder.rb +33 -0
- data/lib/datadog/profiling/tag_builder.rb +48 -0
- data/lib/datadog/profiling/tasks/exec.rb +2 -2
- data/lib/datadog/profiling/tasks/setup.rb +6 -4
- data/lib/datadog/profiling.rb +29 -27
- data/lib/datadog/tracing/buffer.rb +9 -3
- data/lib/datadog/tracing/contrib/action_view/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +2 -2
- data/lib/datadog/tracing/contrib/active_record/utils.rb +1 -1
- data/lib/datadog/tracing/contrib/active_record/vendor/connection_specification.rb +1 -1
- data/lib/datadog/tracing/contrib/active_support/notifications/subscription.rb +4 -2
- data/lib/datadog/tracing/contrib/concurrent_ruby/context_composite_executor_service.rb +10 -3
- data/lib/datadog/tracing/contrib/dalli/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/delayed_job/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/elasticsearch/integration.rb +9 -3
- data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +38 -2
- data/lib/datadog/tracing/contrib/ethon/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/extensions.rb +0 -2
- data/lib/datadog/tracing/contrib/faraday/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/grape/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/graphql/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/grpc/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/kafka/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/lograge/instrumentation.rb +2 -1
- data/lib/datadog/tracing/contrib/qless/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/que/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/racecar/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/rails/log_injection.rb +3 -16
- data/lib/datadog/tracing/contrib/rake/instrumentation.rb +2 -2
- data/lib/datadog/tracing/contrib/rake/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/redis/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/resque/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/rest_client/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/semantic_logger/instrumentation.rb +2 -1
- data/lib/datadog/tracing/contrib/sidekiq/configuration/settings.rb +1 -0
- data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +20 -1
- data/lib/datadog/tracing/contrib/sinatra/framework.rb +11 -0
- data/lib/datadog/tracing/contrib/sinatra/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/sneakers/patcher.rb +0 -1
- data/lib/datadog/tracing/contrib/sucker_punch/patcher.rb +0 -1
- data/lib/datadog/tracing/event.rb +2 -1
- data/lib/datadog/tracing/sampling/priority_sampler.rb +4 -5
- data/lib/datadog/tracing/sampling/rule.rb +12 -6
- data/lib/datadog/tracing/sampling/rule_sampler.rb +3 -5
- data/lib/datadog/tracing/span_operation.rb +2 -3
- data/lib/datadog/tracing/trace_operation.rb +0 -1
- data/lib/ddtrace/transport/http/client.rb +2 -1
- data/lib/ddtrace/transport/http/response.rb +34 -4
- data/lib/ddtrace/transport/io/client.rb +3 -1
- data/lib/ddtrace/version.rb +1 -1
- data/lib/ddtrace.rb +1 -0
- 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
|
-
|
|
6
|
-
CAN_USE_MJIT_HEADER = RUBY_VERSION >= '2.6'
|
|
6
|
+
require_relative 'native_extension_helpers'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
|
89
|
-
|
|
|
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
|
-
|
|
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 <<
|
|
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(
|
|
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
|
+
}
|