diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index e918a1f6d5..674c244eb0 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -361,23 +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)) - { - 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,30 @@ 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 parsedPageSize)) + pageSize = parsedPageSize; + + if (TryGetPages("free") is not long availablePages) + return null; + + availablePages += TryGetPages("speculative") ?? 0; + availablePages += TryGetPages("inactive") ?? 0; + return availablePages * pageSize; + + long? TryGetPages(string pageName) + { + var match = Regex.Match(output, $@"Pages {pageName}:\s+(\d+)"); + return match.Success && long.TryParse(match.Groups[1].Value, out long pages) ? pages : null; + } + } } } diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs index c456e92c23..7df7dbe35b 100644 --- a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -308,5 +308,71 @@ 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 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() + { + 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)); + } } }