From af495b91190f30b074d00eaa07a4632017e68c1d 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. Co-Authored-By: Claude Opus 5.5 --- harness/harness-common.rb | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/harness/harness-common.rb b/harness/harness-common.rb index 073edb04..825ae3a3 100644 --- a/harness/harness-common.rb +++ b/harness/harness-common.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 is_windows + get_windows_memory_counters&.first || 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 @@ -176,6 +178,40 @@ def self.function(name, args, ret) end end +# Memoized like ProcPidRusage, since load_fiddle alone takes about 0.1 ms on Windows. +module WindowsMemoryCounters + @resolved = false + + def self.fn + return @fn if @resolved + + @resolved = true + @fn = begin + load_fiddle + process = WindowsKernel32.function('GetCurrentProcess', [], Fiddle::TYPE_VOIDP).call + get_info = WindowsKernel32.function('K32GetProcessMemoryInfo', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT) + ->(buffer) { get_info.call(process, buffer, buffer.bytesize) } + rescue LoadError, Fiddle::DLError, NameError + warn "Failed to get memory counters: #{$!.message}" + nil + end + end +end + +# Returns [WorkingSetSize, PeakWorkingSetSize] of this process in bytes, the Windows counterparts of RSS and max RSS. +def get_windows_memory_counters + get_info = WindowsMemoryCounters.fn + 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(buffer).zero? + + peak, current = buffer.unpack('x8J2') + [current, peak] +end + def pin_to_cpu(cpu) load_fiddle # Etc.nprocessors counts the CPUs of every processor group, but the mask holds those of one group only. @@ -210,6 +246,8 @@ def raise_priority end def get_maxrss + return get_windows_memory_counters&.last if is_windows + load_fiddle require 'rbconfig/sizeof' From acd67d913438ff49266cee0724a9d02a0a2646bb 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 --- harness/harness-common.rb | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/harness/harness-common.rb b/harness/harness-common.rb index 825ae3a3..d1cd6051 100644 --- a/harness/harness-common.rb +++ b/harness/harness-common.rb @@ -74,7 +74,7 @@ def get_rss rss_desc = File.read(mem_rollup_file).lines.detect { |line| line.start_with?("Rss") }.split(":", 2)[1][/(\d+)/, 1] 1024 * Integer(rss_desc) elsif is_windows - get_windows_memory_counters&.first || 0 + get_windows_memory_counters&.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 @@ -198,18 +198,24 @@ def self.fn end end -# Returns [WorkingSetSize, PeakWorkingSetSize] of this process in bytes, the Windows counterparts of RSS and max RSS. +# Returns 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 get_windows_memory_counters get_info = WindowsMemoryCounters.fn 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(buffer).zero? - peak, current = buffer.unpack('x8J2') - [current, peak] + 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 def pin_to_cpu(cpu) @@ -246,7 +252,7 @@ def raise_priority end def get_maxrss - return get_windows_memory_counters&.last if is_windows + return get_windows_memory_counters&.fetch(:peak_working_set) if is_windows load_fiddle @@ -305,6 +311,10 @@ def return_results(warmup_iterations, bench_iterations, **extra) if maxrss = get_maxrss ruby_bench_results["maxrss"] = maxrss end + if is_windows && (counters = get_windows_memory_counters) + 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