-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The `StackRecorder` is used to wrap a `ddprof_ffi_Profile` in a Ruby object, as well as to expose the libddprof serialization APIs. In the future, it will be used in place of the `OldRecorder`.
- Loading branch information
Showing
6 changed files
with
212 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
ext/ddtrace_profiling_native_extension/stack_recorder.c
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
#include <ruby.h> | ||
#include <ruby/debug.h> | ||
#include <ddprof/ffi.h> | ||
#include "stack_recorder.h" | ||
|
||
// Used to wrap a ddprof_ffi_Profile in a Ruby object and expose Ruby-level serialization APIs | ||
// This file implements the native bits of the Datadog::Profiling::StackRecorder class | ||
|
||
static VALUE ok_symbol = Qnil; // :ok in Ruby | ||
static VALUE error_symbol = Qnil; // :error in Ruby | ||
|
||
static VALUE stack_recorder_class = Qnil; | ||
|
||
#define CPU_TIME_VALUE {.type_ = DDPROF_FFI_CHARSLICE_C("cpu-time"), .unit = DDPROF_FFI_CHARSLICE_C("nanoseconds")} | ||
#define CPU_SAMPLES_VALUE {.type_ = DDPROF_FFI_CHARSLICE_C("cpu-samples"), .unit = DDPROF_FFI_CHARSLICE_C("count")} | ||
#define WALL_TIME_VALUE {.type_ = DDPROF_FFI_CHARSLICE_C("wall-time"), .unit = DDPROF_FFI_CHARSLICE_C("nanoseconds")} | ||
#define ALLOC_SAMPLES_VALUE {.type_ = DDPROF_FFI_CHARSLICE_C("alloc-samples"), .unit = DDPROF_FFI_CHARSLICE_C("count")} | ||
#define ALLOC_SPACE_VALUE {.type_ = DDPROF_FFI_CHARSLICE_C("alloc-space"), .unit = DDPROF_FFI_CHARSLICE_C("bytes")} | ||
#define HEAP_SPACE_VALUE {.type_ = DDPROF_FFI_CHARSLICE_C("heap-space"), .unit = DDPROF_FFI_CHARSLICE_C("bytes")} | ||
|
||
const static ddprof_ffi_ValueType enabled_value_types[] = {CPU_TIME_VALUE, CPU_SAMPLES_VALUE, WALL_TIME_VALUE}; | ||
#define ENABLED_VALUE_TYPES_COUNT (sizeof(enabled_value_types) / sizeof(ddprof_ffi_ValueType)) | ||
|
||
static VALUE _native_new(VALUE klass); | ||
static void stack_recorder_typed_data_free(void *data); | ||
static VALUE _native_serialize(VALUE self, VALUE recorder_instance); | ||
static VALUE ruby_time_from(ddprof_ffi_Timespec ddprof_time); | ||
|
||
void stack_recorder_init(VALUE profiling_module) { | ||
stack_recorder_class = rb_define_class_under(profiling_module, "StackRecorder", rb_cObject); | ||
|
||
// Instances of the StackRecorder class are going to be "TypedData" objects. | ||
// "TypedData" objects are special objects in the Ruby VM that can wrap C structs. | ||
// In our case, we're going to keep a libddprof profile reference inside our object. | ||
// | ||
// Because Ruby doesn't know how to initialize libddprof profiles, we MUST override the allocation function for objects | ||
// of this class so that we can manage this part. Not overriding or disabling the allocation function is a common | ||
// gotcha for "TypedData" objects that can very easily lead to VM crashes, see for instance | ||
// https://bugs.ruby-lang.org/issues/18007 for a discussion around this. | ||
rb_define_alloc_func(stack_recorder_class, _native_new); | ||
|
||
rb_define_singleton_method(stack_recorder_class, "_native_serialize", _native_serialize, 1); | ||
|
||
ok_symbol = ID2SYM(rb_intern_const("ok")); | ||
error_symbol = ID2SYM(rb_intern_const("error")); | ||
|
||
} | ||
|
||
// This structure is used to define a Ruby object that stores a pointer to a ddprof_ffi_Profile instance | ||
// See also https://github.com/ruby/ruby/blob/master/doc/extension.rdoc for how this works | ||
static const rb_data_type_t stack_recorder_typed_data = { | ||
.wrap_struct_name = "Datadog::Profiling::StackRecorder", | ||
.function = { | ||
.dfree = stack_recorder_typed_data_free, | ||
.dsize = NULL, // We don't track profile memory usage (although it'd be cool if we did!) | ||
// No need to provide dmark nor dcompact because we don't directly reference Ruby VALUEs from inside this object | ||
}, | ||
.flags = RUBY_TYPED_FREE_IMMEDIATELY | ||
}; | ||
|
||
static VALUE _native_new(VALUE klass) { | ||
ddprof_ffi_Slice_value_type sample_types = {.ptr = enabled_value_types, .len = ENABLED_VALUE_TYPES_COUNT}; | ||
|
||
ddprof_ffi_Profile *profile = ddprof_ffi_Profile_new(sample_types, NULL /* Period is optional */); | ||
|
||
return TypedData_Wrap_Struct(stack_recorder_class, &stack_recorder_typed_data, profile); | ||
} | ||
|
||
static void stack_recorder_typed_data_free(void *data) { | ||
ddprof_ffi_Profile_free((ddprof_ffi_Profile *) data); | ||
} | ||
|
||
static VALUE _native_serialize(VALUE self, VALUE recorder_instance) { | ||
Check_TypedStruct(recorder_instance, &stack_recorder_typed_data); | ||
|
||
ddprof_ffi_Profile *profile; | ||
TypedData_Get_Struct(recorder_instance, ddprof_ffi_Profile, &stack_recorder_typed_data, profile); | ||
|
||
// TODO: Update this after https://github.com/DataDog/libddprof/pull/42 gets merged | ||
ddprof_ffi_EncodedProfile *serialized_profile = ddprof_ffi_Profile_serialize(profile); | ||
if (serialized_profile == NULL) return rb_ary_new_from_args(2, error_symbol, rb_str_new_cstr("Failed to serialize profile")); | ||
|
||
VALUE encoded_pprof = rb_str_new((char *) serialized_profile->buffer.ptr, serialized_profile->buffer.len); | ||
VALUE start = ruby_time_from(serialized_profile->start); | ||
VALUE finish = ruby_time_from(serialized_profile->end); | ||
|
||
ddprof_ffi_EncodedProfile_delete(serialized_profile); | ||
if (!ddprof_ffi_Profile_reset(profile)) return rb_ary_new_from_args(2, error_symbol, rb_str_new_cstr("Failed to reset profile")); | ||
|
||
return rb_ary_new_from_args(2, ok_symbol, rb_ary_new_from_args(3, start, finish, encoded_pprof)); | ||
} | ||
|
||
static VALUE ruby_time_from(ddprof_ffi_Timespec ddprof_time) { | ||
const int utc = INT_MAX - 1; // From Ruby sources | ||
struct timespec time = {.tv_sec = ddprof_time.seconds, .tv_nsec = ddprof_time.nanoseconds}; | ||
return rb_time_timespec_new(&time, utc); | ||
} | ||
|
||
void record_sample(VALUE recorder_instance, ddprof_ffi_Sample sample) { | ||
Check_TypedStruct(recorder_instance, &stack_recorder_typed_data); | ||
ddprof_ffi_Profile *profile; | ||
TypedData_Get_Struct(recorder_instance, ddprof_ffi_Profile, &stack_recorder_typed_data, profile); | ||
ddprof_ffi_Profile_add(profile, sample); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#pragma once | ||
|
||
void record_sample(VALUE recorder_instance, ddprof_ffi_Sample sample); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# typed: false | ||
|
||
module Datadog | ||
module Profiling | ||
# Used to wrap a ddprof_ffi_Profile in a Ruby object and expose Ruby-level serialization APIs | ||
# Methods prefixed with _native_ are implemented in `stack_recorder.c` | ||
class StackRecorder | ||
def serialize | ||
status, result = self.class._native_serialize(self) | ||
|
||
if status == :ok | ||
start, finish, encoded_pprof = result | ||
|
||
[start, finish, encoded_pprof] | ||
else | ||
error_message = result | ||
|
||
Datadog.logger.error("Failed to serialize profiling data: #{error_message}") | ||
|
||
nil | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# typed: ignore | ||
|
||
require 'datadog/profiling/spec_helper' | ||
require 'datadog/profiling/stack_recorder' | ||
|
||
RSpec.describe Datadog::Profiling::StackRecorder do | ||
before { skip_if_profiling_not_supported(self) } | ||
|
||
subject(:stack_recorder) { described_class.new } | ||
|
||
# NOTE: A lot of libddprof integration behaviors are tested in the Collectors::Stack specs, since we need actual | ||
# samples in order to observe what comes out of libddprof | ||
|
||
describe '#serialize' do | ||
subject(:serialize) { stack_recorder.serialize } | ||
|
||
let(:start) { serialize[0] } | ||
let(:finish) { serialize[1] } | ||
let(:encoded_pprof) { serialize[2] } | ||
|
||
let(:decoded_profile) { ::Perftools::Profiles::Profile.decode(encoded_pprof) } | ||
|
||
context 'when the profile is empty' do | ||
it 'uses the current time as the start and finish time' do | ||
before_serialize = Time.now | ||
serialize | ||
after_serialize = Time.now | ||
|
||
expect(start).to be_between(before_serialize, after_serialize) | ||
expect(finish).to be_between(before_serialize, after_serialize) | ||
expect(start).to be <= finish | ||
end | ||
|
||
it 'returns an empty pprof profile' do | ||
expect(sample_types_from(decoded_profile)).to eq( | ||
'cpu-time' => 'nanoseconds', | ||
'cpu-samples' => 'count', | ||
'wall-time' => 'nanoseconds', | ||
) | ||
|
||
expect(decoded_profile).to have_attributes( | ||
sample: [], | ||
mapping: [], | ||
location: [], | ||
function: [], | ||
drop_frames: 0, | ||
keep_frames: 0, | ||
time_nanos: Datadog::Core::Utils::Time.as_utc_epoch_ns(start), | ||
period_type: nil, | ||
period: 0, | ||
comment: [], | ||
) | ||
end | ||
|
||
def sample_types_from(decoded_profile) | ||
strings = decoded_profile.string_table | ||
decoded_profile.sample_type.map { |sample_type| [strings[sample_type.type], strings[sample_type.unit]] }.to_h | ||
end | ||
end | ||
|
||
context 'when there is a failure during serialization' do | ||
before do | ||
allow(Datadog.logger).to receive(:error) | ||
|
||
# Real failures in serialization are hard to trigger, so we're using a mock failure instead | ||
expect(described_class).to receive(:_native_serialize).and_return([:error, 'test error message']) | ||
end | ||
|
||
it { is_expected.to be nil } | ||
|
||
it 'logs an error message' do | ||
expect(Datadog.logger).to receive(:error).with(/test error message/) | ||
|
||
serialize | ||
end | ||
end | ||
end | ||
end |