ddtrace 1.1.0 → 1.2.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 +59 -1
- data/CONTRIBUTING.md +1 -1
- data/README.md +7 -2
- data/ddtrace.gemspec +5 -2
- data/docs/GettingStarted.md +27 -3
- data/docs/ProfilingDevelopment.md +27 -28
- data/docs/UpgradeGuide.md +1 -1
- data/ext/ddtrace_profiling_loader/ddtrace_profiling_loader.c +1 -1
- data/ext/ddtrace_profiling_loader/extconf.rb +1 -0
- data/ext/ddtrace_profiling_native_extension/NativeExtensionDesign.md +6 -5
- data/ext/ddtrace_profiling_native_extension/clock_id.h +1 -1
- data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +1 -1
- data/ext/ddtrace_profiling_native_extension/clock_id_noop.c +1 -1
- data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time.c +269 -0
- data/ext/ddtrace_profiling_native_extension/collectors_stack.c +12 -12
- data/ext/ddtrace_profiling_native_extension/collectors_stack.h +9 -0
- data/ext/ddtrace_profiling_native_extension/extconf.rb +44 -3
- data/ext/ddtrace_profiling_native_extension/http_transport.c +341 -0
- data/ext/ddtrace_profiling_native_extension/native_extension_helpers.rb +92 -4
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +76 -1
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +3 -0
- data/ext/ddtrace_profiling_native_extension/profiling.c +4 -0
- data/ext/ddtrace_profiling_native_extension/ruby_helpers.h +33 -0
- data/ext/ddtrace_profiling_native_extension/stack_recorder.c +18 -10
- data/ext/ddtrace_profiling_native_extension/stack_recorder.h +10 -1
- data/lib/datadog/core/configuration/components.rb +39 -24
- data/lib/datadog/core/configuration/settings.rb +8 -1
- data/lib/datadog/core/environment/platform.rb +40 -0
- data/lib/datadog/core/utils.rb +1 -1
- data/lib/datadog/opentracer/thread_local_scope_manager.rb +26 -3
- data/lib/datadog/profiling/collectors/code_provenance.rb +1 -0
- data/lib/datadog/profiling/collectors/cpu_and_wall_time.rb +42 -0
- data/lib/datadog/profiling/collectors/stack.rb +2 -0
- data/lib/datadog/profiling/encoding/profile.rb +7 -11
- data/lib/datadog/profiling/exporter.rb +58 -9
- data/lib/datadog/profiling/ext/forking.rb +8 -8
- data/lib/datadog/profiling/ext.rb +2 -15
- data/lib/datadog/profiling/flush.rb +25 -53
- data/lib/datadog/profiling/http_transport.rb +131 -0
- data/lib/datadog/profiling/old_ext.rb +42 -0
- data/lib/datadog/profiling/{recorder.rb → old_recorder.rb} +20 -31
- data/lib/datadog/profiling/scheduler.rb +24 -43
- data/lib/datadog/profiling/transport/http/api/endpoint.rb +9 -31
- data/lib/datadog/profiling/transport/http/client.rb +5 -3
- data/lib/datadog/profiling/transport/http/response.rb +0 -2
- data/lib/datadog/profiling/transport/http.rb +1 -1
- data/lib/datadog/profiling.rb +3 -3
- data/lib/datadog/tracing/context_provider.rb +17 -1
- data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +4 -0
- data/lib/datadog/tracing/contrib/grpc/configuration/settings.rb +1 -0
- data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -1
- data/lib/datadog/tracing/contrib/grpc/datadog_interceptor.rb +4 -0
- data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +35 -0
- data/lib/datadog/tracing/contrib/pg/ext.rb +31 -0
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +129 -0
- data/lib/datadog/tracing/contrib/pg/integration.rb +43 -0
- data/lib/datadog/tracing/contrib/pg/patcher.rb +31 -0
- data/lib/datadog/tracing/contrib/rails/configuration/settings.rb +3 -0
- data/lib/datadog/tracing/contrib/rails/framework.rb +2 -1
- data/lib/datadog/tracing/contrib/rest_client/configuration/settings.rb +1 -0
- data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +1 -1
- data/lib/datadog/tracing/contrib.rb +1 -0
- data/lib/datadog/tracing/distributed/headers/b3.rb +1 -1
- data/lib/datadog/tracing/distributed/headers/b3_single.rb +4 -4
- data/lib/datadog/tracing/distributed/headers/datadog.rb +1 -1
- data/lib/datadog/tracing/distributed/headers/parser.rb +37 -0
- data/lib/datadog/tracing/distributed/helpers.rb +34 -0
- data/lib/datadog/tracing/distributed/metadata/b3.rb +55 -0
- data/lib/datadog/tracing/distributed/metadata/b3_single.rb +66 -0
- data/lib/datadog/tracing/distributed/metadata/datadog.rb +73 -0
- data/lib/datadog/tracing/distributed/metadata/parser.rb +34 -0
- data/lib/datadog/tracing/metadata/ext.rb +25 -0
- data/lib/datadog/tracing/metadata/tagging.rb +6 -0
- data/lib/datadog/tracing/propagation/grpc.rb +65 -55
- data/lib/datadog/tracing/sampling/rate_sampler.rb +2 -2
- data/lib/datadog/tracing/sampling/span/matcher.rb +80 -0
- data/lib/datadog/tracing/span.rb +21 -1
- data/lib/datadog/tracing/span_operation.rb +2 -1
- data/lib/ddtrace/version.rb +1 -1
- metadata +24 -13
- data/lib/datadog/profiling/transport/client.rb +0 -16
- data/lib/datadog/profiling/transport/io/client.rb +0 -29
- data/lib/datadog/profiling/transport/io/response.rb +0 -18
- data/lib/datadog/profiling/transport/io.rb +0 -32
- data/lib/datadog/profiling/transport/parcel.rb +0 -19
- data/lib/datadog/profiling/transport/request.rb +0 -17
- data/lib/datadog/profiling/transport/response.rb +0 -10
- data/lib/datadog/tracing/distributed/parser.rb +0 -70
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/thread.h>
|
|
3
|
+
#include <ddprof/ffi.h>
|
|
4
|
+
#include "libddprof_helpers.h"
|
|
5
|
+
#include "ruby_helpers.h"
|
|
6
|
+
|
|
7
|
+
// Used to report profiling data to Datadog.
|
|
8
|
+
// This file implements the native bits of the Datadog::Profiling::HttpTransport class
|
|
9
|
+
|
|
10
|
+
static VALUE ok_symbol = Qnil; // :ok in Ruby
|
|
11
|
+
static VALUE error_symbol = Qnil; // :error in Ruby
|
|
12
|
+
|
|
13
|
+
static ID agentless_id; // id of :agentless in Ruby
|
|
14
|
+
static ID agent_id; // id of :agent in Ruby
|
|
15
|
+
|
|
16
|
+
static ID log_failure_to_process_tag_id; // id of :log_failure_to_process_tag in Ruby
|
|
17
|
+
|
|
18
|
+
static VALUE http_transport_class = Qnil;
|
|
19
|
+
|
|
20
|
+
struct call_exporter_without_gvl_arguments {
|
|
21
|
+
ddprof_ffi_ProfileExporterV3 *exporter;
|
|
22
|
+
ddprof_ffi_Request *request;
|
|
23
|
+
ddprof_ffi_CancellationToken *cancel_token;
|
|
24
|
+
ddprof_ffi_SendResult result;
|
|
25
|
+
bool send_ran;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
inline static ddprof_ffi_ByteSlice byte_slice_from_ruby_string(VALUE string);
|
|
29
|
+
static VALUE _native_validate_exporter(VALUE self, VALUE exporter_configuration);
|
|
30
|
+
static ddprof_ffi_NewProfileExporterV3Result create_exporter(VALUE exporter_configuration, VALUE tags_as_array);
|
|
31
|
+
static VALUE handle_exporter_failure(ddprof_ffi_NewProfileExporterV3Result exporter_result);
|
|
32
|
+
static ddprof_ffi_EndpointV3 endpoint_from(VALUE exporter_configuration);
|
|
33
|
+
static ddprof_ffi_Vec_tag convert_tags(VALUE tags_as_array);
|
|
34
|
+
static void safely_log_failure_to_process_tag(ddprof_ffi_Vec_tag tags, VALUE err_details);
|
|
35
|
+
static VALUE _native_do_export(
|
|
36
|
+
VALUE self,
|
|
37
|
+
VALUE exporter_configuration,
|
|
38
|
+
VALUE upload_timeout_milliseconds,
|
|
39
|
+
VALUE start_timespec_seconds,
|
|
40
|
+
VALUE start_timespec_nanoseconds,
|
|
41
|
+
VALUE finish_timespec_seconds,
|
|
42
|
+
VALUE finish_timespec_nanoseconds,
|
|
43
|
+
VALUE pprof_file_name,
|
|
44
|
+
VALUE pprof_data,
|
|
45
|
+
VALUE code_provenance_file_name,
|
|
46
|
+
VALUE code_provenance_data,
|
|
47
|
+
VALUE tags_as_array
|
|
48
|
+
);
|
|
49
|
+
static void *call_exporter_without_gvl(void *call_args);
|
|
50
|
+
static void interrupt_exporter_call(void *cancel_token);
|
|
51
|
+
|
|
52
|
+
void http_transport_init(VALUE profiling_module) {
|
|
53
|
+
http_transport_class = rb_define_class_under(profiling_module, "HttpTransport", rb_cObject);
|
|
54
|
+
|
|
55
|
+
rb_define_singleton_method(http_transport_class, "_native_validate_exporter", _native_validate_exporter, 1);
|
|
56
|
+
rb_define_singleton_method(http_transport_class, "_native_do_export", _native_do_export, 11);
|
|
57
|
+
|
|
58
|
+
ok_symbol = ID2SYM(rb_intern_const("ok"));
|
|
59
|
+
error_symbol = ID2SYM(rb_intern_const("error"));
|
|
60
|
+
agentless_id = rb_intern_const("agentless");
|
|
61
|
+
agent_id = rb_intern_const("agent");
|
|
62
|
+
log_failure_to_process_tag_id = rb_intern_const("log_failure_to_process_tag");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
inline static ddprof_ffi_ByteSlice byte_slice_from_ruby_string(VALUE string) {
|
|
66
|
+
Check_Type(string, T_STRING);
|
|
67
|
+
ddprof_ffi_ByteSlice byte_slice = {.ptr = (uint8_t *) StringValuePtr(string), .len = RSTRING_LEN(string)};
|
|
68
|
+
return byte_slice;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static VALUE _native_validate_exporter(VALUE self, VALUE exporter_configuration) {
|
|
72
|
+
Check_Type(exporter_configuration, T_ARRAY);
|
|
73
|
+
ddprof_ffi_NewProfileExporterV3Result exporter_result = create_exporter(exporter_configuration, rb_ary_new());
|
|
74
|
+
|
|
75
|
+
VALUE failure_tuple = handle_exporter_failure(exporter_result);
|
|
76
|
+
if (!NIL_P(failure_tuple)) return failure_tuple;
|
|
77
|
+
|
|
78
|
+
// We don't actually need the exporter for now -- we just wanted to validate that we could create it with the
|
|
79
|
+
// settings we were given
|
|
80
|
+
ddprof_ffi_NewProfileExporterV3Result_drop(exporter_result);
|
|
81
|
+
|
|
82
|
+
return rb_ary_new_from_args(2, ok_symbol, Qnil);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static ddprof_ffi_NewProfileExporterV3Result create_exporter(VALUE exporter_configuration, VALUE tags_as_array) {
|
|
86
|
+
Check_Type(exporter_configuration, T_ARRAY);
|
|
87
|
+
Check_Type(tags_as_array, T_ARRAY);
|
|
88
|
+
|
|
89
|
+
// This needs to be called BEFORE convert_tags since it can raise an exception and thus cause the ddprof_ffi_Vec_tag
|
|
90
|
+
// to be leaked.
|
|
91
|
+
ddprof_ffi_EndpointV3 endpoint = endpoint_from(exporter_configuration);
|
|
92
|
+
|
|
93
|
+
ddprof_ffi_Vec_tag tags = convert_tags(tags_as_array);
|
|
94
|
+
|
|
95
|
+
ddprof_ffi_NewProfileExporterV3Result exporter_result =
|
|
96
|
+
ddprof_ffi_ProfileExporterV3_new(DDPROF_FFI_CHARSLICE_C("ruby"), &tags, endpoint);
|
|
97
|
+
|
|
98
|
+
ddprof_ffi_Vec_tag_drop(tags);
|
|
99
|
+
|
|
100
|
+
return exporter_result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static VALUE handle_exporter_failure(ddprof_ffi_NewProfileExporterV3Result exporter_result) {
|
|
104
|
+
if (exporter_result.tag == DDPROF_FFI_NEW_PROFILE_EXPORTER_V3_RESULT_OK) return Qnil;
|
|
105
|
+
|
|
106
|
+
VALUE err_details = ruby_string_from_vec_u8(exporter_result.err);
|
|
107
|
+
|
|
108
|
+
ddprof_ffi_NewProfileExporterV3Result_drop(exporter_result);
|
|
109
|
+
|
|
110
|
+
return rb_ary_new_from_args(2, error_symbol, err_details);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static ddprof_ffi_EndpointV3 endpoint_from(VALUE exporter_configuration) {
|
|
114
|
+
Check_Type(exporter_configuration, T_ARRAY);
|
|
115
|
+
|
|
116
|
+
ID working_mode = SYM2ID(rb_ary_entry(exporter_configuration, 0)); // SYM2ID verifies its input so we can do this safely
|
|
117
|
+
|
|
118
|
+
if (working_mode != agentless_id && working_mode != agent_id) {
|
|
119
|
+
rb_raise(rb_eArgError, "Failed to initialize transport: Unexpected working mode, expected :agentless or :agent");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (working_mode == agentless_id) {
|
|
123
|
+
VALUE site = rb_ary_entry(exporter_configuration, 1);
|
|
124
|
+
VALUE api_key = rb_ary_entry(exporter_configuration, 2);
|
|
125
|
+
Check_Type(site, T_STRING);
|
|
126
|
+
Check_Type(api_key, T_STRING);
|
|
127
|
+
|
|
128
|
+
return ddprof_ffi_EndpointV3_agentless(char_slice_from_ruby_string(site), char_slice_from_ruby_string(api_key));
|
|
129
|
+
} else { // agent_id
|
|
130
|
+
VALUE base_url = rb_ary_entry(exporter_configuration, 1);
|
|
131
|
+
Check_Type(base_url, T_STRING);
|
|
132
|
+
|
|
133
|
+
return ddprof_ffi_EndpointV3_agent(char_slice_from_ruby_string(base_url));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
__attribute__((warn_unused_result))
|
|
138
|
+
static ddprof_ffi_Vec_tag convert_tags(VALUE tags_as_array) {
|
|
139
|
+
Check_Type(tags_as_array, T_ARRAY);
|
|
140
|
+
|
|
141
|
+
long tags_count = RARRAY_LEN(tags_as_array);
|
|
142
|
+
ddprof_ffi_Vec_tag tags = ddprof_ffi_Vec_tag_new();
|
|
143
|
+
|
|
144
|
+
for (long i = 0; i < tags_count; i++) {
|
|
145
|
+
VALUE name_value_pair = rb_ary_entry(tags_as_array, i);
|
|
146
|
+
|
|
147
|
+
if (!RB_TYPE_P(name_value_pair, T_ARRAY)) {
|
|
148
|
+
ddprof_ffi_Vec_tag_drop(tags);
|
|
149
|
+
Check_Type(name_value_pair, T_ARRAY);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Note: We can index the array without checking its size first because rb_ary_entry returns Qnil if out of bounds
|
|
153
|
+
VALUE tag_name = rb_ary_entry(name_value_pair, 0);
|
|
154
|
+
VALUE tag_value = rb_ary_entry(name_value_pair, 1);
|
|
155
|
+
|
|
156
|
+
if (!(RB_TYPE_P(tag_name, T_STRING) && RB_TYPE_P(tag_value, T_STRING))) {
|
|
157
|
+
ddprof_ffi_Vec_tag_drop(tags);
|
|
158
|
+
Check_Type(tag_name, T_STRING);
|
|
159
|
+
Check_Type(tag_value, T_STRING);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ddprof_ffi_PushTagResult push_result =
|
|
163
|
+
ddprof_ffi_Vec_tag_push(&tags, char_slice_from_ruby_string(tag_name), char_slice_from_ruby_string(tag_value));
|
|
164
|
+
|
|
165
|
+
if (push_result.tag == DDPROF_FFI_PUSH_TAG_RESULT_ERR) {
|
|
166
|
+
VALUE err_details = ruby_string_from_vec_u8(push_result.err);
|
|
167
|
+
ddprof_ffi_PushTagResult_drop(push_result);
|
|
168
|
+
|
|
169
|
+
// libddprof validates tags and may catch invalid tags that ddtrace didn't actually catch.
|
|
170
|
+
// We warn users about such tags, and then just ignore them.
|
|
171
|
+
safely_log_failure_to_process_tag(tags, err_details);
|
|
172
|
+
} else {
|
|
173
|
+
ddprof_ffi_PushTagResult_drop(push_result);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return tags;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static VALUE log_failure_to_process_tag(VALUE err_details) {
|
|
181
|
+
return rb_funcall(http_transport_class, log_failure_to_process_tag_id, 1, err_details);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Since we are calling into Ruby code, it may raise an exception. This method ensure that dynamically-allocated tags
|
|
185
|
+
// get cleaned before propagating the exception.
|
|
186
|
+
static void safely_log_failure_to_process_tag(ddprof_ffi_Vec_tag tags, VALUE err_details) {
|
|
187
|
+
int exception_state;
|
|
188
|
+
rb_protect(log_failure_to_process_tag, err_details, &exception_state);
|
|
189
|
+
|
|
190
|
+
if (exception_state) { // An exception was raised
|
|
191
|
+
ddprof_ffi_Vec_tag_drop(tags); // clean up
|
|
192
|
+
rb_jump_tag(exception_state); // "Re-raise" exception
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Note: This function handles a bunch of libddprof dynamically-allocated objects, so it MUST not use any Ruby APIs
|
|
197
|
+
// which can raise exceptions, otherwise the objects will be leaked.
|
|
198
|
+
static VALUE perform_export(
|
|
199
|
+
ddprof_ffi_NewProfileExporterV3Result valid_exporter_result, // Must be called with a valid exporter result
|
|
200
|
+
ddprof_ffi_Timespec start,
|
|
201
|
+
ddprof_ffi_Timespec finish,
|
|
202
|
+
ddprof_ffi_Slice_file slice_files,
|
|
203
|
+
ddprof_ffi_Vec_tag *additional_tags,
|
|
204
|
+
uint64_t timeout_milliseconds
|
|
205
|
+
) {
|
|
206
|
+
ddprof_ffi_ProfileExporterV3 *exporter = valid_exporter_result.ok;
|
|
207
|
+
ddprof_ffi_CancellationToken *cancel_token = ddprof_ffi_CancellationToken_new();
|
|
208
|
+
ddprof_ffi_Request *request =
|
|
209
|
+
ddprof_ffi_ProfileExporterV3_build(exporter, start, finish, slice_files, additional_tags, timeout_milliseconds);
|
|
210
|
+
|
|
211
|
+
// We'll release the Global VM Lock while we're calling send, so that the Ruby VM can continue to work while this
|
|
212
|
+
// is pending
|
|
213
|
+
struct call_exporter_without_gvl_arguments args =
|
|
214
|
+
{.exporter = exporter, .request = request, .cancel_token = cancel_token, .send_ran = false};
|
|
215
|
+
|
|
216
|
+
// We use rb_thread_call_without_gvl2 instead of rb_thread_call_without_gvl as the gvl2 variant never raises any
|
|
217
|
+
// exceptions.
|
|
218
|
+
//
|
|
219
|
+
// (With rb_thread_call_without_gvl, if someone calls Thread#kill or something like it on the current thread,
|
|
220
|
+
// the exception will be raised without us being able to clean up dynamically-allocated stuff, which would leak.)
|
|
221
|
+
//
|
|
222
|
+
// Instead, we take care of our own exception checking, and delay the exception raising (`rb_jump_tag` call) until
|
|
223
|
+
// after we cleaned up any dynamically-allocated resources.
|
|
224
|
+
//
|
|
225
|
+
// We run rb_thread_call_without_gvl2 in a loop since an "interrupt" may cause it to return before even running
|
|
226
|
+
// our code. In such a case, we retry the call -- unless the interrupt was caused by an exception being pending,
|
|
227
|
+
// and in that case we also give up and break out of the loop.
|
|
228
|
+
int pending_exception = 0;
|
|
229
|
+
|
|
230
|
+
while (!args.send_ran && !pending_exception) {
|
|
231
|
+
rb_thread_call_without_gvl2(call_exporter_without_gvl, &args, interrupt_exporter_call, cancel_token);
|
|
232
|
+
if (!args.send_ran) pending_exception = check_if_pending_exception();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
VALUE ruby_status;
|
|
236
|
+
VALUE ruby_result;
|
|
237
|
+
|
|
238
|
+
if (pending_exception) {
|
|
239
|
+
// We're in a weird situation that libddprof doesn't quite support. The ddprof_ffi_Request payload is dynamically
|
|
240
|
+
// allocated and needs to be freed, but libddprof doesn't have an API for dropping a request.
|
|
241
|
+
//
|
|
242
|
+
// There's plans to add a `ddprof_ffi_Request_drop`
|
|
243
|
+
// (https://github.com/DataDog/dd-trace-rb/pull/1923#discussion_r882096221); once that happens, we can use it here.
|
|
244
|
+
//
|
|
245
|
+
// As a workaround, we get libddprof to clean up the request by asking for the send to be cancelled, and then calling
|
|
246
|
+
// it anyway. This will make libddprof free the request and return immediately which gets us the expected effect.
|
|
247
|
+
interrupt_exporter_call((void *) cancel_token);
|
|
248
|
+
call_exporter_without_gvl((void *) &args);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
ddprof_ffi_SendResult result = args.result;
|
|
252
|
+
bool success = result.tag == DDPROF_FFI_SEND_RESULT_HTTP_RESPONSE;
|
|
253
|
+
|
|
254
|
+
ruby_status = success ? ok_symbol : error_symbol;
|
|
255
|
+
ruby_result = success ? UINT2NUM(result.http_response.code) : ruby_string_from_vec_u8(result.failure);
|
|
256
|
+
|
|
257
|
+
// Clean up all dynamically-allocated things
|
|
258
|
+
ddprof_ffi_SendResult_drop(args.result);
|
|
259
|
+
ddprof_ffi_CancellationToken_drop(cancel_token);
|
|
260
|
+
ddprof_ffi_NewProfileExporterV3Result_drop(valid_exporter_result);
|
|
261
|
+
// The request itself does not need to be freed as libddprof takes care of it.
|
|
262
|
+
|
|
263
|
+
// We've cleaned up everything, so if there's an exception to be raised, let's have it
|
|
264
|
+
if (pending_exception) rb_jump_tag(pending_exception);
|
|
265
|
+
|
|
266
|
+
return rb_ary_new_from_args(2, ruby_status, ruby_result);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
static VALUE _native_do_export(
|
|
270
|
+
VALUE self,
|
|
271
|
+
VALUE exporter_configuration,
|
|
272
|
+
VALUE upload_timeout_milliseconds,
|
|
273
|
+
VALUE start_timespec_seconds,
|
|
274
|
+
VALUE start_timespec_nanoseconds,
|
|
275
|
+
VALUE finish_timespec_seconds,
|
|
276
|
+
VALUE finish_timespec_nanoseconds,
|
|
277
|
+
VALUE pprof_file_name,
|
|
278
|
+
VALUE pprof_data,
|
|
279
|
+
VALUE code_provenance_file_name,
|
|
280
|
+
VALUE code_provenance_data,
|
|
281
|
+
VALUE tags_as_array
|
|
282
|
+
) {
|
|
283
|
+
Check_Type(upload_timeout_milliseconds, T_FIXNUM);
|
|
284
|
+
Check_Type(start_timespec_seconds, T_FIXNUM);
|
|
285
|
+
Check_Type(start_timespec_nanoseconds, T_FIXNUM);
|
|
286
|
+
Check_Type(finish_timespec_seconds, T_FIXNUM);
|
|
287
|
+
Check_Type(finish_timespec_nanoseconds, T_FIXNUM);
|
|
288
|
+
Check_Type(pprof_file_name, T_STRING);
|
|
289
|
+
Check_Type(pprof_data, T_STRING);
|
|
290
|
+
Check_Type(code_provenance_file_name, T_STRING);
|
|
291
|
+
|
|
292
|
+
// Code provenance can be disabled and in that case will be set to nil
|
|
293
|
+
bool have_code_provenance = !NIL_P(code_provenance_data);
|
|
294
|
+
if (have_code_provenance) Check_Type(code_provenance_data, T_STRING);
|
|
295
|
+
|
|
296
|
+
uint64_t timeout_milliseconds = NUM2ULONG(upload_timeout_milliseconds);
|
|
297
|
+
|
|
298
|
+
ddprof_ffi_Timespec start =
|
|
299
|
+
{.seconds = NUM2LONG(start_timespec_seconds), .nanoseconds = NUM2UINT(start_timespec_nanoseconds)};
|
|
300
|
+
ddprof_ffi_Timespec finish =
|
|
301
|
+
{.seconds = NUM2LONG(finish_timespec_seconds), .nanoseconds = NUM2UINT(finish_timespec_nanoseconds)};
|
|
302
|
+
|
|
303
|
+
int files_to_report = 1 + (have_code_provenance ? 1 : 0);
|
|
304
|
+
ddprof_ffi_File files[files_to_report];
|
|
305
|
+
ddprof_ffi_Slice_file slice_files = {.ptr = files, .len = files_to_report};
|
|
306
|
+
|
|
307
|
+
files[0] = (ddprof_ffi_File) {
|
|
308
|
+
.name = char_slice_from_ruby_string(pprof_file_name),
|
|
309
|
+
.file = byte_slice_from_ruby_string(pprof_data)
|
|
310
|
+
};
|
|
311
|
+
if (have_code_provenance) {
|
|
312
|
+
files[1] = (ddprof_ffi_File) {
|
|
313
|
+
.name = char_slice_from_ruby_string(code_provenance_file_name),
|
|
314
|
+
.file = byte_slice_from_ruby_string(code_provenance_data)
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
ddprof_ffi_Vec_tag *null_additional_tags = NULL;
|
|
319
|
+
|
|
320
|
+
ddprof_ffi_NewProfileExporterV3Result exporter_result = create_exporter(exporter_configuration, tags_as_array);
|
|
321
|
+
// Note: Do not add anything that can raise exceptions after this line, as otherwise the exporter memory will leak
|
|
322
|
+
|
|
323
|
+
VALUE failure_tuple = handle_exporter_failure(exporter_result);
|
|
324
|
+
if (!NIL_P(failure_tuple)) return failure_tuple;
|
|
325
|
+
|
|
326
|
+
return perform_export(exporter_result, start, finish, slice_files, null_additional_tags, timeout_milliseconds);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
static void *call_exporter_without_gvl(void *call_args) {
|
|
330
|
+
struct call_exporter_without_gvl_arguments *args = (struct call_exporter_without_gvl_arguments*) call_args;
|
|
331
|
+
|
|
332
|
+
args->result = ddprof_ffi_ProfileExporterV3_send(args->exporter, args->request, args->cancel_token);
|
|
333
|
+
args->send_ran = true;
|
|
334
|
+
|
|
335
|
+
return NULL; // Unused
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Called by Ruby when it wants to interrupt call_exporter_without_gvl above, e.g. when the app wants to exit cleanly
|
|
339
|
+
static void interrupt_exporter_call(void *cancel_token) {
|
|
340
|
+
ddprof_ffi_CancellationToken_cancel((ddprof_ffi_CancellationToken *) cancel_token);
|
|
341
|
+
}
|
|
@@ -3,15 +3,69 @@
|
|
|
3
3
|
# typed: ignore
|
|
4
4
|
|
|
5
5
|
require 'libddprof'
|
|
6
|
+
require 'pathname'
|
|
6
7
|
|
|
7
8
|
module Datadog
|
|
8
9
|
module Profiling
|
|
10
|
+
# Helpers for extconf.rb
|
|
9
11
|
module NativeExtensionHelpers
|
|
12
|
+
# Can be set when customers want to skip compiling the native extension entirely
|
|
10
13
|
ENV_NO_EXTENSION = 'DD_PROFILING_NO_EXTENSION'
|
|
14
|
+
# Can be set to force rubygems to fail gem installation when profiling extension could not be built
|
|
15
|
+
ENV_FAIL_INSTALL_IF_MISSING_EXTENSION = 'DD_PROFILING_FAIL_INSTALL_IF_MISSING_EXTENSION'
|
|
11
16
|
|
|
12
17
|
# Older Rubies don't have the MJIT header, used by the JIT compiler, so we need to use a different approach
|
|
13
18
|
CAN_USE_MJIT_HEADER = RUBY_VERSION >= '2.6'
|
|
14
19
|
|
|
20
|
+
def self.fail_install_if_missing_extension?
|
|
21
|
+
ENV[ENV_FAIL_INSTALL_IF_MISSING_EXTENSION].to_s.strip.downcase == 'true'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Used as an workaround for a limitation with how dynamic linking works in environments where ddtrace and
|
|
25
|
+
# libddprof are moved after the extension gets compiled.
|
|
26
|
+
#
|
|
27
|
+
# Because the libddpprof native library is installed on a non-standard system path, in order for it to be
|
|
28
|
+
# found by the system dynamic linker (e.g. what takes care of dlopen(), which is used to load the profiling
|
|
29
|
+
# native extension), we need to add a "runpath" -- a list of folders to search for libddprof.
|
|
30
|
+
#
|
|
31
|
+
# This runpath gets hardcoded at native library linking time. You can look at it using the `readelf` tool in
|
|
32
|
+
# Linux: e.g. `readelf -d ddtrace_profiling_native_extension.2.7.3_x86_64-linux.so`.
|
|
33
|
+
#
|
|
34
|
+
# In ddtrace 1.1.0, we only set as runpath an absolute path to libddprof. (This gets set automatically by the call
|
|
35
|
+
# to `pkg_config('ddprof_ffi_with_rpath')` in `extconf.rb`). This worked fine as long as libddprof was **NOT**
|
|
36
|
+
# moved from the folder it was present at ddtrace installation/linking time.
|
|
37
|
+
#
|
|
38
|
+
# Unfortunately, environments such as Heroku and AWS Elastic Beanstalk move gems around in the filesystem after
|
|
39
|
+
# installation. Thus, the profiling native extension could not be loaded in these environments
|
|
40
|
+
# (see https://github.com/DataDog/dd-trace-rb/issues/2067) because libddprof could not be found.
|
|
41
|
+
#
|
|
42
|
+
# To workaround this issue, this method computes the **relative** path between the folder where the profiling
|
|
43
|
+
# native extension is going to be installed and the folder where libddprof is installed, and returns it
|
|
44
|
+
# to be set as an additional runpath. (Yes, you can set multiple runpath folders to be searched).
|
|
45
|
+
#
|
|
46
|
+
# This way, if both gems are moved together (and it turns out that they are in these environments),
|
|
47
|
+
# the relative path can still be traversed to find libddprof.
|
|
48
|
+
#
|
|
49
|
+
# This is incredibly awful, and it's kinda bizarre how it's not possible to just find these paths at runtime
|
|
50
|
+
# and set them correctly; rather than needing to set stuff at linking-time and then praying to $deity that
|
|
51
|
+
# weird moves don't happen.
|
|
52
|
+
#
|
|
53
|
+
# As a curiosity, `LD_LIBRARY_PATH` can be used to influence the folders that get searched but **CANNOT BE
|
|
54
|
+
# SET DYNAMICALLY**, e.g. it needs to be set at the start of the process (Ruby VM) and thus it's not something
|
|
55
|
+
# we could setup when doing a `require`.
|
|
56
|
+
#
|
|
57
|
+
def self.libddprof_folder_relative_to_native_lib_folder(
|
|
58
|
+
current_folder: __dir__,
|
|
59
|
+
libddprof_pkgconfig_folder: Libddprof.pkgconfig_folder
|
|
60
|
+
)
|
|
61
|
+
return unless libddprof_pkgconfig_folder
|
|
62
|
+
|
|
63
|
+
profiling_native_lib_folder = "#{current_folder}/../../lib/"
|
|
64
|
+
libddprof_lib_folder = "#{libddprof_pkgconfig_folder}/../"
|
|
65
|
+
|
|
66
|
+
Pathname.new(libddprof_lib_folder).relative_path_from(Pathname.new(profiling_native_lib_folder)).to_s
|
|
67
|
+
end
|
|
68
|
+
|
|
15
69
|
# Used to check if profiler is supported, including user-visible clear messages explaining why their
|
|
16
70
|
# system may not be supported.
|
|
17
71
|
# rubocop:disable Metrics/ModuleLength
|
|
@@ -37,15 +91,30 @@ module Datadog
|
|
|
37
91
|
end
|
|
38
92
|
|
|
39
93
|
# This banner will show up in the logs/terminal while compiling the native extension
|
|
40
|
-
def self.failure_banner_for(reason:, suggested:)
|
|
41
|
-
prettify_lines = proc { |lines| lines.map { |line| "| #{line.ljust(76)} |" }.join("\n") }
|
|
94
|
+
def self.failure_banner_for(reason:, suggested:, fail_install:)
|
|
95
|
+
prettify_lines = proc { |lines| Array(lines).map { |line| "| #{line.ljust(76)} |" }.join("\n") }
|
|
96
|
+
outcome =
|
|
97
|
+
if fail_install
|
|
98
|
+
[
|
|
99
|
+
'Failing installation immediately because the ',
|
|
100
|
+
"`#{ENV_FAIL_INSTALL_IF_MISSING_EXTENSION}` environment variable is set",
|
|
101
|
+
'to `true`.',
|
|
102
|
+
'When contacting support, please include the <mkmf.log> file that is shown ',
|
|
103
|
+
'below.',
|
|
104
|
+
]
|
|
105
|
+
else
|
|
106
|
+
[
|
|
107
|
+
'The Datadog Continuous Profiler will not be available,',
|
|
108
|
+
'but all other ddtrace features will work fine!',
|
|
109
|
+
]
|
|
110
|
+
end
|
|
111
|
+
|
|
42
112
|
%(
|
|
43
113
|
+------------------------------------------------------------------------------+
|
|
44
114
|
| Could not compile the Datadog Continuous Profiler because |
|
|
45
115
|
#{prettify_lines.call(reason)}
|
|
46
116
|
| |
|
|
47
|
-
|
|
48
|
-
| but all other ddtrace features will work fine! |
|
|
117
|
+
#{prettify_lines.call(outcome)}
|
|
49
118
|
| |
|
|
50
119
|
#{prettify_lines.call(suggested)}
|
|
51
120
|
+------------------------------------------------------------------------------+
|
|
@@ -57,6 +126,14 @@ module Datadog
|
|
|
57
126
|
[*reason, *suggested].join(' ')
|
|
58
127
|
end
|
|
59
128
|
|
|
129
|
+
# mkmf sets $PKGCONFIG after the `pkg_config` gets used in extconf.rb. When `pkg_config` is unsuccessful, we use
|
|
130
|
+
# this helper to decide if we can show more specific error message vs a generic "something went wrong".
|
|
131
|
+
def self.pkg_config_missing?(command: $PKGCONFIG) # rubocop:disable Style/GlobalVars
|
|
132
|
+
pkg_config_available = command && xsystem("#{command} --version")
|
|
133
|
+
|
|
134
|
+
pkg_config_available != true
|
|
135
|
+
end
|
|
136
|
+
|
|
60
137
|
CONTACT_SUPPORT = [
|
|
61
138
|
'For help solving this issue, please contact Datadog support at',
|
|
62
139
|
'<https://docs.datadoghq.com/help/>.',
|
|
@@ -84,6 +161,17 @@ module Datadog
|
|
|
84
161
|
suggested: CONTACT_SUPPORT,
|
|
85
162
|
)
|
|
86
163
|
|
|
164
|
+
# Validation for this check is done in extconf.rb because it relies on mkmf
|
|
165
|
+
PKG_CONFIG_IS_MISSING = explain_issue(
|
|
166
|
+
#+-----------------------------------------------------------------------------+
|
|
167
|
+
'the `pkg-config` system tool is missing.',
|
|
168
|
+
'This issue can usually be fixed by installing:',
|
|
169
|
+
'1. the `pkg-config` package on Homebrew and Debian/Ubuntu-based Linux;',
|
|
170
|
+
'2. the `pkgconf` package on Arch and Alpine-based Linux;',
|
|
171
|
+
'3. the `pkgconf-pkg-config` package on Fedora/Red Hat-based Linux.',
|
|
172
|
+
suggested: CONTACT_SUPPORT,
|
|
173
|
+
)
|
|
174
|
+
|
|
87
175
|
private_class_method def self.disabled_via_env?
|
|
88
176
|
disabled_via_env = explain_issue(
|
|
89
177
|
'the `DD_PROFILING_NO_EXTENSION` environment variable is/was set to',
|
|
@@ -35,7 +35,12 @@ static inline rb_thread_t *thread_struct_from_object(VALUE thread) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
rb_nativethread_id_t pthread_id_for(VALUE thread) {
|
|
38
|
-
|
|
38
|
+
// struct rb_native_thread was introduced in Ruby 3.2 (preview2): https://github.com/ruby/ruby/pull/5836
|
|
39
|
+
#ifndef NO_RB_NATIVE_THREAD
|
|
40
|
+
return thread_struct_from_object(thread)->nt->thread_id;
|
|
41
|
+
#else
|
|
42
|
+
return thread_struct_from_object(thread)->thread_id;
|
|
43
|
+
#endif
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
// Returns the stack depth by using the same approach as rb_profile_frames and backtrace_each: get the positions
|
|
@@ -58,6 +63,76 @@ ptrdiff_t stack_depth_for(VALUE thread) {
|
|
|
58
63
|
return end_cfp <= cfp ? 0 : end_cfp - cfp - 1;
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
// This was renamed in Ruby 3.2
|
|
67
|
+
#if !defined(ccan_list_for_each) && defined(list_for_each)
|
|
68
|
+
#define ccan_list_for_each list_for_each
|
|
69
|
+
#endif
|
|
70
|
+
|
|
71
|
+
#ifndef USE_LEGACY_LIVING_THREADS_ST // Ruby > 2.1
|
|
72
|
+
// Tries to match rb_thread_list() but that method isn't accessible to extensions
|
|
73
|
+
VALUE ddtrace_thread_list(void) {
|
|
74
|
+
VALUE result = rb_ary_new();
|
|
75
|
+
rb_thread_t *thread = NULL;
|
|
76
|
+
|
|
77
|
+
// Ruby 3 Safety: Our implementation is inspired by `rb_ractor_thread_list` BUT that method wraps the operations below
|
|
78
|
+
// with `RACTOR_LOCK` and `RACTOR_UNLOCK`.
|
|
79
|
+
//
|
|
80
|
+
// This initially made me believe that one MUST grab the ractor lock (which is different from the ractor-scoped Global
|
|
81
|
+
// VM Lock) in able to iterate the `threads.set`. This turned out not to be the case: upon further study of the VM
|
|
82
|
+
// codebase in 3.2-master, 3.1 and 3.0, there's quite a few places where `threads.set` is accessed without grabbing
|
|
83
|
+
// the ractor lock: `ractor_mark` (ractor.c), `thgroup_list` (thread.c), `rb_check_deadlock` (thread.c), etc.
|
|
84
|
+
//
|
|
85
|
+
// I suspect the design in `rb_ractor_thread_list` may be done that way to perhaps in the future expose it to be
|
|
86
|
+
// called from a different Ractor, but I'm not sure...
|
|
87
|
+
#ifdef HAVE_RUBY_RACTOR_H
|
|
88
|
+
rb_ractor_t *current_ractor = GET_RACTOR();
|
|
89
|
+
ccan_list_for_each(¤t_ractor->threads.set, thread, lt_node) {
|
|
90
|
+
#else
|
|
91
|
+
rb_vm_t *vm = thread_struct_from_object(rb_thread_current())->vm;
|
|
92
|
+
list_for_each(&vm->living_threads, thread, vmlt_node) {
|
|
93
|
+
#endif
|
|
94
|
+
switch (thread->status) {
|
|
95
|
+
case THREAD_RUNNABLE:
|
|
96
|
+
case THREAD_STOPPED:
|
|
97
|
+
case THREAD_STOPPED_FOREVER:
|
|
98
|
+
rb_ary_push(result, thread->self);
|
|
99
|
+
default:
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
#else // USE_LEGACY_LIVING_THREADS_ST
|
|
107
|
+
static int ddtrace_thread_list_each(st_data_t thread_object, st_data_t _value, void *result_object);
|
|
108
|
+
|
|
109
|
+
// Alternative ddtrace_thread_list implementation for Ruby 2.1. In this Ruby version, living threads were stored in a
|
|
110
|
+
// hashmap (st) instead of a list.
|
|
111
|
+
VALUE ddtrace_thread_list() {
|
|
112
|
+
VALUE result = rb_ary_new();
|
|
113
|
+
st_foreach(thread_struct_from_object(rb_thread_current())->vm->living_threads, ddtrace_thread_list_each, result);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static int ddtrace_thread_list_each(st_data_t thread_object, st_data_t _value, void *result_object) {
|
|
118
|
+
VALUE result = (VALUE) result_object;
|
|
119
|
+
rb_thread_t *thread = thread_struct_from_object((VALUE) thread_object);
|
|
120
|
+
switch (thread->status) {
|
|
121
|
+
case THREAD_RUNNABLE:
|
|
122
|
+
case THREAD_STOPPED:
|
|
123
|
+
case THREAD_STOPPED_FOREVER:
|
|
124
|
+
rb_ary_push(result, thread->self);
|
|
125
|
+
default:
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
return ST_CONTINUE;
|
|
129
|
+
}
|
|
130
|
+
#endif // USE_LEGACY_LIVING_THREADS_ST
|
|
131
|
+
|
|
132
|
+
bool is_thread_alive(VALUE thread) {
|
|
133
|
+
return thread_struct_from_object(thread)->status != THREAD_KILLED;
|
|
134
|
+
}
|
|
135
|
+
|
|
61
136
|
// -----------------------------------------------------------------------------
|
|
62
137
|
// The sources below are modified versions of code extracted from the Ruby project.
|
|
63
138
|
// Each function is annotated with its origin, why we imported it, and the changes made.
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
rb_nativethread_id_t pthread_id_for(VALUE thread);
|
|
19
19
|
ptrdiff_t stack_depth_for(VALUE thread);
|
|
20
|
+
VALUE ddtrace_thread_list(void);
|
|
21
|
+
bool is_thread_alive(VALUE thread);
|
|
22
|
+
|
|
20
23
|
int ddtrace_rb_profile_frames(VALUE thread, int start, int limit, VALUE *buff, int *lines, bool* is_ruby_frame);
|
|
21
24
|
|
|
22
25
|
// Ruby 3.0 finally added support for showing CFUNC frames (frames for methods written using native code)
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
#include "clock_id.h"
|
|
4
4
|
|
|
5
5
|
// Each class/module here is implemented in their separate file
|
|
6
|
+
void collectors_cpu_and_wall_time_init(VALUE profiling_module);
|
|
6
7
|
void collectors_stack_init(VALUE profiling_module);
|
|
8
|
+
void http_transport_init(VALUE profiling_module);
|
|
7
9
|
void stack_recorder_init(VALUE profiling_module);
|
|
8
10
|
|
|
9
11
|
static VALUE native_working_p(VALUE self);
|
|
@@ -20,7 +22,9 @@ void DDTRACE_EXPORT Init_ddtrace_profiling_native_extension(void) {
|
|
|
20
22
|
|
|
21
23
|
rb_define_singleton_method(native_extension_module, "clock_id_for", clock_id_for, 1); // from clock_id.h
|
|
22
24
|
|
|
25
|
+
collectors_cpu_and_wall_time_init(profiling_module);
|
|
23
26
|
collectors_stack_init(profiling_module);
|
|
27
|
+
http_transport_init(profiling_module);
|
|
24
28
|
stack_recorder_init(profiling_module);
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <ruby.h>
|
|
4
|
+
|
|
5
|
+
// Processes any pending interruptions, including exceptions to be raised.
|
|
6
|
+
// If there's an exception to be raised, it raises it. In that case, this function does not return.
|
|
7
|
+
static inline VALUE process_pending_interruptions(VALUE _unused) {
|
|
8
|
+
rb_thread_check_ints();
|
|
9
|
+
return Qnil;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Calls process_pending_interruptions BUT "rescues" any exceptions to be raised, returning them instead as
|
|
13
|
+
// a non-zero `pending_exception`.
|
|
14
|
+
//
|
|
15
|
+
// Thus, if there's a non-zero `pending_exception`, the caller MUST call `rb_jump_tag(pending_exception)` after any
|
|
16
|
+
// needed clean-ups.
|
|
17
|
+
//
|
|
18
|
+
// Usage example:
|
|
19
|
+
//
|
|
20
|
+
// ```c
|
|
21
|
+
// foo = ruby_xcalloc(...);
|
|
22
|
+
// pending_exception = check_if_pending_exception();
|
|
23
|
+
// if (pending_exception) {
|
|
24
|
+
// ruby_xfree(foo);
|
|
25
|
+
// rb_jump_tag(pending_exception); // Re-raises exception
|
|
26
|
+
// }
|
|
27
|
+
// ```
|
|
28
|
+
__attribute__((warn_unused_result))
|
|
29
|
+
static inline int check_if_pending_exception(void) {
|
|
30
|
+
int pending_exception;
|
|
31
|
+
rb_protect(process_pending_interruptions, Qnil, &pending_exception);
|
|
32
|
+
return pending_exception;
|
|
33
|
+
}
|