Skip to content

Commit

Permalink
rename some C functions, write more comments
Browse files Browse the repository at this point in the history
  • Loading branch information
anmarchenko committed Jul 10, 2024
1 parent ca32796 commit 955843c
Showing 1 changed file with 98 additions and 72 deletions.
170 changes: 98 additions & 72 deletions ext/datadog_cov/datadog_cov.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@

#include <stdbool.h>

// This is a native extension that collects a list of Ruby files that were executed during the test run.
// It is used to optimize the test suite by running only the tests that are affected by the changes.

#define PROFILE_FRAMES_BUFFER_SIZE 1

// threading modes
#define SINGLE_THREADED_COVERAGE_MODE 0
#define MULTI_THREADED_COVERAGE_MODE 1

// functions declarations
static void process_newobj_event(VALUE tracepoint_data, void *data);

// utility functions
static char *ruby_strndup(const char *str, size_t size)
{
char *dup;
Expand Down Expand Up @@ -42,27 +47,22 @@ static VALUE rescue_nil(VALUE (*function_to_call_safely)(VALUE), VALUE function_
);
}

static VALUE get_source_location(VALUE klass_name)
{
return rb_funcall(rb_cObject, rb_intern("const_source_location"), 1, klass_name);
}

static VALUE safely_get_source_location(VALUE klass_name)
{
return rescue_nil(get_source_location, klass_name);
}

// Data structure
struct dd_cov_data
{
// Ruby hash with filenames impacted by the test.
VALUE impacted_files;

// Root is the path to the root folder of the project under test.
// Files located outside of the root are ignored.
char *root;
long root_len;

// Ignored path contains path to the folder where bundled gems are located if
// gems are installed in the project folder.
char *ignored_path;
long ignored_path_len;

VALUE coverage;

// Line tracepoint optimisation: cache last seen filename pointer to avoid
// unnecessary string comparison if we stay in the same file.
uintptr_t last_filename_ptr;
Expand Down Expand Up @@ -92,7 +92,7 @@ struct dd_cov_data
static void dd_cov_mark(void *ptr)
{
struct dd_cov_data *dd_cov_data = ptr;
rb_gc_mark_movable(dd_cov_data->coverage);
rb_gc_mark_movable(dd_cov_data->impacted_files);
rb_gc_mark_movable(dd_cov_data->th_covered);
rb_gc_mark_movable(dd_cov_data->object_allocation_tracepoint);
rb_gc_mark_movable(dd_cov_data->classes_covered_by_allocation);
Expand All @@ -109,7 +109,7 @@ static void dd_cov_free(void *ptr)
static void dd_cov_compact(void *ptr)
{
struct dd_cov_data *dd_cov_data = ptr;
dd_cov_data->coverage = rb_gc_location(dd_cov_data->coverage);
dd_cov_data->impacted_files = rb_gc_location(dd_cov_data->impacted_files);
dd_cov_data->th_covered = rb_gc_location(dd_cov_data->th_covered);
dd_cov_data->object_allocation_tracepoint = rb_gc_location(dd_cov_data->object_allocation_tracepoint);
dd_cov_data->classes_covered_by_allocation = rb_gc_location(dd_cov_data->classes_covered_by_allocation);
Expand All @@ -129,7 +129,7 @@ static VALUE dd_cov_allocate(VALUE klass)
struct dd_cov_data *dd_cov_data;
VALUE dd_cov = TypedData_Make_Struct(klass, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);

dd_cov_data->coverage = rb_hash_new();
dd_cov_data->impacted_files = rb_hash_new();
dd_cov_data->root = NULL;
dd_cov_data->root_len = 0;
dd_cov_data->ignored_path = NULL;
Expand All @@ -144,59 +144,11 @@ static VALUE dd_cov_allocate(VALUE klass)
return dd_cov;
}

// DDCov methods
static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self)
{
VALUE opt;

rb_scan_args(argc, argv, "10", &opt);
VALUE rb_root = rb_hash_lookup(opt, ID2SYM(rb_intern("root")));
if (!RTEST(rb_root))
{
rb_raise(rb_eArgError, "root is required");
}
VALUE rb_ignored_path = rb_hash_lookup(opt, ID2SYM(rb_intern("ignored_path")));

VALUE rb_threading_mode = rb_hash_lookup(opt, ID2SYM(rb_intern("threading_mode")));
int threading_mode;
if (rb_threading_mode == ID2SYM(rb_intern("multi")))
{
threading_mode = MULTI_THREADED_COVERAGE_MODE;
}
else if (rb_threading_mode == ID2SYM(rb_intern("single")))
{
threading_mode = SINGLE_THREADED_COVERAGE_MODE;
}
else
{
rb_raise(rb_eArgError, "threading mode is invalid");
}

VALUE rb_allocation_tracing_enabled = rb_hash_lookup(opt, ID2SYM(rb_intern("use_allocation_tracing")));
if (rb_allocation_tracing_enabled == Qtrue && threading_mode == SINGLE_THREADED_COVERAGE_MODE)
{
rb_raise(rb_eArgError, "allocation tracing is not supported in single threaded mode");
}

struct dd_cov_data *dd_cov_data;
TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);

dd_cov_data->threading_mode = threading_mode;
dd_cov_data->root_len = RSTRING_LEN(rb_root);
dd_cov_data->root = ruby_strndup(RSTRING_PTR(rb_root), dd_cov_data->root_len);

if (RTEST(rb_ignored_path))
{
dd_cov_data->ignored_path_len = RSTRING_LEN(rb_ignored_path);
dd_cov_data->ignored_path = ruby_strndup(RSTRING_PTR(rb_ignored_path), dd_cov_data->ignored_path_len);
}

dd_cov_data->allocation_tracing_enabled = (rb_allocation_tracing_enabled == Qtrue);

return Qnil;
}
// Helper functions (available in C only)

