ddtrace 1.7.0 → 1.8.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 +50 -1
- data/README.md +2 -2
- data/ext/ddtrace_profiling_loader/extconf.rb +4 -1
- data/ext/ddtrace_profiling_native_extension/NativeExtensionDesign.md +1 -1
- data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +3 -2
- data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.c +15 -41
- data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.h +1 -1
- data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +284 -74
- data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.c +142 -0
- data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.h +14 -0
- data/ext/ddtrace_profiling_native_extension/collectors_idle_sampling_helper.c +241 -0
- data/ext/ddtrace_profiling_native_extension/collectors_idle_sampling_helper.h +3 -0
- data/ext/ddtrace_profiling_native_extension/extconf.rb +21 -7
- data/ext/ddtrace_profiling_native_extension/helpers.h +5 -0
- data/ext/ddtrace_profiling_native_extension/native_extension_helpers.rb +8 -0
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +108 -24
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +9 -0
- data/ext/ddtrace_profiling_native_extension/profiling.c +205 -0
- data/ext/ddtrace_profiling_native_extension/ruby_helpers.c +86 -0
- data/ext/ddtrace_profiling_native_extension/ruby_helpers.h +28 -6
- data/ext/ddtrace_profiling_native_extension/setup_signal_handler.c +23 -4
- data/ext/ddtrace_profiling_native_extension/setup_signal_handler.h +4 -0
- data/ext/ddtrace_profiling_native_extension/stack_recorder.c +15 -18
- data/ext/ddtrace_profiling_native_extension/time_helpers.c +17 -0
- data/ext/ddtrace_profiling_native_extension/time_helpers.h +10 -0
- data/lib/datadog/core/configuration/components.rb +27 -6
- data/lib/datadog/core/configuration/ext.rb +18 -0
- data/lib/datadog/core/configuration/settings.rb +14 -341
- data/lib/datadog/core/diagnostics/health.rb +4 -22
- data/lib/datadog/core/environment/variable_helpers.rb +58 -10
- data/lib/datadog/core/utils.rb +0 -21
- data/lib/datadog/core.rb +21 -1
- data/lib/datadog/opentracer/distributed_headers.rb +2 -2
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +16 -5
- data/lib/datadog/profiling/collectors/dynamic_sampling_rate.rb +14 -0
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +68 -0
- data/lib/datadog/profiling/stack_recorder.rb +14 -0
- data/lib/datadog/profiling.rb +2 -0
- data/lib/datadog/tracing/configuration/ext.rb +33 -3
- data/lib/datadog/tracing/configuration/settings.rb +433 -0
- data/lib/datadog/tracing/contrib/aws/configuration/settings.rb +4 -1
- data/lib/datadog/tracing/contrib/aws/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/dalli/configuration/settings.rb +4 -1
- data/lib/datadog/tracing/contrib/dalli/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +5 -1
- data/lib/datadog/tracing/contrib/elasticsearch/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/ethon/configuration/settings.rb +6 -1
- data/lib/datadog/tracing/contrib/ethon/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +5 -1
- data/lib/datadog/tracing/contrib/excon/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +5 -1
- data/lib/datadog/tracing/contrib/faraday/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/grpc/configuration/settings.rb +6 -1
- data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +9 -4
- data/lib/datadog/tracing/contrib/grpc/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/http/configuration/settings.rb +6 -1
- data/lib/datadog/tracing/contrib/http/distributed/propagation.rb +9 -4
- data/lib/datadog/tracing/contrib/http/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +6 -1
- data/lib/datadog/tracing/contrib/httpclient/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +6 -1
- data/lib/datadog/tracing/contrib/httprb/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/mongodb/configuration/settings.rb +5 -1
- data/lib/datadog/tracing/contrib/mongodb/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +4 -1
- data/lib/datadog/tracing/contrib/mysql2/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -2
- data/lib/datadog/tracing/contrib/patcher.rb +3 -2
- data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +4 -1
- data/lib/datadog/tracing/contrib/pg/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -2
- data/lib/datadog/tracing/contrib/presto/configuration/settings.rb +4 -1
- data/lib/datadog/tracing/contrib/presto/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +10 -12
- data/lib/datadog/tracing/contrib/redis/configuration/settings.rb +4 -1
- data/lib/datadog/tracing/contrib/redis/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/redis/instrumentation.rb +30 -23
- data/lib/datadog/tracing/contrib/redis/integration.rb +34 -2
- data/lib/datadog/tracing/contrib/redis/patcher.rb +18 -14
- data/lib/datadog/tracing/contrib/redis/quantize.rb +12 -9
- data/lib/datadog/tracing/contrib/redis/tags.rb +4 -6
- data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +72 -0
- data/lib/datadog/tracing/contrib/rest_client/configuration/settings.rb +6 -1
- data/lib/datadog/tracing/contrib/rest_client/ext.rb +1 -0
- data/lib/datadog/{core → tracing}/diagnostics/ext.rb +1 -6
- data/lib/datadog/tracing/diagnostics/health.rb +40 -0
- data/lib/datadog/tracing/distributed/{b3.rb → b3_multi.rb} +2 -2
- data/lib/datadog/tracing/distributed/helpers.rb +2 -1
- data/lib/datadog/tracing/distributed/none.rb +19 -0
- data/lib/datadog/tracing/distributed/trace_context.rb +369 -0
- data/lib/datadog/tracing/metadata/ext.rb +1 -1
- data/lib/datadog/tracing/sampling/priority_sampler.rb +11 -0
- data/lib/datadog/tracing/sampling/rate_sampler.rb +3 -3
- data/lib/datadog/tracing/span.rb +3 -19
- data/lib/datadog/tracing/span_operation.rb +5 -4
- data/lib/datadog/tracing/trace_digest.rb +75 -2
- data/lib/datadog/tracing/trace_operation.rb +5 -4
- data/lib/datadog/tracing/utils.rb +50 -0
- data/lib/ddtrace/version.rb +1 -1
- metadata +20 -5
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
|
|
3
|
+
#include "collectors_dynamic_sampling_rate.h"
|
|
4
|
+
#include "helpers.h"
|
|
5
|
+
#include "ruby_helpers.h"
|
|
6
|
+
#include "time_helpers.h"
|
|
7
|
+
|
|
8
|
+
// Used to pace the rate of profiling samples based on the last observed time for a sample.
|
|
9
|
+
//
|
|
10
|
+
// This file implements the native bits of the Datadog::Profiling::Collectors::DynamicSamplingRate module, and is
|
|
11
|
+
// only exposed to Ruby for testing (it's always and only invoked by other C code in production).
|
|
12
|
+
|
|
13
|
+
// ---
|
|
14
|
+
// ## Dynamic Sampling Rate
|
|
15
|
+
//
|
|
16
|
+
// Our profilers get deployed in quite unpredictable situations in terms of system resources. While they can provide key
|
|
17
|
+
// information to help customers solve their performance problems, the profilers must always be careful not to make
|
|
18
|
+
// performance problems worse. This is where the idea of a dynamic sampling rate comes in.
|
|
19
|
+
//
|
|
20
|
+
// Instead of sampling at a fixed sample rate, the actual sampling rate should be decided by also observing the impact
|
|
21
|
+
// that running the profiler is having. This protects against issues such as the profiler being deployed in very busy
|
|
22
|
+
//machines or containers with unrealistic CPU restrictions.
|
|
23
|
+
//
|
|
24
|
+
// ### Implementation
|
|
25
|
+
//
|
|
26
|
+
// The APIs exposed by this file are used by the `CpuAndWallTimeWorker`.
|
|
27
|
+
//
|
|
28
|
+
// The main idea of the implementation below is the following: whenever the profiler takes a sample, the time we spent
|
|
29
|
+
// sampling and the current wall-time are recorded by calling `dynamic_sampling_rate_after_sample()`.
|
|
30
|
+
//
|
|
31
|
+
// Inside `dynamic_sampling_rate_after_sample()`, both values are combined to decide a future wall-time before which
|
|
32
|
+
// we should not sample. That is, we may decide that the next sample should happen no less than 200ms from now.
|
|
33
|
+
//
|
|
34
|
+
// Before taking a sample, the profiler checks using `dynamic_sampling_rate_should_sample()`, if it's time or not to
|
|
35
|
+
// sample. If it's not, it will skip sampling.
|
|
36
|
+
//
|
|
37
|
+
// Finally, as an additional optimization, there's a `dynamic_sampling_rate_get_sleep()` which, given the current
|
|
38
|
+
// wall-time, will return the time remaining (*there's an exception, check below) until the next sample.
|
|
39
|
+
//
|
|
40
|
+
// ---
|
|
41
|
+
|
|
42
|
+
// This is the wall-time overhead we're targeting. E.g. we target to spend no more than 2%, or 1.2 seconds per minute,
|
|
43
|
+
// taking profiling samples.
|
|
44
|
+
#define WALL_TIME_OVERHEAD_TARGET_PERCENTAGE 2.0 // %
|
|
45
|
+
// See `dynamic_sampling_rate_get_sleep()` for details
|
|
46
|
+
#define MAX_SLEEP_TIME_NS MILLIS_AS_NS(100)
|
|
47
|
+
// See `dynamic_sampling_rate_after_sample()` for details
|
|
48
|
+
#define MAX_TIME_UNTIL_NEXT_SAMPLE_NS SECONDS_AS_NS(10)
|
|
49
|
+
|
|
50
|
+
void dynamic_sampling_rate_init(dynamic_sampling_rate_state *state) {
|
|
51
|
+
atomic_init(&state->next_sample_after_monotonic_wall_time_ns, 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void dynamic_sampling_rate_reset(dynamic_sampling_rate_state *state) {
|
|
55
|
+
atomic_store(&state->next_sample_after_monotonic_wall_time_ns, 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
uint64_t dynamic_sampling_rate_get_sleep(dynamic_sampling_rate_state *state, long current_monotonic_wall_time_ns) {
|
|
59
|
+
long next_sample_after_ns = atomic_load(&state->next_sample_after_monotonic_wall_time_ns);
|
|
60
|
+
long delta_ns = next_sample_after_ns - current_monotonic_wall_time_ns;
|
|
61
|
+
|
|
62
|
+
if (delta_ns > 0 && next_sample_after_ns > 0) {
|
|
63
|
+
// We don't want to sleep for too long as the profiler may be trying to stop.
|
|
64
|
+
//
|
|
65
|
+
// Instead, here we sleep for at most this time. Worst case, the profiler will still try to sample before
|
|
66
|
+
// `next_sample_after_monotonic_wall_time_ns`, BUT `dynamic_sampling_rate_should_sample()` will still be false
|
|
67
|
+
// so we still get the intended behavior.
|
|
68
|
+
return uint64_min_of(delta_ns, MAX_SLEEP_TIME_NS);
|
|
69
|
+
} else {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
bool dynamic_sampling_rate_should_sample(dynamic_sampling_rate_state *state, long wall_time_ns_before_sample) {
|
|
75
|
+
return wall_time_ns_before_sample >= atomic_load(&state->next_sample_after_monotonic_wall_time_ns);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
void dynamic_sampling_rate_after_sample(dynamic_sampling_rate_state *state, long wall_time_ns_after_sample, uint64_t sampling_time_ns) {
|
|
79
|
+
double overhead_target = (double) WALL_TIME_OVERHEAD_TARGET_PERCENTAGE;
|
|
80
|
+
|
|
81
|
+
// The idea here is that we're targeting a maximum % of wall-time spent sampling.
|
|
82
|
+
// So for instance, if sampling_time_ns is 2% of the time we spend working, how much is the 98% we should spend
|
|
83
|
+
// sleeping? As an example, if the last sample took 1ms and the target overhead is 2%, we should sleep for 49ms.
|
|
84
|
+
uint64_t time_to_sleep_ns = sampling_time_ns * ((100.0 - overhead_target)/overhead_target);
|
|
85
|
+
|
|
86
|
+
// In case a sample took an unexpected long time (e.g. maybe a VM was paused, or a laptop was suspended), we clamp the
|
|
87
|
+
// value so it doesn't get too crazy
|
|
88
|
+
time_to_sleep_ns = uint64_min_of(time_to_sleep_ns, MAX_TIME_UNTIL_NEXT_SAMPLE_NS);
|
|
89
|
+
|
|
90
|
+
atomic_store(&state->next_sample_after_monotonic_wall_time_ns, wall_time_ns_after_sample + time_to_sleep_ns);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---
|
|
94
|
+
// Below here is boilerplate to expose the above code to Ruby so that we can test it with RSpec as usual.
|
|
95
|
+
|
|
96
|
+
VALUE _native_get_sleep(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE current_monotonic_wall_time_ns);
|
|
97
|
+
VALUE _native_should_sample(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE wall_time_ns_before_sample);
|
|
98
|
+
VALUE _native_after_sample(DDTRACE_UNUSED VALUE self, VALUE wall_time_ns_after_sample, VALUE sampling_time_ns);
|
|
99
|
+
|
|
100
|
+
void collectors_dynamic_sampling_rate_init(VALUE profiling_module) {
|
|
101
|
+
VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
|
|
102
|
+
VALUE dynamic_sampling_rate_module = rb_define_module_under(collectors_module, "DynamicSamplingRate");
|
|
103
|
+
VALUE testing_module = rb_define_module_under(dynamic_sampling_rate_module, "Testing");
|
|
104
|
+
|
|
105
|
+
rb_define_singleton_method(testing_module, "_native_get_sleep", _native_get_sleep, 2);
|
|
106
|
+
rb_define_singleton_method(testing_module, "_native_should_sample", _native_should_sample, 2);
|
|
107
|
+
rb_define_singleton_method(testing_module, "_native_after_sample", _native_after_sample, 2);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
VALUE _native_get_sleep(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE current_monotonic_wall_time_ns) {
|
|
111
|
+
ENFORCE_TYPE(simulated_next_sample_after_monotonic_wall_time_ns, T_FIXNUM);
|
|
112
|
+
ENFORCE_TYPE(current_monotonic_wall_time_ns, T_FIXNUM);
|
|
113
|
+
|
|
114
|
+
dynamic_sampling_rate_state state;
|
|
115
|
+
dynamic_sampling_rate_init(&state);
|
|
116
|
+
atomic_store(&state.next_sample_after_monotonic_wall_time_ns, NUM2LONG(simulated_next_sample_after_monotonic_wall_time_ns));
|
|
117
|
+
|
|
118
|
+
return ULL2NUM(dynamic_sampling_rate_get_sleep(&state, NUM2LONG(current_monotonic_wall_time_ns)));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
VALUE _native_should_sample(DDTRACE_UNUSED VALUE self, VALUE simulated_next_sample_after_monotonic_wall_time_ns, VALUE wall_time_ns_before_sample) {
|
|
122
|
+
ENFORCE_TYPE(simulated_next_sample_after_monotonic_wall_time_ns, T_FIXNUM);
|
|
123
|
+
ENFORCE_TYPE(wall_time_ns_before_sample, T_FIXNUM);
|
|
124
|
+
|
|
125
|
+
dynamic_sampling_rate_state state;
|
|
126
|
+
dynamic_sampling_rate_init(&state);
|
|
127
|
+
atomic_store(&state.next_sample_after_monotonic_wall_time_ns, NUM2LONG(simulated_next_sample_after_monotonic_wall_time_ns));
|
|
128
|
+
|
|
129
|
+
return dynamic_sampling_rate_should_sample(&state, NUM2LONG(wall_time_ns_before_sample)) ? Qtrue : Qfalse;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
VALUE _native_after_sample(DDTRACE_UNUSED VALUE self, VALUE wall_time_ns_after_sample, VALUE sampling_time_ns) {
|
|
133
|
+
ENFORCE_TYPE(wall_time_ns_after_sample, T_FIXNUM);
|
|
134
|
+
ENFORCE_TYPE(sampling_time_ns, T_FIXNUM);
|
|
135
|
+
|
|
136
|
+
dynamic_sampling_rate_state state;
|
|
137
|
+
dynamic_sampling_rate_init(&state);
|
|
138
|
+
|
|
139
|
+
dynamic_sampling_rate_after_sample(&state, NUM2LONG(wall_time_ns_after_sample), NUM2ULL(sampling_time_ns));
|
|
140
|
+
|
|
141
|
+
return ULL2NUM(atomic_load(&state.next_sample_after_monotonic_wall_time_ns));
|
|
142
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <stdatomic.h>
|
|
4
|
+
#include <stdbool.h>
|
|
5
|
+
|
|
6
|
+
typedef struct {
|
|
7
|
+
atomic_long next_sample_after_monotonic_wall_time_ns;
|
|
8
|
+
} dynamic_sampling_rate_state;
|
|
9
|
+
|
|
10
|
+
void dynamic_sampling_rate_init(dynamic_sampling_rate_state *state);
|
|
11
|
+
void dynamic_sampling_rate_reset(dynamic_sampling_rate_state *state);
|
|
12
|
+
uint64_t dynamic_sampling_rate_get_sleep(dynamic_sampling_rate_state *state, long current_monotonic_wall_time_ns);
|
|
13
|
+
bool dynamic_sampling_rate_should_sample(dynamic_sampling_rate_state *state, long wall_time_ns_before_sample);
|
|
14
|
+
void dynamic_sampling_rate_after_sample(dynamic_sampling_rate_state *state, long wall_time_ns_after_sample, uint64_t sampling_time_ns);
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/thread.h>
|
|
3
|
+
#include <pthread.h>
|
|
4
|
+
#include <stdbool.h>
|
|
5
|
+
|
|
6
|
+
#include "helpers.h"
|
|
7
|
+
#include "ruby_helpers.h"
|
|
8
|
+
#include "collectors_idle_sampling_helper.h"
|
|
9
|
+
|
|
10
|
+
// Used by the Collectors::CpuAndWallTimeWorker to gather samples when the Ruby process is idle.
|
|
11
|
+
//
|
|
12
|
+
// Specifically, the IdleSamplingHelper is expected to be triggered by the CpuAndWallTimeWorker whenever it needs to
|
|
13
|
+
// trigger a sample, but the VM is otherwise idle. See implementation of CpuAndWallTimeWorker for details.
|
|
14
|
+
//
|
|
15
|
+
// The IdleSamplingHelper keeps a background thread that waits for functions to run on a single-element "queue".
|
|
16
|
+
// Other threads communicate with it by asking it to ACTION_RUN a `requested_action` or ACTION_STOP to terminate.
|
|
17
|
+
//
|
|
18
|
+
// The state is protected by the `wakeup_mutex`, and the background thread is woken up after changes using the
|
|
19
|
+
// `wakeup` condition variable.
|
|
20
|
+
|
|
21
|
+
typedef enum { ACTION_WAIT, ACTION_RUN, ACTION_STOP } action;
|
|
22
|
+
|
|
23
|
+
// Contains state for a single CpuAndWallTimeWorker instance
|
|
24
|
+
struct idle_sampling_loop_state {
|
|
25
|
+
pthread_mutex_t wakeup_mutex;
|
|
26
|
+
pthread_cond_t wakeup;
|
|
27
|
+
action requested_action;
|
|
28
|
+
void (*run_action_function)(void);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
static VALUE _native_new(VALUE klass);
|
|
32
|
+
static void reset_state(struct idle_sampling_loop_state *state);
|
|
33
|
+
static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
|
|
34
|
+
static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance);
|
|
35
|
+
static void *run_idle_sampling_loop(void *state_ptr);
|
|
36
|
+
static void interrupt_idle_sampling_loop(void *state_ptr);
|
|
37
|
+
static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance);
|
|
38
|
+
static VALUE _native_idle_sampling_helper_request_action(DDTRACE_UNUSED VALUE self, VALUE self_instance);
|
|
39
|
+
static void *request_testing_action(void *self_instance_ptr);
|
|
40
|
+
static void grab_gvl_and_run_testing_action(void);
|
|
41
|
+
static void *run_testing_action(DDTRACE_UNUSED void *unused);
|
|
42
|
+
|
|
43
|
+
void collectors_idle_sampling_helper_init(VALUE profiling_module) {
|
|
44
|
+
VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
|
|
45
|
+
VALUE collectors_idle_sampling_helper_class = rb_define_class_under(collectors_module, "IdleSamplingHelper", rb_cObject);
|
|
46
|
+
// Hosts methods used for testing the native code using RSpec
|
|
47
|
+
VALUE testing_module = rb_define_module_under(collectors_idle_sampling_helper_class, "Testing");
|
|
48
|
+
|
|
49
|
+
// Instances of the IdleSamplingHelper class are "TypedData" objects.
|
|
50
|
+
// "TypedData" objects are special objects in the Ruby VM that can wrap C structs.
|
|
51
|
+
// In this case, it wraps the idle_sampling_loop_state.
|
|
52
|
+
//
|
|
53
|
+
// Because Ruby doesn't know how to initialize native-level structs, we MUST override the allocation function for objects
|
|
54
|
+
// of this class so that we can manage this part. Not overriding or disabling the allocation function is a common
|
|
55
|
+
// gotcha for "TypedData" objects that can very easily lead to VM crashes, see for instance
|
|
56
|
+
// https://bugs.ruby-lang.org/issues/18007 for a discussion around this.
|
|
57
|
+
rb_define_alloc_func(collectors_idle_sampling_helper_class, _native_new);
|
|
58
|
+
|
|
59
|
+
rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_idle_sampling_loop", _native_idle_sampling_loop, 1);
|
|
60
|
+
rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_stop", _native_stop, 1);
|
|
61
|
+
rb_define_singleton_method(collectors_idle_sampling_helper_class, "_native_reset", _native_reset, 1);
|
|
62
|
+
rb_define_singleton_method(testing_module, "_native_idle_sampling_helper_request_action", _native_idle_sampling_helper_request_action, 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// This structure is used to define a Ruby object that stores a pointer to a struct idle_sampling_loop_state
|
|
66
|
+
// See also https://github.com/ruby/ruby/blob/master/doc/extension.rdoc for how this works
|
|
67
|
+
static const rb_data_type_t idle_sampling_helper_typed_data = {
|
|
68
|
+
.wrap_struct_name = "Datadog::Profiling::Collectors::IdleSamplingHelper",
|
|
69
|
+
.function = {
|
|
70
|
+
.dmark = NULL, // We don't store references to Ruby objects so we don't need to mark any of them
|
|
71
|
+
.dfree = RUBY_DEFAULT_FREE,
|
|
72
|
+
.dsize = NULL, // We don't track memory usage (although it'd be cool if we did!)
|
|
73
|
+
//.dcompact = NULL, // Not needed -- we don't store references to Ruby objects
|
|
74
|
+
},
|
|
75
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
static VALUE _native_new(VALUE klass) {
|
|
79
|
+
struct idle_sampling_loop_state *state = ruby_xcalloc(1, sizeof(struct idle_sampling_loop_state));
|
|
80
|
+
|
|
81
|
+
reset_state(state);
|
|
82
|
+
|
|
83
|
+
return TypedData_Wrap_Struct(klass, &idle_sampling_helper_typed_data, state);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static void reset_state(struct idle_sampling_loop_state *state) {
|
|
87
|
+
state->wakeup_mutex = (pthread_mutex_t) PTHREAD_MUTEX_INITIALIZER;
|
|
88
|
+
state->wakeup = (pthread_cond_t) PTHREAD_COND_INITIALIZER;
|
|
89
|
+
state->requested_action = ACTION_WAIT;
|
|
90
|
+
state->run_action_function = NULL;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The same instance of the IdleSamplingHelper can be reused multiple times, and this resets it back to
|
|
94
|
+
// a pristine state before recreating the worker thread (this includes resetting the mutex in case it was left
|
|
95
|
+
// locked halfway through the VM forking)
|
|
96
|
+
static VALUE _native_reset(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
|
|
97
|
+
struct idle_sampling_loop_state *state;
|
|
98
|
+
TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
|
|
99
|
+
|
|
100
|
+
reset_state(state);
|
|
101
|
+
|
|
102
|
+
return Qtrue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static VALUE _native_idle_sampling_loop(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
|
|
106
|
+
struct idle_sampling_loop_state *state;
|
|
107
|
+
TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
|
|
108
|
+
|
|
109
|
+
// Release GVL and run the loop waiting for requests
|
|
110
|
+
rb_thread_call_without_gvl(run_idle_sampling_loop, state, interrupt_idle_sampling_loop, state);
|
|
111
|
+
|
|
112
|
+
return Qtrue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static void *run_idle_sampling_loop(void *state_ptr) {
|
|
116
|
+
struct idle_sampling_loop_state *state = (struct idle_sampling_loop_state *) state_ptr;
|
|
117
|
+
int error = 0;
|
|
118
|
+
|
|
119
|
+
while (true) {
|
|
120
|
+
ENFORCE_SUCCESS_NO_GVL(pthread_mutex_lock(&state->wakeup_mutex));
|
|
121
|
+
|
|
122
|
+
action next_action;
|
|
123
|
+
void (*run_action_function)(void);
|
|
124
|
+
|
|
125
|
+
// Await for an action
|
|
126
|
+
while ((next_action = state->requested_action) == ACTION_WAIT) {
|
|
127
|
+
error = pthread_cond_wait(&state->wakeup, &state->wakeup_mutex);
|
|
128
|
+
if (error) {
|
|
129
|
+
// If something went wrong, try to leave the mutex unlocked at least
|
|
130
|
+
pthread_mutex_unlock(&state->wakeup_mutex);
|
|
131
|
+
ENFORCE_SUCCESS_NO_GVL(error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// There's an action to be taken!
|
|
136
|
+
|
|
137
|
+
// Record function, if any
|
|
138
|
+
run_action_function = state->run_action_function;
|
|
139
|
+
|
|
140
|
+
// Reset buffer for next request
|
|
141
|
+
state->requested_action = ACTION_WAIT;
|
|
142
|
+
|
|
143
|
+
// Unlock the mutex immediately so other threads can continue to request actions without blocking
|
|
144
|
+
ENFORCE_SUCCESS_NO_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
|
|
145
|
+
|
|
146
|
+
// Process pending action
|
|
147
|
+
if (next_action == ACTION_RUN) {
|
|
148
|
+
if (run_action_function == NULL) {
|
|
149
|
+
grab_gvl_and_raise(rb_eRuntimeError, "Unexpected NULL run_action_function in run_idle_sampling_loop");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
run_action_function();
|
|
153
|
+
} else { // ACTION_STOP
|
|
154
|
+
return NULL;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static void interrupt_idle_sampling_loop(void *state_ptr) {
|
|
160
|
+
struct idle_sampling_loop_state *state = (struct idle_sampling_loop_state *) state_ptr;
|
|
161
|
+
int error = 0;
|
|
162
|
+
|
|
163
|
+
// Note about the error handling in this situation: Something bad happening at this stage is really really awkward to
|
|
164
|
+
// handle because we get called by the VM in a situation where we can't really raise exceptions, and the VM really really
|
|
165
|
+
// just wants us to stop what we're doing and return control of the thread to it.
|
|
166
|
+
//
|
|
167
|
+
// So if we return immediately on error, we may leave the VM hanging because we didn't actually interrupt the thread.
|
|
168
|
+
// We're also not at a great location to flag errors.
|
|
169
|
+
// That's why: a) I chose to log to stderr, as a last-ditch effort; b) even if something goes wrong we still try to
|
|
170
|
+
// ask the thread to stop, instead of exiting early.
|
|
171
|
+
|
|
172
|
+
error = pthread_mutex_lock(&state->wakeup_mutex);
|
|
173
|
+
if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_mutex_lock in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
|
|
174
|
+
|
|
175
|
+
state->requested_action = ACTION_STOP;
|
|
176
|
+
|
|
177
|
+
error = pthread_mutex_unlock(&state->wakeup_mutex);
|
|
178
|
+
if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_mutex_unlock in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
|
|
179
|
+
|
|
180
|
+
error = pthread_cond_broadcast(&state->wakeup);
|
|
181
|
+
if (error) { fprintf(stderr, "[DDTRACE] Error during pthread_cond_broadcast in interrupt_idle_sampling_loop (%s)\n", strerror(error)); }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static VALUE _native_stop(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
|
|
185
|
+
struct idle_sampling_loop_state *state;
|
|
186
|
+
TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
|
|
187
|
+
|
|
188
|
+
ENFORCE_SUCCESS_GVL(pthread_mutex_lock(&state->wakeup_mutex));
|
|
189
|
+
state->requested_action = ACTION_STOP;
|
|
190
|
+
ENFORCE_SUCCESS_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
|
|
191
|
+
|
|
192
|
+
// Wake up worker thread, if needed; It's OK to call broadcast after releasing the mutex
|
|
193
|
+
ENFORCE_SUCCESS_GVL(pthread_cond_broadcast(&state->wakeup));
|
|
194
|
+
|
|
195
|
+
return Qtrue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Assumption: Function gets called without the global VM lock
|
|
199
|
+
void idle_sampling_helper_request_action(VALUE self_instance, void (*run_action_function)(void)) {
|
|
200
|
+
struct idle_sampling_loop_state *state;
|
|
201
|
+
if (!rb_typeddata_is_kind_of(self_instance, &idle_sampling_helper_typed_data)) {
|
|
202
|
+
grab_gvl_and_raise(rb_eTypeError, "Wrong argument for idle_sampling_helper_request_action");
|
|
203
|
+
}
|
|
204
|
+
// This should never fail the the above check passes
|
|
205
|
+
TypedData_Get_Struct(self_instance, struct idle_sampling_loop_state, &idle_sampling_helper_typed_data, state);
|
|
206
|
+
|
|
207
|
+
ENFORCE_SUCCESS_NO_GVL(pthread_mutex_lock(&state->wakeup_mutex));
|
|
208
|
+
if (state->requested_action == ACTION_WAIT) {
|
|
209
|
+
state->requested_action = ACTION_RUN;
|
|
210
|
+
state->run_action_function = run_action_function;
|
|
211
|
+
}
|
|
212
|
+
ENFORCE_SUCCESS_NO_GVL(pthread_mutex_unlock(&state->wakeup_mutex));
|
|
213
|
+
|
|
214
|
+
// Wake up worker thread, if needed; It's OK to call broadcast after releasing the mutex
|
|
215
|
+
ENFORCE_SUCCESS_NO_GVL(pthread_cond_broadcast(&state->wakeup));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Because the idle_sampling_helper_request_action is built to be called without the global VM lock, here we release it
|
|
219
|
+
// to be able to call that API.
|
|
220
|
+
static VALUE _native_idle_sampling_helper_request_action(DDTRACE_UNUSED VALUE self, VALUE self_instance) {
|
|
221
|
+
rb_thread_call_without_gvl(request_testing_action, (void *) self_instance, NULL, NULL);
|
|
222
|
+
return Qtrue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static void *request_testing_action(void *self_instance_ptr) {
|
|
226
|
+
VALUE self_instance = (VALUE) self_instance_ptr;
|
|
227
|
+
idle_sampling_helper_request_action(self_instance, grab_gvl_and_run_testing_action);
|
|
228
|
+
return NULL;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// This gets called by the worker thread, which is not holding the global VM lock. To be able to actually run the action,
|
|
232
|
+
// we need to acquire it
|
|
233
|
+
static void grab_gvl_and_run_testing_action(void) {
|
|
234
|
+
rb_thread_call_with_gvl(run_testing_action, NULL);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static void *run_testing_action(DDTRACE_UNUSED void *unused) {
|
|
238
|
+
VALUE idle_sampling_helper_testing_action = rb_gv_get("$idle_sampling_helper_testing_action");
|
|
239
|
+
rb_funcall(idle_sampling_helper_testing_action, rb_intern("call"), 0);
|
|
240
|
+
return NULL;
|
|
241
|
+
}
|
|
@@ -67,6 +67,10 @@ $stderr.puts(
|
|
|
67
67
|
# that may fail on an environment not properly setup for building Ruby extensions.
|
|
68
68
|
require 'mkmf'
|
|
69
69
|
|
|
70
|
+
Logging.message(" [ddtrace] Using compiler:\n")
|
|
71
|
+
xsystem("#{CONFIG['CC']} -v")
|
|
72
|
+
Logging.message(" [ddtrace] End of compiler information\n")
|
|
73
|
+
|
|
70
74
|
# mkmf on modern Rubies actually has an append_cflags that does something similar
|
|
71
75
|
# (see https://github.com/ruby/ruby/pull/5760), but as usual we need a bit more boilerplate to deal with legacy Rubies
|
|
72
76
|
def add_compiler_flag(flag)
|
|
@@ -87,7 +91,7 @@ add_compiler_flag '-Werror' if ENV['DDTRACE_CI'] == 'true'
|
|
|
87
91
|
# (https://github.com/msgpack/msgpack-ruby/blob/18ce08f6d612fe973843c366ac9a0b74c4e50599/ext/msgpack/extconf.rb#L8)
|
|
88
92
|
add_compiler_flag '-std=gnu99'
|
|
89
93
|
|
|
90
|
-
# Gets really noisy when we include the MJIT header, let's omit it
|
|
94
|
+
# Gets really noisy when we include the MJIT header, let's omit it (TODO: Use #pragma GCC diagnostic instead?)
|
|
91
95
|
add_compiler_flag '-Wno-unused-function'
|
|
92
96
|
|
|
93
97
|
# Allow defining variables at any point in a function
|
|
@@ -108,6 +112,9 @@ add_compiler_flag '-Wunused-parameter'
|
|
|
108
112
|
# For more details see https://gcc.gnu.org/wiki/Visibility
|
|
109
113
|
add_compiler_flag '-fvisibility=hidden'
|
|
110
114
|
|
|
115
|
+
# Avoid legacy C definitions
|
|
116
|
+
add_compiler_flag '-Wold-style-definition'
|
|
117
|
+
|
|
111
118
|
# Enable all other compiler warnings
|
|
112
119
|
add_compiler_flag '-Wall'
|
|
113
120
|
add_compiler_flag '-Wextra'
|
|
@@ -126,6 +133,9 @@ end
|
|
|
126
133
|
# On older Rubies, there was no struct rb_native_thread. See private_vm_api_acccess.c for details.
|
|
127
134
|
$defs << '-DNO_RB_NATIVE_THREAD' if RUBY_VERSION < '3.2'
|
|
128
135
|
|
|
136
|
+
# On older Rubies, there was no struct rb_thread_sched (it was struct rb_global_vm_lock_struct)
|
|
137
|
+
$defs << '-DNO_RB_THREAD_SCHED' if RUBY_VERSION < '3.2'
|
|
138
|
+
|
|
129
139
|
# On older Rubies, there was no tid member in the internal thread structure
|
|
130
140
|
$defs << '-DNO_THREAD_TID' if RUBY_VERSION < '3.1'
|
|
131
141
|
|
|
@@ -135,9 +145,15 @@ $defs << '-DUSE_BACKPORTED_RB_PROFILE_FRAME_METHOD_NAME' if RUBY_VERSION < '3'
|
|
|
135
145
|
# On older Rubies, there are no Ractors
|
|
136
146
|
$defs << '-DNO_RACTORS' if RUBY_VERSION < '3'
|
|
137
147
|
|
|
148
|
+
# On older Rubies, rb_global_vm_lock_struct did not include the owner field
|
|
149
|
+
$defs << '-DNO_GVL_OWNER' if RUBY_VERSION < '2.6'
|
|
150
|
+
|
|
138
151
|
# On older Rubies, we need to use rb_thread_t instead of rb_execution_context_t
|
|
139
152
|
$defs << '-DUSE_THREAD_INSTEAD_OF_EXECUTION_CONTEXT' if RUBY_VERSION < '2.5'
|
|
140
153
|
|
|
154
|
+
# On older Rubies, extensions can't use GET_VM()
|
|
155
|
+
$defs << '-DNO_GET_VM' if RUBY_VERSION < '2.5'
|
|
156
|
+
|
|
141
157
|
# On older Rubies...
|
|
142
158
|
if RUBY_VERSION < '2.4'
|
|
143
159
|
# ...we need to use RUBY_VM_NORMAL_ISEQ_P instead of VM_FRAME_RUBYFRAME_P
|
|
@@ -154,8 +170,6 @@ if RUBY_VERSION < '2.3'
|
|
|
154
170
|
$defs << '-DUSE_LEGACY_RB_PROFILE_FRAMES'
|
|
155
171
|
# ... you couldn't name threads
|
|
156
172
|
$defs << '-DNO_THREAD_NAMES'
|
|
157
|
-
# ...the ruby_thread_has_gvl_p function was not exposed to users outside of the VM
|
|
158
|
-
$defs << '-DNO_THREAD_HAS_GVL'
|
|
159
173
|
end
|
|
160
174
|
|
|
161
175
|
# If we got here, libdatadog is available and loaded
|
|
@@ -173,6 +187,10 @@ unless pkg_config('datadog_profiling_with_rpath')
|
|
|
173
187
|
)
|
|
174
188
|
end
|
|
175
189
|
|
|
190
|
+
unless have_type('atomic_int', ['stdatomic.h'])
|
|
191
|
+
skip_building_extension!(Datadog::Profiling::NativeExtensionHelpers::Supported::COMPILER_ATOMIC_MISSING)
|
|
192
|
+
end
|
|
193
|
+
|
|
176
194
|
# See comments on the helper method being used for why we need to additionally set this.
|
|
177
195
|
# The extremely excessive escaping around ORIGIN below seems to be correct and was determined after a lot of
|
|
178
196
|
# experimentation. We need to get these special characters across a lot of tools untouched...
|
|
@@ -181,10 +199,6 @@ $LDFLAGS += \
|
|
|
181
199
|
"#{Datadog::Profiling::NativeExtensionHelpers.libdatadog_folder_relative_to_native_lib_folder}"
|
|
182
200
|
Logging.message(" [ddtrace] After pkg-config $LDFLAGS were set to: #{$LDFLAGS.inspect}\n")
|
|
183
201
|
|
|
184
|
-
Logging.message(" [ddtrace] Using compiler:\n")
|
|
185
|
-
xsystem("#{CONFIG['CC']} --version")
|
|
186
|
-
Logging.message(" [ddtrace] End of compiler information\n")
|
|
187
|
-
|
|
188
202
|
# Tag the native extension library with the Ruby version and Ruby platform.
|
|
189
203
|
# This makes it easier for development (avoids "oops I forgot to rebuild when I switched my Ruby") and ensures that
|
|
190
204
|
# the wrong library is never loaded.
|
|
@@ -10,3 +10,8 @@
|
|
|
10
10
|
#else
|
|
11
11
|
#define DDTRACE_UNUSED
|
|
12
12
|
#endif
|
|
13
|
+
|
|
14
|
+
// @ivoanjo: After trying to read through https://stackoverflow.com/questions/3437404/min-and-max-in-c I decided I
|
|
15
|
+
// don't like C and I just implemented this as a function.
|
|
16
|
+
inline static uint64_t uint64_max_of(uint64_t a, uint64_t b) { return a > b ? a : b; }
|
|
17
|
+
inline static uint64_t uint64_min_of(uint64_t a, uint64_t b) { return a > b ? b : a; }
|
|
@@ -179,6 +179,14 @@ module Datadog
|
|
|
179
179
|
suggested: CONTACT_SUPPORT,
|
|
180
180
|
)
|
|
181
181
|
|
|
182
|
+
# Validation for this check is done in extconf.rb because it relies on mkmf
|
|
183
|
+
COMPILER_ATOMIC_MISSING = explain_issue(
|
|
184
|
+
'your C compiler is missing support for the <stdatomic.h> header.',
|
|
185
|
+
'This issue can usually be fixed by upgrading to a later version of your',
|
|
186
|
+
'operating system image or compiler.',
|
|
187
|
+
suggested: CONTACT_SUPPORT,
|
|
188
|
+
)
|
|
189
|
+
|
|
182
190
|
private_class_method def self.disabled_via_env?
|
|
183
191
|
disabled_via_env = explain_issue(
|
|
184
192
|
'the `DD_PROFILING_NO_EXTENSION` environment variable is/was set to',
|