# HG changeset patch
# User Kamil Trzciński <ayufan@ayufan.eu>
# Date 1611246128 -3600
#      Thu Jan 21 17:22:08 2021 +0100
# Node ID 7d3ddbfdbd80d842dfa57ba5d7d248273a88e972
# Parent  68dd1b76fe20241e8b6b9f6f8fe0b384bcecfcdb
Patch Ruby 2.7 with counting memory allocations

This includes backported patch from
https://github.com/ruby/ruby/pull/3978.

diff --git a/changelogs/unreleased/thread-memory-allocations.yml b/changelogs/unreleased/thread-memory-allocations.yml
new file mode 100644
--- /dev/null
+++ b/changelogs/unreleased/thread-memory-allocations.yml
@@ -0,0 +1,5 @@
+---
+title: Patch Ruby 2.7 with counting memory allocations
+merge_request:
+author:
+type: added
diff --git a/config/patches/ruby/thread-memory-allocations-2.7.patch b/config/patches/ruby/thread-memory-allocations-2.7.patch
new file mode 100644
--- /dev/null
+++ b/config/patches/ruby/thread-memory-allocations-2.7.patch
@@ -0,0 +1,256 @@
+From 97f14ebfd8d24d71e10c450e0a90b6322f9c0d59 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= <ayufan@ayufan.eu>
+Date: Tue, 22 Dec 2020 15:33:08 +0100
+Subject: [PATCH] Expose `Thread#memory_allocations` counters
+
+This provides currently a per-thread GC heap slots
+and malloc allocations statistics.
+
+This is designed to measure a memory allocations
+in a multi-threaded environments (concurrent requests
+processing) with an accurate information about allocated
+memory within a given execution context.
+
+Example: Measure memory pressure generated by a given
+requests to easier find requests with a lot of allocations.
+---
+ gc.c                                          | 20 ++++++
+ .../test_thread_trace_memory_allocations.rb   | 67 +++++++++++++++++++
+ thread.c                                      | 55 +++++++++++++++
+ vm_core.h                                     | 17 +++++
+ 4 files changed, 159 insertions(+)
+ create mode 100644 test/ruby/test_thread_trace_memory_allocations.rb
+
+diff --git a/gc.c b/gc.c
+index 73faf46b128b..f2dcd2935052 100644
+--- a/gc.c
++++ b/gc.c
+@@ -2172,6 +2172,13 @@ newobj_init(VALUE klass, VALUE flags, VALUE v1, VALUE v2, VALUE v3, int wb_prote
+     GC_ASSERT(!SPECIAL_CONST_P(obj)); /* check alignment */
+ #endif
+ 
++#if THREAD_TRACE_MEMORY_ALLOCATIONS
++    rb_thread_t *th = ruby_threadptr_for_trace_memory_allocations();
++    if (th) {
++	ATOMIC_SIZE_INC(th->memory_allocations.total_allocated_objects);
++    }
++#endif
++
+     objspace->total_allocated_objects++;
+ 
+     gc_report(5, objspace, "newobj: %s\n", obj_info(obj));
+@@ -9732,6 +9739,19 @@ objspace_malloc_increase(rb_objspace_t *objspace, void *mem, size_t new_size, si
+ #endif
+     }
+ 
++#if THREAD_TRACE_MEMORY_ALLOCATIONS
++    rb_thread_t *th = ruby_threadptr_for_trace_memory_allocations();
++    if (th) {
++	if (new_size > old_size) {
++	    ATOMIC_SIZE_ADD(th->memory_allocations.total_malloc_bytes, new_size - old_size);
++	}
++
++	if (type == MEMOP_TYPE_MALLOC) {
++	    ATOMIC_SIZE_INC(th->memory_allocations.total_mallocs);
++	}
++    }
++#endif
++
+     if (type == MEMOP_TYPE_MALLOC) {
+       retry:
+ 	if (malloc_increase > malloc_limit && ruby_native_thread_p() && !dont_gc) {
+diff --git a/test/ruby/test_thread_trace_memory_allocations.rb b/test/ruby/test_thread_trace_memory_allocations.rb
+new file mode 100644
+index 000000000000..2e281513578b
+--- /dev/null
++++ b/test/ruby/test_thread_trace_memory_allocations.rb
+@@ -0,0 +1,67 @@
++# frozen_string_literal: true
++
++require 'test/unit'
++
++class TestThreadTraceMemoryAllocations < Test::Unit::TestCase
++  def test_disabled_trace_memory_allocations
++    Thread.trace_memory_allocations = false
++
++    assert_predicate Thread.current.memory_allocations, :nil?
++  end
++
++  def test_enabled_trace_memory_allocations
++    Thread.trace_memory_allocations = true
++
++    assert_not_nil(Thread.current.memory_allocations)
++  end
++
++  def test_only_this_thread_allocations_are_counted
++    changed = {
++      total_allocated_objects: 1000,
++      total_malloc_bytes: 1_000_000,
++      total_mallocs: 100
++    }
++
++    Thread.trace_memory_allocations = true
++
++    assert_less_than(changed) do
++      Thread.new do
++       assert_greater_than(changed) do
++          # This will allocate: 5k objects, 5k mallocs, 5MB
++          allocate(5000, 1000)
++       end
++      end.join
++
++      # This will allocate: 50 objects, 50 mallocs, 500 bytes
++      allocate(50, 10)
++    end
++  end
++
++  private
++
++  def allocate(slots, bytes)
++    Array.new(slots).map do
++      '0' * bytes
++    end
++  end
++
++  def assert_greater_than(keys)
++    before = Thread.current.memory_allocations
++    yield
++    after = Thread.current.memory_allocations
++
++    keys.each do |key, by|
++      assert_operator(by, :<=, after[key]-before[key], "expected the #{key} to change more than #{by}")
++    end
++  end
++
++  def assert_less_than(keys)
++    before = Thread.current.memory_allocations
++    yield
++    after = Thread.current.memory_allocations
++
++    keys.each do |key, by|
++      assert_operator(by, :>, after[key]-before[key], "expected the #{key} to change less than #{by}")
++    end
++  end
++end
+diff --git a/thread.c b/thread.c
+index 708aaa471d99..d68a59e9f2d6 100644
+--- a/thread.c
++++ b/thread.c
+@@ -5143,6 +5143,55 @@ rb_thread_backtrace_locations_m(int argc, VALUE *argv, VALUE thval)
+     return rb_vm_thread_backtrace_locations(argc, argv, thval);
+ }
+ 
++#if THREAD_TRACE_MEMORY_ALLOCATIONS
++rb_thread_t *
++ruby_threadptr_for_trace_memory_allocations(void)
++{
++    // The order of this checks is important due
++    // to how Ruby VM is initialized
++    if (GET_VM()->thread_trace_memory_allocations && GET_EC() != NULL) {
++        return GET_THREAD();
++    }
++
++    return NULL;
++}
++
++static VALUE
++rb_thread_s_trace_memory_allocations(VALUE _)
++{
++    return GET_THREAD()->vm->thread_trace_memory_allocations ? Qtrue : Qfalse;
++}
++
++static VALUE
++rb_thread_s_trace_memory_allocations_set(VALUE self, VALUE val)
++{
++    GET_THREAD()->vm->thread_trace_memory_allocations = RTEST(val);
++    return val;
++}
++
++static VALUE
++rb_thread_memory_allocations(VALUE self)
++{
++    rb_thread_t *th = rb_thread_ptr(self);
++
++    if (!th->vm->thread_trace_memory_allocations) {
++        return Qnil;
++    }
++
++    VALUE ret = rb_hash_new();
++
++	VALUE total_allocated_objects = ID2SYM(rb_intern_const("total_allocated_objects"));
++	VALUE total_malloc_bytes = ID2SYM(rb_intern_const("total_malloc_bytes"));
++	VALUE total_mallocs = ID2SYM(rb_intern_const("total_mallocs"));
++
++	rb_hash_aset(ret, total_allocated_objects, SIZET2NUM(th->memory_allocations.total_allocated_objects));
++	rb_hash_aset(ret, total_malloc_bytes, SIZET2NUM(th->memory_allocations.total_malloc_bytes));
++	rb_hash_aset(ret, total_mallocs, SIZET2NUM(th->memory_allocations.total_mallocs));
++
++    return ret;
++}
++#endif
++
+ /*
+  *  Document-class: ThreadError
+  *
+@@ -5230,6 +5279,12 @@ Init_Thread(void)
+     rb_define_method(rb_cThread, "to_s", rb_thread_to_s, 0);
+     rb_define_alias(rb_cThread, "inspect", "to_s");
+ 
++#if THREAD_TRACE_MEMORY_ALLOCATIONS
++    rb_define_singleton_method(rb_cThread, "trace_memory_allocations", rb_thread_s_trace_memory_allocations, 0);
++    rb_define_singleton_method(rb_cThread, "trace_memory_allocations=", rb_thread_s_trace_memory_allocations_set, 1);
++    rb_define_method(rb_cThread, "memory_allocations", rb_thread_memory_allocations, 0);
++#endif
++
+     rb_vm_register_special_exception(ruby_error_stream_closed, rb_eIOError,
+ 				     "stream closed in another thread");
+ 
+diff --git a/vm_core.h b/vm_core.h
+index 12c3ac377551..63cdf55fa6ed 100644
+--- a/vm_core.h
++++ b/vm_core.h
+@@ -69,6 +69,13 @@
+ # define VM_INSN_INFO_TABLE_IMPL 2
+ #endif
+ 
++/*
++ * track a per thread memory allocations
++ */
++#ifndef THREAD_TRACE_MEMORY_ALLOCATIONS
++# define THREAD_TRACE_MEMORY_ALLOCATIONS 1
++#endif
++
+ #include "ruby/ruby.h"
+ #include "ruby/st.h"
+ 
+@@ -602,6 +609,7 @@ typedef struct rb_vm_struct {
+     unsigned int running: 1;
+     unsigned int thread_abort_on_exception: 1;
+     unsigned int thread_report_on_exception: 1;
++    unsigned int thread_trace_memory_allocations: 1;
+ 
+     unsigned int safe_level_: 1;
+     int sleeper;
+@@ -960,6 +968,14 @@ typedef struct rb_thread_struct {
+ 
+     rb_thread_list_t *join_list;
+ 
++#if THREAD_TRACE_MEMORY_ALLOCATIONS
++    struct {
++        size_t total_allocated_objects;
++        size_t total_malloc_bytes;
++        size_t total_mallocs;
++    } memory_allocations;
++#endif
++
+     union {
+         struct {
+             VALUE proc;
+@@ -1852,6 +1868,7 @@ void rb_threadptr_interrupt(rb_thread_t *th);
+ void rb_threadptr_unlock_all_locking_mutexes(rb_thread_t *th);
+ void rb_threadptr_pending_interrupt_clear(rb_thread_t *th);
+ void rb_threadptr_pending_interrupt_enque(rb_thread_t *th, VALUE v);
++rb_thread_t *ruby_threadptr_for_trace_memory_allocations(void);
+ VALUE rb_ec_get_errinfo(const rb_execution_context_t *ec);
+ void rb_ec_error_print(rb_execution_context_t * volatile ec, volatile VALUE errinfo);
+ void rb_execution_context_update(const rb_execution_context_t *ec);
diff --git a/config/software/ruby.rb b/config/software/ruby.rb
--- a/config/software/ruby.rb
+++ b/config/software/ruby.rb
@@ -134,6 +134,10 @@
     # be fixed.
   end
 
+  # Enable custom patch created by ayufan that allows to count memory allocations
+  # per-thread. This is asked to be upstreamed as part of https://github.com/ruby/ruby/pull/3978
+  patch source: 'thread-memory-allocations-2.7.patch', plevel: 1, env: patch_env
+
   # Fix reserve stack segmentation fault when building on RHEL5 or below
   # Currently only affects 2.1.7 and 2.2.3. This patch taken from the fix
   # in Ruby trunk and expected to be included in future point releases.