Skip to content

Commit

Permalink
Merge pull request #14 from Shopify/cross-thread-access
Browse files Browse the repository at this point in the history
Implement LocalTimer.for to access another thread counters
  • Loading branch information
casperisfine authored Jan 19, 2024
2 parents 76409e0 + 0db8bf6 commit 0f63911
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [Unreleased]

- Fixed compatibility with Ruby 3.3.0.
- Added `GVLTools::LocalTimer.for(thread)` to access another thread counter (Ruby 3.3+ only).

## [0.3.0] - 2023-04-11

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ class GVLInstrumentationMiddleware
end
```

Starting from Ruby 3.3, a thread local timer can be accessed from another thread:

```ruby
def fibonacci(n)
if n < 2
n
else
fibonacci(n - 1) + fibonacci(n - 2)
end
end

GVLTools::LocalTimer.enable
thread = Thread.new do
fibonacci(20)
end
thread.join(1)
local_timer = GVLTools::LocalTimer.for(thread)
local_timer.monotonic_time # => 127000
```

### GlobalTimer

`GlobalTimer` records the overall time spent waiting on the GVL by all threads combined.
Expand Down
47 changes: 39 additions & 8 deletions ext/gvltools/instrumentation.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

typedef unsigned long long counter_t;

VALUE rb_cLocalTimer = Qnil;

// Metrics
static rb_internal_thread_event_hook_t *gt_hook = NULL;

Expand All @@ -17,8 +19,8 @@ static unsigned int enabled_mask = 0;

typedef struct {
bool was_ready;
counter_t timer_total;
counter_t waiting_threads_ready_generation;
_Atomic counter_t timer_total;
_Atomic counter_t waiting_threads_ready_generation;
struct timespec timer_ready_at;
} thread_local_state;

Expand All @@ -42,7 +44,7 @@ static const rb_data_type_t thread_local_state_type = {
static inline thread_local_state *GT_LOCAL_STATE(VALUE thread, bool allocate) {
thread_local_state *state = rb_internal_thread_specific_get(thread, thread_storage_key);
if (!state && allocate) {
VALUE wrapper = TypedData_Make_Struct(rb_cObject, thread_local_state, &thread_local_state_type, state);
VALUE wrapper = TypedData_Make_Struct(rb_cLocalTimer, thread_local_state, &thread_local_state_type, state);
rb_thread_local_aset(thread, rb_intern("__gvltools_local_state"), wrapper);
RB_GC_GUARD(wrapper);
rb_internal_thread_specific_set(thread, thread_storage_key, state);
Expand Down Expand Up @@ -124,15 +126,35 @@ static VALUE global_timer_reset(VALUE module) {
return Qtrue;
}

static VALUE local_timer_monotonic_time(VALUE module) {
static VALUE local_timer_m_monotonic_time(VALUE module) {
return ULL2NUM(GT_CURRENT_THREAD_LOCAL_STATE()->timer_total);
}

static VALUE local_timer_reset(VALUE module) {
static VALUE local_timer_m_reset(VALUE module) {
GT_CURRENT_THREAD_LOCAL_STATE()->timer_total = 0;
return Qtrue;
}

#ifdef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET
static VALUE local_timer_for(VALUE module, VALUE thread) {
GT_LOCAL_STATE(thread, true);
return rb_thread_local_aref(thread, rb_intern("__gvltools_local_state"));
}

static VALUE local_timer_monotonic_time(VALUE timer) {
thread_local_state *state;
TypedData_Get_Struct(timer, thread_local_state, &thread_local_state_type, state);
return ULL2NUM(state->timer_total);
}

static VALUE local_timer_reset(VALUE timer) {
thread_local_state *state;
TypedData_Get_Struct(timer, thread_local_state, &thread_local_state_type, state);
state->timer_total = 0;
return Qtrue;
}
#endif

// Thread counts
static _Atomic counter_t waiting_threads_total = 0;
static _Atomic counter_t waiting_threads_current_generation = 1;
Expand All @@ -157,13 +179,15 @@ static void gt_thread_callback(rb_event_flag_t event, const rb_internal_thread_e
}
break;
case RUBY_INTERNAL_THREAD_EVENT_EXITED: {
#ifndef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET
thread_local_state *state = GT_EVENT_LOCAL_STATE(event_data, false);
if (state) {
// MRI can re-use native threads, so we need to reset thread local state,
// otherwise it will leak from one Ruby thread from another.
state->was_ready = false;
state->timer_total = 0;
}
#endif
}
break;
case RUBY_INTERNAL_THREAD_EVENT_READY: {
Expand Down Expand Up @@ -229,9 +253,16 @@ void Init_instrumentation(void) {
rb_define_singleton_method(rb_mGlobalTimer, "reset", global_timer_reset, 0);
rb_define_singleton_method(rb_mGlobalTimer, "monotonic_time", global_timer_monotonic_time, 0);

VALUE rb_mLocalTimer = rb_const_get(rb_mGVLTools, rb_intern("LocalTimer"));
rb_define_singleton_method(rb_mLocalTimer, "reset", local_timer_reset, 0);
rb_define_singleton_method(rb_mLocalTimer, "monotonic_time", local_timer_monotonic_time, 0);
rb_global_variable(&rb_cLocalTimer);
rb_cLocalTimer = rb_const_get(rb_mGVLTools, rb_intern("LocalTimer"));
rb_undef_alloc_func(rb_cLocalTimer);
rb_define_singleton_method(rb_cLocalTimer, "reset", local_timer_m_reset, 0);
rb_define_singleton_method(rb_cLocalTimer, "monotonic_time", local_timer_m_monotonic_time, 0);
#ifdef HAVE_RB_INTERNAL_THREAD_SPECIFIC_GET
rb_define_singleton_method(rb_cLocalTimer, "for", local_timer_for, 1);
rb_define_method(rb_cLocalTimer, "reset", local_timer_reset, 0);
rb_define_method(rb_cLocalTimer, "monotonic_time", local_timer_monotonic_time, 0);
#endif

VALUE rb_mWaitingThreads = rb_const_get(rb_mGVLTools, rb_intern("WaitingThreads"));
rb_define_singleton_method(rb_mWaitingThreads, "_reset", waiting_threads_reset, 0);
Expand Down
2 changes: 1 addition & 1 deletion lib/gvltools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def metric
end
end

module LocalTimer
class LocalTimer
extend AbstractInstrumenter

class << self
Expand Down
21 changes: 21 additions & 0 deletions test/gvl_tools/test_timer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,26 @@ def test_local_timer_init
thread_local_time = spawn_thread { LocalTimer.monotonic_time }.join.value
assert_predicate thread_local_time, :zero?
end

if RUBY_VERSION >= "3.3"
def test_local_timer_for_other_thread
LocalTimer.enable

threads = 5.times.map do
spawn_thread do
cpu_work
LocalTimer.monotonic_time
end
end
cpu_work
timers = threads.each(&:join).to_h { |t| [t, t.value] }
external_timers = threads.to_h { |t| [t, LocalTimer.for(t).monotonic_time] }
assert_equal timers, external_timers
end
else
def test_local_timer_for_other_thread
refute_respond_to LocalTimer, :for
end
end
end
end

0 comments on commit 0f63911

Please sign in to comment.