From 327a944fd54975e108d4af0d1385094e16586003 Mon Sep 17 00:00:00 2001 From: abdulrahmanhossam Date: Mon, 2 Mar 2026 02:21:20 +0200 Subject: [PATCH 1/5] Add physical memory size (Total RAM) to environment summary (#846) --- .../Environments/HostEnvironmentInfo.cs | 26 ++-- .../Environments/PhysicalMemoryInfo.cs | 125 ++++++++++++++++++ 2 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs diff --git a/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs b/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs index cfb31e0841..f8ed25e861 100644 --- a/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs +++ b/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using BenchmarkDotNet.Detectors; +using BenchmarkDotNet.Detectors; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; -using BenchmarkDotNet.Models; using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Models; using BenchmarkDotNet.Portability; using BenchmarkDotNet.Properties; using BenchmarkDotNet.Reports; @@ -13,9 +10,12 @@ using JetBrains.Annotations; using Perfolizer.Helpers; using Perfolizer.Horology; -using Perfolizer.Models; using Perfolizer.Metrology; -using BenchmarkDotNet.Extensions; +using Perfolizer.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; namespace BenchmarkDotNet.Environments { @@ -69,6 +69,9 @@ public class HostEnvironmentInfo : BenchmarkEnvironmentInfo // TODO: Join with OsInfo public Lazy VirtualMachineHypervisor { get; protected set; } + private readonly Lazy physicalMemory = new Lazy(SystemMemory.GetPhysicalMemory); + public PhysicalMemoryInfo? PhysicalMemory => physicalMemory.Value; + protected HostEnvironmentInfo() { BenchmarkDotNetVersion = BenchmarkDotNetInfo.Instance.BrandVersion; @@ -95,7 +98,12 @@ public override IEnumerable ToFormattedString() else yield return $"{BenchmarkDotNetCaption} v{BenchmarkDotNetVersion}, {Os.Value.ToBrandString()}"; - yield return Cpu.Value.ToFullBrandName(); + string cpuInfo = Cpu.Value.ToFullBrandName(); + if (PhysicalMemory != null) + yield return $"{cpuInfo}, {PhysicalMemory.ToFormattedString()} RAM"; + else + yield return cpuInfo; + if (HardwareTimerKind != HardwareTimerKind.Unknown) { string frequency = PerfolizerMeasurementFormatter.Instance.Format( diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs new file mode 100644 index 0000000000..d234178c89 --- /dev/null +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -0,0 +1,125 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace BenchmarkDotNet.Environments +{ + public class PhysicalMemoryInfo + { + public long TotalPhysicalBytes { get; } + + public PhysicalMemoryInfo(long totalPhysicalBytes) + { + TotalPhysicalBytes = totalPhysicalBytes; + } + + public string ToFormattedString() + { + double gb = TotalPhysicalBytes / (1024.0 * 1024.0 * 1024.0); + return $"{Math.Round(gb, 2)} GB"; + } + } + + public static class SystemMemory + { + public static PhysicalMemoryInfo? GetPhysicalMemory() + { + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return GetWindowsMemory(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return GetLinuxMemory(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return GetMacMemory(); + } + catch (Exception) + { + + } + + return null; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + private class MEMORYSTATUSEX + { + public uint dwLength; + public uint dwMemoryLoad; + public ulong ullTotalPhys; + public ulong ullAvailPhys; + public ulong ullTotalPageFile; + public ulong ullAvailPageFile; + public ulong ullTotalVirtual; + public ulong ullAvailVirtual; + public ulong ullAvailExtendedVirtual; + + public MEMORYSTATUSEX() + { + dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX)); + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GlobalMemoryStatusEx([In, Out] MEMORYSTATUSEX lpBuffer); + + private static PhysicalMemoryInfo? GetWindowsMemory() + { + var memStatus = new MEMORYSTATUSEX(); + if (GlobalMemoryStatusEx(memStatus)) + { + return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys); + } + return null; + } + + private static PhysicalMemoryInfo? GetLinuxMemory() + { + const string path = "/proc/meminfo"; + if (File.Exists(path)) + { + foreach (var line in File.ReadAllLines(path)) + { + if (line.StartsWith("MemTotal:")) + { + var match = Regex.Match(line, @"\d+"); + if (match.Success && long.TryParse(match.Value, out long kb)) + { + return new PhysicalMemoryInfo(kb * 1024); + } + } + } + } + return null; + } + + private static PhysicalMemoryInfo? GetMacMemory() + { + var info = new ProcessStartInfo("sysctl", "-n hw.memsize") + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = Process.Start(info)) + { + if (process != null) + { + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + if (long.TryParse(output.Trim(), out long bytes)) + { + return new PhysicalMemoryInfo(bytes); + } + } + } + return null; + } + } +} \ No newline at end of file From 4d47b0d132c5a35aad9792be1b300470550d72ea Mon Sep 17 00:00:00 2001 From: abdulrahmanhossam Date: Mon, 2 Mar 2026 03:08:39 +0200 Subject: [PATCH 2/5] Move RAM info to a separate line per maintainer feedback --- .../Environments/HostEnvironmentInfo.cs | 13 ++++++------- .../Builders/HostEnvironmentInfoBuilder.cs | 11 ++++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs b/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs index f8ed25e861..ba053a57b7 100644 --- a/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs +++ b/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs @@ -69,8 +69,7 @@ public class HostEnvironmentInfo : BenchmarkEnvironmentInfo // TODO: Join with OsInfo public Lazy VirtualMachineHypervisor { get; protected set; } - private readonly Lazy physicalMemory = new Lazy(SystemMemory.GetPhysicalMemory); - public PhysicalMemoryInfo? PhysicalMemory => physicalMemory.Value; + public Lazy PhysicalMemory { get; protected set; } protected HostEnvironmentInfo() { @@ -83,6 +82,7 @@ protected HostEnvironmentInfo() VirtualMachineHypervisor = new Lazy(RuntimeInformation.GetVirtualMachineHypervisor); Os = new Lazy(OsDetector.GetOs); Cpu = new Lazy(() => CpuDetector.CrossPlatform.Detect() ?? CpuInfo.Unknown); + PhysicalMemory = new Lazy(SystemMemory.GetPhysicalMemory); } public new static HostEnvironmentInfo GetCurrent() => current ??= new HostEnvironmentInfo(); @@ -98,11 +98,10 @@ public override IEnumerable ToFormattedString() else yield return $"{BenchmarkDotNetCaption} v{BenchmarkDotNetVersion}, {Os.Value.ToBrandString()}"; - string cpuInfo = Cpu.Value.ToFullBrandName(); - if (PhysicalMemory != null) - yield return $"{cpuInfo}, {PhysicalMemory.ToFormattedString()} RAM"; - else - yield return cpuInfo; + yield return Cpu.Value.ToFullBrandName(); + + if (PhysicalMemory.Value != null) + yield return $"Memory: {PhysicalMemory.Value.ToFormattedString()}"; if (HardwareTimerKind != HardwareTimerKind.Unknown) { diff --git a/tests/BenchmarkDotNet.Tests/Builders/HostEnvironmentInfoBuilder.cs b/tests/BenchmarkDotNet.Tests/Builders/HostEnvironmentInfoBuilder.cs index e2965cd2b1..a9ef3c9e6f 100644 --- a/tests/BenchmarkDotNet.Tests/Builders/HostEnvironmentInfoBuilder.cs +++ b/tests/BenchmarkDotNet.Tests/Builders/HostEnvironmentInfoBuilder.cs @@ -1,10 +1,10 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using BenchmarkDotNet.Environments; +using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Portability; using Perfolizer.Horology; using Perfolizer.Models; +using System; +using System.Diagnostics.CodeAnalysis; namespace BenchmarkDotNet.Tests.Builders { @@ -60,7 +60,7 @@ public HostEnvironmentInfo Build() { return new MockHostEnvironmentInfo(architecture, benchmarkDotNetVersion, chronometerFrequency, configuration, dotNetSdkVersion, hardwareTimerKind, hasAttachedDebugger, hasRyuJit, isConcurrentGC, isServerGC, - jitInfo, jitModules, os, cpu, runtimeVersion, virtualMachineHypervisor); + jitInfo, jitModules, os, cpu, runtimeVersion, virtualMachineHypervisor, null); } } @@ -70,7 +70,7 @@ public MockHostEnvironmentInfo( string architecture, string benchmarkDotNetVersion, Frequency chronometerFrequency, string configuration, string dotNetSdkVersion, HardwareTimerKind hardwareTimerKind, bool hasAttachedDebugger, bool hasRyuJit, bool isConcurrentGC, bool isServerGC, string jitInfo, string jitModules, OsInfo os, CpuInfo cpu, - string runtimeVersion, VirtualMachineHypervisor? virtualMachineHypervisor) + string runtimeVersion, VirtualMachineHypervisor? virtualMachineHypervisor, PhysicalMemoryInfo? physicalMemory = null) { Architecture = architecture; BenchmarkDotNetVersion = benchmarkDotNetVersion; @@ -88,6 +88,7 @@ public MockHostEnvironmentInfo( Cpu = new Lazy(() => cpu); RuntimeVersion = runtimeVersion; VirtualMachineHypervisor = new Lazy(() => virtualMachineHypervisor); + PhysicalMemory = new Lazy(() => physicalMemory); } } } \ No newline at end of file From 3eab1f20de4e159c76670fbc9d11a5f8a162e4c5 Mon Sep 17 00:00:00 2001 From: abdulrahmanhossam Date: Mon, 2 Mar 2026 03:16:10 +0200 Subject: [PATCH 3/5] Include Available/Free memory in environment summary --- .../Environments/PhysicalMemoryInfo.cs | 71 +++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index d234178c89..2c06acdda4 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -9,16 +9,25 @@ namespace BenchmarkDotNet.Environments public class PhysicalMemoryInfo { public long TotalPhysicalBytes { get; } + public long? AvailablePhysicalBytes { get; } - public PhysicalMemoryInfo(long totalPhysicalBytes) + public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null) { TotalPhysicalBytes = totalPhysicalBytes; + AvailablePhysicalBytes = availablePhysicalBytes; } public string ToFormattedString() { - double gb = TotalPhysicalBytes / (1024.0 * 1024.0 * 1024.0); - return $"{Math.Round(gb, 2)} GB"; + double totalGb = TotalPhysicalBytes / (1024.0 * 1024.0 * 1024.0); + + if (AvailablePhysicalBytes.HasValue) + { + double availableGb = AvailablePhysicalBytes.Value / (1024.0 * 1024.0 * 1024.0); + return $"{Math.Round(totalGb, 2)} GB Total, {Math.Round(availableGb, 2)} GB Available"; + } + + return $"{Math.Round(totalGb, 2)} GB"; } } @@ -39,7 +48,7 @@ public static class SystemMemory } catch (Exception) { - + // Ignore errors } return null; @@ -73,7 +82,7 @@ public MEMORYSTATUSEX() var memStatus = new MEMORYSTATUSEX(); if (GlobalMemoryStatusEx(memStatus)) { - return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys); + return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys); } return null; } @@ -83,43 +92,79 @@ public MEMORYSTATUSEX() const string path = "/proc/meminfo"; if (File.Exists(path)) { + long total = 0; + long? available = null; + foreach (var line in File.ReadAllLines(path)) { if (line.StartsWith("MemTotal:")) { var match = Regex.Match(line, @"\d+"); if (match.Success && long.TryParse(match.Value, out long kb)) - { - return new PhysicalMemoryInfo(kb * 1024); - } + total = kb * 1024; + } + else if (line.StartsWith("MemAvailable:") || line.StartsWith("MemFree:")) + { + var match = Regex.Match(line, @"\d+"); + if (match.Success && long.TryParse(match.Value, out long kb) && available == null) + available = kb * 1024; } } + + if (total > 0) + return new PhysicalMemoryInfo(total, available); } return null; } private static PhysicalMemoryInfo? GetMacMemory() { - var info = new ProcessStartInfo("sysctl", "-n hw.memsize") + long total = 0; + long? available = null; + + // 1. Get Total Memory + var sysctlInfo = new ProcessStartInfo("sysctl", "-n hw.memsize") + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = Process.Start(sysctlInfo)) + { + if (process != null) + { + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + long.TryParse(output.Trim(), out total); + } + } + + if (total == 0) return null; + + // 2. Get Free Memory using vm_stat + var vmStatInfo = new ProcessStartInfo("vm_stat") { RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; - using (var process = Process.Start(info)) + using (var process = Process.Start(vmStatInfo)) { if (process != null) { string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); - if (long.TryParse(output.Trim(), out long bytes)) + var match = Regex.Match(output, @"Pages free:\s+(\d+)"); + if (match.Success && long.TryParse(match.Groups[1].Value, out long pagesFree)) { - return new PhysicalMemoryInfo(bytes); + available = pagesFree * 4096; } } } - return null; + + return new PhysicalMemoryInfo(total, available); } } } \ No newline at end of file From fa6633c1c82e95bc03419613edc4f00bb5a8b585 Mon Sep 17 00:00:00 2001 From: abdulrahmanhossam Date: Mon, 2 Mar 2026 03:47:37 +0200 Subject: [PATCH 4/5] Move RAM info after CPU frequency per code review --- src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs b/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs index ba053a57b7..aa20faa14c 100644 --- a/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs +++ b/src/BenchmarkDotNet/Environments/HostEnvironmentInfo.cs @@ -100,9 +100,6 @@ public override IEnumerable ToFormattedString() yield return Cpu.Value.ToFullBrandName(); - if (PhysicalMemory.Value != null) - yield return $"Memory: {PhysicalMemory.Value.ToFormattedString()}"; - if (HardwareTimerKind != HardwareTimerKind.Unknown) { string frequency = PerfolizerMeasurementFormatter.Instance.Format( @@ -116,6 +113,9 @@ public override IEnumerable ToFormattedString() yield return $"Frequency: {frequency}, Resolution: {resolution}, Timer: {timer}"; } + if (PhysicalMemory.Value != null) + yield return $"Memory: {PhysicalMemory.Value.ToFormattedString()}"; + if (RuntimeInformation.IsNetCore && IsDotNetCliInstalled()) { // this wonderful version number contains words like "preview" and ... 5 segments, so it can not be parsed by Version.Parse. Example: "5.0.100-preview.8.20362.3" From 2ecaaeaf53448ee8778f2bbc4be4d2b9ace6c2ab Mon Sep 17 00:00:00 2001 From: abdulrahmanhossam Date: Mon, 2 Mar 2026 05:13:07 +0200 Subject: [PATCH 5/5] Parse macOS page size dynamically to support ARM64 runners --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 2c06acdda4..37a0d94433 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -156,10 +156,18 @@ public MEMORYSTATUSEX() { string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); + + 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; + } + var match = Regex.Match(output, @"Pages free:\s+(\d+)"); if (match.Success && long.TryParse(match.Groups[1].Value, out long pagesFree)) { - available = pagesFree * 4096; + available = pagesFree * pageSize; } } }