static void dd_store_covered_filename(struct dd_cov_data *dd_cov_data, VALUE filename)
// Checks if the filename is located under the root folder of the project (but not
// in the ignored folder) and adds it to the impacted_files hash.
static void record_impacted_file(struct dd_cov_data *dd_cov_data, VALUE filename)
{
char *filename_ptr = RSTRING_PTR(filename);
// if the current filename is not located under the root, we skip it
Expand All @@ -212,9 +164,10 @@ static void dd_store_covered_filename(struct dd_cov_data *dd_cov_data, VALUE fil
return;
}

rb_hash_aset(dd_cov_data->coverage, filename, Qtrue);
rb_hash_aset(dd_cov_data->impacted_files, filename, Qtrue);
}

// Executed on RUBY_EVENT_LINE event and captures the filename from rb_profile_frames.
static void process_line_event(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass)
{
struct dd_cov_data *dd_cov_data;
Expand Down Expand Up @@ -248,9 +201,23 @@ static void process_line_event(rb_event_flag_t event, VALUE data, VALUE self, ID
return;
}

dd_store_covered_filename(dd_cov_data, filename);
record_impacted_file(dd_cov_data, filename);
}

// Get source location for a given class name
static VALUE get_source_location(VALUE klass_name)
{
return rb_funcall(rb_cObject, rb_intern("const_source_location"), 1, klass_name);
}

// Get source location for a given class name and swallow any exceptions
static VALUE safely_get_source_location(VALUE klass_name)
{
return rescue_nil(get_source_location, klass_name);
}

// Executed on RUBY_INTERNAL_EVENT_NEWOBJ event and captures the source file for the
// allocated object's class.
static void process_newobj_event(VALUE tracepoint_data, void *data)
{
VALUE self = (VALUE)data;
Expand Down Expand Up @@ -310,9 +277,62 @@ static void process_newobj_event(VALUE tracepoint_data, void *data)
return;
}

dd_store_covered_filename(dd_cov_data, filename);
record_impacted_file(dd_cov_data, filename);
}

