ruby/ruby-3.2.0-define-unsupported-gc-compaction-methods-as-rb_f_notimplement.patch
Jarek Prokop 7993206359 Undefine GC compaction and related methods for ppc64le.
The following falcon source:
<https://github.com/socketry/falcon/blob/v0.42.3/lib/falcon/command/serve.rb#L153-L155>
contains the following snippet:
~~~
if GC.respond_to?(:compact)
	GC.compact
end
~~~

The `if` guard for this feature is recommended from documentation since Ruby >= 3.2:
<https://docs.ruby-lang.org/en/3.2/GC.html#method-c-compact>:
```
 To test whether GC compaction is supported, use the idiom:
 ~~~
 GC.respond_to?(:compact)
 ~~~
```

Gems make use of this idiom in various situations. However on Ruby 3.0
without this patch the `GC.respond_to?` method will return true for
`GC.compact` and compaction related methods even though an attempt to call
them will raise "NotImplementedError" exception.

We observe this behavior due to the methods being implemented and the
NotImplementedError is not returned because methods are not implemented,
but because it is a runtime behavior from inside the called methods.

The applied patchset undefines the methods in C code to ensure even the
`GC.respond_to?` will return false and accomodate libraries that call
`GC.compact` like falcon does:
<https://github.com/socketry/falcon/blob/v0.42.3/lib/falcon/command/serve.rb#L153-L155>

The methods are undefined on build time on affected platforms that do
not support compaction. Currently this is only affecting ppc64le.

The important portion of the patch undefines the `GC.compact` and other
methods, so we can expect the following on ppc64le architecture:
Before:
~~~
$ ruby -e 'p GC.respond_to? :compact'
true
~~~

After:
~~~
$ ruby -e 'p GC.respond_to? :compact'
false
~~~

As long as Ruby gems guard the call to `GC.compact` correctly, there
should no longer be a situation where GC reports compaction methods
as implemented even though it will throw an exception when trying to use the methods.

Most importantly, this is only an issue with ppc64le. The other
architectures such as x86_64, aarch64, s390x should continue to be able
to compact. This is guarded on compile-time.

However the patches touch the source for pre-generated files.
Mainly meaning the file `gc.rb` that is the source for the
`gc.rbinc` and `miniprelude.c` need to be re-generated.
The files use Ruby to generate the C counterparts. To prevent cyclic dependency,
they are re-generated and patches are created against the old version.
The approach to regenerate the files for 3.0 is the same as for 3.1
with the exception of also having to first patch with the file
`ruby-3.1.0-Use-mmap-for-allocating-heap-pages-in-the-GC.patch`,
since the subsequent patches are made on top of that patch.

The mmap patch changes parts of gc.c, that the required patch
also changes, so it had to be backported on top of that patch.

The sequence of actions is as follows:
~~~
tar -Jxvf ./ruby-3.0.7.tar.xz
git clone https://github.com/ruby/ruby.git
cd ruby && git checkout v3_0_7
patch -p1 < ../ruby-3.1.0-Use-mmap-for-allocating-heap-pages-in-the-GC.patch
patch -p1 < ../ruby-3.2.0-define-unsupported-gc-compaction-methods-as-rb_f_notimplement.patch
./autogen.sh && ./configure
make gc.rbinc miniprelude.c
cd ..
diff -u {ruby-3.0.7,ruby}/gc.rbinc > ruby-3.2.0-define-unsupported-gc-compaction-methods_generated-files.patch
diff -u {ruby-3.0.7,ruby}/miniprelude.c >> ruby-3.2.0-define-unsupported-gc-compaction-methods_generated-files.patch
~~~
Firstly we unpact the upstream tar archive, which creates `ruby-3.0.7`
directory.
Then we clone the upstream git repository and check out the Ruby version
tag that is source for the tar archive which creates `ruby` repository.

The `ruby-3.0.7` directory contains the pre-generated files we will use as
the original for the patch.

In the `ruby` repository we apply the patches mentioned in the code
snippet.

Then, generate configure script with autogen.sh, and we create the
gc.rbinc and miniprelude.c files that are generated.

