From 448b3aa0ae1dac1f2376dae4f1b6bfd108a260d3 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 12 Oct 2026 15:15:07 +0900 Subject: [PATCH 1/2] Report RSS on Windows Windows has neither /proc nor ps nor getrusage, so RSS and MAXRSS were always 0. The working set and its peak from GetProcessMemoryInfo are the closest equivalents, read in a file that only Windows requires. Fiddle is loaded with the harness, because loading it after Bundler.setup resets Bundler between iterations. Co-Authored-By: Claude Opus 5.5 --- lib/harness.rb | 7 +++++++ lib/harness/windows_memory_counters.rb | 28 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 lib/harness/windows_memory_counters.rb diff --git a/lib/harness.rb b/lib/harness.rb index 35dab045..f5341868 100644 --- a/lib/harness.rb +++ b/lib/harness.rb @@ -73,6 +73,8 @@ def get_rss # First, grab a line like "62796 kB". Checking the Linux kernel source, Rss will always be in kB. rss_desc = File.read(mem_rollup_file).lines.detect { |line| line.start_with?("Rss") }.split(":", 2)[1][/(\d+)/, 1] 1024 * Integer(rss_desc) + elsif defined?(WindowsMemoryCounters) + WindowsMemoryCounters.read&.fetch(:working_set) || 0 else # Collect our own peak mem usage as soon as reasonable after finishing the last iteration. # This method is only accurate to kilobytes, but is nicely portable and doesn't require @@ -163,7 +165,12 @@ def macos_resident_size buffer[RI_RESIDENT_SIZE_OFFSET, 8].unpack1('Q') end +# Loaded after load_fiddle is defined, since it calls it right away. +require_relative "harness/windows_memory_counters" if RUBY_PLATFORM.match?(/mswin|mingw/) + def get_maxrss + return WindowsMemoryCounters.read&.fetch(:peak_working_set) if defined?(WindowsMemoryCounters) + load_fiddle require 'rbconfig/sizeof' diff --git a/lib/harness/windows_memory_counters.rb b/lib/harness/windows_memory_counters.rb new file mode 100644 index 00000000..94fd7bb7 --- /dev/null +++ b/lib/harness/windows_memory_counters.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Resolved when the harness loads, before use_gemfile. Loading fiddle after Bundler.setup goes through Bundler.reset!, +# which redefines Kernel#gem and invalidates YJIT code if it happens between iterations. A dropped Fiddle::Handle is +# freed by a deferred finalizer, whose interrupt also sends YJIT code back to the interpreter, so kernel32 stays open. +module WindowsMemoryCounters + begin + load_fiddle + @kernel32 = Fiddle.dlopen("kernel32") + @process = Fiddle::Function.new(@kernel32['GetCurrentProcess'], [], Fiddle::TYPE_VOIDP).call + @get_info = Fiddle::Function.new(@kernel32['K32GetProcessMemoryInfo'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT) + rescue LoadError, Fiddle::DLError, NameError + warn "Failed to get memory counters: #{$!.message}" + end + + # The working set of this process and its peak in bytes, the Windows counterparts of RSS and max RSS. + def self.read + return nil unless @get_info + + # PROCESS_MEMORY_COUNTERS: two DWORDs followed by eight SIZE_Ts, the first two being PeakWorkingSetSize and WorkingSetSize. + buffer = "\0".b * (8 + 8 * Fiddle::SIZEOF_SIZE_T) + buffer[0, 4] = [buffer.bytesize].pack('L') + return nil if @get_info.call(@process, buffer, buffer.bytesize).zero? + + peak_working_set, working_set = buffer.unpack('x8J2') + { working_set: working_set, peak_working_set: peak_working_set } + end +end From a4c594dfb992daff83ab48d246bc6543dcedfdc7 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 12 Oct 2026 15:15:08 +0900 Subject: [PATCH 2/2] Record the commit charge on Windows Changes in private memory on Windows show up in the commit charge (PagefileUsage of GetProcessMemoryInfo) more than in the working set. The result JSON keeps it and its peak as commit_charge and peak_commit_charge. Co-Authored-By: Claude Opus 5.5 --- lib/harness.rb | 4 ++++ lib/harness/windows_memory_counters.rb | 14 ++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/harness.rb b/lib/harness.rb index f5341868..fd3645c7 100644 --- a/lib/harness.rb +++ b/lib/harness.rb @@ -228,6 +228,10 @@ def return_results(warmup_iterations, bench_iterations, **extra) if maxrss = get_maxrss ruby_bench_results["maxrss"] = maxrss end + if defined?(WindowsMemoryCounters) && (counters = WindowsMemoryCounters.read) + ruby_bench_results["commit_charge"] = counters[:commit_charge] + ruby_bench_results["peak_commit_charge"] = counters[:peak_commit_charge] + end # If YJIT or ZJIT is enabled, show some of its stats unless it does by itself. if yjit_stats diff --git a/lib/harness/windows_memory_counters.rb b/lib/harness/windows_memory_counters.rb index 94fd7bb7..915f6e73 100644 --- a/lib/harness/windows_memory_counters.rb +++ b/lib/harness/windows_memory_counters.rb @@ -13,16 +13,22 @@ module WindowsMemoryCounters warn "Failed to get memory counters: #{$!.message}" end - # The working set of this process and its peak in bytes, the Windows counterparts of RSS and max RSS. + # Memory counters of this process in bytes. The working set is the Windows counterpart of RSS, + # and the commit charge (PagefileUsage) is the private memory the process has committed. def self.read return nil unless @get_info - # PROCESS_MEMORY_COUNTERS: two DWORDs followed by eight SIZE_Ts, the first two being PeakWorkingSetSize and WorkingSetSize. + # PROCESS_MEMORY_COUNTERS: two DWORDs followed by eight SIZE_Ts. buffer = "\0".b * (8 + 8 * Fiddle::SIZEOF_SIZE_T) buffer[0, 4] = [buffer.bytesize].pack('L') return nil if @get_info.call(@process, buffer, buffer.bytesize).zero? - peak_working_set, working_set = buffer.unpack('x8J2') - { working_set: working_set, peak_working_set: peak_working_set } + peak_working_set, working_set, _, _, _, _, commit_charge, peak_commit_charge = buffer.unpack('x8J8') + { + working_set: working_set, + peak_working_set: peak_working_set, + commit_charge: commit_charge, + peak_commit_charge: peak_commit_charge, + } end end