From bab81cb96db44087973fd60aefd7bcfc03978d18 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 2 Oct 2026 05:29:10 -0600 Subject: [PATCH 01/18] feat: Add RAM Frequency and Type to Environment Summary (#3041) --- .../Environments/PhysicalMemoryInfo.cs | 70 +++++++++++++++++-- .../Environments/PhysicalMemoryInfoTests.cs | 17 +++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 114836c438..48dca14c1b 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -1,7 +1,11 @@ using BenchmarkDotNet.Detectors; +using BenchmarkDotNet.Extensions; +using System; using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using System.Text; using System.Text.RegularExpressions; using Windows.Win32; using Windows.Win32.System.SystemInformation; @@ -12,24 +16,44 @@ public class PhysicalMemoryInfo { public long TotalPhysicalBytes { get; } public long? AvailablePhysicalBytes { get; } + public long? FrequencyMHz { get; } + public string? MemoryType { get; } - public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null) + public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null, long? frequencyMHz = null, string? memoryType = null) { TotalPhysicalBytes = totalPhysicalBytes; AvailablePhysicalBytes = availablePhysicalBytes; + FrequencyMHz = frequencyMHz; + MemoryType = memoryType; } public string ToFormattedString() { double totalGb = TotalPhysicalBytes / (1024.0 * 1024.0 * 1024.0); + var sb = new StringBuilder(); + sb.Append($"{Math.Round(totalGb, 2)} GB"); + + if (MemoryType.IsNotBlank() || FrequencyMHz.HasValue) + { + sb.Append(" ("); + if (MemoryType.IsNotBlank()) + sb.Append(MemoryType); + if (FrequencyMHz.HasValue) + { + if (MemoryType.IsNotBlank()) + sb.Append('-'); + sb.Append($"{FrequencyMHz}MHz"); + } + sb.Append(')'); + } 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"; + sb.Append($", {Math.Round(availableGb, 2)} GB Available"); } - return $"{Math.Round(totalGb, 2)} GB"; + return sb.ToString(); } } @@ -61,7 +85,10 @@ public static class SystemMemory { var memStatus = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf() }; if (PInvoke.GlobalMemoryStatusEx(ref memStatus)) - return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys); + { + var (freq, type) = GetLinuxOrWindowsMemoryDetails(); + return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys, freq, type); + } return null; } @@ -91,11 +118,44 @@ public static class SystemMemory } if (total > 0) - return new PhysicalMemoryInfo(total, available); + { + var (freq, type) = GetLinuxOrWindowsMemoryDetails(); + return new PhysicalMemoryInfo(total, available, freq, type); + } } return null; } + private static (long? FrequencyMHz, string? MemoryType) GetLinuxOrWindowsMemoryDetails() + { + try + { + // Try reading /sys/devices/system/edac/mc/ on Linux + if (Directory.Exists("/sys/devices/system/edac/mc")) + { + foreach (var mcDir in Directory.GetDirectories("/sys/devices/system/edac/mc", "mc*")) + { + foreach (var dimmDir in Directory.GetDirectories(mcDir, "dimm*")) + { + string typePath = Path.Combine(dimmDir, "dimm_mem_type"); + if (File.Exists(typePath)) + { + string memType = File.ReadAllText(typePath).Trim(); + if (!string.IsNullOrEmpty(memType) && memType != "Unspecified") + return (null, memType); + } + } + } + } + } + catch + { + // Ignore fallback failures + } + + return (null, null); + } + private static PhysicalMemoryInfo? GetMacMemory() { long total = 0; diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs new file mode 100644 index 0000000000..490f26670b --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -0,0 +1,17 @@ +using BenchmarkDotNet.Environments; +using Xunit; + +namespace BenchmarkDotNet.Tests.Environments +{ + public class PhysicalMemoryInfoTests + { + [Fact] + public void PhysicalMemoryInfo_ParsesAndConstructsCorrectly() + { + var memInfo = new PhysicalMemoryInfo(16000000000, null, 3200, "DDR4"); + Assert.Equal(16000000000L, memInfo.TotalPhysicalBytes); + Assert.Equal(3200L, memInfo.FrequencyMHz); + Assert.Equal("DDR4", memInfo.MemoryType); + } + } +} From 207e88f0e53757b47206d59bd775ef5daf8c548e Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 2 Oct 2026 11:20:56 -0600 Subject: [PATCH 02/18] feat: Add SMBIOS GetSystemFirmwareTable P/Invoke for Windows memory details --- .../Environments/PhysicalMemoryInfo.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 48dca14c1b..7a1a2cd47c 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -130,6 +130,11 @@ private static (long? FrequencyMHz, string? MemoryType) GetLinuxOrWindowsMemoryD { try { + if (OsDetector.IsWindows7OrLater()) + { + return GetWindowsSmbiosMemoryDetails(); + } + // Try reading /sys/devices/system/edac/mc/ on Linux if (Directory.Exists("/sys/devices/system/edac/mc")) { @@ -156,6 +161,97 @@ private static (long? FrequencyMHz, string? MemoryType) GetLinuxOrWindowsMemoryD return (null, null); } + [SupportedOSPlatform("windows5.1.2600")] + private static (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosMemoryDetails() + { + try + { + const uint rsMb = 0x52534D42; // 'RSMB' + uint bufferSize = NativeMethods.GetSystemFirmwareTable(rsMb, 0, IntPtr.Zero, 0); + if (bufferSize == 0) + return (null, null); + + byte[] buffer = new byte[bufferSize]; + if (NativeMethods.GetSystemFirmwareTable(rsMb, 0, buffer, bufferSize) == 0) + return (null, null); + + // SMBIOS table header: Data at offset 8 + int offset = 8; + long? maxSpeed = null; + string? detectedType = null; + + while (offset + 4 <= buffer.Length) + { + byte type = buffer[offset]; + byte length = buffer[offset + 1]; + + if (length < 4 || offset + length > buffer.Length) + break; + + // Type 17: Memory Device + if (type == 17 && length >= 0x15) + { + // Memory Type at offset 0x12 + byte rawMemoryType = buffer[offset + 0x12]; + string? memTypeStr = MapSmbiosMemoryType(rawMemoryType); + if (memTypeStr != null && detectedType == null) + { + detectedType = memTypeStr; + } + + // Speed in MHz at offset 0x15 (WORD) + if (length >= 0x17) + { + ushort speed = (ushort)(buffer[offset + 0x15] | (buffer[offset + 0x16] << 8)); + if (speed > 0 && speed < 0xFFFF) + { + if (maxSpeed == null || speed > maxSpeed.Value) + { + maxSpeed = speed; + } + } + } + } + + // Move past formatted area + offset += length; + + // Skip unformatted string section (double null-terminated) + while (offset < buffer.Length - 1 && !(buffer[offset] == 0 && buffer[offset + 1] == 0)) + { + offset++; + } + offset += 2; + } + + return (maxSpeed, detectedType); + } + catch + { + return (null, null); + } + } + + private static class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint GetSystemFirmwareTable(uint firmwareTableProviderSignature, uint firmwareTableID, byte[] pFirmwareTableBuffer, uint bufferSize); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint GetSystemFirmwareTable(uint firmwareTableProviderSignature, uint firmwareTableID, IntPtr pFirmwareTableBuffer, uint bufferSize); + } + + private static string? MapSmbiosMemoryType(byte type) => type switch + { + 0x1A => "DDR3", + 0x1A + 1 => "DDR4", + 0x1A + 2 => "LPDDR3", + 0x1A + 3 => "LPDDR4", + 0x22 => "DDR5", + 0x23 => "LPDDR5", + _ => null + }; + private static PhysicalMemoryInfo? GetMacMemory() { long total = 0; From 05eaa816397a3a41d07d4a808038393417db926a Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 2 Oct 2026 11:57:04 -0600 Subject: [PATCH 03/18] refactor: Use CsWin32 generated PInvoke GetSystemFirmwareTable --- .../Environments/PhysicalMemoryInfo.cs | 25 ++++++++----------- src/BenchmarkDotNet/NativeMethods.txt | 1 + 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 7a1a2cd47c..eaa37b5af4 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -80,7 +80,7 @@ public static class SystemMemory return null; } - [SupportedOSPlatform("windows5.1.2600")] + [SupportedOSPlatform("windows6.0.6000")] private static PhysicalMemoryInfo? GetWindowsMemory() { var memStatus = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf() }; @@ -161,19 +161,23 @@ private static (long? FrequencyMHz, string? MemoryType) GetLinuxOrWindowsMemoryD return (null, null); } - [SupportedOSPlatform("windows5.1.2600")] - private static (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosMemoryDetails() + [SupportedOSPlatform("windows6.0.6000")] + private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosMemoryDetails() { try { const uint rsMb = 0x52534D42; // 'RSMB' - uint bufferSize = NativeMethods.GetSystemFirmwareTable(rsMb, 0, IntPtr.Zero, 0); + var provider = (FIRMWARE_TABLE_PROVIDER)rsMb; + uint bufferSize = PInvoke.GetSystemFirmwareTable(provider, rsMb, null, 0); if (bufferSize == 0) return (null, null); byte[] buffer = new byte[bufferSize]; - if (NativeMethods.GetSystemFirmwareTable(rsMb, 0, buffer, bufferSize) == 0) - return (null, null); + fixed (byte* pBuffer = buffer) + { + if (PInvoke.GetSystemFirmwareTable(provider, rsMb, pBuffer, bufferSize) == 0) + return (null, null); + } // SMBIOS table header: Data at offset 8 int offset = 8; @@ -232,15 +236,6 @@ private static (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosMemoryDe } } - private static class NativeMethods - { - [DllImport("kernel32.dll", SetLastError = true)] - public static extern uint GetSystemFirmwareTable(uint firmwareTableProviderSignature, uint firmwareTableID, byte[] pFirmwareTableBuffer, uint bufferSize); - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern uint GetSystemFirmwareTable(uint firmwareTableProviderSignature, uint firmwareTableID, IntPtr pFirmwareTableBuffer, uint bufferSize); - } - private static string? MapSmbiosMemoryType(byte type) => type switch { 0x1A => "DDR3", diff --git a/src/BenchmarkDotNet/NativeMethods.txt b/src/BenchmarkDotNet/NativeMethods.txt index c687e02914..5b0e9f3d13 100644 --- a/src/BenchmarkDotNet/NativeMethods.txt +++ b/src/BenchmarkDotNet/NativeMethods.txt @@ -13,3 +13,4 @@ PowerGetActiveScheme LocalFree GlobalMemoryStatusEx GetLogicalProcessorInformationEx +GetSystemFirmwareTable From 0b9b9a5301e696b60aa2b7f39a3bbbd4cca89cf2 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 2 Oct 2026 12:31:30 -0600 Subject: [PATCH 04/18] fix: Correct SMBIOS memory type enumeration mapping (DDR3=0x18, DDR4=0x1A) --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index eaa37b5af4..87c45001d7 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -238,10 +238,10 @@ private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosM private static string? MapSmbiosMemoryType(byte type) => type switch { - 0x1A => "DDR3", - 0x1A + 1 => "DDR4", - 0x1A + 2 => "LPDDR3", - 0x1A + 3 => "LPDDR4", + 0x18 => "DDR3", + 0x1A => "DDR4", + 0x1B => "LPDDR3", + 0x1C => "LPDDR4", 0x22 => "DDR5", 0x23 => "LPDDR5", _ => null From cb20cab9c8ac5f82f0854fd60852217e43247ebe Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 2 Oct 2026 14:18:05 -0600 Subject: [PATCH 05/18] fix: Pass 0 for FirmwareTableID in GetSystemFirmwareTable to parse full raw SMBIOS table --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 87c45001d7..9cece28471 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -168,14 +168,14 @@ private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosM { const uint rsMb = 0x52534D42; // 'RSMB' var provider = (FIRMWARE_TABLE_PROVIDER)rsMb; - uint bufferSize = PInvoke.GetSystemFirmwareTable(provider, rsMb, null, 0); + uint bufferSize = PInvoke.GetSystemFirmwareTable(provider, 0, null, 0); if (bufferSize == 0) return (null, null); byte[] buffer = new byte[bufferSize]; fixed (byte* pBuffer = buffer) { - if (PInvoke.GetSystemFirmwareTable(provider, rsMb, pBuffer, bufferSize) == 0) + if (PInvoke.GetSystemFirmwareTable(provider, 0, pBuffer, bufferSize) == 0) return (null, null); } From ab023a61cddad78a8d80d2bac40a75ff0c3d6816 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 2 Oct 2026 14:25:38 -0600 Subject: [PATCH 06/18] fix: Update SMBIOS LPDDR, MRDIMM and LPDDR6 memory type mappings per DSP0134 spec --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 9cece28471..da6899958e 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -240,10 +240,14 @@ private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosM { 0x18 => "DDR3", 0x1A => "DDR4", - 0x1B => "LPDDR3", - 0x1C => "LPDDR4", + 0x1B => "LPDDR", + 0x1C => "LPDDR2", + 0x1D => "LPDDR3", + 0x1E => "LPDDR4", 0x22 => "DDR5", 0x23 => "LPDDR5", + 0x25 => "MRDIMM", + 0x26 => "LPDDR6", _ => null }; From f80c348ef64bb762e0d0722deeac45cb345d91b4 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Fri, 2 Oct 2026 14:29:25 -0600 Subject: [PATCH 07/18] fix: Expand SMBIOS memory type mappings per DMTF DSP0134 3.10.0 spec --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index da6899958e..5a16957f04 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -238,14 +238,19 @@ private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosM private static string? MapSmbiosMemoryType(byte type) => type switch { + 0x12 => "DDR", + 0x13 => "DDR2", 0x18 => "DDR3", 0x1A => "DDR4", 0x1B => "LPDDR", 0x1C => "LPDDR2", 0x1D => "LPDDR3", 0x1E => "LPDDR4", + 0x20 => "HBM", + 0x21 => "HBM2", 0x22 => "DDR5", 0x23 => "LPDDR5", + 0x24 => "HBM3", 0x25 => "MRDIMM", 0x26 => "LPDDR6", _ => null From 79ded607c0d7637134a75b33c20af2d6fe503f72 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sat, 10 Oct 2026 02:26:23 -0600 Subject: [PATCH 08/18] feat: Add RAM frequency and memory type detection for macOS Implement GetMacMemoryDetails() using system_profiler SPMemoryDataType to extract memory speed and type information on macOS, matching Windows and Linux implementations. Addresses feedback from @timcassell on PR #3281. Co-Authored-By: Claude Haiku 4.5 --- .../Environments/PhysicalMemoryInfo.cs | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 5a16957f04..c55606ac1d 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -311,7 +311,57 @@ private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosM } } - return new PhysicalMemoryInfo(total, available); + var (freq, type) = GetMacMemoryDetails(); + return new PhysicalMemoryInfo(total, available, freq, type); + } + + private static (long? FrequencyMHz, string? MemoryType) GetMacMemoryDetails() + { + try + { + // Try system_profiler for detailed memory info + var profilerInfo = new ProcessStartInfo("system_profiler", "SPMemoryDataType") + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = Process.Start(profilerInfo)) + { + if (process != null) + { + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + long? freq = null; + string? type = null; + + // Pattern: "Speed: 6400 MHz" or "Size: 24 GB" + var speedMatch = Regex.Match(output, @"Speed:\s+(\d+)\s*MHz"); + if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speed)) + { + freq = speed; + } + + // Pattern: "Type: DDR5" or "Type: LPDDR5" + var typeMatch = Regex.Match(output, @"Type:\s+([A-Z0-9]+)"); + if (typeMatch.Success) + { + type = typeMatch.Groups[1].Value; + } + + if (freq.HasValue || type != null) + return (freq, type); + } + } + } + catch + { + // Fallback + } + + return (null, null); } } } \ No newline at end of file From 1eb536765591ac60d3a97a725500c35edd583f7c Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sat, 10 Oct 2026 02:41:44 -0600 Subject: [PATCH 09/18] refactor: Separate macOS memory specs extraction into GetMacMemorySpecs() Extract frequency and memory type detection into a dedicated method to match the architectural pattern used by Windows (GetWindowsSmbiosMemoryDetails) and Linux (GetLinuxOrWindowsMemoryDetails). Addresses feedback from @timcassell that implementation should use separate methods. Co-Authored-By: Claude Haiku 4.5 --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index c55606ac1d..4702137147 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -311,15 +311,14 @@ private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosM } } - var (freq, type) = GetMacMemoryDetails(); + var (freq, type) = GetMacMemorySpecs(); return new PhysicalMemoryInfo(total, available, freq, type); } - private static (long? FrequencyMHz, string? MemoryType) GetMacMemoryDetails() + private static (long? Frequency, string? Type) GetMacMemorySpecs() { try { - // Try system_profiler for detailed memory info var profilerInfo = new ProcessStartInfo("system_profiler", "SPMemoryDataType") { RedirectStandardOutput = true, @@ -337,22 +336,19 @@ private static (long? FrequencyMHz, string? MemoryType) GetMacMemoryDetails() long? freq = null; string? type = null; - // Pattern: "Speed: 6400 MHz" or "Size: 24 GB" var speedMatch = Regex.Match(output, @"Speed:\s+(\d+)\s*MHz"); if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speed)) { freq = speed; } - // Pattern: "Type: DDR5" or "Type: LPDDR5" var typeMatch = Regex.Match(output, @"Type:\s+([A-Z0-9]+)"); if (typeMatch.Success) { type = typeMatch.Groups[1].Value; } - if (freq.HasValue || type != null) - return (freq, type); + return (freq, type); } } } From e3e745148e1d39938d5611e3dfb30bfe21c713ed Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sat, 10 Oct 2026 04:39:49 -0600 Subject: [PATCH 10/18] refactor: Use ProcessHelper to avoid deadlock on large stdout buffers Use ProcessHelper.RunAndReadOutput instead of manual ProcessStartInfo calls in GetMacMemory() and GetMacMemorySpecs() to avoid potential deadlock when system_profiler output exceeds the stdout buffer size. Co-Authored-By: Claude Haiku 4.5 --- .../Environments/PhysicalMemoryInfo.cs | 98 ++++++------------- 1 file changed, 30 insertions(+), 68 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 4702137147..daee2d67da 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using System; using System.Diagnostics; using System.IO; @@ -262,52 +263,25 @@ private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosM long? available = null; // 1. Get Total Memory - var sysctlInfo = new ProcessStartInfo("sysctl", "-n hw.memsize") - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; + string? sysctlOutput = ProcessHelper.RunAndReadOutput("sysctl", "-n hw.memsize"); + if (string.IsNullOrEmpty(sysctlOutput) || !long.TryParse(sysctlOutput!.Trim(), out total) || total == 0) + return null; - using (var process = Process.Start(sysctlInfo)) + // 2. Get Free Memory using vm_stat + string? vmStatOutput = ProcessHelper.RunAndReadOutput("vm_stat"); + if (!string.IsNullOrEmpty(vmStatOutput)) { - if (process != null) + 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)) { - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - long.TryParse(output.Trim(), out total); + pageSize = parsedPageSize; } - } - - 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(vmStatInfo)) - { - if (process != null) + var match = Regex.Match(vmStatOutput, @"Pages free:\s+(\d+)"); + if (match.Success && long.TryParse(match.Groups[1].Value, out long pagesFree)) { - 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 * pageSize; - } + available = pagesFree * pageSize; } } @@ -319,38 +293,26 @@ private static (long? Frequency, string? Type) GetMacMemorySpecs() { try { - var profilerInfo = new ProcessStartInfo("system_profiler", "SPMemoryDataType") - { - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using (var process = Process.Start(profilerInfo)) - { - if (process != null) - { - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - long? freq = null; - string? type = null; + string? output = ProcessHelper.RunAndReadOutput("system_profiler", "SPMemoryDataType"); + if (string.IsNullOrEmpty(output)) + return (null, null); - var speedMatch = Regex.Match(output, @"Speed:\s+(\d+)\s*MHz"); - if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speed)) - { - freq = speed; - } + long? freq = null; + string? type = null; - var typeMatch = Regex.Match(output, @"Type:\s+([A-Z0-9]+)"); - if (typeMatch.Success) - { - type = typeMatch.Groups[1].Value; - } + var speedMatch = Regex.Match(output, @"Speed:\s+(\d+)\s*MHz"); + if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speed)) + { + freq = speed; + } - return (freq, type); - } + var typeMatch = Regex.Match(output, @"Type:\s+([A-Z0-9]+)"); + if (typeMatch.Success) + { + type = typeMatch.Groups[1].Value; } + + return (freq, type); } catch { From b49d075158fb5b00bec78e2b2ed41da8ffd8f7aa Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sat, 10 Oct 2026 16:58:41 -0600 Subject: [PATCH 11/18] refactor: Change frequency unit from MHz to MT/s per SMBIOS 2.3+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert MHz to MT/s (Megatransfers per second) for DDR memory types - Update FrequencyMHz property to FrequencyMts - Apply 2x multiplier: MT/s = MHz × 2 for DDR memory - Remove "Unspecified" filter from Linux memory type detection - Update unit label in output from MHz to MT/s Co-Authored-By: Claude Haiku 4.5 --- .../Environments/PhysicalMemoryInfo.cs | 33 ++++++++++--------- .../Environments/PhysicalMemoryInfoTests.cs | 4 +-- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index daee2d67da..4b3ffc978b 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -17,14 +17,14 @@ public class PhysicalMemoryInfo { public long TotalPhysicalBytes { get; } public long? AvailablePhysicalBytes { get; } - public long? FrequencyMHz { get; } + public long? FrequencyMts { get; } public string? MemoryType { get; } - public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null, long? frequencyMHz = null, string? memoryType = null) + public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null, long? frequencyMts = null, string? memoryType = null) { TotalPhysicalBytes = totalPhysicalBytes; AvailablePhysicalBytes = availablePhysicalBytes; - FrequencyMHz = frequencyMHz; + FrequencyMts = frequencyMts; MemoryType = memoryType; } @@ -34,16 +34,16 @@ public string ToFormattedString() var sb = new StringBuilder(); sb.Append($"{Math.Round(totalGb, 2)} GB"); - if (MemoryType.IsNotBlank() || FrequencyMHz.HasValue) + if (MemoryType.IsNotBlank() || FrequencyMts.HasValue) { sb.Append(" ("); if (MemoryType.IsNotBlank()) sb.Append(MemoryType); - if (FrequencyMHz.HasValue) + if (FrequencyMts.HasValue) { if (MemoryType.IsNotBlank()) sb.Append('-'); - sb.Append($"{FrequencyMHz}MHz"); + sb.Append($"{FrequencyMts}MT/s"); } sb.Append(')'); } @@ -127,7 +127,7 @@ public static class SystemMemory return null; } - private static (long? FrequencyMHz, string? MemoryType) GetLinuxOrWindowsMemoryDetails() + private static (long? FrequencyMts, string? MemoryType) GetLinuxOrWindowsMemoryDetails() { try { @@ -147,7 +147,7 @@ private static (long? FrequencyMHz, string? MemoryType) GetLinuxOrWindowsMemoryD if (File.Exists(typePath)) { string memType = File.ReadAllText(typePath).Trim(); - if (!string.IsNullOrEmpty(memType) && memType != "Unspecified") + if (!string.IsNullOrEmpty(memType)) return (null, memType); } } @@ -163,7 +163,7 @@ private static (long? FrequencyMHz, string? MemoryType) GetLinuxOrWindowsMemoryD } [SupportedOSPlatform("windows6.0.6000")] - private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosMemoryDetails() + private static unsafe (long? FrequencyMts, string? MemoryType) GetWindowsSmbiosMemoryDetails() { try { @@ -204,15 +204,16 @@ private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosM detectedType = memTypeStr; } - // Speed in MHz at offset 0x15 (WORD) + // Speed in MHz at offset 0x15 (WORD), convert to MT/s if (length >= 0x17) { - ushort speed = (ushort)(buffer[offset + 0x15] | (buffer[offset + 0x16] << 8)); - if (speed > 0 && speed < 0xFFFF) + ushort speedMhz = (ushort)(buffer[offset + 0x15] | (buffer[offset + 0x16] << 8)); + if (speedMhz > 0 && speedMhz < 0xFFFF) { - if (maxSpeed == null || speed > maxSpeed.Value) + long speedMts = speedMhz * 2; // DDR: MT/s = MHz × 2 + if (maxSpeed == null || speedMts > maxSpeed.Value) { - maxSpeed = speed; + maxSpeed = speedMts; } } } @@ -301,9 +302,9 @@ private static (long? Frequency, string? Type) GetMacMemorySpecs() string? type = null; var speedMatch = Regex.Match(output, @"Speed:\s+(\d+)\s*MHz"); - if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speed)) + if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speedMhz)) { - freq = speed; + freq = speedMhz * 2; // Convert MHz to MT/s } var typeMatch = Regex.Match(output, @"Type:\s+([A-Z0-9]+)"); diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs index 490f26670b..a979b73b27 100644 --- a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -8,9 +8,9 @@ public class PhysicalMemoryInfoTests [Fact] public void PhysicalMemoryInfo_ParsesAndConstructsCorrectly() { - var memInfo = new PhysicalMemoryInfo(16000000000, null, 3200, "DDR4"); + var memInfo = new PhysicalMemoryInfo(16000000000, null, 6400, "DDR4"); Assert.Equal(16000000000L, memInfo.TotalPhysicalBytes); - Assert.Equal(3200L, memInfo.FrequencyMHz); + Assert.Equal(6400L, memInfo.FrequencyMts); Assert.Equal("DDR4", memInfo.MemoryType); } } From 891ef4f78731ccdc58477b5003d5c03f39ba4435 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sat, 10 Oct 2026 17:11:40 -0600 Subject: [PATCH 12/18] fix: Improve memory type regex to capture full type string Use non-whitespace pattern (\S+) instead of uppercase-only pattern to properly capture memory type strings like UDIMM, LRDIMM, etc. Co-Authored-By: Claude Haiku 4.5 --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 4b3ffc978b..de8875bd76 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -307,7 +307,7 @@ private static (long? Frequency, string? Type) GetMacMemorySpecs() freq = speedMhz * 2; // Convert MHz to MT/s } - var typeMatch = Regex.Match(output, @"Type:\s+([A-Z0-9]+)"); + var typeMatch = Regex.Match(output, @"Type:\s+(\S+)"); if (typeMatch.Success) { type = typeMatch.Groups[1].Value; From 20a7f2f2f4e42bc58946365e0458300233226ec8 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sat, 10 Oct 2026 17:41:31 -0600 Subject: [PATCH 13/18] feat: Add memory timing (CAS latency) detection from SMBIOS - Add CasLatency property to PhysicalMemoryInfo - Extract CAS latency from SMBIOS Type 17 offset 0x1C on Windows - Display in format: 8GB (DDR4-6400MT/s-CAS16) - Update unit tests to verify CAS latency property Co-Authored-By: Claude Haiku 4.5 --- .../Environments/PhysicalMemoryInfo.cs | 49 +++++++++++++------ .../Environments/PhysicalMemoryInfoTests.cs | 3 +- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index de8875bd76..7e529e9aea 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -19,13 +19,15 @@ public class PhysicalMemoryInfo public long? AvailablePhysicalBytes { get; } public long? FrequencyMts { get; } public string? MemoryType { get; } + public byte? CasLatency { get; } - public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null, long? frequencyMts = null, string? memoryType = null) + public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null, long? frequencyMts = null, string? memoryType = null, byte? casLatency = null) { TotalPhysicalBytes = totalPhysicalBytes; AvailablePhysicalBytes = availablePhysicalBytes; FrequencyMts = frequencyMts; MemoryType = memoryType; + CasLatency = casLatency; } public string ToFormattedString() @@ -34,7 +36,7 @@ public string ToFormattedString() var sb = new StringBuilder(); sb.Append($"{Math.Round(totalGb, 2)} GB"); - if (MemoryType.IsNotBlank() || FrequencyMts.HasValue) + if (MemoryType.IsNotBlank() || FrequencyMts.HasValue || CasLatency.HasValue) { sb.Append(" ("); if (MemoryType.IsNotBlank()) @@ -45,6 +47,12 @@ public string ToFormattedString() sb.Append('-'); sb.Append($"{FrequencyMts}MT/s"); } + if (CasLatency.HasValue) + { + if (MemoryType.IsNotBlank() || FrequencyMts.HasValue) + sb.Append('-'); + sb.Append($"CAS{CasLatency}"); + } sb.Append(')'); } @@ -87,8 +95,8 @@ public static class SystemMemory var memStatus = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf() }; if (PInvoke.GlobalMemoryStatusEx(ref memStatus)) { - var (freq, type) = GetLinuxOrWindowsMemoryDetails(); - return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys, freq, type); + var (freq, type, cas) = GetLinuxOrWindowsMemoryDetails(); + return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys, freq, type, cas); } return null; @@ -120,14 +128,14 @@ public static class SystemMemory if (total > 0) { - var (freq, type) = GetLinuxOrWindowsMemoryDetails(); - return new PhysicalMemoryInfo(total, available, freq, type); + var (freq, type, cas) = GetLinuxOrWindowsMemoryDetails(); + return new PhysicalMemoryInfo(total, available, freq, type, cas); } } return null; } - private static (long? FrequencyMts, string? MemoryType) GetLinuxOrWindowsMemoryDetails() + private static (long? FrequencyMts, string? MemoryType, byte? CasLatency) GetLinuxOrWindowsMemoryDetails() { try { @@ -148,7 +156,7 @@ private static (long? FrequencyMts, string? MemoryType) GetLinuxOrWindowsMemoryD { string memType = File.ReadAllText(typePath).Trim(); if (!string.IsNullOrEmpty(memType)) - return (null, memType); + return (null, memType, null); } } } @@ -159,11 +167,11 @@ private static (long? FrequencyMts, string? MemoryType) GetLinuxOrWindowsMemoryD // Ignore fallback failures } - return (null, null); + return (null, null, null); } [SupportedOSPlatform("windows6.0.6000")] - private static unsafe (long? FrequencyMts, string? MemoryType) GetWindowsSmbiosMemoryDetails() + private static unsafe (long? FrequencyMts, string? MemoryType, byte? CasLatency) GetWindowsSmbiosMemoryDetails() { try { @@ -171,19 +179,20 @@ private static unsafe (long? FrequencyMts, string? MemoryType) GetWindowsSmbiosM var provider = (FIRMWARE_TABLE_PROVIDER)rsMb; uint bufferSize = PInvoke.GetSystemFirmwareTable(provider, 0, null, 0); if (bufferSize == 0) - return (null, null); + return (null, null, null); byte[] buffer = new byte[bufferSize]; fixed (byte* pBuffer = buffer) { if (PInvoke.GetSystemFirmwareTable(provider, 0, pBuffer, bufferSize) == 0) - return (null, null); + return (null, null, null); } // SMBIOS table header: Data at offset 8 int offset = 8; long? maxSpeed = null; string? detectedType = null; + byte? casLatency = null; while (offset + 4 <= buffer.Length) { @@ -217,6 +226,16 @@ private static unsafe (long? FrequencyMts, string? MemoryType) GetWindowsSmbiosM } } } + + // CAS Latency at offset 0x1C (BYTE) + if (length >= 0x1D && casLatency == null) + { + byte cas = buffer[offset + 0x1C]; + if (cas > 0 && cas < 100) // Reasonable CAS latency range + { + casLatency = cas; + } + } } // Move past formatted area @@ -230,11 +249,11 @@ private static unsafe (long? FrequencyMts, string? MemoryType) GetWindowsSmbiosM offset += 2; } - return (maxSpeed, detectedType); + return (maxSpeed, detectedType, casLatency); } catch { - return (null, null); + return (null, null, null); } } @@ -287,7 +306,7 @@ private static unsafe (long? FrequencyMts, string? MemoryType) GetWindowsSmbiosM } var (freq, type) = GetMacMemorySpecs(); - return new PhysicalMemoryInfo(total, available, freq, type); + return new PhysicalMemoryInfo(total, available, freq, type, null); } private static (long? Frequency, string? Type) GetMacMemorySpecs() diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs index a979b73b27..530a75c6e1 100644 --- a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -8,10 +8,11 @@ public class PhysicalMemoryInfoTests [Fact] public void PhysicalMemoryInfo_ParsesAndConstructsCorrectly() { - var memInfo = new PhysicalMemoryInfo(16000000000, null, 6400, "DDR4"); + var memInfo = new PhysicalMemoryInfo(16000000000, null, 6400, "DDR4", 16); Assert.Equal(16000000000L, memInfo.TotalPhysicalBytes); Assert.Equal(6400L, memInfo.FrequencyMts); Assert.Equal("DDR4", memInfo.MemoryType); + Assert.Equal((byte?)16, memInfo.CasLatency); } } } From d1a11d7e4394024695f1218a10b127cf5d456f24 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sun, 11 Oct 2026 07:49:04 -0600 Subject: [PATCH 14/18] fix: Use system_profiler JSON output for robust macOS memory detection Replace regex-based parsing of localized system_profiler text with JSON output. This eliminates issues with different locales and handles varying output formats on different macOS versions. Changes: - Use system_profiler -json SPMemoryDataType instead of text parsing - Parse JSON with System.Text.Json for robustness - Filter out "Unknown" memory types from output - Extract speed and type from either direct properties or _properties array - Gracefully handle missing or malformed output Addresses feedback on regex fragility and locale-specific output variations. --- .../Environments/PhysicalMemoryInfo.cs | 84 +++++++++++++++---- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 7e529e9aea..3ae8974ddf 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using Windows.Win32; using Windows.Win32.System.SystemInformation; @@ -313,33 +314,84 @@ private static (long? Frequency, string? Type) GetMacMemorySpecs() { try { - string? output = ProcessHelper.RunAndReadOutput("system_profiler", "SPMemoryDataType"); - if (string.IsNullOrEmpty(output)) + string? jsonOutput = ProcessHelper.RunAndReadOutput("system_profiler", "-json SPMemoryDataType"); + if (string.IsNullOrEmpty(jsonOutput)) return (null, null); - long? freq = null; - string? type = null; - - var speedMatch = Regex.Match(output, @"Speed:\s+(\d+)\s*MHz"); - if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speedMhz)) + using (JsonDocument doc = JsonDocument.Parse(jsonOutput!)) { - freq = speedMhz * 2; // Convert MHz to MT/s - } + var root = doc.RootElement; - var typeMatch = Regex.Match(output, @"Type:\s+(\S+)"); - if (typeMatch.Success) - { - type = typeMatch.Groups[1].Value; - } + if (!root.TryGetProperty("SPMemoryDataType", out JsonElement memoryArray) || memoryArray.ValueKind != JsonValueKind.Array) + return (null, null); + + long? maxFreq = null; + string? detectedType = null; + + foreach (var memoryItem in memoryArray.EnumerateArray()) + { + // Try to find Speed property (can be in different formats) + if (memoryItem.TryGetProperty("Speed", out JsonElement speedDirect) && speedDirect.ValueKind == JsonValueKind.String) + { + ParseSpeedValue(speedDirect.GetString(), ref maxFreq); + } + + // Try _properties array structure + if (memoryItem.TryGetProperty("_properties", out JsonElement properties) && properties.ValueKind == JsonValueKind.Array) + { + foreach (var prop in properties.EnumerateArray()) + { + if (prop.TryGetProperty("Speed", out JsonElement speedProp) && speedProp.ValueKind == JsonValueKind.String) + { + ParseSpeedValue(speedProp.GetString(), ref maxFreq); + } + + if (prop.TryGetProperty("Type", out JsonElement typeProp) && typeProp.ValueKind == JsonValueKind.String) + { + ParseTypeValue(typeProp.GetString(), ref detectedType); + } + } + } - return (freq, type); + // Try Type property (can be at top level) + if (memoryItem.TryGetProperty("Type", out JsonElement typeDirect) && typeDirect.ValueKind == JsonValueKind.String) + { + ParseTypeValue(typeDirect.GetString(), ref detectedType); + } + } + + return (maxFreq, detectedType); + } } catch { - // Fallback + // Fallback on parsing error } return (null, null); } + + private static void ParseSpeedValue(string? speedStr, ref long? maxFreq) + { + if (string.IsNullOrEmpty(speedStr)) + return; + + var speedMatch = Regex.Match(speedStr, @"(\d+)\s*MHz"); + if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speedMhz) && speedMhz > 0) + { + long speedMts = speedMhz * 2; // DDR: MT/s = MHz × 2 + if (maxFreq == null || speedMts > maxFreq.Value) + maxFreq = speedMts; + } + } + + private static void ParseTypeValue(string? typeStr, ref string? detectedType) + { + if (string.IsNullOrEmpty(typeStr) || typeStr == "Unknown") + return; + + if (detectedType == null) + detectedType = typeStr; + } } } \ No newline at end of file From bb66b8c41dd542285eb2e3dc653c33a1504ea1b6 Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Sun, 11 Oct 2026 16:06:56 -0600 Subject: [PATCH 15/18] fix: Add 'Total' label to memory output format Display 'GB Total' instead of just 'GB' to clarify total memory vs available memory. Output now shows: '16 GB Total (DDR4-6400MT/s-CAS16), 8 GB Available' --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 3ae8974ddf..84236de013 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -35,7 +35,7 @@ public string ToFormattedString() { double totalGb = TotalPhysicalBytes / (1024.0 * 1024.0 * 1024.0); var sb = new StringBuilder(); - sb.Append($"{Math.Round(totalGb, 2)} GB"); + sb.Append($"{Math.Round(totalGb, 2)} GB Total"); if (MemoryType.IsNotBlank() || FrequencyMts.HasValue || CasLatency.HasValue) { From 89880247a548d1cfd31bd1ce42e48e99576b646b Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Mon, 12 Oct 2026 02:23:17 -0600 Subject: [PATCH 16/18] fix: Use MT/s directly from SMBIOS, remove unnecessary DDR conversion SMBIOS specification defines memory speed at offset 0x15 as MT/s, not MHz. Remove the DDR multiplication logic (speedMhz * 2) since the value is already in the correct unit. Also simplify CAS latency validation by removing the unnecessary cas < 100 guard, keeping only cas > 0. Co-Authored-By: Claude Haiku 4.5 --- src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 84236de013..940e2e0ae8 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -214,13 +214,12 @@ private static unsafe (long? FrequencyMts, string? MemoryType, byte? CasLatency) detectedType = memTypeStr; } - // Speed in MHz at offset 0x15 (WORD), convert to MT/s + // Speed at offset 0x15 (WORD) in MT/s (SMBIOS spec) if (length >= 0x17) { - ushort speedMhz = (ushort)(buffer[offset + 0x15] | (buffer[offset + 0x16] << 8)); - if (speedMhz > 0 && speedMhz < 0xFFFF) + ushort speedMts = (ushort)(buffer[offset + 0x15] | (buffer[offset + 0x16] << 8)); + if (speedMts > 0) { - long speedMts = speedMhz * 2; // DDR: MT/s = MHz × 2 if (maxSpeed == null || speedMts > maxSpeed.Value) { maxSpeed = speedMts; @@ -232,7 +231,7 @@ private static unsafe (long? FrequencyMts, string? MemoryType, byte? CasLatency) if (length >= 0x1D && casLatency == null) { byte cas = buffer[offset + 0x1C]; - if (cas > 0 && cas < 100) // Reasonable CAS latency range + if (cas > 0) { casLatency = cas; } From a14c574b170cc74521270ea751d6a3b79be3334f Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 17 Oct 2026 05:17:52 -0400 Subject: [PATCH 17/18] fix: correct memory speed/type detection and add parser tests - SMBIOS: read Configured Memory Speed (0x20) with rated Speed (0x15) as fallback, honor the 0xFFFF Extended Speed fields, skip empty slots, stop at end-of-table, and bound parsing by the RawSMBIOSData table length. - Remove CAS latency: SMBIOS type 17 has no timing fields (0x1C is the Extended Size low byte). - macOS: parse the real system_profiler keys (dimm_type/dimm_speed, Intel _items and Apple silicon layouts) without doubling the reported speed; give system_profiler a timeout. - Linux: read the SMBIOS table from /sys/firmware/dmi/tables/DMI when readable, falling back to EDAC with normalized type names. - Restore the shipped PhysicalMemoryInfo(long, long?) constructor and the "X GB" output when available memory is unknown. - Split the Windows/Linux detail helpers and extract testable parsers. Co-Authored-By: Claude Opus 5.5 --- .../Environments/PhysicalMemoryInfo.cs | 336 ++++++++++-------- src/BenchmarkDotNet/Helpers/ProcessHelper.cs | 20 +- .../Environments/PhysicalMemoryInfoTests.cs | 221 +++++++++++- 3 files changed, 412 insertions(+), 165 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 940e2e0ae8..24bc373de1 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -1,9 +1,7 @@ using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; -using System; -using System.Diagnostics; -using System.IO; +using System.Buffers.Binary; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; @@ -20,24 +18,30 @@ public class PhysicalMemoryInfo public long? AvailablePhysicalBytes { get; } public long? FrequencyMts { get; } public string? MemoryType { get; } - public byte? CasLatency { get; } - public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null, long? frequencyMts = null, string? memoryType = null, byte? casLatency = null) + public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes = null) + : this(totalPhysicalBytes, availablePhysicalBytes, frequencyMts: null, memoryType: null) + { + } + + public PhysicalMemoryInfo(long totalPhysicalBytes, long? availablePhysicalBytes, long? frequencyMts, string? memoryType) { TotalPhysicalBytes = totalPhysicalBytes; AvailablePhysicalBytes = availablePhysicalBytes; FrequencyMts = frequencyMts; MemoryType = memoryType; - CasLatency = casLatency; } public string ToFormattedString() { double totalGb = TotalPhysicalBytes / (1024.0 * 1024.0 * 1024.0); var sb = new StringBuilder(); - sb.Append($"{Math.Round(totalGb, 2)} GB Total"); + sb.Append($"{Math.Round(totalGb, 2)} GB"); - if (MemoryType.IsNotBlank() || FrequencyMts.HasValue || CasLatency.HasValue) + if (AvailablePhysicalBytes.HasValue) + sb.Append(" Total"); + + if (MemoryType.IsNotBlank() || FrequencyMts.HasValue) { sb.Append(" ("); if (MemoryType.IsNotBlank()) @@ -48,12 +52,6 @@ public string ToFormattedString() sb.Append('-'); sb.Append($"{FrequencyMts}MT/s"); } - if (CasLatency.HasValue) - { - if (MemoryType.IsNotBlank() || FrequencyMts.HasValue) - sb.Append('-'); - sb.Append($"CAS{CasLatency}"); - } sb.Append(')'); } @@ -69,6 +67,8 @@ public string ToFormattedString() public static class SystemMemory { + private static readonly TimeSpan SystemProfilerTimeout = TimeSpan.FromSeconds(5); + public static PhysicalMemoryInfo? GetPhysicalMemory() { try @@ -96,8 +96,8 @@ public static class SystemMemory var memStatus = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf() }; if (PInvoke.GlobalMemoryStatusEx(ref memStatus)) { - var (freq, type, cas) = GetLinuxOrWindowsMemoryDetails(); - return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys, freq, type, cas); + var (speed, type) = GetWindowsMemoryDetails(); + return new PhysicalMemoryInfo((long)memStatus.ullTotalPhys, (long)memStatus.ullAvailPhys, speed, type); } return null; @@ -129,35 +129,47 @@ public static class SystemMemory if (total > 0) { - var (freq, type, cas) = GetLinuxOrWindowsMemoryDetails(); - return new PhysicalMemoryInfo(total, available, freq, type, cas); + var (speed, type) = GetLinuxMemoryDetails(); + return new PhysicalMemoryInfo(total, available, speed, type); } } return null; } - private static (long? FrequencyMts, string? MemoryType, byte? CasLatency) GetLinuxOrWindowsMemoryDetails() + private static (long? SpeedMts, string? MemoryType) GetLinuxMemoryDetails() { + // The raw SMBIOS table is readable only by root on most distros. try { - if (OsDetector.IsWindows7OrLater()) + const string dmiTablePath = "/sys/firmware/dmi/tables/DMI"; + if (File.Exists(dmiTablePath)) { - return GetWindowsSmbiosMemoryDetails(); + var details = ParseSmbiosMemoryDevices(File.ReadAllBytes(dmiTablePath)); + if (details.SpeedMts.HasValue || details.MemoryType != null) + return details; } + } + catch + { + // Fall back to EDAC + } - // Try reading /sys/devices/system/edac/mc/ on Linux - if (Directory.Exists("/sys/devices/system/edac/mc")) + // EDAC is world-readable but usually only present on ECC systems, and it does not expose the speed. + try + { + const string edacPath = "/sys/devices/system/edac/mc"; + if (Directory.Exists(edacPath)) { - foreach (var mcDir in Directory.GetDirectories("/sys/devices/system/edac/mc", "mc*")) + foreach (var mcDir in Directory.GetDirectories(edacPath, "mc*")) { foreach (var dimmDir in Directory.GetDirectories(mcDir, "dimm*")) { string typePath = Path.Combine(dimmDir, "dimm_mem_type"); if (File.Exists(typePath)) { - string memType = File.ReadAllText(typePath).Trim(); - if (!string.IsNullOrEmpty(memType)) - return (null, memType, null); + string? memType = NormalizeEdacMemoryType(File.ReadAllText(typePath).Trim()); + if (memType != null) + return (null, memType); } } } @@ -168,11 +180,23 @@ private static (long? FrequencyMts, string? MemoryType, byte? CasLatency) GetLin // Ignore fallback failures } - return (null, null, null); + return (null, null); + } + + /// + /// Maps a kernel EDAC memory type name (e.g. "Registered-DDR4", "Low-Power-DDR5-RAM") to the plain memory type ("DDR4", "LPDDR5"). + /// + internal static string? NormalizeEdacMemoryType(string edacType) + { + var match = Regex.Match(edacType, @"DDR\d*"); + if (!match.Success) + return null; + + return edacType.StartsWith("Low-Power", StringComparison.Ordinal) ? "LP" + match.Value : match.Value; } [SupportedOSPlatform("windows6.0.6000")] - private static unsafe (long? FrequencyMts, string? MemoryType, byte? CasLatency) GetWindowsSmbiosMemoryDetails() + private static unsafe (long? SpeedMts, string? MemoryType) GetWindowsMemoryDetails() { try { @@ -180,81 +204,99 @@ private static unsafe (long? FrequencyMts, string? MemoryType, byte? CasLatency) var provider = (FIRMWARE_TABLE_PROVIDER)rsMb; uint bufferSize = PInvoke.GetSystemFirmwareTable(provider, 0, null, 0); if (bufferSize == 0) - return (null, null, null); + return (null, null); byte[] buffer = new byte[bufferSize]; fixed (byte* pBuffer = buffer) { if (PInvoke.GetSystemFirmwareTable(provider, 0, pBuffer, bufferSize) == 0) - return (null, null, null); + return (null, null); } - // SMBIOS table header: Data at offset 8 - int offset = 8; - long? maxSpeed = null; - string? detectedType = null; - byte? casLatency = null; - - while (offset + 4 <= buffer.Length) - { - byte type = buffer[offset]; - byte length = buffer[offset + 1]; + // RawSMBIOSData header: 4 version bytes, then the DWORD length of the structure table that follows. + const int headerSize = 8; + if (buffer.Length < headerSize) + return (null, null); + int tableLength = (int)Math.Min(BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(4)), (uint)(buffer.Length - headerSize)); + return ParseSmbiosMemoryDevices(buffer.AsSpan(headerSize, tableLength)); + } + catch + { + return (null, null); + } + } - if (length < 4 || offset + length > buffer.Length) - break; + /// + /// Reads the memory type and speed from the Memory Device (type 17) structures of an SMBIOS structure table (DMTF DSP0134). + /// + internal static (long? SpeedMts, string? MemoryType) ParseSmbiosMemoryDevices(ReadOnlySpan table) + { + const byte memoryDeviceType = 17; + const byte endOfTableType = 127; - // Type 17: Memory Device - if (type == 17 && length >= 0x15) - { - // Memory Type at offset 0x12 - byte rawMemoryType = buffer[offset + 0x12]; - string? memTypeStr = MapSmbiosMemoryType(rawMemoryType); - if (memTypeStr != null && detectedType == null) - { - detectedType = memTypeStr; - } + long? speed = null; + string? memoryType = null; + int offset = 0; - // Speed at offset 0x15 (WORD) in MT/s (SMBIOS spec) - if (length >= 0x17) - { - ushort speedMts = (ushort)(buffer[offset + 0x15] | (buffer[offset + 0x16] << 8)); - if (speedMts > 0) - { - if (maxSpeed == null || speedMts > maxSpeed.Value) - { - maxSpeed = speedMts; - } - } - } - - // CAS Latency at offset 0x1C (BYTE) - if (length >= 0x1D && casLatency == null) - { - byte cas = buffer[offset + 0x1C]; - if (cas > 0) - { - casLatency = cas; - } - } - } + while (offset + 4 <= table.Length) + { + byte structureType = table[offset]; + byte length = table[offset + 1]; - // Move past formatted area - offset += length; + if (structureType == endOfTableType || length < 4 || offset + length > table.Length) + break; - // Skip unformatted string section (double null-terminated) - while (offset < buffer.Length - 1 && !(buffer[offset] == 0 && buffer[offset + 1] == 0)) + if (structureType == memoryDeviceType) + { + var device = table.Slice(offset, length); + if (IsInstalledMemoryDevice(device)) { - offset++; + if (memoryType == null && device.Length > 0x12) + memoryType = MapSmbiosMemoryType(device[0x12]); + + // Mixed modules all run at the speed of the slowest one. + long? deviceSpeed = GetMemoryDeviceSpeed(device); + if (deviceSpeed.HasValue && (speed == null || deviceSpeed.Value < speed.Value)) + speed = deviceSpeed; } - offset += 2; } - return (maxSpeed, detectedType, casLatency); - } - catch - { - return (null, null, null); + // Skip the formatted area, then the string-set, which ends with a double null. + offset += length; + while (offset + 1 < table.Length && (table[offset] != 0 || table[offset + 1] != 0)) + offset++; + offset += 2; } + + return (speed, memoryType); + } + + // Size (WORD at 0x0C) is 0 for an empty slot. + private static bool IsInstalledMemoryDevice(ReadOnlySpan device) + => device.Length < 0x0E || BinaryPrimitives.ReadUInt16LittleEndian(device.Slice(0x0C)) != 0; + + // Configured Memory Speed (0x20, SMBIOS 2.7+) is what the module actually runs at. + // Speed (0x15) is the module's maximum rated speed, used only when the configured speed is not reported. + private static long? GetMemoryDeviceSpeed(ReadOnlySpan device) + => ReadSmbiosSpeed(device, wordOffset: 0x20, extendedOffset: 0x58) + ?? ReadSmbiosSpeed(device, wordOffset: 0x15, extendedOffset: 0x54); + + private static long? ReadSmbiosSpeed(ReadOnlySpan device, int wordOffset, int extendedOffset) + { + if (device.Length < wordOffset + 2) + return null; + + ushort speed = BinaryPrimitives.ReadUInt16LittleEndian(device.Slice(wordOffset)); + if (speed == 0) + return null; // unknown + if (speed != 0xFFFF) + return speed; + + // 0xFFFF means the speed is in the DWORD Extended field (SMBIOS 3.3+), whose bit 31 is reserved. + if (device.Length < extendedOffset + 4) + return null; + uint extendedSpeed = BinaryPrimitives.ReadUInt32LittleEndian(device.Slice(extendedOffset)) & 0x7FFFFFFF; + return extendedSpeed == 0 ? null : extendedSpeed; } private static string? MapSmbiosMemoryType(byte type) => type switch @@ -305,62 +347,17 @@ private static unsafe (long? FrequencyMts, string? MemoryType, byte? CasLatency) } } - var (freq, type) = GetMacMemorySpecs(); - return new PhysicalMemoryInfo(total, available, freq, type, null); + var (speed, type) = GetMacMemoryDetails(); + return new PhysicalMemoryInfo(total, available, speed, type); } - private static (long? Frequency, string? Type) GetMacMemorySpecs() + private static (long? SpeedMts, string? MemoryType) GetMacMemoryDetails() { try { - string? jsonOutput = ProcessHelper.RunAndReadOutput("system_profiler", "-json SPMemoryDataType"); - if (string.IsNullOrEmpty(jsonOutput)) - return (null, null); - - using (JsonDocument doc = JsonDocument.Parse(jsonOutput!)) - { - var root = doc.RootElement; - - if (!root.TryGetProperty("SPMemoryDataType", out JsonElement memoryArray) || memoryArray.ValueKind != JsonValueKind.Array) - return (null, null); - - long? maxFreq = null; - string? detectedType = null; - - foreach (var memoryItem in memoryArray.EnumerateArray()) - { - // Try to find Speed property (can be in different formats) - if (memoryItem.TryGetProperty("Speed", out JsonElement speedDirect) && speedDirect.ValueKind == JsonValueKind.String) - { - ParseSpeedValue(speedDirect.GetString(), ref maxFreq); - } - - // Try _properties array structure - if (memoryItem.TryGetProperty("_properties", out JsonElement properties) && properties.ValueKind == JsonValueKind.Array) - { - foreach (var prop in properties.EnumerateArray()) - { - if (prop.TryGetProperty("Speed", out JsonElement speedProp) && speedProp.ValueKind == JsonValueKind.String) - { - ParseSpeedValue(speedProp.GetString(), ref maxFreq); - } - - if (prop.TryGetProperty("Type", out JsonElement typeProp) && typeProp.ValueKind == JsonValueKind.String) - { - ParseTypeValue(typeProp.GetString(), ref detectedType); - } - } - } - - // Try Type property (can be at top level) - if (memoryItem.TryGetProperty("Type", out JsonElement typeDirect) && typeDirect.ValueKind == JsonValueKind.String) - { - ParseTypeValue(typeDirect.GetString(), ref detectedType); - } - } - - return (maxFreq, detectedType); - } + string? json = ProcessHelper.RunAndReadOutput("system_profiler", "-json SPMemoryDataType", timeout: SystemProfilerTimeout); + if (!string.IsNullOrEmpty(json)) + return ParseMacMemoryJson(json!); } catch { @@ -370,27 +367,58 @@ private static (long? Frequency, string? Type) GetMacMemorySpecs() return (null, null); } - private static void ParseSpeedValue(string? speedStr, ref long? maxFreq) + /// + /// Parses the output of system_profiler -json SPMemoryDataType. + /// Apple silicon reports a single entry with dimm_type and no speed; Intel Macs report one _items entry per slot. + /// + internal static (long? SpeedMts, string? MemoryType) ParseMacMemoryJson(string json) { - if (string.IsNullOrEmpty(speedStr)) - return; + using var doc = JsonDocument.Parse(json); + + if (!doc.RootElement.TryGetProperty("SPMemoryDataType", out var entries) || entries.ValueKind != JsonValueKind.Array) + return (null, null); - var speedMatch = Regex.Match(speedStr, @"(\d+)\s*MHz"); - if (speedMatch.Success && long.TryParse(speedMatch.Groups[1].Value, out long speedMhz) && speedMhz > 0) + long? speed = null; + string? memoryType = null; + + foreach (var entry in entries.EnumerateArray()) { - long speedMts = speedMhz * 2; // DDR: MT/s = MHz × 2 - if (maxFreq == null || speedMts > maxFreq.Value) - maxFreq = speedMts; + ReadMemoryDevice(entry); + + if (entry.TryGetProperty("_items", out var items) && items.ValueKind == JsonValueKind.Array) + { + foreach (var item in items.EnumerateArray()) + ReadMemoryDevice(item); + } } - } - private static void ParseTypeValue(string? typeStr, ref string? detectedType) - { - if (string.IsNullOrEmpty(typeStr) || typeStr == "Unknown") - return; + return (speed, memoryType); + + void ReadMemoryDevice(JsonElement device) + { + if (device.ValueKind != JsonValueKind.Object) + return; + + // Empty slots report "empty" for every field. + if (memoryType == null + && device.TryGetProperty("dimm_type", out var typeElement) && typeElement.ValueKind == JsonValueKind.String + && typeElement.GetString() is string type && type.IsNotBlank() + && !type.Equals("empty", StringComparison.OrdinalIgnoreCase) && !type.Equals("unknown", StringComparison.OrdinalIgnoreCase)) + { + memoryType = type; + } - if (detectedType == null) - detectedType = typeStr; + // Despite the "MHz" label, the value is the transfer rate: DDR4-2667 is reported as "2667 MHz". + if (device.TryGetProperty("dimm_speed", out var speedElement) && speedElement.ValueKind == JsonValueKind.String) + { + var match = Regex.Match(speedElement.GetString() ?? "", @"^\s*(\d+)\s*MHz"); + if (match.Success && long.TryParse(match.Groups[1].Value, out long deviceSpeed) && deviceSpeed > 0 + && (speed == null || deviceSpeed < speed.Value)) + { + speed = deviceSpeed; + } + } + } } } -} \ No newline at end of file +} diff --git a/src/BenchmarkDotNet/Helpers/ProcessHelper.cs b/src/BenchmarkDotNet/Helpers/ProcessHelper.cs index c93133d293..4a570c8716 100644 --- a/src/BenchmarkDotNet/Helpers/ProcessHelper.cs +++ b/src/BenchmarkDotNet/Helpers/ProcessHelper.cs @@ -14,10 +14,10 @@ internal static class ProcessHelper { /// /// Run external process and return the console output. - /// In the case of any exception, null will be returned. + /// In the case of any exception, or if the process does not finish within , null will be returned. /// internal static string? RunAndReadOutput(string fileName, string arguments = "", ILogger? logger = null, - Dictionary? environmentVariables = null) + Dictionary? environmentVariables = null, TimeSpan? timeout = null) { var processStartInfo = new ProcessStartInfo { @@ -43,9 +43,19 @@ internal static class ProcessHelper { return null; } - string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - return output; + + if (timeout is null) + { + string output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + return output; + } + + // On timeout, disposing the cleanup helper kills the still-running process. + var readTask = process.StandardOutput.ReadToEndAsync(); + if (!readTask.Wait(timeout.Value) || !process.WaitForExit((int)timeout.Value.TotalMilliseconds)) + return null; + return readTask.Result; } } diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs index 530a75c6e1..1833f976c1 100644 --- a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -1,18 +1,227 @@ using BenchmarkDotNet.Environments; +using System.Buffers.Binary; using Xunit; namespace BenchmarkDotNet.Tests.Environments { public class PhysicalMemoryInfoTests { + private const long SixteenGb = 16L * 1024 * 1024 * 1024; + private const long EightGb = 8L * 1024 * 1024 * 1024; + + private const byte Ddr4 = 0x1A; + private const byte Ddr5 = 0x22; + private const byte UnknownType = 0x02; + + // Structure lengths per SMBIOS version (DSP0134). + private const byte Smbios23Length = 0x1B; + private const byte Smbios28Length = 0x28; + private const byte Smbios33Length = 0x5C; + + [Theory] + [InlineData(null, null, null, "16 GB")] + [InlineData(EightGb, null, null, "16 GB Total, 8 GB Available")] + [InlineData(null, 6400L, "DDR5", "16 GB (DDR5-6400MT/s)")] + [InlineData(EightGb, 6400L, "DDR5", "16 GB Total (DDR5-6400MT/s), 8 GB Available")] + [InlineData(EightGb, null, "LPDDR5", "16 GB Total (LPDDR5), 8 GB Available")] + [InlineData(EightGb, 3200L, null, "16 GB Total (3200MT/s), 8 GB Available")] + public void ToFormattedString(long? available, long? speed, string? type, string expected) + { + var info = new PhysicalMemoryInfo(SixteenGb, available, speed, type); + Assert.Equal(expected, info.ToFormattedString()); + } + + [Fact] + public void TwoArgumentConstructorLeavesDetailsUnset() + { + var info = new PhysicalMemoryInfo(SixteenGb, EightGb); + Assert.Null(info.FrequencyMts); + Assert.Null(info.MemoryType); + } + + [Fact] + public void SmbiosPrefersConfiguredSpeedOverRatedSpeed() + { + // A DDR4-3600 module running at 2133 (XMP off). + byte[] table = Table(MemoryDevice(Smbios28Length, Ddr4, speed: 3600, configuredSpeed: 2133)); + + Assert.Equal((2133L, "DDR4"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosFallsBackToRatedSpeedBeforeVersion27() + { + byte[] table = Table(MemoryDevice(Smbios23Length, Ddr4, speed: 2400)); + + Assert.Equal((2400L, "DDR4"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosFallsBackToRatedSpeedWhenConfiguredSpeedIsUnknown() + { + byte[] table = Table(MemoryDevice(Smbios28Length, Ddr4, speed: 2400, configuredSpeed: 0)); + + Assert.Equal((2400L, "DDR4"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosReadsExtendedSpeedWhenWordIsSaturated() + { + byte[] table = Table(MemoryDevice(Smbios33Length, Ddr5, speed: 0xFFFF, configuredSpeed: 0xFFFF, extendedSpeed: 88000, extendedConfiguredSpeed: 70000)); + + Assert.Equal((70000L, "DDR5"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosIgnoresSaturatedSpeedWithoutExtendedField() + { + byte[] table = Table(MemoryDevice(Smbios28Length, Ddr5, speed: 0xFFFF, configuredSpeed: 0xFFFF)); + + Assert.Equal(((long?)null, "DDR5"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosSkipsEmptySlotsAndOtherStructures() + { + byte[] table = Table( + OtherStructure(type: 16, "Board"), + MemoryDevice(Smbios28Length, UnknownType, speed: 0, configuredSpeed: 0, size: 0, strings: ["DIMM_A1"]), + MemoryDevice(Smbios28Length, Ddr5, speed: 4800, configuredSpeed: 4800, strings: ["DIMM_A2", "Vendor"])); + + Assert.Equal((4800L, "DDR5"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + [Fact] - public void PhysicalMemoryInfo_ParsesAndConstructsCorrectly() + public void SmbiosReportsSlowestModule() + { + byte[] table = Table( + MemoryDevice(Smbios28Length, Ddr5, speed: 6000, configuredSpeed: 5600), + MemoryDevice(Smbios28Length, Ddr5, speed: 4800, configuredSpeed: 4800)); + + Assert.Equal((4800L, "DDR5"), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosStopsAtEndOfTable() + { + byte[] table = Table( + OtherStructure(type: 127), + MemoryDevice(Smbios28Length, Ddr5, speed: 4800, configuredSpeed: 4800)); + + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseSmbiosMemoryDevices(table)); + } + + [Fact] + public void SmbiosToleratesTruncatedTable() + { + byte[] full = Table(MemoryDevice(Smbios28Length, Ddr5, speed: 4800, configuredSpeed: 4800)); + + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseSmbiosMemoryDevices(full.AsSpan(0, 10))); + } + + [Fact] + public void MacIntelReadsPerSlotItems() + { + const string json = """ + { + "SPMemoryDataType" : [ + { + "_items" : [ + { "_name" : "BANK 0/ChannelA-DIMM0", "dimm_size" : "8 GB", "dimm_speed" : "2667 MHz", "dimm_status" : "ok", "dimm_type" : "DDR4" }, + { "_name" : "BANK 1/ChannelB-DIMM0", "dimm_size" : "empty", "dimm_speed" : "empty", "dimm_status" : "empty", "dimm_type" : "empty" } + ], + "_name" : "Memory Slots", + "global_ecc_state" : "ecc_disabled", + "is_memory_upgradeable" : "Yes" + } + ] + } + """; + + Assert.Equal((2667L, "DDR4"), SystemMemory.ParseMacMemoryJson(json)); + } + + [Fact] + public void MacAppleSiliconReadsTypeWithoutSpeed() + { + const string json = """ + { + "SPMemoryDataType" : [ + { "dimm_manufacturer" : "Hynix", "dimm_type" : "LPDDR5", "SPMemoryDataType" : "16 GB" } + ] + } + """; + + Assert.Equal(((long?)null, "LPDDR5"), SystemMemory.ParseMacMemoryJson(json)); + } + + [Fact] + public void MacWithoutMemoryDataReturnsNothing() + { + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseMacMemoryJson("""{ "SPMemoryDataType" : [] }""")); + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseMacMemoryJson("{}")); + } + + [Theory] + [InlineData("Unbuffered-DDR4", "DDR4")] + [InlineData("Registered-DDR5", "DDR5")] + [InlineData("Load-Reduced-DDR4-RAM", "DDR4")] + [InlineData("Low-Power-DDR5-RAM", "LPDDR5")] + [InlineData("Empty", null)] + [InlineData("Unknown", null)] + public void EdacMemoryTypeIsNormalized(string edacType, string? expected) + { + Assert.Equal(expected, SystemMemory.NormalizeEdacMemoryType(edacType)); + } + + private static byte[] Table(params byte[][] structures) + { + var table = new List(); + foreach (var structure in structures) + table.AddRange(structure); + return table.ToArray(); + } + + private static byte[] OtherStructure(byte type, params string[] strings) + { + var formatted = new byte[4]; + formatted[0] = type; + formatted[1] = (byte)formatted.Length; + return WithStrings(formatted, strings); + } + + private static byte[] MemoryDevice(byte length, byte memoryType, ushort speed, ushort configuredSpeed = 0, uint extendedSpeed = 0, + uint extendedConfiguredSpeed = 0, ushort size = 8192, string[]? strings = null) + { + var formatted = new byte[length]; + formatted[0] = 17; + formatted[1] = length; + BinaryPrimitives.WriteUInt16LittleEndian(formatted.AsSpan(0x0C), size); + formatted[0x12] = memoryType; + BinaryPrimitives.WriteUInt16LittleEndian(formatted.AsSpan(0x15), speed); + if (length >= 0x22) + BinaryPrimitives.WriteUInt16LittleEndian(formatted.AsSpan(0x20), configuredSpeed); + if (length >= 0x5C) + { + BinaryPrimitives.WriteUInt32LittleEndian(formatted.AsSpan(0x54), extendedSpeed); + BinaryPrimitives.WriteUInt32LittleEndian(formatted.AsSpan(0x58), extendedConfiguredSpeed); + } + return WithStrings(formatted, strings ?? []); + } + + // The string-set is a sequence of null-terminated strings followed by one more null; with no strings it is two nulls. + private static byte[] WithStrings(byte[] formatted, string[] strings) { - var memInfo = new PhysicalMemoryInfo(16000000000, null, 6400, "DDR4", 16); - Assert.Equal(16000000000L, memInfo.TotalPhysicalBytes); - Assert.Equal(6400L, memInfo.FrequencyMts); - Assert.Equal("DDR4", memInfo.MemoryType); - Assert.Equal((byte?)16, memInfo.CasLatency); + var bytes = new List(formatted); + foreach (var s in strings) + { + bytes.AddRange(System.Text.Encoding.ASCII.GetBytes(s)); + bytes.Add(0); + } + if (strings.Length == 0) + bytes.Add(0); + bytes.Add(0); + return bytes.ToArray(); } } } From a19b31b9fc0a152e34b88770e7ed7b5ff9d9cbde Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 17 Oct 2026 07:33:29 -0400 Subject: [PATCH 18/18] fix: ignore placeholder memory types reported by macOS VMs The GitHub Actions macOS runners report "Unknown" (arm64) and "RAM" with a fake speed (x64) for dimm_type, so only use slots whose type names a real DRAM type. Co-Authored-By: Claude Opus 5.5 --- .../Environments/PhysicalMemoryInfo.cs | 13 +++++++------ .../Environments/PhysicalMemoryInfoTests.cs | 11 +++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 24bc373de1..b70d1d83f3 100644 --- a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs +++ b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs @@ -399,15 +399,16 @@ void ReadMemoryDevice(JsonElement device) if (device.ValueKind != JsonValueKind.Object) return; - // Empty slots report "empty" for every field. - if (memoryType == null - && device.TryGetProperty("dimm_type", out var typeElement) && typeElement.ValueKind == JsonValueKind.String - && typeElement.GetString() is string type && type.IsNotBlank() - && !type.Equals("empty", StringComparison.OrdinalIgnoreCase) && !type.Equals("unknown", StringComparison.OrdinalIgnoreCase)) + // Empty slots report "empty" and virtual machines report placeholders such as "Unknown" or "RAM" (with a made-up speed), + // so only devices with a real memory type are used. + if (!device.TryGetProperty("dimm_type", out var typeElement) || typeElement.ValueKind != JsonValueKind.String + || typeElement.GetString() is not string type || !Regex.IsMatch(type, "DDR|HBM")) { - memoryType = type; + return; } + memoryType ??= type; + // Despite the "MHz" label, the value is the transfer rate: DDR4-2667 is reported as "2667 MHz". if (device.TryGetProperty("dimm_speed", out var speedElement) && speedElement.ValueKind == JsonValueKind.String) { diff --git a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs index 1833f976c1..26e99e19e5 100644 --- a/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs +++ b/tests/BenchmarkDotNet.Tests/Environments/PhysicalMemoryInfoTests.cs @@ -155,6 +155,17 @@ public void MacAppleSiliconReadsTypeWithoutSpeed() Assert.Equal(((long?)null, "LPDDR5"), SystemMemory.ParseMacMemoryJson(json)); } + [Fact] + public void MacVirtualMachinePlaceholdersAreIgnored() + { + // Placeholder values seen on the GitHub Actions macOS arm64 and x64 runners. + const string arm64 = """{ "SPMemoryDataType" : [ { "dimm_manufacturer" : "Unknown", "dimm_type" : "Unknown", "SPMemoryDataType" : "7 GB" } ] }"""; + const string x64 = """{ "SPMemoryDataType" : [ { "_items" : [ { "_name" : "DIMM0", "dimm_size" : "14 GB", "dimm_speed" : "1600 MHz", "dimm_type" : "RAM" } ] } ] }"""; + + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseMacMemoryJson(arm64)); + Assert.Equal(((long?)null, (string?)null), SystemMemory.ParseMacMemoryJson(x64)); + } + [Fact] public void MacWithoutMemoryDataReturnsNothing() {