Skip to content

Commit 1bcb7ad

Browse files
Switch the GC harnesses to the GC.stat process: keyword
ruby/ruby#19383 (d56354b27b) replaced GC.stat(scope:) with a process: keyword and make some changes to how counting global GC's work in the stats. This commit updates the GC harness, and stats module to report using the new API
1 parent ab26700 commit 1bcb7ad

5 files changed

Lines changed: 54 additions & 26 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,8 @@ The `--ractor-gc` option of `run_benchmarks.rb` collects Ractor-local GC
499499
metrics for benchmarks that use the Ractor harness (`--category ractor`),
500500
in both the per-worker mode and scenario mode.
501501
The target must use Ruby 4.1 or newer with per-Ractor global GC attribution
502-
([ruby/ruby#19147](https://github.com/ruby/ruby/pull/19147)); older targets
502+
through `GC.stat(:global_gc_invocation_count)` and `GC.stat(process: true)`
503+
([ruby/ruby#19383](https://github.com/ruby/ruby/pull/19383)); older targets
503504
fail before warmup.
504505

505506
```sh
@@ -530,9 +531,11 @@ data when it is zero or `N/A` in every row.
530531
* Tables marked `worker sum` add the Ractor-local counters and GC times of
531532
the sampled workers of each iteration. `GCs/iter` is the sum of `minor/iter`,
532533
`major/iter`, and `global/iter`; a global cycle counts under `global` on
533-
the Ractor that initiated it, not under `major`. Single-executable reports
534-
also show `GC ms/worker`, which divides each iteration's worker-sum GC
535-
time by its sampled worker count, then averages.
534+
the Ractor that initiated it (`GC.stat(:global_gc_invocation_count)`), not
535+
under `major`. The worker `GCs/iter` excludes global cycles that other
536+
Ractors started, so each cycle counts once in the sum. Single-executable
537+
reports also show `GC ms/worker`, which divides each iteration's worker-sum
538+
GC time by its sampled worker count, then averages.
536539
* `compacts*` shows the main Ractor's
537540
`GC.stat(:compact_count)` delta. Every global compacting cycle increments
538541
it in every object space, so it is not summed across workers.

‎harness/gc.rb‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def run_benchmark(_num_itrs_hint, **, &block)
3030

3131
has_marking = GC.stat.key?(:marking_time)
3232
has_sweeping = GC.stat.key?(:sweeping_time)
33-
has_global_gc = GCStats.stat_available?(:global_gc_count)
33+
has_global_gc = GCStats.stat_available?(:global_gc_invocation_count)
3434

3535
header = "itr: time"
3636
header << " marking" if has_marking
@@ -44,7 +44,6 @@ def run_benchmark(_num_itrs_hint, **, &block)
4444

4545
begin
4646
gc_before = GCStats.snapshot
47-
global_gc_before = GC.stat(:global_gc_count) if has_global_gc
4847

4948
time = realtime(&block)
5049
num_itrs += 1
@@ -57,7 +56,7 @@ def run_benchmark(_num_itrs_hint, **, &block)
5756
count_delta = sample["gc_count"]
5857
major_delta = sample["gc_major_count"]
5958
minor_delta = sample["gc_minor_count"]
60-
global_delta = has_global_gc ? GC.stat(:global_gc_count) - global_gc_before : nil
59+
global_delta = sample["gc_global_count"]
6160
ratio_str = minor_delta > 0 ? "%.2f" % (major_delta.to_f / minor_delta) : "-"
6261

6362
itr_str = "%4s %6s" % ["##{num_itrs}:", "#{time_ms}ms"]

‎lib/gc_stats.rb‎

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
module GCStats
44
module_function
55

6-
SCALAR_FIELDS = [
7-
["gc_count", :count],
8-
["gc_global_count", :global_gc_count],
6+
STAT_FIELDS = [
7+
["gc_global_count", :global_gc_invocation_count],
98
["gc_major_count", :major_gc_count],
109
["gc_minor_count", :minor_gc_count],
1110
["gc_marking_time", :marking_time],
@@ -14,7 +13,7 @@ module GCStats
1413

1514
TOTAL_TIME_FIELD = "gc_total_time_ns"
1615

17-
SCALAR_FIELD_NAMES = (SCALAR_FIELDS.map(&:first) + [TOTAL_TIME_FIELD]).freeze
16+
SCALAR_FIELD_NAMES = (["gc_count"] + STAT_FIELDS.map(&:first) + [TOTAL_TIME_FIELD]).freeze
1817

1918
RACTOR_SERIES = {
2019
"gc_count_bench" => "gc_count",
@@ -44,11 +43,15 @@ def global_gc_attributed?
4443
Warning[:experimental] = false
4544
$stderr.reopen(File::NULL, "w")
4645
begin
47-
main_before = GC.stat(:global_gc_count)
48-
process_before = GC.stat(:global_gc_count, scope: :global)
49-
Ractor.new { GC.start(full_mark: true, immediate_mark: true, immediate_sweep: true, global: true) }.join
50-
attributed = GC.stat(:global_gc_count) == main_before
51-
ran = GC.stat(:global_gc_count, scope: :global) > process_before
46+
main_before = GC.stat(:global_gc_invocation_count)
47+
process_before = GC.stat(:global_gc_count, process: true)
48+
started = Ractor.new do
49+
before = GC.stat(:global_gc_invocation_count)
50+
GC.start(full_mark: true, immediate_mark: true, immediate_sweep: true, global: true)
51+
GC.stat(:global_gc_invocation_count) - before
52+
end.value
53+
attributed = started == 1 && GC.stat(:global_gc_invocation_count) == main_before
54+
ran = GC.stat(:global_gc_count, process: true) > process_before
5255
exit!(attributed && ran ? 0 : 1)
5356
rescue StandardError
5457
exit!(2)
@@ -66,7 +69,7 @@ def check_ractor_gc_support!
6669
raise NotImplementedError, "Ractor GC metrics require GC.total_time and GC.measure_total_time="
6770
end
6871
unless global_gc_attributed?
69-
raise NotImplementedError, "Ractor GC metrics require per-Ractor global GC attribution (ruby/ruby#19147)"
72+
raise NotImplementedError, "Ractor GC metrics require per-Ractor global GC attribution (ruby/ruby#19383)"
7073
end
7174
end
7275

@@ -93,17 +96,27 @@ def numeric_delta(before, after)
9396
after - before if before.is_a?(Numeric) && after.is_a?(Numeric)
9497
end
9598

99+
# :count includes global GCs that other Ractors started. Count only the ones
100+
# this Ractor started, so that sums across Ractors count each GC once.
101+
def started_gc_count(stat)
102+
count, swept, started = stat.values_at(:count, :global_gc_count, :global_gc_invocation_count)
103+
return count unless [count, swept, started].all?(Numeric)
104+
105+
count - swept + started
106+
end
107+
96108
def snapshot
97109
{
98-
stat: GC.stat(scope: :ractor),
110+
stat: GC.stat,
99111
heap: heap_snapshot,
100112
total_time_ns: GC.respond_to?(:total_time) ? GC.total_time : nil,
101113
}
102114
end
103115

104116
def delta(before, after)
105-
sample = SCALAR_FIELDS.each_with_object({}) do |(name, key), result|
106-
result[name] = numeric_delta(before[:stat][key], after[:stat][key])
117+
sample = { "gc_count" => numeric_delta(started_gc_count(before[:stat]), started_gc_count(after[:stat])) }
118+
STAT_FIELDS.each do |name, key|
119+
sample[name] = numeric_delta(before[:stat][key], after[:stat][key])
107120
end
108121
sample[TOTAL_TIME_FIELD] = numeric_delta(before[:total_time_ns], after[:total_time_ns])
109122
sample["gc_stat_heap_delta"] = heap_delta(before[:heap], after[:heap])

‎test/gc_stats_test.rb‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ def snapshot(count:, major:, minor:, marking: nil, sweeping: nil, total_ns: nil,
5454
assert_equal 250_000, GCStats.delta(before, after)['gc_total_time_ns']
5555
end
5656

57+
it 'counts only the global GCs that this Ractor started' do
58+
before = snapshot(count: 10, major: 2, minor: 5)
59+
before[:stat].merge!(global_gc_count: 3, global_gc_invocation_count: 1)
60+
after = snapshot(count: 16, major: 2, minor: 6)
61+
after[:stat].merge!(global_gc_count: 8, global_gc_invocation_count: 2)
62+
63+
sample = GCStats.delta(before, after)
64+
65+
assert_equal 2, sample['gc_count']
66+
assert_equal 1, sample['gc_global_count']
67+
assert_equal sample['gc_major_count'] + sample['gc_minor_count'] + sample['gc_global_count'], sample['gc_count']
68+
end
69+
5770
it 'keeps heap gauge deltas signed and omits heaps/keys absent from the before snapshot' do
5871
before = snapshot(count: 0, major: 0, minor: 0, heap: {
5972
0 => { slot_size: 40, heap_eden_slots: 100, heap_live_slots: 50 }
@@ -178,8 +191,8 @@ def snapshot(count:, major:, minor:, marking: nil, sweeping: nil, total_ns: nil,
178191
end
179192

180193
describe '.global_gc_attributed?' do
181-
it 'is false when GC.stat lacks the global_gc_count key' do
182-
skip 'this build has the key; attribution depends on ruby/ruby#19147' if GCStats.stat_available?(:global_gc_count)
194+
it 'is false when GC.stat lacks the global_gc_invocation_count key' do
195+
skip 'this build has the key; attribution depends on ruby/ruby#19383' if GCStats.stat_available?(:global_gc_invocation_count)
183196
refute GCStats.global_gc_attributed?
184197
end
185198
end

‎test/ractor_gc_harness_test.rb‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def GCStats.global_gc_attributed?(*) = false
189189

190190
_out, attr_err, attr_status = Open3.capture3(CLEAN_ENV, @ruby, '--disable-gems', '-r', File.join(ROOT, 'lib', 'gc_stats'), '-e', 'exit(GCStats.global_gc_attributed? ? 0 : 1)')
191191
unless attr_status.success?
192-
detail = "target #{@ruby} lacks per-Ractor global GC attribution (ruby/ruby#19147): #{attr_err.strip}"
192+
detail = "target #{@ruby} lacks per-Ractor global GC attribution (ruby/ruby#19383): #{attr_err.strip}"
193193
flunk("RACTOR_GC_TEST_RUBY #{detail}") if @explicit_target
194194
skip("test ruby #{detail}")
195195
end
@@ -317,9 +317,9 @@ def run_workload(body, env: {})
317317
worker = Ractor.new do
318318
GC.disable
319319
Ractor.receive
320-
before = GC.stat(:count, scope: :ractor)
320+
before = GC.stat(:count)
321321
10.times { GC.start(full_mark: false, immediate_sweep: true, global: false) }
322-
Ractor.main.send(GC.stat(:count, scope: :ractor) - before)
322+
Ractor.main.send(GC.stat(:count) - before)
323323
Ractor.receive
324324
end
325325
@@ -532,7 +532,7 @@ def run_workload(body, env: {})
532532
it 'fails before warmup when the target lacks per-Ractor global GC attribution' do
533533
run_workload(UNATTRIBUTED_GLOBAL_BODY) do |stdout, stderr, status, result_path|
534534
assert status.success?, "probe script itself failed:\n#{stdout}\n#{stderr}"
535-
assert_includes stdout, 'raised: Ractor GC metrics require per-Ractor global GC attribution (ruby/ruby#19147)'
535+
assert_includes stdout, 'raised: Ractor GC metrics require per-Ractor global GC attribution (ruby/ruby#19383)'
536536
assert_includes stdout, 'workload_ran=false'
537537
refute File.exist?(result_path), 'no results file may be written for an unsupported target'
538538
end

0 commit comments

Comments
 (0)