// DDCov instance methods available in Ruby
static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self)
{
VALUE opt;

rb_scan_args(argc, argv, "10", &opt);
VALUE rb_root = rb_hash_lookup(opt, ID2SYM(rb_intern("root")));
if (!RTEST(rb_root))
{
rb_raise(rb_eArgError, "root is required");
}
VALUE rb_ignored_path = rb_hash_lookup(opt, ID2SYM(rb_intern("ignored_path")));

VALUE rb_threading_mode = rb_hash_lookup(opt, ID2SYM(rb_intern("threading_mode")));
int threading_mode;
if (rb_threading_mode == ID2SYM(rb_intern("multi")))
{
threading_mode = MULTI_THREADED_COVERAGE_MODE;
}
else if (rb_threading_mode == ID2SYM(rb_intern("single")))
{
threading_mode = SINGLE_THREADED_COVERAGE_MODE;
}
else
{
rb_raise(rb_eArgError, "threading mode is invalid");
}

VALUE rb_allocation_tracing_enabled = rb_hash_lookup(opt, ID2SYM(rb_intern("use_allocation_tracing")));
if (rb_allocation_tracing_enabled == Qtrue && threading_mode == SINGLE_THREADED_COVERAGE_MODE)
{
rb_raise(rb_eArgError, "allocation tracing is not supported in single threaded mode");
}

struct dd_cov_data *dd_cov_data;
TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);

dd_cov_data->threading_mode = threading_mode;
dd_cov_data->root_len = RSTRING_LEN(rb_root);
dd_cov_data->root = ruby_strndup(RSTRING_PTR(rb_root), dd_cov_data->root_len);

if (RTEST(rb_ignored_path))
{
dd_cov_data->ignored_path_len = RSTRING_LEN(rb_ignored_path);
dd_cov_data->ignored_path = ruby_strndup(RSTRING_PTR(rb_ignored_path), dd_cov_data->ignored_path_len);
}

dd_cov_data->allocation_tracing_enabled = (rb_allocation_tracing_enabled == Qtrue);

return Qnil;
}

// starts test impact collection, executed before the start of each test
static VALUE dd_cov_start(VALUE self)
{
struct dd_cov_data *dd_cov_data;
Expand All @@ -323,6 +343,7 @@ static VALUE dd_cov_start(VALUE self)
rb_raise(rb_eRuntimeError, "root is required");
}

// add line tracepoint
if (dd_cov_data->threading_mode == SINGLE_THREADED_COVERAGE_MODE)
{
VALUE thval = rb_thread_current();
Expand All @@ -334,6 +355,7 @@ static VALUE dd_cov_start(VALUE self)
rb_add_event_hook(process_line_event, RUBY_EVENT_LINE, self);
}

// add object allocation tracepoint
if (dd_cov_data->allocation_tracing_enabled)
{
dd_cov_data->classes_covered_by_allocation = rb_hash_new();
Expand All @@ -343,11 +365,14 @@ static VALUE dd_cov_start(VALUE self)
return self;
}

// stops test impact collection, executed after the end of each test
// returns the hash with impacted files and resets the internal state
static VALUE dd_cov_stop(VALUE self)
{
struct dd_cov_data *dd_cov_data;
TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data);

// stop line tracepoint
if (dd_cov_data->threading_mode == SINGLE_THREADED_COVERAGE_MODE)
{
VALUE thval = rb_thread_current();
Expand All @@ -364,14 +389,15 @@ static VALUE dd_cov_stop(VALUE self)
rb_remove_event_hook(process_line_event);
}

// stop object allocation tracepoint
if (dd_cov_data->object_allocation_tracepoint != Qnil)
{
rb_tracepoint_disable(dd_cov_data->object_allocation_tracepoint);
}

VALUE res = dd_cov_data->coverage;
VALUE res = dd_cov_data->impacted_files;

dd_cov_data->coverage = rb_hash_new();
dd_cov_data->impacted_files = rb_hash_new();
dd_cov_data->last_filename_ptr = 0;

return res;
Expand Down

0 comments on commit 955843c

Please sign in to comment.