Then create the patch for the generated files. `diff` is used as the
generated files are not tracked via git in upstream, and the diff is
executed against the expanded upstream tar archive that does not ship a
git repository with it. While it would be possible to `git add` the
files before patching them, this ensures that the files are applicable
without a problem on top of the archive.

This commit is partially backported from Ruby 3.1's Fedora counterparts:
b7b5473796
ca94aff023
649a6e3083
instead of re-doing the entire patchset from 3.2 branch back to 3.0.

There is less disruption in GC related code when comparing 3.0 to 3.1
than there is 3.1 to 3.2. Therefore the upstream sources for the patches
were first applied in the Ruby upstream git repository on top of tag v3_1_2.
Namely, the following is the patch source:
https://github.com/ruby/ruby/pull/5934
0de1495f35
0c36ba5319
available in Fedora as part of:
649a6e3083

The version of Ruby that is the version when the mentioned Fedora commits
were introduced. And then cherry picked on top of patch
"ruby-3.1.0-Use-mmap-for-allocating-heap-pages-in-the-GC.patch"
applied in v3_0_7 tag that is present here in downstream as the
difference brought by the patch is significant enough to disrupt
patch application. Git allows easier resolution of the patches here.

In short,
1. Take the upstream compaction patches, cherry-pick them on top of v3_1_2 so that
the changes are present in the git tree, as they have not been backported
to 3.1 upstream.
2. Apply the "ruby-3.1.0-Use-mmap-for-allocating-heap-pages-in-the-GC.patch" on top of v3_0_7.
3. Cherry pick the 3.1 patches to v3_0_7 and resolve differences.

There might have been a few sections where git marked changes from both
patches as a conflict to resolve.
I preferred the approach where, despite the checks introduced
from the 3.1 branch patches should be enough to guard against bad
behavior around compaction, the "mmap" patch's checks are retained for safety.
One of the reasons are that the checks are also present outside the
compact methods in other parts of `gc.c` code.

References for the specific Fedora patches:
b7b5473796
649a6e3083
<b7b5473796>
<649a6e3083>
Uptream bug: <https://bugs.ruby-lang.org/issues/18779>
Upstream PR: <https://github.com/ruby/ruby/pull/5934>

ca94aff023
<ca94aff023>
Related upstream issue: <https://bugs.ruby-lang.org/issues/18829>
<https://github.com/ruby/ruby/pull/6019>
<2c19086323>

Resolves: RHEL-83136
2025-03-27 15:03:49 +01:00

494 lines
16 KiB
Diff

