Skip to content

[PROF-11524] Upgrade libdatadog dependency to version 18.1 #4577

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
53f6679
Update `profiler_http_transport` benchmark to generate a new profile …
ivoanjo Apr 11, 2025
ba6125b
Remove explicitly setting `runtime_platform` profiler tag
ivoanjo Apr 11, 2025
d10feb2
Port `HttpTransport` to libdatadog 17 exporter APIs
ivoanjo Apr 11, 2025
2770db5
Port `StackRecorder` to libdatadog 17 API changes
ivoanjo Apr 11, 2025
9abd956
Temporarily disable crashtracker shutdown to get to green
ivoanjo Apr 11, 2025
7d0c686
Crashtracker changes for libdatadog 17
ivoanjo Apr 11, 2025
314b311
Fix not cloning ptr
ivoanjo Apr 11, 2025
95b4c70
Fix comment
ivoanjo May 15, 2025
e27786f
Clean up `_native_stop` for crashtracker
ivoanjo May 15, 2025
188436f
Fix crashtracking broken due to too strict timeout
ivoanjo May 15, 2025
a9a715c
Add test coverage for crash report including symbolicated frames
ivoanjo May 15, 2025
207326a
Add test coverage for crashtracking reconfiguration
ivoanjo May 15, 2025
d3950db
Fix crashtracking missing reconfiguration/disable
ivoanjo May 21, 2025
ee72d57
Update assertions to match current crashtracking format
ivoanjo May 30, 2025
d068a45
Relax test timeout expectations to avoid flakiness
ivoanjo May 30, 2025
9d131a5
Add dummy language/service and assert on them being in the applicatio…
ivoanjo May 30, 2025
e8aa092
Upgrade libdatadog dependency to 18.1.0
ivoanjo May 30, 2025
07e5453
Merge branch 'master' into ivoanjo/prof-11524-upgrade-libdatadog17-try3
ivoanjo May 30, 2025
8a79ccd
[🤖] Lock Dependency: https://github.com/DataDog/dd-trace-rb/actions/r…
ivoanjo May 30, 2025
2cf6513
Minor: Flip `validation_token` condition for better readibility
ivoanjo May 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 11 additions & 7 deletions benchmarks/profiling_http_transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@ def initialize
api_key: nil,
upload_timeout_seconds: 10,
)
flush_finish = Time.now.utc
@flush = Datadog::Profiling::Flush.new(
start: flush_finish - 60,
finish: flush_finish,
encoded_profile: Datadog::Profiling::StackRecorder.for_testing.serialize!,
@flush_finish = Time.now.utc
@stack_recorder = Datadog::Profiling::StackRecorder.for_testing
end

def flush
Datadog::Profiling::Flush.new(
start: @flush_finish - 60,
finish: @flush_finish,
encoded_profile: @stack_recorder.serialize!,
code_provenance_file_name: 'example_code_provenance_file_name.json',
code_provenance_data: '', # Random.new(1).bytes(4_000),
code_provenance_data: '',
tags_as_array: [],
internal_metadata: { no_signals_workaround_enabled: false },
info_json: JSON.generate({ profiler: { benchmarking: true } }),
Expand Down Expand Up @@ -89,7 +93,7 @@ def run_benchmark
end

def run_once
success = @transport.export(@flush)
success = @transport.export(flush)

raise('Unexpected: Export failed') unless success
end
Expand Down
2 changes: 1 addition & 1 deletion datadog.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Gem::Specification.new do |spec|

# When updating the version here, please also update the version in `libdatadog_extconf_helpers.rb`
# (and yes we have a test for it)
spec.add_dependency 'libdatadog', '~> 16.0.1.1.0'
spec.add_dependency 'libdatadog', '~> 18.1.0.1.0'

# Will no longer be a default gem on Ruby 3.5, see
# https://github.com/ruby/ruby/commit/d7e558e3c48c213d0e8bedca4fb547db55613f7c and
Expand Down
34 changes: 22 additions & 12 deletions ext/datadog_profiling_native_extension/encoded_profile.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ VALUE from_ddog_prof_EncodedProfile(ddog_prof_EncodedProfile profile) {
return TypedData_Wrap_Struct(encoded_profile_class, &encoded_profile_typed_data, state);
}

static ddog_ByteSlice get_bytes(ddog_prof_EncodedProfile *state) {
ddog_prof_Result_ByteSlice raw_bytes = ddog_prof_EncodedProfile_bytes(state);
if (raw_bytes.tag == DDOG_PROF_RESULT_BYTE_SLICE_ERR_BYTE_SLICE) {
rb_raise(rb_eRuntimeError, "Failed to get bytes from profile: %"PRIsVALUE, get_error_details_and_drop(&raw_bytes.err));
}
return raw_bytes.ok;
}

static ddog_prof_EncodedProfile *internal_to_ddog_prof_EncodedProfile(VALUE object) {
ddog_prof_EncodedProfile *state;
TypedData_Get_Struct(object, ddog_prof_EncodedProfile, &encoded_profile_typed_data, state);
return state;
}

ddog_prof_EncodedProfile *to_ddog_prof_EncodedProfile(VALUE object) {
ddog_prof_EncodedProfile *state = internal_to_ddog_prof_EncodedProfile(object);
get_bytes(state); // Validate profile is still usable -- if it's not, this will raise an exception
return state;
}

static void encoded_profile_typed_data_free(void *state_ptr) {
ddog_prof_EncodedProfile *state = (ddog_prof_EncodedProfile *) state_ptr;

Expand All @@ -49,18 +69,8 @@ static void encoded_profile_typed_data_free(void *state_ptr) {
}

static VALUE _native_bytes(VALUE self) {
ddog_prof_EncodedProfile *state;
TypedData_Get_Struct(self, ddog_prof_EncodedProfile, &encoded_profile_typed_data, state);

return ruby_string_from_vec_u8(state->buffer);

// TODO: This will be used for libdatadog 17
/*ddog_prof_Result_ByteSlice raw_bytes = ddog_prof_EncodedProfile_bytes(state);
if (raw_bytes.tag == DDOG_PROF_RESULT_BYTE_SLICE_ERR_BYTE_SLICE) {
rb_raise(rb_eRuntimeError, "Failed to get bytes from profile: %"PRIsVALUE, get_error_details_and_drop(&raw_bytes.err));
}

return rb_str_new((const char *) raw_bytes.ok.ptr, raw_bytes.ok.len);*/
ddog_ByteSlice bytes = get_bytes(internal_to_ddog_prof_EncodedProfile(self));
return rb_str_new((const char *) bytes.ptr, bytes.len);
}

VALUE enforce_encoded_profile_instance(VALUE object) {
Expand Down
1 change: 1 addition & 0 deletions ext/datadog_profiling_native_extension/encoded_profile.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

VALUE from_ddog_prof_EncodedProfile(ddog_prof_EncodedProfile profile);
VALUE enforce_encoded_profile_instance(VALUE object);
ddog_prof_EncodedProfile *to_ddog_prof_EncodedProfile(VALUE object);
117 changes: 45 additions & 72 deletions ext/datadog_profiling_native_extension/http_transport.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,22 @@ static VALUE error_symbol = Qnil; // :error in Ruby
static VALUE library_version_string = Qnil;

typedef struct {
ddog_prof_Exporter *exporter;
ddog_prof_Exporter_Request_BuildResult *build_result;
ddog_prof_ProfileExporter *exporter;
ddog_prof_Request_Result *build_result;
ddog_CancellationToken *cancel_token;
ddog_prof_Exporter_SendResult result;
ddog_prof_Result_HttpStatus result;
bool send_ran;
} call_exporter_without_gvl_arguments;

static inline ddog_ByteSlice byte_slice_from_ruby_string(VALUE string);
static VALUE _native_validate_exporter(VALUE self, VALUE exporter_configuration);
static ddog_prof_Exporter_NewResult create_exporter(VALUE exporter_configuration, VALUE tags_as_array);
static VALUE handle_exporter_failure(ddog_prof_Exporter_NewResult exporter_result);
static ddog_prof_ProfileExporter_Result create_exporter(VALUE exporter_configuration, VALUE tags_as_array);
static VALUE handle_exporter_failure(ddog_prof_ProfileExporter_Result exporter_result);
static VALUE _native_do_export(
VALUE self,
VALUE exporter_configuration,
VALUE upload_timeout_milliseconds,
VALUE flush,
VALUE start_timespec_seconds,
VALUE start_timespec_nanoseconds,
VALUE finish_timespec_seconds,
VALUE finish_timespec_nanoseconds
VALUE flush
);
static void *call_exporter_without_gvl(void *call_args);
static void interrupt_exporter_call(void *cancel_token);
Expand All @@ -43,7 +39,7 @@ void http_transport_init(VALUE profiling_module) {
VALUE http_transport_class = rb_define_class_under(profiling_module, "HttpTransport", rb_cObject);

rb_define_singleton_method(http_transport_class, "_native_validate_exporter", _native_validate_exporter, 1);
rb_define_singleton_method(http_transport_class, "_native_do_export", _native_do_export, 7);
rb_define_singleton_method(http_transport_class, "_native_do_export", _native_do_export, 3);

ok_symbol = ID2SYM(rb_intern_const("ok"));
error_symbol = ID2SYM(rb_intern_const("error"));
Expand All @@ -60,14 +56,14 @@ static inline ddog_ByteSlice byte_slice_from_ruby_string(VALUE string) {

static VALUE _native_validate_exporter(DDTRACE_UNUSED VALUE _self, VALUE exporter_configuration) {
ENFORCE_TYPE(exporter_configuration, T_ARRAY);
ddog_prof_Exporter_NewResult exporter_result = create_exporter(exporter_configuration, rb_ary_new());
ddog_prof_ProfileExporter_Result exporter_result = create_exporter(exporter_configuration, rb_ary_new());

VALUE failure_tuple = handle_exporter_failure(exporter_result);
if (!NIL_P(failure_tuple)) return failure_tuple;

// We don't actually need the exporter for now -- we just wanted to validate that we could create it with the
// settings we were given
ddog_prof_Exporter_drop(exporter_result.ok);
ddog_prof_Exporter_drop(&exporter_result.ok);

return rb_ary_new_from_args(2, ok_symbol, Qnil);
}
Expand All @@ -93,7 +89,7 @@ static ddog_prof_Endpoint endpoint_from(VALUE exporter_configuration) {
}
}

static ddog_prof_Exporter_NewResult create_exporter(VALUE exporter_configuration, VALUE tags_as_array) {
static ddog_prof_ProfileExporter_Result create_exporter(VALUE exporter_configuration, VALUE tags_as_array) {
ENFORCE_TYPE(exporter_configuration, T_ARRAY);
ENFORCE_TYPE(tags_as_array, T_ARRAY);

Expand All @@ -107,55 +103,58 @@ static ddog_prof_Exporter_NewResult create_exporter(VALUE exporter_configuration
ddog_CharSlice library_version = char_slice_from_ruby_string(library_version_string);
ddog_CharSlice profiling_family = DDOG_CHARSLICE_C("ruby");

ddog_prof_Exporter_NewResult exporter_result =
ddog_prof_ProfileExporter_Result exporter_result =
ddog_prof_Exporter_new(library_name, library_version, profiling_family, &tags, endpoint);

ddog_Vec_Tag_drop(tags);

return exporter_result;
}

static VALUE handle_exporter_failure(ddog_prof_Exporter_NewResult exporter_result) {
return exporter_result.tag == DDOG_PROF_EXPORTER_NEW_RESULT_OK ?
static void validate_token(ddog_CancellationToken token, const char *file, int line) {
if (token.inner == NULL) rb_raise(rb_eRuntimeError, "Unexpected: Validation token was empty at %s:%d", file, line);
}

static VALUE handle_exporter_failure(ddog_prof_ProfileExporter_Result exporter_result) {
return exporter_result.tag == DDOG_PROF_PROFILE_EXPORTER_RESULT_OK_HANDLE_PROFILE_EXPORTER ?
Qnil :
rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&exporter_result.err));
}

// Note: This function handles a bunch of libdatadog dynamically-allocated objects, so it MUST not use any Ruby APIs
// which can raise exceptions, otherwise the objects will be leaked.
static VALUE perform_export(
ddog_prof_Exporter *exporter,
ddog_Timespec start,
ddog_Timespec finish,
ddog_prof_ProfileExporter *exporter,
ddog_prof_EncodedProfile *profile,
ddog_prof_Exporter_Slice_File files_to_compress_and_export,
ddog_prof_Exporter_Slice_File files_to_export_unmodified,
ddog_CharSlice internal_metadata,
ddog_CharSlice info
) {
ddog_prof_ProfiledEndpointsStats *endpoints_stats = NULL; // Not in use yet
ddog_prof_Exporter_Request_BuildResult build_result = ddog_prof_Exporter_Request_build(
ddog_prof_Request_Result build_result = ddog_prof_Exporter_Request_build(
exporter,
start,
finish,
profile,
files_to_compress_and_export,
files_to_export_unmodified,
/* files_to_export_unmodified: */ ddog_prof_Exporter_Slice_File_empty(),
/* optional_additional_tags: */ NULL,
endpoints_stats,
&internal_metadata,
&info
);

if (build_result.tag == DDOG_PROF_EXPORTER_REQUEST_BUILD_RESULT_ERR) {
if (build_result.tag == DDOG_PROF_REQUEST_RESULT_ERR_HANDLE_REQUEST) {
ddog_prof_Exporter_drop(exporter);
return rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&build_result.err));
}

ddog_CancellationToken *cancel_token = ddog_CancellationToken_new();
ddog_CancellationToken cancel_token_request = ddog_CancellationToken_new();
ddog_CancellationToken cancel_token_interrupt = ddog_CancellationToken_clone(&cancel_token_request);

validate_token(cancel_token_request, __FILE__, __LINE__);
validate_token(cancel_token_interrupt, __FILE__, __LINE__);

// We'll release the Global VM Lock while we're calling send, so that the Ruby VM can continue to work while this
// is pending
call_exporter_without_gvl_arguments args =
{.exporter = exporter, .build_result = &build_result, .cancel_token = cancel_token, .send_ran = false};
{.exporter = exporter, .build_result = &build_result, .cancel_token = &cancel_token_request, .send_ran = false};

// We use rb_thread_call_without_gvl2 instead of rb_thread_call_without_gvl as the gvl2 variant never raises any
// exceptions.
Expand All @@ -172,14 +171,15 @@ static VALUE perform_export(
int pending_exception = 0;

while (!args.send_ran && !pending_exception) {
rb_thread_call_without_gvl2(call_exporter_without_gvl, &args, interrupt_exporter_call, cancel_token);
rb_thread_call_without_gvl2(call_exporter_without_gvl, &args, interrupt_exporter_call, &cancel_token_interrupt);

// To make sure we don't leak memory, we never check for pending exceptions if send ran
if (!args.send_ran) pending_exception = check_if_pending_exception();
}

// Cleanup exporter and token, no longer needed
ddog_CancellationToken_drop(cancel_token);
ddog_CancellationToken_drop(&cancel_token_request);
ddog_CancellationToken_drop(&cancel_token_interrupt);
ddog_prof_Exporter_drop(exporter);

if (pending_exception) {
Expand All @@ -192,22 +192,18 @@ static VALUE perform_export(

// The request itself does not need to be freed as libdatadog takes ownership of it as part of sending.

ddog_prof_Exporter_SendResult result = args.result;
ddog_prof_Result_HttpStatus result = args.result;

return result.tag == DDOG_PROF_EXPORTER_SEND_RESULT_HTTP_RESPONSE ?
rb_ary_new_from_args(2, ok_symbol, UINT2NUM(result.http_response.code)) :
return result.tag == DDOG_PROF_RESULT_HTTP_STATUS_OK_HTTP_STATUS ?
rb_ary_new_from_args(2, ok_symbol, UINT2NUM(result.ok.code)) :
rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&result.err));
}

static VALUE _native_do_export(
DDTRACE_UNUSED VALUE _self,
VALUE exporter_configuration,
VALUE upload_timeout_milliseconds,
VALUE flush,
VALUE start_timespec_seconds,
VALUE start_timespec_nanoseconds,
VALUE finish_timespec_seconds,
VALUE finish_timespec_nanoseconds
VALUE flush
) {
VALUE encoded_profile = rb_funcall(flush, rb_intern("encoded_profile"), 0);
VALUE code_provenance_file_name = rb_funcall(flush, rb_intern("code_provenance_file_name"), 0);
Expand All @@ -217,10 +213,6 @@ static VALUE _native_do_export(
VALUE info_json = rb_funcall(flush, rb_intern("info_json"), 0);

ENFORCE_TYPE(upload_timeout_milliseconds, T_FIXNUM);
ENFORCE_TYPE(start_timespec_seconds, T_FIXNUM);
ENFORCE_TYPE(start_timespec_nanoseconds, T_FIXNUM);
ENFORCE_TYPE(finish_timespec_seconds, T_FIXNUM);
ENFORCE_TYPE(finish_timespec_nanoseconds, T_FIXNUM);
enforce_encoded_profile_instance(encoded_profile);
ENFORCE_TYPE(code_provenance_file_name, T_STRING);
ENFORCE_TYPE(tags_as_array, T_ARRAY);
Expand All @@ -233,28 +225,9 @@ static VALUE _native_do_export(

uint64_t timeout_milliseconds = NUM2ULONG(upload_timeout_milliseconds);

ddog_Timespec start =
{.seconds = NUM2LONG(start_timespec_seconds), .nanoseconds = NUM2UINT(start_timespec_nanoseconds)};
ddog_Timespec finish =
{.seconds = NUM2LONG(finish_timespec_seconds), .nanoseconds = NUM2UINT(finish_timespec_nanoseconds)};

int to_compress_length = have_code_provenance ? 1 : 0;
ddog_prof_Exporter_File to_compress[to_compress_length];
int already_compressed_length = 1; // pprof
ddog_prof_Exporter_File already_compressed[already_compressed_length];

ddog_prof_Exporter_Slice_File files_to_compress_and_export = {.ptr = to_compress, .len = to_compress_length};
ddog_prof_Exporter_Slice_File files_to_export_unmodified = {.ptr = already_compressed, .len = already_compressed_length};

// TODO: Hardcoding the file name will go away with libdatadog 17
VALUE pprof_file_name = rb_str_new_cstr("rubyprofile.pprof");
VALUE pprof_data = rb_funcall(encoded_profile, rb_intern("_native_bytes"), 0);
ENFORCE_TYPE(pprof_data, T_STRING);

already_compressed[0] = (ddog_prof_Exporter_File) {
.name = char_slice_from_ruby_string(pprof_file_name),
.file = byte_slice_from_ruby_string(pprof_data),
};

if (have_code_provenance) {
to_compress[0] = (ddog_prof_Exporter_File) {
Expand All @@ -266,27 +239,25 @@ static VALUE _native_do_export(
ddog_CharSlice internal_metadata = char_slice_from_ruby_string(internal_metadata_json);
ddog_CharSlice info = char_slice_from_ruby_string(info_json);

ddog_prof_Exporter_NewResult exporter_result = create_exporter(exporter_configuration, tags_as_array);
ddog_prof_ProfileExporter_Result exporter_result = create_exporter(exporter_configuration, tags_as_array);
// Note: Do not add anything that can raise exceptions after this line, as otherwise the exporter memory will leak

VALUE failure_tuple = handle_exporter_failure(exporter_result);
if (!NIL_P(failure_tuple)) return failure_tuple;

ddog_prof_MaybeError timeout_result = ddog_prof_Exporter_set_timeout(exporter_result.ok, timeout_milliseconds);
if (timeout_result.tag == DDOG_PROF_OPTION_ERROR_SOME_ERROR) {
ddog_VoidResult timeout_result = ddog_prof_Exporter_set_timeout(&exporter_result.ok, timeout_milliseconds);
if (timeout_result.tag == DDOG_VOID_RESULT_ERR) {
// NOTE: Seems a bit harsh to fail the upload if we can't set a timeout. OTOH, this is only expected to fail
// if the exporter is not well built. Because such a situation should already be caught above I think it's
// preferable to leave this here as a virtually unreachable exception rather than ignoring it.
ddog_prof_Exporter_drop(exporter_result.ok);
return rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&timeout_result.some));
ddog_prof_Exporter_drop(&exporter_result.ok);
return rb_ary_new_from_args(2, error_symbol, get_error_details_and_drop(&timeout_result.err));
}

return perform_export(
exporter_result.ok,
start,
finish,
&exporter_result.ok,
to_ddog_prof_EncodedProfile(encoded_profile),
files_to_compress_and_export,
files_to_export_unmodified,
internal_metadata,
info
);
Expand All @@ -303,5 +274,7 @@ static void *call_exporter_without_gvl(void *call_args) {

// Called by Ruby when it wants to interrupt call_exporter_without_gvl above, e.g. when the app wants to exit cleanly
static void interrupt_exporter_call(void *cancel_token) {
// TODO: False here can mean two things: it was already cancelled OR it failed to cancel.
// Would be nice to change libdatadog to be able to distinguish between them...
ddog_CancellationToken_cancel((ddog_CancellationToken *) cancel_token);
}
Loading
Loading