Skip to content
172 changes: 166 additions & 6 deletions src/BenchmarkDotNet/Environments/PhysicalMemoryInfo.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
}
}

Expand All @@ -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<MEMORYSTATUSEX>() };
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;
}
Expand Down Expand Up @@ -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()
Comment thread
timcassell marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this should be 2 separate methods.

{
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;
Expand Down
1 change: 1 addition & 0 deletions src/BenchmarkDotNet/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ PowerGetActiveScheme
LocalFree
GlobalMemoryStatusEx
GetLogicalProcessorInformationEx
GetSystemFirmwareTable
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading