diff --git a/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs b/src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs index 114836c438..5a16957f04 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(); } } @@ -56,12 +80,15 @@ 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() }; 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,144 @@ 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 + { + if (OsDetector.IsWindows7OrLater()) + { + return GetWindowsSmbiosMemoryDetails(); + } + + // 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); + } + + [SupportedOSPlatform("windows6.0.6000")] + private static unsafe (long? FrequencyMHz, string? MemoryType) GetWindowsSmbiosMemoryDetails() + { + try + { + const uint rsMb = 0x52534D42; // 'RSMB' + var provider = (FIRMWARE_TABLE_PROVIDER)rsMb; + 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, 0, pBuffer, 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 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 + }; + private static PhysicalMemoryInfo? GetMacMemory() { long total = 0; 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 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); + } + } +}