ext/datadog_cov/datadog_cov.c in datadog-ci-1.0.0 vs ext/datadog_cov/datadog_cov.c in datadog-ci-1.0.1

- old
+ new

@@ -1,66 +1,64 @@ #include <ruby.h> #include <ruby/debug.h> -// constants -#define DD_COV_TARGET_FILES 1 -#define DD_COV_TARGET_LINES 2 +#define PROFILE_FRAMES_BUFFER_SIZE 1 -static int is_prefix(VALUE prefix, const char *str) +// threading modes +#define SINGLE_THREADED_COVERAGE_MODE 0 +#define MULTI_THREADED_COVERAGE_MODE 1 + +char *ruby_strndup(const char *str, size_t size) { - if (prefix == Qnil) - { - return 0; - } + char *dup; - const char *c_prefix = RSTRING_PTR(prefix); - if (c_prefix == NULL) - { - return 0; - } + dup = xmalloc(size + 1); + memcpy(dup, str, size); + dup[size] = '\0'; - long prefix_len = RSTRING_LEN(prefix); - if (strncmp(c_prefix, str, prefix_len) == 0) - { - return 1; - } - else - { - return 0; - } + return dup; } // Data structure struct dd_cov_data { - VALUE root; - VALUE ignored_path; - int mode; + char *root; + long root_len; + + char *ignored_path; + long ignored_path_len; + VALUE coverage; + + uintptr_t last_filename_ptr; + + // for single threaded mode: thread that is being covered + VALUE th_covered; + + int threading_mode; }; 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->root); - rb_gc_mark_movable(dd_cov_data->ignored_path); + rb_gc_mark_movable(dd_cov_data->th_covered); } static void dd_cov_free(void *ptr) { struct dd_cov_data *dd_cov_data = ptr; - + xfree(dd_cov_data->root); + xfree(dd_cov_data->ignored_path); xfree(dd_cov_data); } 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->root = rb_gc_location(dd_cov_data->root); - dd_cov_data->ignored_path = rb_gc_location(dd_cov_data->ignored_path); + dd_cov_data->th_covered = rb_gc_location(dd_cov_data->th_covered); } const rb_data_type_t dd_cov_data_type = { .wrap_struct_name = "dd_cov", .function = { @@ -72,135 +70,167 @@ static VALUE dd_cov_allocate(VALUE klass) { struct dd_cov_data *dd_cov_data; VALUE obj = 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->root = Qnil; - dd_cov_data->ignored_path = Qnil; - dd_cov_data->mode = DD_COV_TARGET_FILES; + dd_cov_data->root = NULL; + dd_cov_data->root_len = 0; + dd_cov_data->ignored_path = NULL; + dd_cov_data->ignored_path_len = 0; + dd_cov_data->last_filename_ptr = 0; + dd_cov_data->threading_mode = MULTI_THREADED_COVERAGE_MODE; + return obj; } // DDCov methods static VALUE dd_cov_initialize(int argc, VALUE *argv, VALUE self) { VALUE opt; - int mode; 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_mode = rb_hash_lookup(opt, ID2SYM(rb_intern("mode"))); - if (!RTEST(rb_mode) || rb_mode == ID2SYM(rb_intern("files"))) + VALUE rb_threading_mode = rb_hash_lookup(opt, ID2SYM(rb_intern("threading_mode"))); + int threading_mode; + if (rb_threading_mode == ID2SYM(rb_intern("multi"))) { - mode = DD_COV_TARGET_FILES; + threading_mode = MULTI_THREADED_COVERAGE_MODE; } - else if (rb_mode == ID2SYM(rb_intern("lines"))) + else if (rb_threading_mode == ID2SYM(rb_intern("single"))) { - mode = DD_COV_TARGET_LINES; + threading_mode = SINGLE_THREADED_COVERAGE_MODE; } else { - rb_raise(rb_eArgError, "mode is invalid"); + rb_raise(rb_eArgError, "threading mode is invalid"); } 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->root = rb_root; - dd_cov_data->ignored_path = rb_ignored_path; - dd_cov_data->mode = mode; + 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); + } + return Qnil; } -static void dd_cov_update_line_coverage(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass) +static void dd_cov_update_coverage(rb_event_flag_t event, VALUE data, VALUE self, ID id, VALUE klass) { struct dd_cov_data *dd_cov_data; TypedData_Get_Struct(data, struct dd_cov_data, &dd_cov_data_type, dd_cov_data); - const char *filename = rb_sourcefile(); - if (filename == NULL) + const char *c_filename = rb_sourcefile(); + + // skip if we cover the same file again + uintptr_t current_filename_ptr = (uintptr_t)c_filename; + if (dd_cov_data->last_filename_ptr == current_filename_ptr) { return; } + dd_cov_data->last_filename_ptr = current_filename_ptr; - // if given filename is not located under the root, we skip it - if (is_prefix(dd_cov_data->root, filename) == 0) + VALUE top_frame; + int captured_frames = rb_profile_frames( + 0 /* stack starting depth */, + PROFILE_FRAMES_BUFFER_SIZE, + &top_frame, + NULL); + + if (captured_frames != PROFILE_FRAMES_BUFFER_SIZE) { return; } - // if ignored_path is provided and given filename is located under the ignored_path, we skip it too - // this is useful for ignoring bundled gems location - if (RTEST(dd_cov_data->ignored_path) && is_prefix(dd_cov_data->ignored_path, filename) == 1) + VALUE filename = rb_profile_frame_path(top_frame); + if (filename == Qnil) { return; } - VALUE rb_str_source_file = rb_str_new2(filename); - - if (dd_cov_data->mode == DD_COV_TARGET_FILES) + char *filename_ptr = RSTRING_PTR(filename); + // if the current filename is not located under the root, we skip it + if (strncmp(dd_cov_data->root, filename_ptr, dd_cov_data->root_len) != 0) { - rb_hash_aset(dd_cov_data->coverage, rb_str_source_file, Qtrue); return; } - // this isn't optimized yet, this is a POC to show that lines coverage is possible - // ITR beta is going to use files coverage, we'll get back to this part when - // we need to implement lines coverage - if (dd_cov_data->mode == DD_COV_TARGET_LINES) + // if ignored_path is provided and the current filename is located under the ignored_path, we skip it too + // this is useful for ignoring bundled gems location + if (dd_cov_data->ignored_path_len != 0 && strncmp(dd_cov_data->ignored_path, filename_ptr, dd_cov_data->ignored_path_len) == 0) { - int line_number = rb_sourceline(); - if (line_number <= 0) - { - return; - } - - VALUE rb_lines = rb_hash_aref(dd_cov_data->coverage, rb_str_source_file); - if (rb_lines == Qnil) - { - rb_lines = rb_hash_new(); - rb_hash_aset(dd_cov_data->coverage, rb_str_source_file, rb_lines); - } - - rb_hash_aset(rb_lines, INT2FIX(line_number), Qtrue); + return; } + + rb_hash_aset(dd_cov_data->coverage, filename, Qtrue); } static VALUE dd_cov_start(VALUE self) { - // get current thread - VALUE thval = rb_thread_current(); - // add event hook - rb_thread_add_event_hook(thval, dd_cov_update_line_coverage, RUBY_EVENT_LINE, self); + struct dd_cov_data *dd_cov_data; + TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data); + if (dd_cov_data->root_len == 0) + { + rb_raise(rb_eRuntimeError, "root is required"); + } + + if (dd_cov_data->threading_mode == SINGLE_THREADED_COVERAGE_MODE) + { + VALUE thval = rb_thread_current(); + rb_thread_add_event_hook(thval, dd_cov_update_coverage, RUBY_EVENT_LINE, self); + dd_cov_data->th_covered = thval; + } + else + { + rb_add_event_hook(dd_cov_update_coverage, RUBY_EVENT_LINE, self); + } + return self; } static VALUE dd_cov_stop(VALUE self) { - // get current thread - VALUE thval = rb_thread_current(); - // remove event hook for the current thread - rb_thread_remove_event_hook(thval, dd_cov_update_line_coverage); - struct dd_cov_data *dd_cov_data; TypedData_Get_Struct(self, struct dd_cov_data, &dd_cov_data_type, dd_cov_data); - VALUE cov = dd_cov_data->coverage; + if (dd_cov_data->threading_mode == SINGLE_THREADED_COVERAGE_MODE) + { + VALUE thval = rb_thread_current(); + if (!rb_equal(thval, dd_cov_data->th_covered)) + { + rb_raise(rb_eRuntimeError, "Coverage was not started by this thread"); + } + rb_thread_remove_event_hook(dd_cov_data->th_covered, dd_cov_update_coverage); + dd_cov_data->th_covered = Qnil; + } + else + { + rb_remove_event_hook(dd_cov_update_coverage); + } + + VALUE res = dd_cov_data->coverage; + dd_cov_data->coverage = rb_hash_new(); + dd_cov_data->last_filename_ptr = 0; - return cov; + return res; } void Init_datadog_cov(void) { VALUE mDatadog = rb_define_module("Datadog");