From 70bd9fa7d4d28618046a4a9d08244a41cbf7397d Mon Sep 17 00:00:00 2001 From: Sabtain Ali Date: Sat, 17 Oct 2026 16:20:21 +0500 Subject: [PATCH 1/2] Include reusable pages in macOS memory reporting --- .../Environments/PhysicalMemoryInfo.cs | 34 ++++++++++-------- .../Environments/PhysicalMemoryInfoTests.cs | 35 +++++++++++++++++++ 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index e918a1f6d5..a46ce9b287 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -364,20 +364,7 @@ private static bool IsInstalledMemoryDevice(ReadOnlySpan device) // 2. Get Free Memory using vm_stat string? vmStatOutput = ProcessHelper.RunAndReadOutput("vm_stat"); if (!string.IsNullOrEmpty(vmStatOutput)) - { - long pageSize = 4096; - var pageSizeMatch = Regex.Match(vmStatOutput, @"page size of (\d+) bytes"); - if (pageSizeMatch.Success && long.TryParse(pageSizeMatch.Groups[1].Value, out long parsedPageSize)) - { - pageSize = parsedPageSize; - } - - var match = Regex.Match(vmStatOutput, @"Pages free:\s+(\d+)"); - if (match.Success && long.TryParse(match.Groups[1].Value, out long pagesFree)) - { - available = pagesFree * pageSize; - } - } + available = ParseMacAvailableMemory(vmStatOutput); var (speed, type) = GetMacMemoryDetails(); return new PhysicalMemoryInfo(total, available, speed, type); @@ -453,5 +440,24 @@ void ReadMemoryDevice(JsonElement device) } } } + + internal static long? ParseMacAvailableMemory(string output) + { + var pageSizeMatch = Regex.Match(output, @"page size of (\d+) bytes"); + if (!pageSizeMatch.Success || !long.TryParse(pageSizeMatch.Groups[1].Value, out long pageSize)) + return null; + + long availablePages = 0; + foreach (string pageName in new[] { "free", "inactive", "speculative" }) + { + var match = Regex.Match(output, $@"Pages {pageName}:\s+(\d+)"); + if (!match.Success || !long.TryParse(match.Groups[1].Value, out long pages)) + return null; + + availablePages += pages; + } + + return availablePages * pageSize; + } } } diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs index c456e92c23..00ed994788 100644 --- a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -308,5 +308,40 @@ private static byte[] WithStrings(byte[] formatted, string[] strings) bytes.Add(0); return bytes.ToArray(); } + [Fact] + public void MacAvailableMemoryIncludesReusablePages() + { + const string vmStat = """ + Mach Virtual Memory Statistics: (page size of 16384 bytes) + Pages free: 100. + Pages active: 2000. + Pages inactive: 300. + Pages speculative: 50. + Pages throttled: 0. + Pages wired down: 500. + """; + + Assert.Equal(7_372_800, SystemMemory.ParseMacAvailableMemory(vmStat)); + } + + [Fact] + public void MacAvailableMemoryReturnsZeroWhenAllAvailablePagesAreZero() + { + const string vmStat = """ + Mach Virtual Memory Statistics: (page size of 4096 bytes) + Pages free: 0. + Pages inactive: 0. + Pages speculative: 0. + """; + + Assert.Equal(0, SystemMemory.ParseMacAvailableMemory(vmStat)); + } + + [Fact] + public void MacAvailableMemoryReturnsNullWhenPageDataIsIncomplete() + { + Assert.Null(SystemMemory.ParseMacAvailableMemory( + "Mach Virtual Memory Statistics: (page size of 4096 bytes)")); + } } } From 9a72fde4e55399921d62ec91708277899f9bbe81 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Thu, 22 Oct 2026 15:01:29 -0400 Subject: [PATCH 2/2] Fix netstandard2.0 build and keep macOS memory fallbacks Only "Pages free" is required; a missing page-size header falls back to 4096 bytes and missing inactive/speculative counts are treated as zero, so a partial vm_stat output no longer drops the available value. Co-Authored-By: Claude Opus 5.5 --- .../Environments/PhysicalMemoryInfo.cs | 28 ++++++++------ .../Environments/PhysicalMemoryInfoTests.cs | 37 +++++++++++++++++-- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index a46ce9b287..674c244eb0 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -361,10 +361,10 @@ private static bool IsInstalledMemoryDevice(ReadOnlySpan device) if (string.IsNullOrEmpty(sysctlOutput) || !long.TryParse(sysctlOutput!.Trim(), out total) || total == 0) return null; - // 2. Get Free Memory using vm_stat + // 2. Get Available Memory using vm_stat string? vmStatOutput = ProcessHelper.RunAndReadOutput("vm_stat"); if (!string.IsNullOrEmpty(vmStatOutput)) - available = ParseMacAvailableMemory(vmStatOutput); + available = ParseMacAvailableMemory(vmStatOutput!); var (speed, type) = GetMacMemoryDetails(); return new PhysicalMemoryInfo(total, available, speed, type); @@ -441,23 +441,29 @@ void ReadMemoryDevice(JsonElement device) } } + /// + /// Available memory is free + speculative + inactive pages, the pages macOS can reuse for new allocations. + /// vm_stat reports speculative pages separately from "Pages free". Only "Pages free" is required. + /// internal static long? ParseMacAvailableMemory(string output) { + long pageSize = 4096; var pageSizeMatch = Regex.Match(output, @"page size of (\d+) bytes"); - if (!pageSizeMatch.Success || !long.TryParse(pageSizeMatch.Groups[1].Value, out long pageSize)) + if (pageSizeMatch.Success && long.TryParse(pageSizeMatch.Groups[1].Value, out long parsedPageSize)) + pageSize = parsedPageSize; + + if (TryGetPages("free") is not long availablePages) return null; - long availablePages = 0; - foreach (string pageName in new[] { "free", "inactive", "speculative" }) + availablePages += TryGetPages("speculative") ?? 0; + availablePages += TryGetPages("inactive") ?? 0; + return availablePages * pageSize; + + long? TryGetPages(string pageName) { var match = Regex.Match(output, $@"Pages {pageName}:\s+(\d+)"); - if (!match.Success || !long.TryParse(match.Groups[1].Value, out long pages)) - return null; - - availablePages += pages; + return match.Success && long.TryParse(match.Groups[1].Value, out long pages) ? pages : null; } - - return availablePages * pageSize; } } } diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs index 00ed994788..7df7dbe35b 100644 --- a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -308,6 +308,7 @@ private static byte[] WithStrings(byte[] formatted, string[] strings) bytes.Add(0); return bytes.ToArray(); } + [Fact] public void MacAvailableMemoryIncludesReusablePages() { @@ -338,10 +339,40 @@ public void MacAvailableMemoryReturnsZeroWhenAllAvailablePagesAreZero() } [Fact] - public void MacAvailableMemoryReturnsNullWhenPageDataIsIncomplete() + public void MacAvailableMemoryReturnsNullWhenFreePagesAreMissing() + { + const string vmStat = """ + Mach Virtual Memory Statistics: (page size of 4096 bytes) + Pages inactive: 300. + Pages speculative: 50. + """; + + Assert.Null(SystemMemory.ParseMacAvailableMemory(vmStat)); + } + + [Fact] + public void MacAvailableMemoryTreatsMissingReusablePagesAsZero() { - Assert.Null(SystemMemory.ParseMacAvailableMemory( - "Mach Virtual Memory Statistics: (page size of 4096 bytes)")); + const string vmStat = """ + Mach Virtual Memory Statistics: (page size of 16384 bytes) + Pages free: 100. + Pages inactive: 300. + """; + + Assert.Equal(400 * 16384, SystemMemory.ParseMacAvailableMemory(vmStat)); + } + + [Fact] + public void MacAvailableMemoryDefaultsTo4KPagesWhenPageSizeIsMissing() + { + const string vmStat = """ + Mach Virtual Memory Statistics: + Pages free: 100. + Pages inactive: 300. + Pages speculative: 50. + """; + + Assert.Equal(450 * 4096, SystemMemory.ParseMacAvailableMemory(vmStat)); } } }