Enable DTrace USDT GC probes in the modular default GC

When Ruby is built with `--with-modular-gc`, the default GC module is
built with `BUILDING_MODULAR_GC` defined.

The existing code stubbed out `probes.h` and `RUBY_DTRACE_GC_HOOK` in
this build path, so any DTrace script attached against the modular
default GC saw zero GC events.

This PR enables the GC probes in the modular GC library so they fire
when Ruby is built with `--enable-dtrace --with-modular-gc`

To test: build with `./configure --enable-dtrace
--with-modular-gc=/tmp/modgc`, then `make install-modular-gc
MODULAR_GC=default`.

Probes are present in the shared library:

```console
$ sudo bpftrace -l 'usdt:/tmp/modgc/librubygc.default.so:ruby:*'
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__mark__begin
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__mark__end
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__sweep__begin
usdt:/tmp/modgc/librubygc.default.so:ruby:gc__sweep__end
```

They fire under a GC-heavy workload (`test.rb` contains `100000.times { Object.new }; GC.start`):

```console
$ sudo bpftrace -e '
  usdt:/tmp/modgc/librubygc.default.so:ruby:gc__mark__begin  { @mark_begin++ }
  usdt:/tmp/modgc/librubygc.default.so:ruby:gc__mark__end    { @mark_end++ }
  usdt:/tmp/modgc/librubygc.default.so:ruby:gc__sweep__begin { @sweep_begin++ }
  usdt:/tmp/modgc/librubygc.default.so:ruby:gc__sweep__end   { @sweep_end++ }
' -c 'env RUBY_GC_LIBRARY=default ./miniruby --disable=gems test.rb'

@mark_begin: 2
@mark_end: 2
@sweep_begin: 18
@sweep_end: 18
```
This commit is contained in:
Matt Valentine-House authored and eightbitraptor committed 2026-06-11 19:10:16 +01:00
1 parent 8e8682fc23
commit 4fb0596b01
5 files changed
+88 -15
Notes: git 2026-06-11 18:10:54 +00:00

No files matched your search

