Skip to content

Commit

Permalink
Add libddprof-based StackRecorder
Browse files Browse the repository at this point in the history
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
ivoanjo committed Apr 4, 2022
1 parent 4d7997a commit 9078e11
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 2 deletions.
2 changes: 2 additions & 0 deletions ext/ddtrace_profiling_native_extension/profiling.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

// Each class/module here is implemented in their separate file
void http_transport_init(VALUE profiling_module);
void stack_recorder_init(VALUE profiling_module);

static VALUE native_working_p(VALUE self);

Expand All @@ -18,6 +19,7 @@ void Init_ddtrace_profiling_native_extension(void) {
rb_define_singleton_method(native_extension_module, "clock_id_for", clock_id_for, 1); // from clock_id.h

http_transport_init(profiling_module);
stack_recorder_init(profiling_module);
}

static VALUE native_working_p(VALUE self) {
Expand Down
104 changes: 104 additions & 0 deletions ext/ddtrace_profiling_native_extension/stack_recorder.c
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);
}
3 changes: 3 additions & 0 deletions ext/ddtrace_profiling_native_extension/stack_recorder.h
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);
25 changes: 25 additions & 0 deletions lib/datadog/profiling/stack_recorder.rb
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
2 changes: 0 additions & 2 deletions spec/datadog/profiling/http_transport_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# typed: ignore

require 'datadog/profiling/spec_helper'

require 'datadog/profiling/http_transport'
require 'datadog/profiling'

require 'webrick'
require 'socket'
Expand Down
78 changes: 78 additions & 0 deletions spec/datadog/profiling/stack_recorder_spec.rb
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

0 comments on commit 9078e11

Please sign in to comment.