From 2b48aa5d9d7bfcf80ea23c352ceb10f0cf4e4ee0 Mon Sep 17 00:00:00 2001
From: Mike Dalessio <mike.dalessio@gmail.com>
Date: Mon, 23 May 2022 15:40:22 -0400
Subject: [PATCH 1/2] Move compaction-related methods into gc.c
These methods are removed from gc.rb and added to gc.c:
- GC.compact
- GC.auto_compact
- GC.auto_compact=
- GC.latest_compact_info
- GC.verify_compaction_references
This is a prefactor to allow setting these methods to
`rb_f_notimplement` in a followup commit.
---
gc.c | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++------
gc.rb | 68 ---------------------------------------
2 files changed, 91 insertions(+), 78 deletions(-)
diff --git a/gc.c b/gc.c
index b821310934..ef0e66ee46 100644
--- a/gc.c
+++ b/gc.c
@@ -9410,8 +9410,20 @@ gc_update_references(rb_objspace_t * objspace, rb_heap_t *heap)
static VALUE type_sym(size_t type);
+/*
+ * call-seq:
+ * GC.latest_compact_info -> {:considered=>{:T_CLASS=>11}, :moved=>{:T_CLASS=>11}}
+ *
+ * Returns information about object moved in the most recent GC compaction.
+ *
+ * The returned hash has two keys :considered and :moved. The hash for
+ * :considered lists the number of objects that were considered for movement
+ * by the compactor, and the :moved hash lists the number of objects that
+ * were actually moved. Some objects can't be moved (maybe they were pinned)
+ * so these numbers can be used to calculate compaction efficiency.
+ */
static VALUE
-gc_compact_stats(rb_execution_context_t *ec, VALUE self)
+gc_compact_stats(VALUE self)
{
size_t i;
rb_objspace_t *objspace = &rb_objspace;
@@ -9484,22 +9496,70 @@ heap_check_moved_i(void *vstart, void *vend, size_t stride, void *data)
return 0;
}
+/*
+ * call-seq:
+ * GC.compact
+ *
+ * This function compacts objects together in Ruby's heap. It eliminates
+ * unused space (or fragmentation) in the heap by moving objects in to that
+ * unused space. This function returns a hash which contains statistics about
+ * which objects were moved. See `GC.latest_gc_info` for details about
+ * compaction statistics.
+ *
+ * This method is implementation specific and not expected to be implemented
+ * in any implementation besides MRI.
+ */
static VALUE
-gc_compact(rb_execution_context_t *ec, VALUE self)
+gc_compact(VALUE self)
{
/* Run GC with compaction enabled */
- gc_start_internal(ec, self, Qtrue, Qtrue, Qtrue, Qtrue);
+ gc_start_internal(NULL, self, Qtrue, Qtrue, Qtrue, Qtrue);
- return gc_compact_stats(ec, self);
+ return gc_compact_stats(self);
}
+/*
+ * call-seq:
+ * GC.verify_compaction_references(toward: nil, double_heap: false) -> hash
+ *
+ * Verify compaction reference consistency.
+ *
+ * This method is implementation specific. During compaction, objects that
+ * were moved are replaced with T_MOVED objects. No object should have a
+ * reference to a T_MOVED object after compaction.
+ *
+ * This function doubles the heap to ensure room to move all objects,
+ * compacts the heap to make sure everything moves, updates all references,
+ * then performs a full GC. If any object contains a reference to a T_MOVED
+ * object, that object should be pushed on the mark stack, and will
+ * make a SEGV.
+ */
static VALUE
-gc_verify_compaction_references(rb_execution_context_t *ec, VALUE self, VALUE double_heap, VALUE toward_empty)
+gc_verify_compaction_references(int argc, VALUE *argv, VALUE self)
{
rb_objspace_t *objspace = &rb_objspace;
+ VALUE kwargs, double_heap = Qfalse, toward_empty = Qfalse;
+ static ID id_toward, id_double_heap, id_empty;
+
+ if (!id_toward) {
+ id_toward = rb_intern("toward");
+ id_double_heap = rb_intern("double_heap");
+ id_empty = rb_intern("empty");
+ }
+
+ rb_scan_args(argc, argv, ":", &kwargs);
+ if (!NIL_P(kwargs)) {
+ if (rb_hash_has_key(kwargs, ID2SYM(id_toward))) {
+ VALUE toward = rb_hash_aref(kwargs, ID2SYM(id_toward));
+ toward_empty = (toward == ID2SYM(id_empty)) ? Qtrue : Qfalse;
+ }
+ if (rb_hash_has_key(kwargs, ID2SYM(id_double_heap))) {
+ double_heap = rb_hash_aref(kwargs, ID2SYM(id_double_heap));
+ }
+ }
/* Clear the heap. */
- gc_start_internal(ec, self, Qtrue, Qtrue, Qtrue, Qfalse);
+ gc_start_internal(NULL, self, Qtrue, Qtrue, Qtrue, Qfalse);
RB_VM_LOCK_ENTER();
{
@@ -9515,12 +9575,12 @@ gc_verify_compaction_references(rb_execution_context_t *ec, VALUE self, VALUE do
}
RB_VM_LOCK_LEAVE();
- gc_start_internal(ec, self, Qtrue, Qtrue, Qtrue, Qtrue);
+ gc_start_internal(NULL, self, Qtrue, Qtrue, Qtrue, Qtrue);
objspace_reachable_objects_from_root(objspace, root_obj_check_moved_i, NULL);
objspace_each_objects(objspace, heap_check_moved_i, NULL);
- return gc_compact_stats(ec, self);
+ return gc_compact_stats(self);
}
VALUE
@@ -9978,8 +10038,18 @@ gc_disable(rb_execution_context_t *ec, VALUE _)
return rb_gc_disable();
}
+/*
+ * call-seq:
+ * GC.auto_compact = flag
+ *
+ * Updates automatic compaction mode.
+ *
+ * When enabled, the compactor will execute on every major collection.
+ *
+ * Enabling compaction will degrade performance on major collections.
+ */
static VALUE
-gc_set_auto_compact(rb_execution_context_t *ec, VALUE _, VALUE v)
+gc_set_auto_compact(VALUE _, VALUE v)
{
/* If not MinGW, Windows, or does not have mmap, we cannot use mprotect for
* the read barrier, so we must disable automatic compaction. */
@@ -9993,8 +10063,14 @@ gc_set_auto_compact(rb_execution_context_t *ec, VALUE _, VALUE v)
return v;
}
+/*
+ * call-seq:
+ * GC.auto_compact -> true or false
+ *
+ * Returns whether or not automatic compaction has been enabled.
+ */
static VALUE
-gc_get_auto_compact(rb_execution_context_t *ec, VALUE _)
+gc_get_auto_compact(VALUE _)
{
return ruby_enable_autocompact ? Qtrue : Qfalse;
}
@@ -12824,6 +12900,11 @@ Init_GC(void)
rb_define_singleton_method(rb_mGC, "malloc_allocated_size", gc_malloc_allocated_size, 0);
rb_define_singleton_method(rb_mGC, "malloc_allocations", gc_malloc_allocations, 0);
#endif
+ rb_define_singleton_method(rb_mGC, "compact", gc_compact, 0);
+ rb_define_singleton_method(rb_mGC, "auto_compact", gc_get_auto_compact, 0);
+ rb_define_singleton_method(rb_mGC, "auto_compact=", gc_set_auto_compact, 1);
+ rb_define_singleton_method(rb_mGC, "latest_compact_info", gc_compact_stats, 0);
+ rb_define_singleton_method(rb_mGC, "verify_compaction_references", gc_verify_compaction_references, -1);
#if GC_DEBUG_STRESS_TO_CLASS
rb_define_singleton_method(rb_mGC, "add_stress_to_class", rb_gcdebug_add_stress_to_class, -1);
diff --git a/gc.rb b/gc.rb
index 8a00b406ce..4a80e09443 100644
--- a/gc.rb
+++ b/gc.rb
@@ -38,27 +38,6 @@ def garbage_collect full_mark: true, immediate_mark: true, immediate_sweep: true
Primitive.gc_start_internal full_mark, immediate_mark, immediate_sweep, false
end
- # call-seq:
- # GC.auto_compact -> true or false
- #
- # Returns whether or not automatic compaction has been enabled.
- #
- def self.auto_compact
- Primitive.gc_get_auto_compact
- end
-
- # call-seq:
- # GC.auto_compact = flag
- #
- # Updates automatic compaction mode.
- #
- # When enabled, the compactor will execute on every major collection.
- #
- # Enabling compaction will degrade performance on major collections.
- def self.auto_compact=(flag)
- Primitive.gc_set_auto_compact(flag)
- end
-
# call-seq:
# GC.enable -> true or false
#
@@ -183,53 +162,6 @@ def self.stat hash_or_key = nil
def self.latest_gc_info hash_or_key = nil
Primitive.gc_latest_gc_info hash_or_key
end
-
- # call-seq:
- # GC.latest_compact_info -> {:considered=>{:T_CLASS=>11}, :moved=>{:T_CLASS=>11}}
- #
- # Returns information about object moved in the most recent GC compaction.
- #
- # The returned hash has two keys :considered and :moved. The hash for
- # :considered lists the number of objects that were considered for movement
- # by the compactor, and the :moved hash lists the number of objects that
- # were actually moved. Some objects can't be moved (maybe they were pinned)
- # so these numbers can be used to calculate compaction efficiency.
- def self.latest_compact_info
- Primitive.gc_compact_stats
- end
-
- # call-seq:
- # GC.compact
- #
- # This function compacts objects together in Ruby's heap. It eliminates
- # unused space (or fragmentation) in the heap by moving objects in to that
- # unused space. This function returns a hash which contains statistics about
- # which objects were moved. See `GC.latest_gc_info` for details about
- # compaction statistics.
- #
- # This method is implementation specific and not expected to be implemented
- # in any implementation besides MRI.
- def self.compact
- Primitive.gc_compact
- end
-
- # call-seq:
- # GC.verify_compaction_references(toward: nil, double_heap: false) -> hash
- #
- # Verify compaction reference consistency.
- #
- # This method is implementation specific. During compaction, objects that
- # were moved are replaced with T_MOVED objects. No object should have a
- # reference to a T_MOVED object after compaction.
- #
- # This function doubles the heap to ensure room to move all objects,
- # compacts the heap to make sure everything moves, updates all references,
- # then performs a full GC. If any object contains a reference to a T_MOVED
- # object, that object should be pushed on the mark stack, and will
- # make a SEGV.
- def self.verify_compaction_references(toward: nil, double_heap: false)
- Primitive.gc_verify_compaction_references(double_heap, toward == :empty)
- end
end
module ObjectSpace
From 5c73c78da0ffca162b2dad6f217ae0ec1565ad52 Mon Sep 17 00:00:00 2001
From: Mike Dalessio <mike.dalessio@gmail.com>
Date: Mon, 23 May 2022 17:31:14 -0400
Subject: [PATCH 2/2] Define unsupported GC compaction methods as
rb_f_notimplement
Fixes [Bug #18779]
Define the following methods as `rb_f_notimplement` on unsupported
platforms:
- GC.compact
- GC.auto_compact
- GC.auto_compact=
- GC.latest_compact_info
- GC.verify_compaction_references
This change allows users to call `GC.respond_to?(:compact)` to
properly test for compaction support. Previously, it was necessary to
invoke `GC.compact` or `GC.verify_compaction_references` and check if
those methods raised `NotImplementedError` to determine if compaction
was supported.
This follows the precedent set for other platform-specific
methods. For example, in `process.c` for methods such as
`Process.fork`, `Process.setpgid`, and `Process.getpriority`.
---
gc.c | 26 ++++++++++++++++
test/ruby/test_gc_compact.rb | 58 ++++++++++++++++++++++++++----------
2 files changed, 69 insertions(+), 15 deletions(-)
diff --git a/gc.c b/gc.c
index ef0e66ee46..6a5c739cc4 100644
--- a/gc.c
+++ b/gc.c
@@ -8732,6 +8732,7 @@ gc_move(rb_objspace_t *objspace, VALUE scan, VALUE free)
return (VALUE)src;
}
+#if GC_COMPACTION_SUPPORTED
static int
compare_free_slots(const void *left, const void *right, void *dummy)
{
@@ -8775,6 +8776,7 @@ gc_sort_heap_by_empty_slots(rb_objspace_t *objspace)
free(page_list);
}
+#endif
static void
gc_ref_update_array(rb_objspace_t * objspace, VALUE v)
@@ -9410,6 +9412,7 @@ gc_update_references(rb_objspace_t * objspace, rb_heap_t *heap)
static VALUE type_sym(size_t type);
+#if GC_COMPACTION_SUPPORTED
/*
* call-seq:
* GC.latest_compact_info -> {:considered=>{:T_CLASS=>11}, :moved=>{:T_CLASS=>11}}
@@ -9446,7 +9449,11 @@ gc_compact_stats(VALUE self)
return h;
}
+#else
+# define gc_compact_stats rb_f_notimplement
+#endif
+#if GC_COMPACTION_SUPPORTED
static void
root_obj_check_moved_i(const char *category, VALUE obj, void *data)
{
@@ -9508,6 +9515,10 @@ heap_check_moved_i(void *vstart, void *vend, size_t stride, void *data)
*
* This method is implementation specific and not expected to be implemented
* in any implementation besides MRI.
+ *
+ * To test whether GC compaction is supported, use the idiom:
+ *
+ * GC.respond_to?(:compact)
*/
static VALUE
gc_compact(VALUE self)
@@ -9517,7 +9528,11 @@ gc_compact(VALUE self)
return gc_compact_stats(self);
}
+#else
+# define gc_compact rb_f_notimplement
+#endif
+#if GC_COMPACTION_SUPPORTED
/*
* call-seq:
* GC.verify_compaction_references(toward: nil, double_heap: false) -> hash
@@ -9582,6 +9597,9 @@ gc_verify_compaction_references(int argc, VALUE *argv, VALUE self)
return gc_compact_stats(self);
}
+#else
+# define gc_verify_compaction_references rb_f_notimplement
+#endif
VALUE
rb_gc_start(void)
@@ -10038,6 +10056,7 @@ gc_disable(rb_execution_context_t *ec, VALUE _)
return rb_gc_disable();
}
+#if GC_COMPACTION_SUPPORTED
/*
* call-seq:
* GC.auto_compact = flag
@@ -10062,7 +10081,11 @@ gc_set_auto_compact(VALUE _, VALUE v)
ruby_enable_autocompact = RTEST(v);
return v;
}
+#else
+# define gc_set_auto_compact rb_f_notimplement
+#endif
+#if GC_COMPACTION_SUPPORTED
/*
* call-seq:
* GC.auto_compact -> true or false
@@ -10074,6 +10097,9 @@ gc_get_auto_compact(VALUE _)
{
return ruby_enable_autocompact ? Qtrue : Qfalse;
}
+#else
+# define gc_get_auto_compact rb_f_notimplement
+#endif
static int
get_envparam_size(const char *name, size_t *default_value, size_t lower_bound)
diff --git a/test/ruby/test_gc_compact.rb b/test/ruby/test_gc_compact.rb
index f5cab55ba7..c9c96a7c9c 100644
--- a/test/ruby/test_gc_compact.rb
+++ b/test/ruby/test_gc_compact.rb
@@ -4,14 +4,7 @@
require 'etc'
class TestGCCompact < Test::Unit::TestCase
- module SupportsCompact
- def setup
- skip "autocompact not supported on this platform" unless supports_auto_compact?
- super
- end
-
- private
-
+ module CompactionSupportInspector
def supports_auto_compact?
return true unless defined?(Etc::SC_PAGE_SIZE)
@@ -25,10 +18,19 @@ def supports_auto_compact?
end
end
- include SupportsCompact
+ module OmitUnlessCompactSupported
+ include CompactionSupportInspector
+
+ def setup
+ omit "autocompact not supported on this platform" unless supports_auto_compact?
+ super
+ end
+ end
+
+ include OmitUnlessCompactSupported
class AutoCompact < Test::Unit::TestCase
- include SupportsCompact
+ include OmitUnlessCompactSupported
def test_enable_autocompact
before = GC.auto_compact
@@ -81,13 +83,39 @@ def test_implicit_compaction_does_something
end
end
- def os_page_size
- return true unless defined?(Etc::SC_PAGE_SIZE)
+ class CompactMethodsNotImplemented < Test::Unit::TestCase
+ include CompactionSupportInspector
+
+ def assert_not_implemented(method, *args)
+ omit "autocompact is supported on this platform" if supports_auto_compact?
+
+ assert_raise(NotImplementedError) { GC.send(method, *args) }
+ refute(GC.respond_to?(method), "GC.#{method} should be defined as rb_f_notimplement")
+ end
+
+ def test_gc_compact_not_implemented
+ assert_not_implemented(:compact)
+ end
+
+ def test_gc_auto_compact_get_not_implemented
+ assert_not_implemented(:auto_compact)
+ end
+
+ def test_gc_auto_compact_set_not_implemented
+ assert_not_implemented(:auto_compact=, true)
+ end
+
+ def test_gc_latest_compact_info_not_implemented
+ assert_not_implemented(:latest_compact_info)
+ end
+
+ def test_gc_verify_compaction_references_not_implemented
+ assert_not_implemented(:verify_compaction_references)
+ end
end
- def setup
- skip "autocompact not supported on this platform" unless supports_auto_compact?
- super
+ def os_page_size
+ return true unless defined?(Etc::SC_PAGE_SIZE)
end
def test_gc_compact_stats