+3 -1
View File
@@ -1975,7 +1975,9 @@ gc/distclean gc/realclean::
modular-gc-precheck:
modular-gc: probes.h gc/Makefile
$(Q) $(RUNRUBY) $(srcdir)/ext/extmk.rb \
$(Q) DTRACE='$(DTRACE)' DTRACE_EXT='$(DTRACE_EXT)' \
DTRACE_OBJ='$(DTRACE_OBJ)' DTRACE_REBUILD='$(DTRACE_REBUILD)' \
$(RUNRUBY) $(srcdir)/ext/extmk.rb \
$(SCRIPT_ARGS) \
--make='$(MAKE)' --make-flags="V=$(V) MINIRUBY='$(MINIRUBY)'" \
--gnumake=$(gnumake) --extflags="$(EXTLDFLAGS)" \
+2 -8
View File
@@ -34,9 +34,7 @@
#include "gc/gc.h"
#include "gc/gc_impl.h"
#ifndef BUILDING_MODULAR_GC
# include "probes.h"
#endif
#include "probes.h"
#ifdef BUILDING_MODULAR_GC
# define RB_DEBUG_COUNTER_INC(_name) ((void)0)
@@ -8940,12 +8938,8 @@ gc_prof_timer_stop(rb_objspace_t *objspace)
}
}
#ifdef BUILDING_MODULAR_GC
# define RUBY_DTRACE_GC_HOOK(name)
#else
# define RUBY_DTRACE_GC_HOOK(name) \
#define RUBY_DTRACE_GC_HOOK(name) \
do {if (RUBY_DTRACE_GC_##name##_ENABLED()) RUBY_DTRACE_GC_##name();} while (0)
#endif
static inline void
gc_prof_mark_timer_start(rb_objspace_t *objspace)
+40 -2
View File
@@ -4,11 +4,49 @@ require "mkmf"
srcdir = File.join(__dir__, "..")
$INCFLAGS << " -I#{srcdir}"
$CPPFLAGS << " -DBUILDING_MODULAR_GC"
append_cflags("-fPIC")
DTRACE_RULES = <<~MAKE
probes.stamp: $(GC_DTRACE_OBJS)
$(Q) if test -f $@ -o -f $(GC_DTRACE_OBJ); then \
$(RM) $(GC_DTRACE_OBJS) $@; \
$(ECHO) rebuilding objects which were modified by "dtrace -G"; \
$(MAKE) $(GC_DTRACE_OBJS); \
fi
$(Q) touch $@
$(GC_DTRACE_OBJ): $(top_srcdir)/probes.d $(GC_DTRACE_REBUILD:yes=probes.stamp)
$(ECHO) processing GC probes in object files
$(Q) CC="$(CC)" CFLAGS="$(CFLAGS) $(INCFLAGS) $(CPPFLAGS)" $(GC_DTRACE) -G -C $(INCFLAGS) -s $(top_srcdir)/probes.d -o $@ $(GC_DTRACE_OBJS)
MAKE
def create_gc_makefile(name, &block)
create_makefile("librubygc.#{name}", &block)
dtrace_obj = ENV.fetch("DTRACE_OBJ", "")
dtrace_enabled = (name == "default" && !dtrace_obj.empty?)
if name == "default"
$INCFLAGS << " -I$(topdir)"
$headers << "$(topdir)/probes.h"
$cleanfiles << "probes.stamp"
end
create_makefile("librubygc.#{name}") do |conf|
conf = block.call(conf) if block
next conf unless dtrace_enabled
gc_objs = $objs.join(" ")
conf = Array(conf).join.sub(/^OBJS = .*$/, "OBJS = #{gc_objs} #{dtrace_obj}")
conf + <<~MAKE
GC_DTRACE = #{ENV.fetch("DTRACE")}
GC_DTRACE_OBJ = #{dtrace_obj}
GC_DTRACE_OBJS = #{gc_objs}
GC_DTRACE_REBUILD = #{ENV.fetch("DTRACE_REBUILD", "")}
MAKE
end
File.write("Makefile", DTRACE_RULES, mode: "ab") if dtrace_enabled
end
+6 -4
View File
@@ -115,7 +115,7 @@ module DTrace
IO.popen(cmd, err: [:child, :out], &:readlines)
end
def trap_probe d_program, ruby_program
def trap_probe d_program, ruby_program, env: {}
if Hash === d_program
d_program = d_program[IMPL] or
omit "#{d_program} not implemented for #{IMPL}"
@@ -132,15 +132,17 @@ module DTrace
d_path = d.path
rb_path = rb.path
cmd = "#{RUBYBIN} --disable=gems -I#{INCLUDE} #{rb_path}"
ruby_env = env
env_prefix = ruby_env.map {|key, val| "#{key}=#{val}" }
cmd = [*env_prefix, "#{RUBYBIN} --disable=gems -I#{INCLUDE} #{rb_path}"].join(" ")
if IMPL == :stap
cmd = %W(stap #{d_path} -c #{cmd})
else
cmd = [*DTRACE_CMD, "-q", "-s", d_path, "-c", cmd ]
end
if sudo = @@sudo
NEEDED_ENVS.each do |name|
if val = ENV[name]
(NEEDED_ENVS + ruby_env.keys).uniq.each do |name|
if val = ruby_env[name] || ENV[name]
cmd.unshift("#{name}=#{val}")
end
end
+37
View File
@@ -0,0 +1,37 @@
# frozen_string_literal: false
require_relative 'helper'
module DTrace
class TestModularGC < TestCase
MODULAR_GC_DIR = RbConfig::CONFIG["modular_gc_dir"]
DLEXT = RbConfig::CONFIG["DLEXT"]
DEFAULT_GC = MODULAR_GC_DIR && File.join(MODULAR_GC_DIR, "librubygc.default.#{DLEXT}")
def setup
super
omit "Ruby was not configured with --with-modular-gc" if MODULAR_GC_DIR.nil? || MODULAR_GC_DIR.empty?
omit "default modular GC is not installed" unless DEFAULT_GC && File.file?(DEFAULT_GC)
end
%w[
gc-mark-begin
gc-mark-end
gc-sweep-begin
gc-sweep-end
].each do |probe_name|
define_method(:"test_modular_#{probe_name.gsub(/-/, '_')}") do
probe = "ruby$target:::#{probe_name} { printf(\"#{probe_name}\\n\"); }"
trap_probe(probe, ruby_program, env: {"RUBY_GC_LIBRARY" => "default"}) do |_, _, saw|
assert_operator saw.length, :>, 0
end
end
end
private
def ruby_program
"100000.times { Object.new }"
end
end
end if defined?(DTrace::TestCase)