From d0e1136de6d27da57b3989a6ff8bb1cd12b271d2 Mon Sep 17 00:00:00 2001 From: asksurya Date: Sun, 16 Apr 2023 16:05:20 -0500 Subject: [PATCH] Fixing issues in Thread/Fiber merge (#321) * Correcting the merge thread code to correctly pull the merged methods data from the other thread/fiber * Removing call tree merge test as its no longer valid since it requires method table to merge the call trees * mplementing review comments, correcting the code logic to merge thread correctly. * Fixing tests --------- Co-authored-by: Ashwin --- ext/ruby_prof/rp_call_tree.c | 102 ++-- ext/ruby_prof/rp_call_tree.h | 8 +- ext/ruby_prof/rp_method.c | 1070 ++++++++++++++++++---------------- ext/ruby_prof/rp_method.h | 1 + ext/ruby_prof/rp_thread.c | 7 +- test/call_tree_test.rb | 103 ---- test/merge_test.rb | 59 +- 7 files changed, 634 insertions(+), 716 deletions(-) diff --git a/ext/ruby_prof/rp_call_tree.c b/ext/ruby_prof/rp_call_tree.c index bb4e4eac..f567d440 100644 --- a/ext/ruby_prof/rp_call_tree.c +++ b/ext/ruby_prof/rp_call_tree.c @@ -2,9 +2,10 @@ Please see the LICENSE file for copyright and distribution information */ #include "rp_call_tree.h" +#include "rp_call_trees.h" +#include "rp_thread.h" VALUE cRpCallTree; - /* ======= prof_call_tree_t ========*/ prof_call_tree_t* prof_call_tree_create(prof_method_t* method, prof_call_tree_t* parent, VALUE source_file, int source_line) { @@ -335,53 +336,76 @@ static VALUE prof_call_tree_line(VALUE self) static int prof_call_tree_merge_children(st_data_t key, st_data_t value, st_data_t data) { - prof_call_tree_t* other_child = (prof_call_tree_t*)value; - prof_call_tree_t* self = (prof_call_tree_t*)data; + prof_call_tree_t* other_child = (prof_call_tree_t*)value; + prof_meth_table_self_thread_t* self_thr_tbl = (prof_meth_table_self_thread_t*)data; + prof_call_tree_t* self = self_thr_tbl->call_tree; + st_table* merge_method_table=self_thr_tbl->self_thread_method_table; + + prof_method_t* self_method_ptr = method_table_lookup(merge_method_table, other_child->method->key); + if (self_method_ptr == NULL) + { + return ST_CONTINUE; + } - st_data_t self_child; - if (rb_st_lookup(self->children, other_child->method->key, &self_child)) - { - prof_call_tree_merge_internal((prof_call_tree_t*)self_child, other_child); - } - else - { - prof_call_tree_t* copy = prof_call_tree_copy(other_child); - prof_call_tree_add_child(self, copy); - } - return ST_CONTINUE; + other_child->method = self_method_ptr; + st_data_t self_child; + if (rb_st_lookup(self->children, other_child->method->key, &self_child)) + { + prof_measurement_merge_internal(self->measurement, other_child->measurement); + self_thr_tbl->call_tree = (prof_call_tree_t*)self_child; + } + else + { + prof_call_tree_t* copy = prof_call_tree_copy(other_child); + prof_call_tree_add_parent(copy, self); + prof_add_call_tree(self_method_ptr->call_trees, copy); + self_thr_tbl->call_tree = copy; + + } + rb_st_foreach(other_child->children, prof_call_tree_merge_children, (st_data_t)self_thr_tbl); + return ST_CONTINUE; } - -void prof_call_tree_merge_internal(prof_call_tree_t* self, prof_call_tree_t* other) +prof_meth_table_self_thread_t* prof_meth_table_self_thread_create(prof_call_tree_t* self,st_table* self_thread_table) { - // Make sure the methods are the same - if (self->method->key != other->method->key) - return; - - // Make sure the parents are the same. - // 1. They can both be set and be equal - // 2. They can both be unset (null) - if (self->parent && other->parent) - { - if (self->parent->method->key != other->parent->method->key) - return; - } - else if (self->parent || other->parent) - { - return; - } + prof_meth_table_self_thread_t* result = ALLOC(prof_meth_table_self_thread_t); + result->call_tree = self; + result->self_thread_method_table = self_thread_table; + return result; +} +void prof_call_tree_merge_internal(prof_call_tree_t* self, prof_call_tree_t* other,st_table* self_thread_table) +{ + // Make sure the methods are the same + if (self->method->key != other->method->key) + return; - prof_measurement_merge_internal(self->measurement, other->measurement); - prof_measurement_merge_internal(self->method->measurement, other->method->measurement); + // Make sure the parents are the same. + // 1. They can both be set and be equal + // 2. They can both be unset (null) + if (self->parent && other->parent) + { + if (self->parent->method->key != other->parent->method->key) + return; + } + else if (self->parent || other->parent) + { + return; + } - rb_st_foreach(other->children, prof_call_tree_merge_children, (st_data_t)self); + prof_measurement_merge_internal(self->measurement, other->measurement); + prof_meth_table_self_thread_t* thread_table_struct = prof_meth_table_self_thread_create(self,self_thread_table); + rb_st_foreach(other->children, prof_call_tree_merge_children, (st_data_t)thread_table_struct); + xfree(thread_table_struct); } -VALUE prof_call_tree_merge(VALUE self, VALUE other) + + +VALUE prof_call_tree_merge(VALUE self, VALUE other,VALUE self_thr) { prof_call_tree_t* source = prof_get_call_tree(self); prof_call_tree_t* destination = prof_get_call_tree(other); - prof_call_tree_merge_internal(source, destination); - return other; + thread_data_t* thread_ptr =prof_get_thread(self_thr); + prof_call_tree_merge_internal(source, destination, thread_ptr->method_table); + return self; } /* :nodoc: */ @@ -457,7 +481,7 @@ void rp_init_call_tree() rb_define_method(cRpCallTree, "source_file", prof_call_tree_source_file, 0); rb_define_method(cRpCallTree, "line", prof_call_tree_line, 0); - rb_define_method(cRpCallTree, "merge!", prof_call_tree_merge, 1); + rb_define_method(cRpCallTree, "merge!", prof_call_tree_merge, 2); rb_define_method(cRpCallTree, "_dump_data", prof_call_tree_dump, 0); rb_define_method(cRpCallTree, "_load_data", prof_call_tree_load, 1); diff --git a/ext/ruby_prof/rp_call_tree.h b/ext/ruby_prof/rp_call_tree.h index 9c85ee72..245669b8 100644 --- a/ext/ruby_prof/rp_call_tree.h +++ b/ext/ruby_prof/rp_call_tree.h @@ -26,9 +26,15 @@ typedef struct prof_call_tree_t VALUE source_file; } prof_call_tree_t; +typedef struct prof_meth_table_self_thread_t +{ + st_table* self_thread_method_table; + prof_call_tree_t* call_tree; +}prof_meth_table_self_thread_t; + prof_call_tree_t* prof_call_tree_create(prof_method_t* method, prof_call_tree_t* parent, VALUE source_file, int source_line); prof_call_tree_t* prof_call_tree_copy(prof_call_tree_t* other); -void prof_call_tree_merge_internal(prof_call_tree_t* destination, prof_call_tree_t* other); +void prof_call_tree_merge_internal(prof_call_tree_t* destination, prof_call_tree_t* other,st_table* self_thread_table); void prof_call_tree_mark(void* data); prof_call_tree_t* call_tree_table_lookup(st_table* table, st_data_t key); diff --git a/ext/ruby_prof/rp_method.c b/ext/ruby_prof/rp_method.c index ebfee61e..23dfd9bb 100644 --- a/ext/ruby_prof/rp_method.c +++ b/ext/ruby_prof/rp_method.c @@ -1,518 +1,552 @@ -/* Copyright (C) 2005-2019 Shugo Maeda and Charlie Savage - Please see the LICENSE file for copyright and distribution information */ - -#include "rp_allocation.h" -#include "rp_call_trees.h" -#include "rp_method.h" - -VALUE cRpMethodInfo; - -/* ================ Helper Functions =================*/ -VALUE resolve_klass(VALUE klass, unsigned int* klass_flags) -{ - VALUE result = klass; - - if (klass == 0 || klass == Qnil) - { - result = Qnil; - } - else if (BUILTIN_TYPE(klass) == T_CLASS && FL_TEST(klass, FL_SINGLETON)) - { - /* We have come across a singleton object. First - figure out what it is attached to.*/ - VALUE attached = rb_iv_get(klass, "__attached__"); - - switch (BUILTIN_TYPE(attached)) - { - /* Is this a singleton class acting as a metaclass? */ - case T_CLASS: - { - *klass_flags |= kClassSingleton; - result = attached; - break; - } - /* Is this for singleton methods on a module? */ - case T_MODULE: - { - *klass_flags |= kModuleSingleton; - result = attached; - break; - } - /* Is this for singleton methods on an object? */ - case T_OBJECT: - { - *klass_flags |= kObjectSingleton; - result = rb_class_superclass(klass); - break; - } - /* Ok, this could be other things like an array put onto - a singleton object (yeah, it happens, see the singleton - objects test case). */ - default: - { - *klass_flags |= kOtherSingleton; - result = klass; - break; - } - } - } - /* Is this an include for a module? If so get the actual - module class since we want to combine all profiling - results for that module. */ - else if (BUILTIN_TYPE(klass) == T_ICLASS) - { - unsigned int dummy; - *klass_flags |= kModuleIncludee; - result = resolve_klass(RBASIC_CLASS(klass), &dummy); - } - return result; -} - -VALUE resolve_klass_name(VALUE klass, unsigned int* klass_flags) -{ - VALUE result = Qnil; - - if (klass == Qnil) - { - result = rb_str_new2("[global]"); - } - else if (*klass_flags & kOtherSingleton) - { - result = rb_any_to_s(klass); - } - else - { - result = rb_class_name(klass); - } - - return result; -} - -st_data_t method_key(VALUE klass, VALUE msym) -{ - VALUE resolved_klass = klass; - - /* Is this an include for a module? If so get the actual - module class since we want to combine all profiling - results for that module. */ - if (klass == 0 || klass == Qnil) - { - resolved_klass = Qnil; - } - else if (BUILTIN_TYPE(klass) == T_ICLASS) - { - resolved_klass = RBASIC_CLASS(klass); - } - - st_data_t hash = rb_hash_start(0); - hash = rb_hash_uint(hash, resolved_klass); - hash = rb_hash_uint(hash, msym); - hash = rb_hash_end(hash); - - return hash; -} - -/* ================ prof_method_t =================*/ -prof_method_t* prof_get_method(VALUE self) -{ - /* Can't use Data_Get_Struct because that triggers the event hook - ending up in endless recursion. */ - prof_method_t* result = RTYPEDDATA_DATA(self); - - if (!result) - rb_raise(rb_eRuntimeError, "This RubyProf::MethodInfo instance has already been freed, likely because its profile has been freed."); - - return result; -} - -prof_method_t* prof_method_create(VALUE profile, VALUE klass, VALUE msym, VALUE source_file, int source_line) -{ - prof_method_t* result = ALLOC(prof_method_t); - result->profile = profile; - - result->key = method_key(klass, msym); - result->klass_flags = 0; - - /* Note we do not call resolve_klass_name now because that causes an object allocation that shows up - in the allocation results so we want to avoid it until after the profile run is complete. */ - result->klass = resolve_klass(klass, &result->klass_flags); - result->klass_name = Qnil; - result->method_name = msym; - result->measurement = prof_measurement_create(); - - result->call_trees = prof_call_trees_create(); - result->allocations_table = prof_allocations_create(); - - result->visits = 0; - result->recursive = false; - - result->object = Qnil; - - result->source_file = source_file; - result->source_line = source_line; - - return result; -} - -/* The underlying c structures are freed when the parent profile is freed. - However, on shutdown the Ruby GC frees objects in any will-nilly order. - That means the ruby thread object wrapping the c thread struct may - be freed before the parent profile. Thus we add in a free function - for the garbage collector so that if it does get called will nil - out our Ruby object reference.*/ -static void prof_method_ruby_gc_free(void* data) -{ - if (data) - { - prof_method_t* method = (prof_method_t*)data; - method->object = Qnil; - } -} - -static void prof_method_free(prof_method_t* method) -{ - /* Has this method object been accessed by Ruby? If - yes clean it up so to avoid a segmentation fault. */ - if (method->object != Qnil) - { - RTYPEDDATA(method->object)->data = NULL; - method->object = Qnil; - } - - prof_allocations_free(method->allocations_table); - prof_call_trees_free(method->call_trees); - prof_measurement_free(method->measurement); - xfree(method); -} - -size_t prof_method_size(const void* data) -{ - return sizeof(prof_method_t); -} - -void prof_method_mark(void* data) -{ - if (!data) return; - - prof_method_t* method = (prof_method_t*)data; - - if (method->profile != Qnil) - rb_gc_mark_movable(method->profile); - - if (method->object != Qnil) - rb_gc_mark_movable(method->object); - - rb_gc_mark_movable(method->klass_name); - rb_gc_mark_movable(method->method_name); - rb_gc_mark(method->source_file); - - if (method->klass != Qnil) - rb_gc_mark(method->klass); - - prof_measurement_mark(method->measurement); - prof_allocations_mark(method->allocations_table); -} - -void prof_method_compact(void* data) -{ - prof_method_t* method = (prof_method_t*)data; - method->object = rb_gc_location(method->object); - method->profile = rb_gc_location(method->profile); - method->klass_name = rb_gc_location(method->klass_name); - method->method_name = rb_gc_location(method->method_name); -} - -static VALUE prof_method_allocate(VALUE klass) -{ - prof_method_t* method_data = prof_method_create(Qnil, Qnil, Qnil, Qnil, 0); - method_data->object = prof_method_wrap(method_data); - return method_data->object; -} - -static const rb_data_type_t method_info_type = -{ - .wrap_struct_name = "MethodInfo", - .function = - { - .dmark = prof_method_mark, - .dfree = prof_method_ruby_gc_free, - .dsize = prof_method_size, - .dcompact = prof_method_compact - }, - .data = NULL, - .flags = RUBY_TYPED_FREE_IMMEDIATELY -}; - -VALUE prof_method_wrap(prof_method_t* method) -{ - if (method->object == Qnil) - { - method->object = TypedData_Wrap_Struct(cRpMethodInfo, &method_info_type, method); - } - return method->object; -} - -st_table* method_table_create() -{ - return rb_st_init_numtable(); -} - -static int method_table_free_iterator(st_data_t key, st_data_t value, st_data_t dummy) -{ - prof_method_free((prof_method_t*)value); - return ST_CONTINUE; -} - -void method_table_free(st_table* table) -{ - rb_st_foreach(table, method_table_free_iterator, 0); - rb_st_free_table(table); -} - -size_t method_table_insert(st_table* table, st_data_t key, prof_method_t* val) -{ - return rb_st_insert(table, (st_data_t)key, (st_data_t)val); -} - -prof_method_t* method_table_lookup(st_table* table, st_data_t key) -{ - st_data_t val; - if (rb_st_lookup(table, (st_data_t)key, &val)) - { - return (prof_method_t*)val; - } - else - { - return NULL; - } -} - -/* ================ Method Info =================*/ -/* Document-class: RubyProf::MethodInfo -The RubyProf::MethodInfo class stores profiling data for a method. -One instance of the RubyProf::MethodInfo class is created per method -called per thread. Thus, if a method is called in two different -thread then there will be two RubyProf::MethodInfo objects -created. RubyProf::MethodInfo objects can be accessed via -the RubyProf::Profile object. -*/ - -/* call-seq: - new(klass, method_name) -> method_info - -Creates a new MethodInfo instance. +Klass+ should be a reference to -a Ruby class and +method_name+ a symbol identifying one of its instance methods.*/ -static VALUE prof_method_initialize(VALUE self, VALUE klass, VALUE method_name) -{ - prof_method_t* method_ptr = prof_get_method(self); - method_ptr->klass = klass; - method_ptr->method_name = method_name; - - // Setup method key - method_ptr->key = method_key(klass, method_name); - - // Get method object - VALUE ruby_method = rb_funcall(klass, rb_intern("instance_method"), 1, method_name); - - // Get source file and line number - VALUE location_array = rb_funcall(ruby_method, rb_intern("source_location"), 0); - if (location_array != Qnil && RARRAY_LEN(location_array) == 2) - { - method_ptr->source_file = rb_ary_entry(location_array, 0); - method_ptr->source_line = NUM2INT(rb_ary_entry(location_array, 1)); - } - - return self; -} - -/* call-seq: - hash -> hash - -Returns the hash key for this method info. The hash key is calculated based on the -klass name and method name */ -static VALUE prof_method_hash(VALUE self) -{ - prof_method_t* method_ptr = prof_get_method(self); - return ULL2NUM(method_ptr->key); -} - -/* call-seq: - allocations -> array - -Returns an array of allocation information.*/ -static VALUE prof_method_allocations(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - return prof_allocations_wrap(method->allocations_table); -} - -/* call-seq: - called -> Measurement - -Returns the measurement associated with this method. */ -static VALUE prof_method_measurement(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - return prof_measurement_wrap(method->measurement); -} - -/* call-seq: - source_file => string - -Returns the source file of the method -*/ -static VALUE prof_method_source_file(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - return method->source_file; -} - -/* call-seq: - line_no -> int - - returns the line number of the method */ -static VALUE prof_method_line(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - return INT2FIX(method->source_line); -} - -/* call-seq: - klass_name -> String - -Returns the name of this method's class. Singleton classes -will have the form . */ - -static VALUE prof_method_klass_name(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - if (method->klass_name == Qnil) - method->klass_name = resolve_klass_name(method->klass, &method->klass_flags); - - return method->klass_name; -} - -/* call-seq: - klass_flags -> integer - -Returns the klass flags */ - -static VALUE prof_method_klass_flags(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - return INT2FIX(method->klass_flags); -} - -/* call-seq: - method_name -> Symbol - -Returns the name of this method in the format Object#method. Singletons -methods will be returned in the format #method.*/ - -static VALUE prof_method_name(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - return method->method_name; -} - -/* call-seq: - recursive? -> boolean - - Returns the true if this method is recursively invoked */ -static VALUE prof_method_recursive(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - return method->recursive ? Qtrue : Qfalse; -} - -/* call-seq: - call_trees -> CallTrees - -Returns the CallTrees associated with this method. */ -static VALUE prof_method_call_trees(VALUE self) -{ - prof_method_t* method = prof_get_method(self); - return prof_call_trees_wrap(method->call_trees); -} - -/* :nodoc: */ -static VALUE prof_method_dump(VALUE self) -{ - prof_method_t* method_data = prof_get_method(self); - VALUE result = rb_hash_new(); - - rb_hash_aset(result, ID2SYM(rb_intern("klass_name")), prof_method_klass_name(self)); - rb_hash_aset(result, ID2SYM(rb_intern("klass_flags")), INT2FIX(method_data->klass_flags)); - rb_hash_aset(result, ID2SYM(rb_intern("method_name")), method_data->method_name); - - rb_hash_aset(result, ID2SYM(rb_intern("key")), ULL2NUM(method_data->key)); - rb_hash_aset(result, ID2SYM(rb_intern("recursive")), prof_method_recursive(self)); - rb_hash_aset(result, ID2SYM(rb_intern("source_file")), method_data->source_file); - rb_hash_aset(result, ID2SYM(rb_intern("source_line")), INT2FIX(method_data->source_line)); - - rb_hash_aset(result, ID2SYM(rb_intern("call_trees")), prof_call_trees_wrap(method_data->call_trees)); - rb_hash_aset(result, ID2SYM(rb_intern("measurement")), prof_measurement_wrap(method_data->measurement)); - rb_hash_aset(result, ID2SYM(rb_intern("allocations")), prof_method_allocations(self)); - - return result; -} - -/* :nodoc: */ -static VALUE prof_method_load(VALUE self, VALUE data) -{ - prof_method_t* method_data = prof_get_method(self); - method_data->object = self; - - method_data->klass_name = rb_hash_aref(data, ID2SYM(rb_intern("klass_name"))); - method_data->klass_flags = FIX2INT(rb_hash_aref(data, ID2SYM(rb_intern("klass_flags")))); - method_data->method_name = rb_hash_aref(data, ID2SYM(rb_intern("method_name"))); - method_data->key = RB_NUM2ULL(rb_hash_aref(data, ID2SYM(rb_intern("key")))); - - method_data->recursive = rb_hash_aref(data, ID2SYM(rb_intern("recursive"))) == Qtrue ? true : false; - - method_data->source_file = rb_hash_aref(data, ID2SYM(rb_intern("source_file"))); - method_data->source_line = FIX2INT(rb_hash_aref(data, ID2SYM(rb_intern("source_line")))); - - VALUE call_trees = rb_hash_aref(data, ID2SYM(rb_intern("call_trees"))); - method_data->call_trees = prof_get_call_trees(call_trees); - - VALUE measurement = rb_hash_aref(data, ID2SYM(rb_intern("measurement"))); - method_data->measurement = prof_get_measurement(measurement); - - VALUE allocations = rb_hash_aref(data, ID2SYM(rb_intern("allocations"))); - prof_allocations_unwrap(method_data->allocations_table, allocations); - return data; -} - -void rp_init_method_info() -{ - /* MethodInfo */ - cRpMethodInfo = rb_define_class_under(mProf, "MethodInfo", rb_cObject); - - rb_define_const(cRpMethodInfo, "MODULE_INCLUDEE", INT2NUM(kModuleIncludee)); - rb_define_const(cRpMethodInfo, "CLASS_SINGLETON", INT2NUM(kClassSingleton)); - rb_define_const(cRpMethodInfo, "MODULE_SINGLETON", INT2NUM(kModuleSingleton)); - rb_define_const(cRpMethodInfo, "OBJECT_SINGLETON", INT2NUM(kObjectSingleton)); - rb_define_const(cRpMethodInfo, "OTHER_SINGLETON", INT2NUM(kOtherSingleton)); - - rb_define_alloc_func(cRpMethodInfo, prof_method_allocate); - rb_define_method(cRpMethodInfo, "initialize", prof_method_initialize, 2); - rb_define_method(cRpMethodInfo, "hash", prof_method_hash, 0); - - rb_define_method(cRpMethodInfo, "klass_name", prof_method_klass_name, 0); - rb_define_method(cRpMethodInfo, "klass_flags", prof_method_klass_flags, 0); - rb_define_method(cRpMethodInfo, "method_name", prof_method_name, 0); - - rb_define_method(cRpMethodInfo, "call_trees", prof_method_call_trees, 0); - - rb_define_method(cRpMethodInfo, "allocations", prof_method_allocations, 0); - rb_define_method(cRpMethodInfo, "measurement", prof_method_measurement, 0); - - rb_define_method(cRpMethodInfo, "source_file", prof_method_source_file, 0); - rb_define_method(cRpMethodInfo, "line", prof_method_line, 0); - - rb_define_method(cRpMethodInfo, "recursive?", prof_method_recursive, 0); - - rb_define_method(cRpMethodInfo, "_dump_data", prof_method_dump, 0); - rb_define_method(cRpMethodInfo, "_load_data", prof_method_load, 1); -} +/* Copyright (C) 2005-2019 Shugo Maeda and Charlie Savage + Please see the LICENSE file for copyright and distribution information */ + +#include "rp_allocation.h" +#include "rp_call_trees.h" +#include "rp_method.h" + +VALUE cRpMethodInfo; + +/* ================ Helper Functions =================*/ +VALUE resolve_klass(VALUE klass, unsigned int* klass_flags) +{ + VALUE result = klass; + + if (klass == 0 || klass == Qnil) + { + result = Qnil; + } + else if (BUILTIN_TYPE(klass) == T_CLASS && FL_TEST(klass, FL_SINGLETON)) + { + /* We have come across a singleton object. First + figure out what it is attached to.*/ + VALUE attached = rb_iv_get(klass, "__attached__"); + + switch (BUILTIN_TYPE(attached)) + { + /* Is this a singleton class acting as a metaclass? */ + case T_CLASS: + { + *klass_flags |= kClassSingleton; + result = attached; + break; + } + /* Is this for singleton methods on a module? */ + case T_MODULE: + { + *klass_flags |= kModuleSingleton; + result = attached; + break; + } + /* Is this for singleton methods on an object? */ + case T_OBJECT: + { + *klass_flags |= kObjectSingleton; + result = rb_class_superclass(klass); + break; + } + /* Ok, this could be other things like an array put onto + a singleton object (yeah, it happens, see the singleton + objects test case). */ + default: + { + *klass_flags |= kOtherSingleton; + result = klass; + break; + } + } + } + /* Is this an include for a module? If so get the actual + module class since we want to combine all profiling + results for that module. */ + else if (BUILTIN_TYPE(klass) == T_ICLASS) + { + unsigned int dummy; + *klass_flags |= kModuleIncludee; + result = resolve_klass(RBASIC_CLASS(klass), &dummy); + } + return result; +} + +VALUE resolve_klass_name(VALUE klass, unsigned int* klass_flags) +{ + VALUE result = Qnil; + + if (klass == Qnil) + { + result = rb_str_new2("[global]"); + } + else if (*klass_flags & kOtherSingleton) + { + result = rb_any_to_s(klass); + } + else + { + result = rb_class_name(klass); + } + + return result; +} + +st_data_t method_key(VALUE klass, VALUE msym) +{ + VALUE resolved_klass = klass; + + /* Is this an include for a module? If so get the actual + module class since we want to combine all profiling + results for that module. */ + if (klass == 0 || klass == Qnil) + { + resolved_klass = Qnil; + } + else if (BUILTIN_TYPE(klass) == T_ICLASS) + { + resolved_klass = RBASIC_CLASS(klass); + } + + st_data_t hash = rb_hash_start(0); + hash = rb_hash_uint(hash, resolved_klass); + hash = rb_hash_uint(hash, msym); + hash = rb_hash_end(hash); + + return hash; +} + +/* ================ prof_method_t =================*/ +prof_method_t* prof_get_method(VALUE self) +{ + /* Can't use Data_Get_Struct because that triggers the event hook + ending up in endless recursion. */ + prof_method_t* result = RTYPEDDATA_DATA(self); + + if (!result) + rb_raise(rb_eRuntimeError, "This RubyProf::MethodInfo instance has already been freed, likely because its profile has been freed."); + + return result; +} + +prof_method_t* prof_method_create(VALUE profile, VALUE klass, VALUE msym, VALUE source_file, int source_line) +{ + prof_method_t* result = ALLOC(prof_method_t); + result->profile = profile; + + result->key = method_key(klass, msym); + result->klass_flags = 0; + + /* Note we do not call resolve_klass_name now because that causes an object allocation that shows up + in the allocation results so we want to avoid it until after the profile run is complete. */ + result->klass = resolve_klass(klass, &result->klass_flags); + result->klass_name = Qnil; + result->method_name = msym; + result->measurement = prof_measurement_create(); + + result->call_trees = prof_call_trees_create(); + result->allocations_table = prof_allocations_create(); + + result->visits = 0; + result->recursive = false; + + result->object = Qnil; + + result->source_file = source_file; + result->source_line = source_line; + + return result; +} + +prof_method_t* prof_method_copy(prof_method_t* other) +{ + prof_method_t* result = prof_method_create(other->profile, other->klass, other->method_name, other->source_file, other->source_line); + result->measurement = prof_measurement_copy(other->measurement); + + return result; +} +/* The underlying c structures are freed when the parent profile is freed. + However, on shutdown the Ruby GC frees objects in any will-nilly order. + That means the ruby thread object wrapping the c thread struct may + be freed before the parent profile. Thus we add in a free function + for the garbage collector so that if it does get called will nil + out our Ruby object reference.*/ +static void prof_method_ruby_gc_free(void* data) +{ + if (data) + { + prof_method_t* method = (prof_method_t*)data; + method->object = Qnil; + } +} + +static void prof_method_free(prof_method_t* method) +{ + /* Has this method object been accessed by Ruby? If + yes clean it up so to avoid a segmentation fault. */ + if (method->object != Qnil) + { + RTYPEDDATA(method->object)->data = NULL; + method->object = Qnil; + } + + prof_allocations_free(method->allocations_table); + prof_call_trees_free(method->call_trees); + prof_measurement_free(method->measurement); + xfree(method); +} + +size_t prof_method_size(const void* data) +{ + return sizeof(prof_method_t); +} + +void prof_method_mark(void* data) +{ + if (!data) return; + + prof_method_t* method = (prof_method_t*)data; + + if (method->profile != Qnil) + rb_gc_mark_movable(method->profile); + + if (method->object != Qnil) + rb_gc_mark_movable(method->object); + + rb_gc_mark_movable(method->klass_name); + rb_gc_mark_movable(method->method_name); + rb_gc_mark(method->source_file); + + if (method->klass != Qnil) + rb_gc_mark(method->klass); + + prof_measurement_mark(method->measurement); + prof_allocations_mark(method->allocations_table); +} + +void prof_method_compact(void* data) +{ + prof_method_t* method = (prof_method_t*)data; + method->object = rb_gc_location(method->object); + method->profile = rb_gc_location(method->profile); + method->klass_name = rb_gc_location(method->klass_name); + method->method_name = rb_gc_location(method->method_name); +} + +static VALUE prof_method_allocate(VALUE klass) +{ + prof_method_t* method_data = prof_method_create(Qnil, Qnil, Qnil, Qnil, 0); + method_data->object = prof_method_wrap(method_data); + return method_data->object; +} + +static const rb_data_type_t method_info_type = +{ + .wrap_struct_name = "MethodInfo", + .function = + { + .dmark = prof_method_mark, + .dfree = prof_method_ruby_gc_free, + .dsize = prof_method_size, + .dcompact = prof_method_compact + }, + .data = NULL, + .flags = RUBY_TYPED_FREE_IMMEDIATELY +}; + +VALUE prof_method_wrap(prof_method_t* method) +{ + if (method->object == Qnil) + { + method->object = TypedData_Wrap_Struct(cRpMethodInfo, &method_info_type, method); + } + return method->object; +} + +st_table* method_table_create() +{ + return rb_st_init_numtable(); +} + +static int method_table_free_iterator(st_data_t key, st_data_t value, st_data_t dummy) +{ + prof_method_free((prof_method_t*)value); + return ST_CONTINUE; +} + +void method_table_free(st_table* table) +{ + rb_st_foreach(table, method_table_free_iterator, 0); + rb_st_free_table(table); +} + +size_t method_table_insert(st_table* table, st_data_t key, prof_method_t* val) +{ + return rb_st_insert(table, (st_data_t)key, (st_data_t)val); +} + +static int prof_method_table_merge_internal(st_data_t key, st_data_t value, st_data_t data) +{ + st_table* self_table = (st_table*)data; + prof_method_t* other_child = (prof_method_t*)value; + st_data_t self_child; + if (rb_st_lookup(self_table, other_child->key, &self_child)) + { + prof_measurement_merge_internal(((prof_method_t*)self_child)->measurement, other_child->measurement); + //re-insert the self child to store the updated values or a new child from the other thread + method_table_insert(self_table,((prof_method_t*)self_child)->key,(prof_method_t*)self_child); + } + else + { + prof_method_t* copy = prof_method_copy(other_child); + copy->key = other_child->key; + method_table_insert(self_table,copy->key,copy); + } + + return ST_CONTINUE; + +} + +void prof_method_table_merge(st_table* self, st_table* other) +{ + rb_st_foreach(other,prof_method_table_merge_internal,(st_data_t)self); +} + +prof_method_t* method_table_lookup(st_table* table, st_data_t key) +{ + st_data_t val; + if (rb_st_lookup(table, (st_data_t)key, &val)) + { + return (prof_method_t*)val; + } + else + { + return NULL; + } +} + +/* ================ Method Info =================*/ +/* Document-class: RubyProf::MethodInfo +The RubyProf::MethodInfo class stores profiling data for a method. +One instance of the RubyProf::MethodInfo class is created per method +called per thread. Thus, if a method is called in two different +thread then there will be two RubyProf::MethodInfo objects +created. RubyProf::MethodInfo objects can be accessed via +the RubyProf::Profile object. +*/ + +/* call-seq: + new(klass, method_name) -> method_info + +Creates a new MethodInfo instance. +Klass+ should be a reference to +a Ruby class and +method_name+ a symbol identifying one of its instance methods.*/ +static VALUE prof_method_initialize(VALUE self, VALUE klass, VALUE method_name) +{ + prof_method_t* method_ptr = prof_get_method(self); + method_ptr->klass = klass; + method_ptr->method_name = method_name; + + // Setup method key + method_ptr->key = method_key(klass, method_name); + + // Get method object + VALUE ruby_method = rb_funcall(klass, rb_intern("instance_method"), 1, method_name); + + // Get source file and line number + VALUE location_array = rb_funcall(ruby_method, rb_intern("source_location"), 0); + if (location_array != Qnil && RARRAY_LEN(location_array) == 2) + { + method_ptr->source_file = rb_ary_entry(location_array, 0); + method_ptr->source_line = NUM2INT(rb_ary_entry(location_array, 1)); + } + + return self; +} + +/* call-seq: + hash -> hash + +Returns the hash key for this method info. The hash key is calculated based on the +klass name and method name */ +static VALUE prof_method_hash(VALUE self) +{ + prof_method_t* method_ptr = prof_get_method(self); + return ULL2NUM(method_ptr->key); +} + +/* call-seq: + allocations -> array + +Returns an array of allocation information.*/ +static VALUE prof_method_allocations(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + return prof_allocations_wrap(method->allocations_table); +} + +/* call-seq: + called -> Measurement + +Returns the measurement associated with this method. */ +static VALUE prof_method_measurement(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + return prof_measurement_wrap(method->measurement); +} + +/* call-seq: + source_file => string + +Returns the source file of the method +*/ +static VALUE prof_method_source_file(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + return method->source_file; +} + +/* call-seq: + line_no -> int + + returns the line number of the method */ +static VALUE prof_method_line(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + return INT2FIX(method->source_line); +} + +/* call-seq: + klass_name -> String + +Returns the name of this method's class. Singleton classes +will have the form . */ + +static VALUE prof_method_klass_name(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + if (method->klass_name == Qnil) + method->klass_name = resolve_klass_name(method->klass, &method->klass_flags); + + return method->klass_name; +} + +/* call-seq: + klass_flags -> integer + +Returns the klass flags */ + +static VALUE prof_method_klass_flags(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + return INT2FIX(method->klass_flags); +} + +/* call-seq: + method_name -> Symbol + +Returns the name of this method in the format Object#method. Singletons +methods will be returned in the format #method.*/ + +static VALUE prof_method_name(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + return method->method_name; +} + +/* call-seq: + recursive? -> boolean + + Returns the true if this method is recursively invoked */ +static VALUE prof_method_recursive(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + return method->recursive ? Qtrue : Qfalse; +} + +/* call-seq: + call_trees -> CallTrees + +Returns the CallTrees associated with this method. */ +static VALUE prof_method_call_trees(VALUE self) +{ + prof_method_t* method = prof_get_method(self); + return prof_call_trees_wrap(method->call_trees); +} + +/* :nodoc: */ +static VALUE prof_method_dump(VALUE self) +{ + prof_method_t* method_data = prof_get_method(self); + VALUE result = rb_hash_new(); + + rb_hash_aset(result, ID2SYM(rb_intern("klass_name")), prof_method_klass_name(self)); + rb_hash_aset(result, ID2SYM(rb_intern("klass_flags")), INT2FIX(method_data->klass_flags)); + rb_hash_aset(result, ID2SYM(rb_intern("method_name")), method_data->method_name); + + rb_hash_aset(result, ID2SYM(rb_intern("key")), ULL2NUM(method_data->key)); + rb_hash_aset(result, ID2SYM(rb_intern("recursive")), prof_method_recursive(self)); + rb_hash_aset(result, ID2SYM(rb_intern("source_file")), method_data->source_file); + rb_hash_aset(result, ID2SYM(rb_intern("source_line")), INT2FIX(method_data->source_line)); + + rb_hash_aset(result, ID2SYM(rb_intern("call_trees")), prof_call_trees_wrap(method_data->call_trees)); + rb_hash_aset(result, ID2SYM(rb_intern("measurement")), prof_measurement_wrap(method_data->measurement)); + rb_hash_aset(result, ID2SYM(rb_intern("allocations")), prof_method_allocations(self)); + + return result; +} + +/* :nodoc: */ +static VALUE prof_method_load(VALUE self, VALUE data) +{ + prof_method_t* method_data = prof_get_method(self); + method_data->object = self; + + method_data->klass_name = rb_hash_aref(data, ID2SYM(rb_intern("klass_name"))); + method_data->klass_flags = FIX2INT(rb_hash_aref(data, ID2SYM(rb_intern("klass_flags")))); + method_data->method_name = rb_hash_aref(data, ID2SYM(rb_intern("method_name"))); + method_data->key = RB_NUM2ULL(rb_hash_aref(data, ID2SYM(rb_intern("key")))); + + method_data->recursive = rb_hash_aref(data, ID2SYM(rb_intern("recursive"))) == Qtrue ? true : false; + + method_data->source_file = rb_hash_aref(data, ID2SYM(rb_intern("source_file"))); + method_data->source_line = FIX2INT(rb_hash_aref(data, ID2SYM(rb_intern("source_line")))); + + VALUE call_trees = rb_hash_aref(data, ID2SYM(rb_intern("call_trees"))); + method_data->call_trees = prof_get_call_trees(call_trees); + + VALUE measurement = rb_hash_aref(data, ID2SYM(rb_intern("measurement"))); + method_data->measurement = prof_get_measurement(measurement); + + VALUE allocations = rb_hash_aref(data, ID2SYM(rb_intern("allocations"))); + prof_allocations_unwrap(method_data->allocations_table, allocations); + return data; +} + +void rp_init_method_info() +{ + /* MethodInfo */ + cRpMethodInfo = rb_define_class_under(mProf, "MethodInfo", rb_cObject); + + rb_define_const(cRpMethodInfo, "MODULE_INCLUDEE", INT2NUM(kModuleIncludee)); + rb_define_const(cRpMethodInfo, "CLASS_SINGLETON", INT2NUM(kClassSingleton)); + rb_define_const(cRpMethodInfo, "MODULE_SINGLETON", INT2NUM(kModuleSingleton)); + rb_define_const(cRpMethodInfo, "OBJECT_SINGLETON", INT2NUM(kObjectSingleton)); + rb_define_const(cRpMethodInfo, "OTHER_SINGLETON", INT2NUM(kOtherSingleton)); + + rb_define_alloc_func(cRpMethodInfo, prof_method_allocate); + rb_define_method(cRpMethodInfo, "initialize", prof_method_initialize, 2); + rb_define_method(cRpMethodInfo, "hash", prof_method_hash, 0); + + rb_define_method(cRpMethodInfo, "klass_name", prof_method_klass_name, 0); + rb_define_method(cRpMethodInfo, "klass_flags", prof_method_klass_flags, 0); + rb_define_method(cRpMethodInfo, "method_name", prof_method_name, 0); + + rb_define_method(cRpMethodInfo, "call_trees", prof_method_call_trees, 0); + + rb_define_method(cRpMethodInfo, "allocations", prof_method_allocations, 0); + rb_define_method(cRpMethodInfo, "measurement", prof_method_measurement, 0); + + rb_define_method(cRpMethodInfo, "source_file", prof_method_source_file, 0); + rb_define_method(cRpMethodInfo, "line", prof_method_line, 0); + + rb_define_method(cRpMethodInfo, "recursive?", prof_method_recursive, 0); + + rb_define_method(cRpMethodInfo, "_dump_data", prof_method_dump, 0); + rb_define_method(cRpMethodInfo, "_load_data", prof_method_load, 1); +} diff --git a/ext/ruby_prof/rp_method.h b/ext/ruby_prof/rp_method.h index 771b0ede..e1b21d7f 100644 --- a/ext/ruby_prof/rp_method.h +++ b/ext/ruby_prof/rp_method.h @@ -50,6 +50,7 @@ st_table* method_table_create(void); prof_method_t* method_table_lookup(st_table* table, st_data_t key); size_t method_table_insert(st_table* table, st_data_t key, prof_method_t* val); void method_table_free(st_table* table); +void prof_method_table_merge(st_table* self, st_table* other); prof_method_t* prof_method_create(VALUE profile, VALUE klass, VALUE msym, VALUE source_file, int source_line); prof_method_t* prof_get_method(VALUE self); diff --git a/ext/ruby_prof/rp_thread.c b/ext/ruby_prof/rp_thread.c index 00a434e4..51517dde 100644 --- a/ext/ruby_prof/rp_thread.c +++ b/ext/ruby_prof/rp_thread.c @@ -20,7 +20,6 @@ You cannot create an instance of RubyProf::Thread, instead you access it from a #include "rp_profile.h" VALUE cRpThread; - // ====== thread_data_t ====== thread_data_t* thread_data_create(void) { @@ -362,8 +361,10 @@ static VALUE prof_thread_merge(VALUE self, VALUE other) { thread_data_t* self_ptr = prof_get_thread(self); thread_data_t* other_ptr = prof_get_thread(other); - prof_call_tree_merge_internal(self_ptr->call_tree, other_ptr->call_tree); - + prof_method_table_merge(self_ptr->method_table,other_ptr->method_table); + prof_call_tree_merge_internal(self_ptr->call_tree, other_ptr->call_tree,self_ptr->method_table); + // Setting methods to nil so that the new data is rendered + self_ptr->methods= Qnil; return other; } diff --git a/test/call_tree_test.rb b/test/call_tree_test.rb index cb9381d3..666f21ab 100644 --- a/test/call_tree_test.rb +++ b/test/call_tree_test.rb @@ -91,107 +91,4 @@ def test_add_child_gc GC.stress = false end end - - def test_merge - call_tree_1 = create_call_tree_1 - call_tree_2 = create_call_tree_2 - call_tree_1.merge!(call_tree_2) - - # Root - call_tree = call_tree_1 - assert_equal(:root, call_tree.target.method_name) - assert_in_delta(11.6, call_tree.total_time, 0.00001) - assert_in_delta(0, call_tree.self_time, 0.00001) - assert_in_delta(0.0, call_tree.wait_time, 0.00001) - assert_in_delta(11.6, call_tree.children_time, 0.00001) - - assert_in_delta(11.6, call_tree.target.total_time, 0.00001) - assert_in_delta(0, call_tree.target.self_time, 0.00001) - assert_in_delta(0, call_tree.target.wait_time, 0.00001) - assert_in_delta(11.6, call_tree.target.children_time, 0.00001) - - # a - call_tree = call_tree_1.children[0] - assert_equal(:a, call_tree.target.method_name) - - assert_in_delta(4.1, call_tree.total_time, 0.00001) - assert_in_delta(0, call_tree.self_time, 0.00001) - assert_in_delta(0.0, call_tree.wait_time, 0.00001) - assert_in_delta(4.1, call_tree.children_time, 0.00001) - - assert_in_delta(4.1, call_tree.target.total_time, 0.00001) - assert_in_delta(0, call_tree.target.self_time, 0.00001) - assert_in_delta(0.0, call_tree.target.wait_time, 0.00001) - assert_in_delta(4.1, call_tree.target.children_time, 0.00001) - - # aa - call_tree = call_tree_1.children[0].children[0] - assert_equal(:aa, call_tree.target.method_name) - - assert_in_delta(1.5, call_tree.total_time, 0.00001) - assert_in_delta(1.5, call_tree.self_time, 0.00001) - assert_in_delta(0.0, call_tree.wait_time, 0.00001) - assert_in_delta(0.0, call_tree.children_time, 0.00001) - - assert_in_delta(1.5, call_tree.target.total_time, 0.00001) - assert_in_delta(1.5, call_tree.target.self_time, 0.00001) - assert_in_delta(0.0, call_tree.target.wait_time, 0.00001) - assert_in_delta(0.0, call_tree.target.children_time, 0.00001) - - # ab - call_tree = call_tree_1.children[0].children[1] - assert_equal(:ab, call_tree.target.method_name) - - assert_in_delta(2.6, call_tree.total_time, 0.00001) - assert_in_delta(2.6, call_tree.self_time, 0.00001) - assert_in_delta(0.0, call_tree.wait_time, 0.00001) - assert_in_delta(0.0, call_tree.children_time, 0.00001) - - assert_in_delta(2.6, call_tree.target.total_time, 0.00001) - assert_in_delta(2.6, call_tree.target.self_time, 0.00001) - assert_in_delta(0.0, call_tree.target.wait_time, 0.00001) - assert_in_delta(0.0, call_tree.target.children_time, 0.00001) - - # b - call_tree = call_tree_1.children[1] - assert_equal(:b, call_tree.target.method_name) - - assert_in_delta(7.5, call_tree.total_time, 0.00001) - assert_in_delta(0, call_tree.self_time, 0.00001) - assert_in_delta(0.0, call_tree.wait_time, 0.00001) - assert_in_delta(7.5, call_tree.children_time, 0.00001) - - assert_in_delta(7.5, call_tree.target.total_time, 0.00001) - assert_in_delta(0, call_tree.target.self_time, 0.00001) - assert_in_delta(0.0, call_tree.target.wait_time, 0.00001) - assert_in_delta(7.5, call_tree.target.children_time, 0.00001) - - # bb - call_tree = call_tree_1.children[1].children[0] - assert_equal(:bb, call_tree.target.method_name) - - assert_in_delta(6.6, call_tree.total_time, 0.00001) - assert_in_delta(6.6, call_tree.self_time, 0.00001) - assert_in_delta(0.0, call_tree.wait_time, 0.00001) - assert_in_delta(0.0, call_tree.children_time, 0.00001) - - assert_in_delta(6.6, call_tree.target.total_time, 0.00001) - assert_in_delta(6.6, call_tree.target.self_time, 0.00001) - assert_in_delta(0.0, call_tree.target.wait_time, 0.00001) - assert_in_delta(0.0, call_tree.target.children_time, 0.00001) - - # ba - call_tree = call_tree_1.children[1].children[1] - assert_equal(:ba, call_tree.target.method_name) - - assert_in_delta(0.9, call_tree.total_time, 0.00001) - assert_in_delta(0.7, call_tree.self_time, 0.00001) - assert_in_delta(0.2, call_tree.wait_time, 0.00001) - assert_in_delta(0.0, call_tree.children_time, 0.00001) - - assert_in_delta(0.9, call_tree.target.total_time, 0.00001) - assert_in_delta(0.7, call_tree.target.self_time, 0.00001) - assert_in_delta(0.2, call_tree.target.wait_time, 0.00001) - assert_in_delta(0.0, call_tree.target.children_time, 0.00001) - end end diff --git a/test/merge_test.rb b/test/merge_test.rb index 992a3c4b..0259964c 100644 --- a/test/merge_test.rb +++ b/test/merge_test.rb @@ -8,7 +8,7 @@ require_relative './scheduler' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0') - +puts "Hello" # -- Tests ---- class MergeTest < TestCase def worker1 @@ -34,18 +34,6 @@ def concurrency_mergable scheduler = Scheduler.new Fiber.set_scheduler(scheduler) - 3.times do - Fiber.schedule do - worker1 - end - end - Fiber.scheduler.close - end - - def concurrency_unmergable - scheduler = Scheduler.new - Fiber.set_scheduler(scheduler) - 3.times do |i| Fiber.schedule do method = "worker#{i + 1}".to_sym @@ -55,52 +43,19 @@ def concurrency_unmergable Fiber.scheduler.close end - def test_times_no_merge + def test_times_merge result = RubyProf::Profile.profile(measure_mode: RubyProf::WALL_TIME) { concurrency_mergable } - - assert_equal(4, result.threads.size) - + result.merge! + assert_equal(2, result.threads.size) result.threads.each do |thread| - assert_in_delta(0.5, thread.call_tree.target.total_time, 0.2) + assert_in_delta(0.5, thread.call_tree.target.total_time, 0.5) assert_in_delta(0.0, thread.call_tree.target.self_time) assert_in_delta(0.0, thread.call_tree.target.wait_time) - assert_in_delta(0.5, thread.call_tree.target.children_time, 0.2) + assert_in_delta(0.5, thread.call_tree.target.children_time, 0.5) end + assert_equal(3,result.threads[1].call_tree.target.call_trees.callees.size) end - def test_times_mergable - result = RubyProf::Profile.profile(measure_mode: RubyProf::WALL_TIME) { concurrency_mergable } - result.merge! - - printer = RubyProf::GraphHtmlPrinter.new(result) - File.open("graph.html", "wb") do |file| - printer.print(file) - end - - assert_equal(2, result.threads.size) - - thread = result.threads[0] - assert_in_delta(0.5, thread.call_tree.target.total_time, 0.2) - assert_in_delta(0.0, thread.call_tree.target.self_time) - assert_in_delta(0.0, thread.call_tree.target.wait_time) - assert_in_delta(0.5, thread.call_tree.target.children_time, 0.2) - thread = result.threads[1] - assert_in_delta(1.5, thread.call_tree.target.total_time, 0.2) - assert_in_delta(0.0, thread.call_tree.target.self_time) - assert_in_delta(0.0, thread.call_tree.target.wait_time) - assert_in_delta(1.5, thread.call_tree.target.children_time, 0.2) - end - - def test_unmergable - result = RubyProf::Profile.profile(measure_mode: RubyProf::WALL_TIME) { concurrency_unmergable } - - result.merge! - - printer = RubyProf::GraphPrinter.new(result) - File.open("graph_merged.txt", "wb") do |file| - printer.print(file, :min_percent => 0) - end - end end end